#!/usr/bin/env python3
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — graphical installer (runs in the live session)
#
# A small GTK3 frontend for scripts/deploy-disk.sh. It boots to the console
# of the live ISO and walks through: pick a target disk -> confirm (type the
# device path, like scripts/install.sh) -> install, streaming the deploy log
# into the window. Everything destructive is delegated to deploy-disk.sh,
# which is shipped in the live image at /usr/lib/arcline.
#
# Requires root (the live session runs as root). No network needed.
# ─────────────────────────────────────────────────────────────────────────────
import glob
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk

DEPLOY = "/usr/lib/arcline/scripts/deploy-disk.sh"
ARCHIVE_CANDIDATES = (
    "/install/*.tar.xz",
    "/run/live/medium/install/*.tar.xz",
    "/lib/live/mount/medium/install/*.tar.xz",
    "/media/*/install/*.tar.xz",
    "/mnt/*/install/*.tar.xz",
)


def find_archive():
    for pattern in ARCHIVE_CANDIDATES:
        hits = sorted(glob.glob(pattern))
        if hits:
            return hits[0]
    return None


def current_edition():
    try:
        with open("/etc/arcline-release", encoding="utf-8") as fh:
            for line in fh:
                m = re.match(r"Edition:\s*(\S+)", line)
                if m:
                    return m.group(1)
    except OSError:
        pass
    return "server"


def list_disks():
    try:
        out = subprocess.run(
            ["lsblk", "-J", "-o", "NAME,SIZE,TYPE,MODEL,RM,RO"],
            capture_output=True, text=True, check=True, timeout=10,
        ).stdout
        data = json.loads(out)
    except (subprocess.SubprocessError, json.JSONDecodeError):
        return []
    disks = []
    for d in data.get("blockdevices", []):
        if d.get("type") != "disk" or d.get("ro"):
            continue
        name = d.get("name", "")
        # hide pseudo-devices that are never install targets
        if name.startswith(("zram", "loop", "ram")):
            continue
        disks.append(d)
    return disks


