#!/usr/bin/env python3
"""tanin-greet — TANINUX login greeter (GTK4 / PyGObject).

Custom layout: a black top bar like the eww topbar (brand · clock · power), a
wallpaper background, user + password centered as pills, and a small session
selector tucked in the bottom-right corner. Talks to greetd over GREETD_SOCK.

Run by greetd via cage:  cage -s -- tanin-greet
Preview without greetd:  just run it — the UI renders; only login needs the
socket (set GREETD_SOCK via a mock for a full dry-run).

Env overrides (handy for previewing from the repo):
  TANIN_GREET_CSS   path to the stylesheet   (default /etc/greetd/tanin-greet.css)
  TANIN_GREET_BG    wallpaper image path     (default /usr/share/backgrounds/tanin/login.jpg)
  TANIN_GREET_SESSIONS  wayland-sessions dir (default /usr/share/wayland-sessions)
"""
import gi, os, sys, json, socket, struct, shlex, glob, pwd

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, GLib, Gdk, Gio  # noqa: E402

CSS_PATH     = os.environ.get("TANIN_GREET_CSS", "/etc/greetd/tanin-greet.css")
WALLPAPER    = os.environ.get("TANIN_GREET_BG", "/usr/share/backgrounds/tanin/login.jpg")
SESSION_DIR  = os.environ.get("TANIN_GREET_SESSIONS", "/usr/share/wayland-sessions")
BRAND        = os.environ.get("TANIN_GREET_BRAND", "TANINUX")


# --------------------------------------------------------------------------- #
#  greetd IPC client — native-endian u32 length prefix + JSON payload.
# --------------------------------------------------------------------------- #
class Greetd:
    def __init__(self):
        path = os.environ.get("GREETD_SOCK")
        self.sock = None
        if path:
            try:
                self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
                self.sock.connect(path)
            except OSError:
                # No reachable greetd (e.g. preview without a daemon). Render the
                # UI anyway; login() will report the missing socket on submit.
                self.sock = None

    def _recvn(self, n):
        buf = b""
        while len(buf) < n:
            chunk = self.sock.recv(n - len(buf))
            if not chunk:
                raise RuntimeError("greetd closed the connection")
            buf += chunk
        return buf

    def _send(self, obj):
        data = json.dumps(obj).encode()
        self.sock.sendall(struct.pack("=I", len(data)) + data)

    def _recv(self):
        n = struct.unpack("=I", self._recvn(4))[0]
        return json.loads(self._recvn(n))

    def login(self, username, password, cmd_argv):
        """Authenticate and start the session. Raises on failure."""
        if self.sock is None:
            raise RuntimeError("GREETD_SOCK not set (preview mode — cannot log in)")
        self._send({"type": "create_session", "username": username})
        while True:
            r = self._recv()
            t = r.get("type")
            if t == "auth_message":
                amt = r.get("auth_message_type")
                if amt in ("secret", "visible"):
                    self._send({"type": "post_auth_message_response", "response": password})
                else:  # info / error — acknowledge with no response
                    self._send({"type": "post_auth_message_response"})
            elif t == "success":
                break
            elif t == "error":
                self._send({"type": "cancel_session"})
                raise RuntimeError(r.get("description", "authentication failed"))
            else:
                raise RuntimeError(f"unexpected greetd reply: {r}")
        # authenticated → launch the chosen session
        self._send({"type": "start_session", "cmd": cmd_argv, "env": []})
        r = self._recv()
        if r.get("type") != "success":
            self._send({"type": "cancel_session"})
            raise RuntimeError(r.get("description", "failed to start session"))


# --------------------------------------------------------------------------- #
#  System enumeration
# --------------------------------------------------------------------------- #
def list_users():
    users = []
    for p in pwd.getpwall():
        if 1000 <= p.pw_uid < 65534 and p.pw_shell not in ("/usr/bin/nologin", "/sbin/nologin", "/bin/false"):
            users.append(p.pw_name)
    return users or [os.environ.get("USER", "user")]


def list_sessions():
    sessions = []  # (name, exec_argv)
    for f in sorted(glob.glob(os.path.join(SESSION_DIR, "*.desktop"))):
        name = exec_ = None
        try:
            for line in open(f, encoding="utf-8"):
                if line.startswith("Name=") and name is None:
                    name = line[5:].strip()
                elif line.startswith("Exec=") and exec_ is None:
                    exec_ = line[5:].strip()
        except OSError:
            continue
        if name and exec_:
            sessions.append((name, shlex.split(exec_)))
    return sessions or [("Niri", ["niri-session"])]


