# ABOUTME: Main greeter window — builds the GTK4 UI layout for the Moongreet greeter.
# ABOUTME: Handles user selection, session choice, password entry, and power actions.

import os
import shlex
import socket
import stat
import subprocess
from importlib.resources import files
from pathlib import Path

import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gdk", "4.0")
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf

from moongreet.config import load_config
from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session
from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
from moongreet.sessions import Session, get_sessions
from moongreet.power import reboot, shutdown

LAST_USER_PATH = Path("/var/cache/moongreet/last-user")
PACKAGE_DATA = files("moongreet") / "data"
DEFAULT_AVATAR_PATH = PACKAGE_DATA / "default-avatar.svg"
AVATAR_SIZE = 128


class GreeterWindow(Gtk.ApplicationWindow):
    """The main greeter window with login UI."""

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self.add_css_class("greeter")
        self.set_default_size(1920, 1080)

        self._config = load_config()
        self._users = get_users()
        self._sessions = get_sessions()
        self._selected_user: User | None = None
        self._greetd_sock: socket.socket | None = None
        self._default_avatar_pixbuf: GdkPixbuf.Pixbuf | None = None
        self._avatar_cache: dict[str, GdkPixbuf.Pixbuf] = {}

        self._build_ui()
        self._select_initial_user()
        self._setup_keyboard_navigation()

    def _build_ui(self) -> None:
        """Build the complete greeter UI layout."""
        # Root overlay for layering
        overlay = Gtk.Overlay()
        self.set_child(overlay)

        # Background wallpaper (blurred and darkened)
        if self._config.background and self._config.background.exists():
            background = Gtk.Picture()
            background.set_filename(str(self._config.background))
            background.set_content_fit(Gtk.ContentFit.COVER)
            background.set_hexpand(True)
            background.set_vexpand(True)
        else:
            background = Gtk.Box()
            background.set_hexpand(True)
            background.set_vexpand(True)
        overlay.set_child(background)

        # Main layout: 3 rows (top spacer, center login, bottom bar)
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        main_box.set_hexpand(True)
        main_box.set_vexpand(True)
        overlay.add_overlay(main_box)

        # Top spacer
        top_spacer = Gtk.Box()
        top_spacer.set_vexpand(True)
        main_box.append(top_spacer)

        # Center: login box
        center_box = self._build_login_box()
        center_box.set_halign(Gtk.Align.CENTER)
        main_box.append(center_box)

        # Bottom spacer
        bottom_spacer = Gtk.Box()
        bottom_spacer.set_vexpand(True)
        main_box.append(bottom_spacer)

        # Bottom bar overlay (user list left, power buttons right)
        bottom_bar = self._build_bottom_bar()
        bottom_bar.set_valign(Gtk.Align.END)
        overlay.add_overlay(bottom_bar)

    def _build_login_box(self) -> Gtk.Box:
        """Build the central login area with avatar, name, session, password."""
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        box.add_css_class("login-box")
        box.set_halign(Gtk.Align.CENTER)
        box.set_valign(Gtk.Align.CENTER)
        box.set_spacing(12)

        # Avatar — wrapped in a clipping frame for round shape
        avatar_frame = Gtk.Box()
        avatar_frame.set_size_request(AVATAR_SIZE, AVATAR_SIZE)
        avatar_frame.set_halign(Gtk.Align.CENTER)
        avatar_frame.set_overflow(Gtk.Overflow.HIDDEN)
        avatar_frame.add_css_class("avatar")
        self._avatar_image = Gtk.Image()
        self._avatar_image.set_pixel_size(AVATAR_SIZE)
        avatar_frame.append(self._avatar_image)
        box.append(avatar_frame)

        # Username label
        self._username_label = Gtk.Label(label="")
        self._username_label.add_css_class("username-label")
        box.append(self._username_label)

        # Session dropdown
        self._session_dropdown = Gtk.DropDown()
        self._session_dropdown.add_css_class("session-dropdown")
        self._session_dropdown.set_hexpand(True)
        if self._sessions:
            session_names = [s.name for s in self._sessions]
            string_list = Gtk.StringList.new(session_names)
            self._session_dropdown.set_model(string_list)
        box.append(self._session_dropdown)

        # Password entry
        self._password_entry = Gtk.PasswordEntry()
        self._password_entry.set_hexpand(True)
        self._password_entry.set_property("placeholder-text", "Passwort")
        self._password_entry.set_property("show-peek-icon", True)
        self._password_entry.add_css_class("password-entry")
        self._password_entry.connect("activate", self._on_login_activate)
        box.append(self._password_entry)

        # Error label (hidden by default)
        self._error_label = Gtk.Label(label="")
        self._error_label.add_css_class("error-label")
        self._error_label.set_visible(False)
        box.append(self._error_label)

        return box

    def _build_bottom_bar(self) -> Gtk.Box:
        """Build the bottom bar with user list (left) and power buttons (right)."""
        bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        bar.set_hexpand(True)
        bar.set_margin_start(16)
        bar.set_margin_end(16)
        bar.set_margin_bottom(16)

        # User list (left)
        user_list_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        user_list_box.add_css_class("user-list")
        user_list_box.set_halign(Gtk.Align.START)
        user_list_box.set_valign(Gtk.Align.END)

        for user in self._users:
            btn = Gtk.Button(label=user.display_name)
            btn.add_css_class("user-list-item")
            btn.connect("clicked", self._on_user_clicked, user)
            user_list_box.append(btn)

        bar.append(user_list_box)

        # Spacer
        spacer = Gtk.Box()
        spacer.set_hexpand(True)
        bar.append(spacer)

        # Power buttons (right)
        power_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        power_box.set_halign(Gtk.Align.END)
        power_box.set_valign(Gtk.Align.END)
        power_box.set_spacing(8)

        reboot_btn = Gtk.Button()
        reboot_btn.set_icon_name("system-reboot-symbolic")
        reboot_btn.add_css_class("power-button")
        reboot_btn.set_tooltip_text("Neustart")
        reboot_btn.connect("clicked", self._on_reboot_clicked)
        power_box.append(reboot_btn)

        shutdown_btn = Gtk.Button()
        shutdown_btn.set_icon_name("system-shutdown-symbolic")
        shutdown_btn.add_css_class("power-button")
        shutdown_btn.set_tooltip_text("Herunterfahren")
        shutdown_btn.connect("clicked", self._on_shutdown_clicked)
        power_box.append(shutdown_btn)

        bar.append(power_box)

        return bar

    def _select_initial_user(self) -> None:
        """Select the last user or the first available user."""
        if not self._users:
            return

        # Try to load last user
        last_username = self._load_last_user()
        target_user = None

        if last_username:
            for user in self._users:
                if user.username == last_username:
                    target_user = user
                    break

        if target_user is None:
            target_user = self._users[0]

        self._switch_to_user(target_user)

    def _switch_to_user(self, user: User) -> None:
        """Update the UI to show the selected user."""
        self._selected_user = user
        self._username_label.set_text(user.display_name)
        self._password_entry.set_text("")
        self._error_label.set_visible(False)

        # Update avatar (use cache if available)
        if user.username in self._avatar_cache:
            self._avatar_image.set_from_pixbuf(self._avatar_cache[user.username])
        else:
            avatar_path = get_avatar_path(
                user.username, home_dir=user.home
            )
            if avatar_path and avatar_path.exists():
                self._set_avatar_from_file(avatar_path, user.username)
            elif DEFAULT_AVATAR_PATH.exists():
                self._set_default_avatar()
            else:
                self._avatar_image.set_from_icon_name("avatar-default-symbolic")

        # Apply user's GTK theme if available
        self._apply_user_theme(user)

        # Focus password entry
        self._password_entry.grab_focus()

    def _apply_user_theme(self, user: User) -> None:
        """Load the user's preferred GTK theme from their settings.ini."""
        gtk_config_dir = user.home / ".config" / "gtk-4.0"
        theme_name = get_user_gtk_theme(config_dir=gtk_config_dir)

        settings = Gtk.Settings.get_default()
        if settings is None:
            return

        current = settings.get_property("gtk-theme-name")
        if theme_name and current != theme_name:
            settings.set_property("gtk-theme-name", theme_name)
        elif not theme_name and current:
            settings.reset_property("gtk-theme-name")

    def _get_foreground_color(self) -> str:
        """Get the current GTK theme foreground color as a hex string."""
        rgba = self.get_color()
        r = int(rgba.red * 255)
        g = int(rgba.green * 255)
        b = int(rgba.blue * 255)
        return f"#{r:02x}{g:02x}{b:02x}"

    def _set_default_avatar(self) -> None:
        """Load the default avatar SVG, tinted with the GTK foreground color."""
        if self._default_avatar_pixbuf:
            self._avatar_image.set_from_pixbuf(self._default_avatar_pixbuf)
            return
        try:
            svg_text = DEFAULT_AVATAR_PATH.read_text()
            fg_color = self._get_foreground_color()
            svg_text = svg_text.replace("#PLACEHOLDER", fg_color)
            svg_bytes = svg_text.encode("utf-8")
            loader = GdkPixbuf.PixbufLoader.new_with_type("svg")
            loader.set_size(AVATAR_SIZE, AVATAR_SIZE)
            loader.write(svg_bytes)
            loader.close()
            pixbuf = loader.get_pixbuf()
            if pixbuf:
                self._default_avatar_pixbuf = pixbuf
                self._avatar_image.set_from_pixbuf(pixbuf)
        except (GLib.Error, OSError):
            self._avatar_image.set_from_icon_name("avatar-default-symbolic")

    def _set_avatar_from_file(self, path: Path, username: str | None = None) -> None:
        """Load an image file and set it as the avatar, scaled to AVATAR_SIZE."""
        try:
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
                str(path), AVATAR_SIZE, AVATAR_SIZE, True
            )
            if username:
                self._avatar_cache[username] = pixbuf
            self._avatar_image.set_from_pixbuf(pixbuf)
        except GLib.Error:
            self._avatar_image.set_from_icon_name("avatar-default-symbolic")

    def _setup_keyboard_navigation(self) -> None:
        """Set up keyboard shortcuts."""
        controller = Gtk.EventControllerKey()
        controller.connect("key-pressed", self._on_key_pressed)
        self.add_controller(controller)

    def _on_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        """Handle global key presses."""
        if keyval == Gdk.KEY_Escape:
            self._password_entry.set_text("")
            self._error_label.set_visible(False)
            return True
        return False

    def _on_user_clicked(self, button: Gtk.Button, user: User) -> None:
        """Handle user selection from the user list."""
        self._cancel_pending_session()
        self._switch_to_user(user)

    def _on_login_activate(self, entry: Gtk.PasswordEntry) -> None:
        """Handle Enter key in the password field — attempt login."""
        if not self._selected_user:
            return

        password = entry.get_text()
        session = self._get_selected_session()
        if not session:
            self._show_error("Keine Session ausgewählt")
            return

        self._attempt_login(self._selected_user, password, session)

    def _validate_greetd_sock(self, sock_path: str) -> bool:
        """Validate that GREETD_SOCK points to an absolute path and a real socket."""
        path = Path(sock_path)
        if not path.is_absolute():
            self._show_error("GREETD_SOCK ist kein absoluter Pfad")
            return False
        try:
            mode = path.stat().st_mode
            if not stat.S_ISSOCK(mode):
                self._show_error("GREETD_SOCK zeigt nicht auf einen Socket")
                return False
        except OSError:
            self._show_error("GREETD_SOCK nicht erreichbar")
            return False
        return True

    def _close_greetd_sock(self) -> None:
        """Close the greetd socket and reset the reference."""
        if self._greetd_sock:
            try:
                self._greetd_sock.close()
            except OSError:
                pass
            self._greetd_sock = None

    def _attempt_login(self, user: User, password: str, session: Session) -> None:
        """Attempt to authenticate and start a session via greetd IPC."""
        sock_path = os.environ.get("GREETD_SOCK")
        if not sock_path:
            self._show_error("GREETD_SOCK nicht gesetzt")
            return

        if not self._validate_greetd_sock(sock_path):
            return

        try:
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            sock.settimeout(10.0)
            sock.connect(sock_path)
            self._greetd_sock = sock

            # Step 1: Create session
            response = create_session(sock, user.username)

            if response.get("type") == "error":
                self._show_greetd_error(response, "Authentifizierung fehlgeschlagen")
                self._close_greetd_sock()
                return

            # Step 2: Send password if auth message received
            if response.get("type") == "auth_message":
                response = post_auth_response(sock, password)

                if response.get("type") == "error":
                    self._show_greetd_error(response, "Falsches Passwort")
                    self._close_greetd_sock()
                    return

                if response.get("type") == "auth_message":
                    # Multi-stage auth (e.g. TOTP) is not supported
                    cancel_session(sock)
                    self._close_greetd_sock()
                    self._show_error("Mehrstufige Authentifizierung wird nicht unterstützt")
                    return

            # Step 3: Start session
            if response.get("type") == "success":
                cmd = shlex.split(session.exec_cmd)
                if not cmd or not Path(cmd[0]).is_absolute():
                    self._show_error("Ungültiger Session-Befehl")
                    cancel_session(sock)
                    self._close_greetd_sock()
                    return
                response = start_session(sock, cmd)

                if response.get("type") == "success":
                    self._save_last_user(user.username)
                    self._close_greetd_sock()
                    self.get_application().quit()
                    return
                else:
                    self._show_greetd_error(response, "Session konnte nicht gestartet werden")

            self._close_greetd_sock()

        except ConnectionError as e:
            self._close_greetd_sock()
            self._show_error(f"Verbindungsfehler: {e}")
        except OSError as e:
            self._close_greetd_sock()
            self._show_error(f"Socket-Fehler: {e}")

    def _cancel_pending_session(self) -> None:
        """Cancel any in-progress greetd session."""
        if self._greetd_sock:
            try:
                cancel_session(self._greetd_sock)
            except (ConnectionError, OSError):
                pass
            self._close_greetd_sock()

    def _get_selected_session(self) -> Session | None:
        """Get the currently selected session from the dropdown."""
        if not self._sessions:
            return None
        idx = self._session_dropdown.get_selected()
        if idx < len(self._sessions):
            return self._sessions[idx]
        return None

    MAX_GREETD_ERROR_LENGTH = 200

    def _show_greetd_error(self, response: dict, fallback: str) -> None:
        """Display an error from greetd, using a fallback for missing or oversized descriptions."""
        description = response.get("description", "")
        if description and len(description) <= self.MAX_GREETD_ERROR_LENGTH:
            self._show_error(description)
        else:
            self._show_error(fallback)

    def _show_error(self, message: str) -> None:
        """Display an error message below the password field."""
        self._error_label.set_text(message)
        self._error_label.set_visible(True)
        self._password_entry.set_text("")
        self._password_entry.grab_focus()

    def _on_reboot_clicked(self, button: Gtk.Button) -> None:
        """Handle reboot button click."""
        try:
            reboot()
        except subprocess.CalledProcessError:
            self._show_error("Neustart fehlgeschlagen")

    def _on_shutdown_clicked(self, button: Gtk.Button) -> None:
        """Handle shutdown button click."""
        try:
            shutdown()
        except subprocess.CalledProcessError:
            self._show_error("Herunterfahren fehlgeschlagen")

    @staticmethod
    def _load_last_user() -> str | None:
        """Load the last logged-in username from cache."""
        if LAST_USER_PATH.exists():
            try:
                return LAST_USER_PATH.read_text().strip()
            except OSError:
                return None
        return None

    @staticmethod
    def _save_last_user(username: str) -> None:
        """Save the last logged-in username to cache."""
        try:
            LAST_USER_PATH.parent.mkdir(parents=True, exist_ok=True)
            LAST_USER_PATH.write_text(username)
        except OSError:
            pass  # Non-critical — cache dir may not be writable