class Installer(Gtk.Window):
    def __init__(self):
        super().__init__(title="Arcline OS Installer")
        self.set_default_size(760, 640)
        self.proc = None
        self.tmpdir = None
        self.install_ok = False

        self.archive = find_archive()
        self.edition = current_edition()

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
        box.set_margin_top(16)
        box.set_margin_bottom(16)
        box.set_margin_start(20)
        box.set_margin_end(20)
        self.add(box)

        title = Gtk.Label()
        title.set_markup(f"<big><b>Arcline OS Installer</b> — {self.edition} edition</big>")
        box.pack_start(title, False, False, 0)

        # install source
        if self.archive:
            src = Gtk.Label()
            src.set_halign(Gtk.Align.START)
            src.set_markup(f"<small>Installing from: <tt>{self.archive}</tt></small>")
            box.pack_start(src, False, False, 0)
        else:
            warn = Gtk.Label(label="WARNING: no install archive found (expected /install/*.tar.xz on the ISO)")
            warn.set_name("warn")
            box.pack_start(warn, False, False, 0)

        # target disk
        self.disk_combo = Gtk.ComboBoxText()
        self.disks = list_disks()
        for d in self.disks:
            model = (d.get("model") or "").strip()
            removable = "(removable)" if d.get("rm") else ""
            self.disk_combo.append_text(f"/dev/{d['name']}  {d.get('size','?')}  {model} {removable}".strip())
        if not self.disks:
            self.disk_combo.append_text("(no writable disks found)")
        self.disk_combo.set_active(0)
        row_disk = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row_disk.pack_start(Gtk.Label(label="Target disk:"), False, False, 0)
        row_disk.pack_start(self.disk_combo, True, True, 0)
        box.pack_start(row_disk, False, False, 0)

        # boot mode
        self.boot_combo = Gtk.ComboBoxText()
        for label, val in (("BIOS (grub-pc)", "bios"), ("UEFI (grub-efi-amd64)", "efi")):
            self.boot_combo.append(label, val)
        self.boot_combo.set_active(0)
        row_boot = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row_boot.pack_start(Gtk.Label(label="Boot mode:"), False, False, 0)
        row_boot.pack_start(self.boot_combo, True, True, 0)
        box.pack_start(row_boot, False, False, 0)

        # danger + confirm
        danger = Gtk.Label()
        danger.set_name("warn")
        danger.set_markup("⚠  <b>ALL DATA on the target disk will be destroyed.</b>")
        box.pack_start(danger, False, False, 0)

        row_confirm = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        row_confirm.pack_start(Gtk.Label(label="Type the device path to confirm:"), False, False, 0)
        self.confirm = Gtk.Entry()
        self.confirm.set_placeholder_text("/dev/sda")
        row_confirm.pack_start(self.confirm, True, True, 0)
        box.pack_start(row_confirm, False, False, 0)

        # install button
        self.install_btn = Gtk.Button(label="Install Arcline OS")
        self.install_btn.set_sensitive(self.archive is not None and bool(self.disks))
        self.install_btn.connect("clicked", self.on_install)
        box.pack_start(self.install_btn, False, False, 0)

        # output
        self.output = Gtk.TextView()
        self.output.set_editable(False)
        self.output.set_monospace(True)
        scroller = Gtk.ScrolledWindow()
        scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scroller.set_vexpand(True)
        scroller.add(self.output)
        box.pack_start(scroller, True, True, 0)

        # status + reboot
        self.status = Gtk.Label(label="")
        box.pack_start(self.status, False, False, 0)
        self.reboot_btn = Gtk.Button(label="Reboot")
        self.reboot_btn.set_sensitive(False)
        self.reboot_btn.connect("clicked", lambda *_: subprocess.Popen(["systemctl", "reboot"]))
        box.pack_start(self.reboot_btn, False, False, 0)

        self.connect("destroy", Gtk.main_quit)

    # ── helpers ─────────────────────────────────────────────────────────────
    def append(self, text):
        buf = self.output.get_buffer()
        buf.insert(buf.get_end_iter(), text)

    def selected_device(self):
        i = self.disk_combo.get_active()
        if i < 0 or i >= len(self.disks):
            return None
        return f"/dev/{self.disks[i]['name']}"

    def boot_mode(self):
        return self.boot_combo.get_active_id() or "bios"

    # ── install ─────────────────────────────────────────────────────────────
    def on_install(self, _btn):
        dev = self.selected_device()
        if not dev:
            self.status.set_text("Select a target disk first.")
            return
        if self.confirm.get_text().strip() != dev:
            self.status.set_markup(f"<span color='red'>Type <b>{dev}</b> exactly to confirm.</span>")
            return

        self.install_btn.set_sensitive(False)
        self.status.set_text("Preparing…")
        self.tmpdir = tempfile.mkdtemp(prefix="arcline-install-")
        try:
            shutil.unpack_archive(self.archive, self.tmpdir, format="xztar")
        except Exception as exc:  # noqa: BLE001
            self.status.set_markup(f"<span color='red'>Failed to extract archive: {exc}</span>")
            self.install_btn.set_sensitive(True)
            return

        cmd = [DEPLOY, dev, self.tmpdir, self.edition, "--boot", self.boot_mode()]
        self.append(f"$ {' '.join(cmd)}\n")
        self.proc = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
            text=True, bufsize=1,
        )
        self.status.set_text("Installing… this takes a while.")
        GLib.timeout_add(100, self.drain)

    def drain(self):
        if self.proc is None:
            return False
        line = self.proc.stdout.readline()
        if line:
            self.append(line)
            # keep the tail visible
            adj = self.output.get_parent().get_vadjustment()
            adj.set_value(adj.get_upper())
            return True
        rc = self.proc.poll()
        if rc is None:
            return True
        self.proc.stdout.close()
        self.proc = None
        if rc == 0:
            self.install_ok = True
            self.status.set_markup("<span color='green'>Install complete. Remove the media and reboot.</span>")
            self.reboot_btn.set_sensitive(True)
        else:
            self.status.set_markup(f"<span color='red'>Install failed (exit {rc}). See log above.</span>")
            self.install_btn.set_sensitive(True)
        if self.tmpdir:
            shutil.rmtree(self.tmpdir, ignore_errors=True)
            self.tmpdir = None
        return False


def main():
    if os.geteuid() != 0:
        print("error: the Arcline installer must run as root", file=sys.stderr)
        return 1
    if not os.path.exists(DEPLOY):
        print(f"error: {DEPLOY} not found — installer is incomplete", file=sys.stderr)
        return 1
    win = Installer()
    win.show_all()
    Gtk.main()
    return 0


if __name__ == "__main__":
    sys.exit(main())
