"""Bluetooth — über core.bluetooth (bluetoothctl / BlueZ).

Power-Toggle, Scan (timed), Geräteliste mit Connect/Disconnect. User-Level,
kein root. Live-Output im Log.
"""
from __future__ import annotations

import threading

import gi

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

from taninux.core import bluetooth
from taninux.gui import confirm
from taninux.gui.console import LogConsole, log_expander
from taninux.gui.process import ProcessRunner


def build() -> Gtk.Widget:
    page = Adw.PreferencesPage()

    if not bluetooth.available():
        g = Adw.PreferencesGroup(title="Bluetooth")
        g.add(Adw.ActionRow(
            title="No Bluetooth controller",
            subtitle="bluetoothctl found no adapter / BlueZ not running.",
        ))
        page.add(g)
        return page

    console = LogConsole()
    runner = ProcessRunner()
    runner.connect("line", lambda _r, ln: console.append_line(ln))

    # -- Power + Scan --------------------------------------------------
    g_ctrl = Adw.PreferencesGroup(title="Bluetooth")
    power_sw = Adw.SwitchRow(title="Bluetooth", subtitle="Power the adapter")
    power_sw.set_active(bluetooth.powered())

    def on_power(sw: Adw.SwitchRow, _p) -> None:
        runner.run(bluetooth.set_power_argv(sw.get_active()))

    power_sw.connect("notify::active", on_power)
    g_ctrl.add(power_sw)
    page.add(g_ctrl)

    # -- Geräte (async) ------------------------------------------------
    g_dev = Adw.PreferencesGroup(
        title="Devices",
        description="Pairing PIN-protected devices may need a Bluetooth agent "
                    "and can fail here — check the log.",
    )
    scan_btn = Gtk.Button(icon_name="view-refresh-symbolic")
    scan_btn.add_css_class("flat")
    scan_btn.set_valign(Gtk.Align.CENTER)
    scan_btn.set_tooltip_text("Scan for devices (8 s)")
    g_dev.set_header_suffix(scan_btn)
    page.add(g_dev)

    rows: list[Gtk.Widget] = []
    # Nach erfolgreichem Pair automatisch Trust nachschieben. Da der Runner
    # nur EINEN Prozess gleichzeitig führt, wird die Folge-Aktion erst im
    # nächsten `done` ausgeführt. Liste = [argv, …], FIFO.
    pending: list[list[str]] = []

    def _btn(label: str, *, css: str | None = None) -> Gtk.Button:
        b = Gtk.Button(label=label)
        b.add_css_class("flat")
        if css:
            b.add_css_class(css)
        b.set_valign(Gtk.Align.CENTER)
        return b

    def _clear() -> None:
        for r in rows:
            g_dev.remove(r)
        rows.clear()

    def _render(devs: list[bluetooth.BtDevice]) -> None:
        _clear()
        if not devs:
            empty = Adw.ActionRow(title="No devices",
                                  subtitle="Hit rescan to discover nearby devices.")
            g_dev.add(empty)
            rows.append(empty)
            return
        for d in devs:
            sub = "connected" if d.connected else ("paired" if d.paired else "available")
            if d.paired and d.trusted:
                sub += " · trusted"
            row = Adw.ActionRow(title=d.name, subtitle=sub)
            row.add_prefix(Gtk.Image.new_from_icon_name("bluetooth-symbolic"))

            if not d.paired:
                # Unbekanntes Gerät: Pair (und danach automatisch Trust).
                pair = _btn("Pair", css="suggested-action")
                pair.connect("clicked", lambda _b, m=d.mac: _pair(m))
                row.add_suffix(pair)
            else:
                if d.connected:
                    primary = _btn("Disconnect")
                    primary.connect("clicked",
                                    lambda _b, m=d.mac: _run(bluetooth.disconnect_argv(m)))
                else:
                    primary = _btn("Connect", css="suggested-action")
                    primary.connect("clicked",
                                    lambda _b, m=d.mac: _run(bluetooth.connect_argv(m)))
                row.add_suffix(primary)

                forget = _btn("Forget", css="destructive-action")
                forget.connect("clicked",
                               lambda _b, m=d.mac, n=d.name: _forget(m, n))
                row.add_suffix(forget)

            g_dev.add(row)
            rows.append(row)

    def _run(argv: list[str]) -> None:
        console.clear()
        runner.run(argv)

    def _pair(mac: str) -> None:
        # Pair zuerst; Trust wird nach erfolgreichem Pair nachgereicht.
        pending.append(bluetooth.trust_argv(mac))
        _run(bluetooth.pair_argv(mac))

    def _forget(mac: str, name: str) -> None:
        confirm.confirm(
            page,
            heading="Forget device?",
            body=f"Remove the pairing for {name}. You will have to pair it again.",
            on_confirm=lambda: _run(bluetooth.remove_argv(mac)),
        )

    def _load() -> None:
        def worker() -> None:
            devs = bluetooth.devices()
            GLib.idle_add(lambda: (_render(devs), False)[1])
        threading.Thread(target=worker, daemon=True).start()

    def _scan() -> None:
        _clear()
        spin_row = Adw.ActionRow(title="Scanning … (8 s)")
        sp = Adw.Spinner()
        sp.set_valign(Gtk.Align.CENTER)
        spin_row.add_prefix(sp)
        g_dev.add(spin_row)
        rows.append(spin_row)
        console.clear()
        runner.run(bluetooth.scan_argv(8))

    scan_btn.connect("clicked", lambda _b: _scan())

    def on_done(_r: ProcessRunner, code: int) -> None:
        console.append_line("✔ done" if code == 0 else f"✘ exit {code}")
        power_sw.set_active(bluetooth.powered())
        # Verkettete Folge-Aktion (z. B. Trust nach Pair) nur bei Erfolg.
        if code == 0 and pending:
            nxt = pending.pop(0)
            runner.run(nxt)
            return
        pending.clear()   # bei Fehler offene Kette verwerfen
        _load()   # Liste nach jeder Aktion (Scan/Connect) auffrischen

    runner.connect("done", on_done)
    GLib.idle_add(lambda: (_load(), False)[1])

    # -- Log -----------------------------------------------------------
    page.add(log_expander(console))
    return page