# --------------------------------------------------------------------------- #
#  UI
# --------------------------------------------------------------------------- #
class Greeter(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="ch.gabrielevarano.TaninGreet")
        self.greetd = Greetd()
        self.users = list_users()
        self.sessions = list_sessions()

    def do_activate(self):
        self._load_css()
        Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True)

        win = Gtk.ApplicationWindow(application=self)
        win.set_decorated(False)
        win.fullscreen()
        win.add_css_class("greeter")

        overlay = Gtk.Overlay()
        win.set_child(overlay)

        # background image (falls back to the scrim colour if missing)
        if os.path.exists(WALLPAPER):
            bg = Gtk.Picture.new_for_filename(WALLPAPER)
            bg.set_content_fit(Gtk.ContentFit.COVER)
            overlay.set_child(bg)
        else:
            overlay.set_child(Gtk.Box())

        # dark scrim so text stays readable over any wallpaper
        scrim = Gtk.Box()
        scrim.add_css_class("scrim")
        scrim.set_hexpand(True)
        scrim.set_vexpand(True)
        overlay.add_overlay(scrim)

        # main column: top bar + centered login
        col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        col.append(self._topbar())
        center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        center.set_vexpand(True)
        center.set_valign(Gtk.Align.CENTER)
        center.set_halign(Gtk.Align.CENTER)
        center.append(self._login_card())
        col.append(center)
        col.append(self._footer())          # TANINUX wordmark, bottom center
        overlay.add_overlay(col)

        win.present()
        self.password.grab_focus()

        # debug: auto-open the session dropdown so previews can screenshot the
        # popover without a click (TANIN_GREET_POPUP=1). No effect in production.
        if os.environ.get("TANIN_GREET_POPUP"):
            def _open():
                btn = self.session_dd.get_first_child()
                try:
                    btn.set_active(True)
                except Exception:  # noqa: BLE001
                    pass
                return False
            GLib.timeout_add(900, _open)

    # ---- pieces ----------------------------------------------------------- #
    def _topbar(self):
        bar = Gtk.CenterBox()
        bar.add_css_class("topbar")

        # top-left corner: session dropdown
        bar.set_start_widget(self._session_dd())

        # center: clock — time · weekday, date
        self.clock = Gtk.Label()
        self.clock.add_css_class("clock")
        bar.set_center_widget(self.clock)
        self._tick()
        GLib.timeout_add_seconds(1, self._tick)

        # top-right corner: power
        bar.set_end_widget(self._power_button())
        return bar

    def _session_dd(self):
        self.session_dd = Gtk.DropDown.new_from_strings([s[0] for s in self.sessions])
        self.session_dd.add_css_class("session-dd")
        for i, (name, _) in enumerate(self.sessions):
            if name.lower().startswith("niri"):
                self.session_dd.set_selected(i)
                break
        return self.session_dd

    def _footer(self):
        box = Gtk.Box()
        box.set_halign(Gtk.Align.CENTER)
        box.set_valign(Gtk.Align.END)
        box.set_margin_bottom(18)
        lbl = Gtk.Label(label=BRAND)
        lbl.add_css_class("brand")
        box.append(lbl)
        return box

    def _power_button(self):
        btn = Gtk.MenuButton()
        btn.add_css_class("power")
        btn.set_icon_name("system-shutdown-symbolic")
        pop = Gtk.Popover()
        pop.add_css_class("menu")
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        for label, argv in (("Reboot", ["systemctl", "reboot"]),
                            ("Power Off", ["systemctl", "poweroff"])):
            b = Gtk.Button(label=label)
            b.add_css_class("menuitem")
            b.connect("clicked", lambda _w, a=argv: self._run(a))
            box.append(b)
        pop.set_child(box)
        btn.set_popover(pop)
        return btn

    def _login_card(self):
        card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14)
        card.add_css_class("logincard")   # NOT "card" — libadwaita's .card adds a bg+shadow
        card.set_halign(Gtk.Align.CENTER)

        avatar = Gtk.Image.new_from_icon_name("avatar-default-symbolic")
        avatar.set_pixel_size(64)
        avatar.add_css_class("avatar")
        avatar.set_halign(Gtk.Align.CENTER)
        card.append(avatar)

        # user row — just the NAME as text, with a small dropdown chevron on the
        # right edge (aligned with where the password pill ends). Not a pill.
        name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
        name_row.add_css_class("namerow")
        name_row.set_halign(Gtk.Align.CENTER)

        self.name_label = Gtk.Label(label=self.users[0])
        self.name_label.add_css_class("username")
        self.name_label.set_xalign(0.5)        # centered
        self.name_label.set_hexpand(True)
        name_row.append(self.name_label)

        self.user_btn = Gtk.MenuButton()
        self.user_btn.add_css_class("userswitch")
        self.user_btn.set_icon_name("pan-down-symbolic")
        self.user_btn.set_visible(len(self.users) > 1)  # no point if there's one user
        upop = Gtk.Popover()
        upop.add_css_class("menu")
        ubox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        for u in self.users:
            b = Gtk.Button(label=u)
            b.add_css_class("menuitem")
            b.connect("clicked", lambda _w, name=u: (self.name_label.set_text(name),
                                                     self.user_btn.popdown()))
            ubox.append(b)
        upop.set_child(ubox)
        self.user_btn.set_popover(upop)
        name_row.append(self.user_btn)
        card.append(name_row)

        # password pill
        self.password = Gtk.PasswordEntry()
        self.password.set_show_peek_icon(True)
        self.password.set_property("placeholder-text", "Password")
        self.password.add_css_class("pill")
        self.password.connect("activate", self._on_submit)
        card.append(self.password)

        self.error = Gtk.Label()
        self.error.add_css_class("error")
        self.error.set_visible(False)
        card.append(self.error)
        return card

    # ---- behaviour -------------------------------------------------------- #
    def _tick(self):
        now = GLib.DateTime.new_now_local()
        self.clock.set_text(now.format("%H:%M   ·   %A, %d %B %Y"))
        return True

    def _on_submit(self, _entry):
        user = self.name_label.get_text()
        pw = self.password.get_text()
        cmd = self.sessions[self.session_dd.get_selected()][1]
        try:
            self.greetd.login(user, pw, cmd)
            self.quit()  # greetd takes over from here
        except Exception as e:  # noqa: BLE001
            self.password.set_text("")
            self.error.set_text(str(e))
            self.error.set_visible(True)

    def _run(self, argv):
        try:
            Gio.Subprocess.new(argv, Gio.SubprocessFlags.NONE)
        except GLib.Error as e:
            self.error.set_text(str(e))
            self.error.set_visible(True)

    def _load_css(self):
        if not os.path.exists(CSS_PATH):
            return
        provider = Gtk.CssProvider()
        provider.load_from_path(CSS_PATH)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(), provider,
            Gtk.STYLE_PROVIDER_PRIORITY_USER)


if __name__ == "__main__":
    sys.exit(Greeter().run(None))
