#!/usr/bin/python3
# -------------------------------------------------------------------
# Description: Linux Lite Upgrade Series 8
# Architecture: amd64
# Author: Jerry Bezencon
# Website: https://www.linuxliteos.com
# Language: Python/GTK4
# Licence: GPLv2
# -------------------------------------------------------------------

import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk, GLib, Gio, Gdk

import os
import sys
import subprocess
import shutil
import threading
import logging
from pathlib import Path

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

UPGRADE_VERSION = "8.0"
UPGRADE_FULL = f"Linux Lite {UPGRADE_VERSION}"
APP_NAME = "Lite Upgrade"
APP_ID = "com.linuxlite.upgrade"
ICON_NAME = "lite-upgrade"
LLVER_FILE = "/etc/llver"
LOG_FILE = "/tmp/lite-upgrade.log"
LOG_FILE_DEST = "/var/log/lite-upgrade.log"
REPO_CHECK_URL = "https://repo.linuxliteos.com/linuxlite/db/version"
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------

logger = logging.getLogger("lite-upgrade")
logger.setLevel(logging.DEBUG)
_log_ready = False


def _init_log():
    global _log_ready
    if _log_ready:
        return
    # Remove stale log from a previous run (may be owned by root)
    try:
        os.remove(LOG_FILE)
    except FileNotFoundError:
        pass
    fh = logging.FileHandler(LOG_FILE, mode="w")
    fh.setFormatter(logging.Formatter("[%(asctime)s] %(message)s", datefmt="%D %H:%M:%S"))
    logger.addHandler(fh)
    _log_ready = True


def log(msg):
    if _log_ready:
        logger.info(msg)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def get_current_version():
    """Read /etc/llver and return the full string."""
    try:
        return Path(LLVER_FILE).read_text().strip()
    except FileNotFoundError:
        return ""


def get_version_number():
    """Return just the version number (e.g. '8.0') from /etc/llver."""
    ver = get_current_version()
    parts = ver.split()
    return parts[2] if len(parts) >= 3 else ""


def run_cmd(cmd):
    """Run a shell command, logging output. Returns (returncode, output)."""
    log(f"Running: {cmd}")
    try:
        proc = subprocess.run(
            cmd, shell=True, text=True,
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
        )
        if proc.stdout:
            log(proc.stdout.strip())
        return proc.returncode, proc.stdout or ""
    except Exception as exc:
        log(f"Exception running command: {exc}")
        return 1, str(exc)


def check_internet():
    rc, _ = run_cmd("curl -sk https://google.com -o /dev/null")
    return rc == 0


def check_repository():
    rc, _ = run_cmd(f"curl -sk {REPO_CHECK_URL}")
    return rc == 0


def get_repo_version():
    """Fetch the latest available version number from the repository."""
    try:
        proc = subprocess.run(
            ["curl", "-sk", REPO_CHECK_URL],
            capture_output=True, text=True, timeout=10,
        )
        if proc.returncode == 0 and proc.stdout.strip():
            text = proc.stdout.strip()
            # Handle "8.2" or "Linux Lite 8.2" formats
            for part in reversed(text.split()):
                try:
                    float(part)
                    return part
                except ValueError:
                    continue
            return text
    except Exception:
        pass
    return None


def _parse_version(ver_str):
    """Parse '8.0' into a comparable tuple like (8, 0)."""
    try:
        return tuple(int(x) for x in ver_str.split("."))
    except (ValueError, AttributeError):
        return (0,)


# ---------------------------------------------------------------------------
# Upgrade steps  (each returns True on success)
# ---------------------------------------------------------------------------

def step_kill_package_managers():
    """Kill off any running package managers."""
    log("Killing package managers...")
    for proc in ("synaptic", "gdebi-gtk"):
        run_cmd(f"killall -9 {proc} 2>/dev/null")
    return True


def step_apt_clean():
    log("Cleaning apt cache...")
    rc, _ = run_cmd("apt-get clean all")
    return rc == 0


def step_update_repos():
    log("Updating repositories...")
    rc, _ = run_cmd("apt-get update -y")
    return rc == 0


def step_clear_dpkg():
    log("Clearing dpkg available info...")
    rc, _ = run_cmd("dpkg --clear-avail")
    return rc == 0


def step_dist_upgrade():
    log("Running dist-upgrade and installing new packages...")
    extra = "lite-thememanager sshpass"
    cmd = (
        "DEBIAN_FRONTEND=noninteractive "
        'apt-get dist-upgrade -o Dpkg::Options::="--force-confdef" '
        '-o Dpkg::Options::="--force-confold" -y '
        f"&& apt-get install {extra} -y "
        "&& apt-get autoremove -y "
        "&& apt-get clean"
    )
    rc, _ = run_cmd(cmd)
    return rc == 0


def step_update_version_info():
    log("Updating lsb-release, /etc/issue, /etc/llver...")
    cmds = [
        f'sed -i "s/DISTRIB_DESCRIPTION=.*/DISTRIB_DESCRIPTION=\\"Linux Lite {UPGRADE_VERSION}\\"/g" /etc/lsb-release',
        f'echo "Linux Lite {UPGRADE_VERSION} LTS \\n \\l" > /etc/issue',
        f'echo "Linux Lite {UPGRADE_VERSION}" > /etc/llver',
    ]
    for c in cmds:
        rc, _ = run_cmd(c)
        if rc != 0:
            return False
    return True


def step_update_grub():
    log("Updating GRUB...")
    rc, _ = run_cmd("update-grub")
    return rc == 0


def step_update_plymouth():
    log("Updating Plymouth boot theme...")
    ply = "/usr/share/plymouth/themes/ubuntu-text/ubuntu-text.plymouth"
    cmds = [
        f'sed -i "s/^title=Linux Lite.*$/title=Linux Lite {UPGRADE_VERSION}/g" {ply}',
        f'sed -i "s/^title=Ubuntu.*$/title=Linux Lite {UPGRADE_VERSION}/g" {ply}',
        f'sed -i "s/black=0x2c001e/black=0x000000/g" {ply}',
        f'sed -i "s/brown=0xff4012/brown=0xffff00/g" {ply}',
        f'sed -i "s/blue=0x988592/blue=0x000000/g" {ply}',
        "update-initramfs -u",
    ]
    for c in cmds:
        run_cmd(c)
    return True


# ---------------------------------------------------------------------------
# Build the ordered list of upgrade steps for a given source version
# ---------------------------------------------------------------------------

def build_steps():
    """Return a list of (description, callable) tuples."""
    steps = []
    steps.append(("Stopping running package managers", step_kill_package_managers))
    steps.append(("Cleaning APT cache", step_apt_clean))
    steps.append(("Updating package repositories", step_update_repos))
    steps.append(("Clearing dpkg available info", step_clear_dpkg))
    steps.append(("Downloading and installing updates", step_dist_upgrade))
    steps.append(("Updating version information", step_update_version_info))
    steps.append(("Updating GRUB", step_update_grub))
    steps.append(("Updating boot splash", step_update_plymouth))
    return steps


# ---------------------------------------------------------------------------
# GTK4 Application
# ---------------------------------------------------------------------------

class UpgradeWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title=APP_NAME, default_width=520, default_height=480)
        self.current_version = get_current_version()
        self._cancelled = False

        # Main layout
        self.main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.set_child(self.main_box)

        # Header bar
        header = Gtk.HeaderBar()
        self.set_titlebar(header)

        # Stack for switching between pages
        self.stack = Gtk.Stack(transition_type=Gtk.StackTransitionType.SLIDE_LEFT)
        self.main_box.append(self.stack)

        # Build pages
        self._build_welcome_page()
        self._build_progress_page()
        self._build_done_page()

        self.stack.set_visible_child_name("welcome")

    # -- Welcome / confirmation page ----------------------------------------

    def _build_welcome_page(self):
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16,
                      margin_top=24, margin_bottom=24, margin_start=24, margin_end=24)

        # Status page-like header
        icon = Gtk.Image.new_from_icon_name("system-software-update-symbolic")
        icon.set_pixel_size(64)
        icon.add_css_class("accent")
        box.append(icon)

        title = Gtk.Label(label="Upgrade Available")
        title.add_css_class("title-1")
        box.append(title)

        current_lbl = Gtk.Label(label=f"Current version:  {self.current_version or 'Unknown'}")
        current_lbl.add_css_class("dim-label")
        box.append(current_lbl)

        target_lbl = Gtk.Label(label=f"Upgrade to:  {UPGRADE_FULL}")
        target_lbl.add_css_class("heading")
        box.append(target_lbl)

        info = Gtk.Label(
            label=(
                "Your computer must remain connected to the internet during the upgrade.\n\n"
                "A connectivity check will be performed before proceeding.\n"
                "Once the upgrade is complete you must restart to finish."
            ),
            wrap=True,
            xalign=0,
            margin_top=8,
        )
        info.add_css_class("body")
        box.append(info)

        warn = Gtk.Label(
            label="Please save and close your work before continuing.",
            wrap=True,
            xalign=0,
            margin_top=4,
        )
        warn.add_css_class("warning")
        warn.set_markup("<b>Please save and close your work before continuing.</b>")
        box.append(warn)

        # Buttons
        btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12,
                          halign=Gtk.Align.CENTER, margin_top=20)
        cancel_btn = Gtk.Button(label="Cancel")
        cancel_btn.connect("clicked", lambda _: self.close())
        btn_box.append(cancel_btn)

        upgrade_btn = Gtk.Button(label="Upgrade")
        upgrade_btn.add_css_class("suggested-action")
        upgrade_btn.add_css_class("pill")
        upgrade_btn.connect("clicked", self._on_upgrade_clicked)
        btn_box.append(upgrade_btn)

        box.append(btn_box)
        self.stack.add_named(box, "welcome")

    # -- Progress page ------------------------------------------------------

    def _build_progress_page(self):
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
                      margin_top=24, margin_bottom=24, margin_start=24, margin_end=24)

        title = Gtk.Label(label="Upgrading...")
        title.add_css_class("title-2")
        box.append(title)

        self.progress_bar = Gtk.ProgressBar(show_text=True)
        self.progress_bar.set_margin_top(12)
        box.append(self.progress_bar)

        self.step_label = Gtk.Label(label="Preparing...", xalign=0, wrap=True)
        self.step_label.add_css_class("dim-label")
        box.append(self.step_label)

        # Scrollable log view
        scroll = Gtk.ScrolledWindow(vexpand=True, min_content_height=160)
        scroll.set_margin_top(12)
        self.log_view = Gtk.TextView(editable=False, cursor_visible=False, wrap_mode=Gtk.WrapMode.WORD_CHAR)
        self.log_view.set_monospace(True)
        scroll.set_child(self.log_view)
        box.append(scroll)

        self.stack.add_named(box, "progress")

    # -- Done page ----------------------------------------------------------

    def _build_done_page(self):
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16,
                      margin_top=32, margin_bottom=24, margin_start=24, margin_end=24)

        self.done_icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic")
        self.done_icon.set_pixel_size(64)
        self.done_icon.add_css_class("success")
        box.append(self.done_icon)

        self.done_title = Gtk.Label(label="Upgrade Complete")
        self.done_title.add_css_class("title-1")
        box.append(self.done_title)

        self.done_subtitle = Gtk.Label(
            label="Save and close any open applications before restarting.",
            wrap=True,
        )
        self.done_subtitle.add_css_class("body")
        box.append(self.done_subtitle)

        btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12,
                          halign=Gtk.Align.CENTER, margin_top=20)

        restart_btn = Gtk.Button(label="Restart")
        restart_btn.add_css_class("suggested-action")
        restart_btn.add_css_class("pill")
        restart_btn.connect("clicked", lambda _: run_cmd("systemctl reboot"))
        btn_box.append(restart_btn)

        shutdown_btn = Gtk.Button(label="Shutdown")
        shutdown_btn.add_css_class("pill")
        shutdown_btn.connect("clicked", lambda _: run_cmd("systemctl poweroff"))
        btn_box.append(shutdown_btn)

        log_btn = Gtk.Button(label="View Log")
        log_btn.connect("clicked", self._on_view_log)
        btn_box.append(log_btn)

        close_btn = Gtk.Button(label="Close")
        close_btn.connect("clicked", lambda _: self.close())
        btn_box.append(close_btn)

        box.append(btn_box)
        self.stack.add_named(box, "done")

    # -- Callbacks ----------------------------------------------------------

    def _on_upgrade_clicked(self, _btn):
        log("User clicked Upgrade – running pre-flight checks...")
        self.stack.set_visible_child_name("progress")
        self._append_log("Starting pre-flight checks...\n")

        threading.Thread(target=self._preflight_and_run, daemon=True).start()

    def _on_view_log(self, _btn):
        dest = LOG_FILE_DEST if Path(LOG_FILE_DEST).exists() else LOG_FILE
        subprocess.Popen(["xdg-open", dest])

    # -- Threaded upgrade logic ---------------------------------------------

    def _preflight_and_run(self):
        # Internet check
        self._ui_step("Checking internet connection...")
        if not check_internet():
            self._ui_error(
                "No Internet Connection",
                f"{APP_NAME} cannot continue.\nPlease check your internet connection and try again.",
            )
            return

        log("Internet check passed.")
        self._append_log("Internet connection: OK\n")

        # Repository check
        self._ui_step("Checking Linux Lite repository...")
        if not check_repository():
            self._ui_error(
                "Repository Unreachable",
                "Unable to connect to the Linux Lite master repository.\nPlease try again later.",
            )
            return

        log("Repository check passed.")
        self._append_log("Repository connection: OK\n")

        # Build steps
        steps = build_steps()
        total = len(steps)

        for idx, (desc, func) in enumerate(steps):
            if self._cancelled:
                return
            fraction = idx / total
            self._ui_step(desc)
            self._ui_progress(fraction, f"{desc}...")
            self._append_log(f"[{idx+1}/{total}] {desc}\n")

            success = func()
            if not success:
                log(f"Step failed: {desc}")
                self._append_log(f"  -> FAILED\n")
                try:
                    shutil.move(LOG_FILE, LOG_FILE_DEST)
                except Exception:
                    pass
                self._ui_error("Upgrade Error", f"An error occurred during:\n{desc}\n\nCheck {LOG_FILE_DEST} for details.")
                return

            self._append_log(f"  -> OK\n")

        # Done
        self._ui_progress(1.0, "Completed.")
        log("Upgrade succeeded.")
        # Move log to /var/log
        try:
            shutil.move(LOG_FILE, LOG_FILE_DEST)
        except Exception:
            pass

        GLib.idle_add(self._show_done, True)

    # -- UI helpers (thread-safe) -------------------------------------------

    def _ui_step(self, text):
        GLib.idle_add(self.step_label.set_text, text)

    def _ui_progress(self, fraction, text):
        def _do():
            self.progress_bar.set_fraction(fraction)
            self.progress_bar.set_text(text)
        GLib.idle_add(_do)

    def _append_log(self, text):
        def _do():
            buf = self.log_view.get_buffer()
            buf.insert(buf.get_end_iter(), text, -1)
            # Auto-scroll
            mark = buf.get_insert()
            self.log_view.scroll_mark_onscreen(mark)
        GLib.idle_add(_do)

    def _ui_error(self, title, message):
        def _show():
            dlg = Gtk.AlertDialog()
            dlg.set_message(title)
            dlg.set_detail(message)
            dlg.set_buttons(["Close"])
            dlg.show(self)
            self.done_icon.set_from_icon_name("dialog-error-symbolic")
            self.done_icon.remove_css_class("success")
            self.done_icon.add_css_class("error")
            self.done_title.set_text("Upgrade Failed")
            self.done_subtitle.set_text("The upgrade could not be completed. Check the log for details.")
            self.stack.set_visible_child_name("done")
        GLib.idle_add(_show)

    def _show_done(self, success):
        if success:
            self.done_icon.set_from_icon_name("emblem-ok-symbolic")
            self.done_title.set_text("Upgrade Complete")
            self.done_subtitle.set_text(
                f"You are now running {UPGRADE_FULL}.\n"
                "Save and close any open applications before restarting."
            )
        self.stack.set_visible_child_name("done")


# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------

class LiteUpgradeApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id=APP_ID,
                         flags=Gio.ApplicationFlags.FLAGS_NONE)

    def _apply_css(self):
        css = b"""
        .dark-text { color: #1e1e1e; }
        .dim-label { opacity: 0.75; }
        """
        provider = Gtk.CssProvider()
        provider.load_from_data(css)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(), provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

    def do_activate(self):
        Gtk.Window.set_default_icon_name(ICON_NAME)
        self._apply_css()
        # Pre-flight: already on latest?
        ver = get_current_version()
        if f"Linux Lite {UPGRADE_VERSION}" in ver:
            log(f"Already running {UPGRADE_VERSION} – nothing to do.")
            self._show_already_latest()
            return

        # Check if repo actually has a newer version than what we're running
        current_num = get_version_number()
        repo_num = get_repo_version()
        if repo_num and current_num:
            if _parse_version(current_num) >= _parse_version(repo_num):
                log(f"Current {current_num} >= repo {repo_num} – no upgrade available.")
                self._show_already_latest()
                return

        win = UpgradeWindow(self)
        win.present()

    def _show_already_latest(self):
        # Minimal window with a message
        win = Gtk.ApplicationWindow(application=self, title=APP_NAME,
                                    default_width=380, default_height=220)
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        win.set_child(box)
        win.set_titlebar(Gtk.HeaderBar())

        inner = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
                        margin_top=24, margin_bottom=24, margin_start=24, margin_end=24,
                        valign=Gtk.Align.CENTER, vexpand=True)
        icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic")
        icon.set_pixel_size(48)
        icon.add_css_class("success")
        inner.append(icon)
        lbl = Gtk.Label(label="No Upgrade Needed")
        lbl.add_css_class("title-2")
        inner.append(lbl)
        sub = Gtk.Label(label="You are already running the latest release.", wrap=True)
        inner.append(sub)
        btn = Gtk.Button(label="Close")
        btn.set_halign(Gtk.Align.CENTER)
        btn.add_css_class("pill")
        btn.connect("clicked", lambda _: win.close())
        inner.append(btn)
        box.append(inner)
        win.present()


# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------

def main():
    # If not root, re-launch via pkexec
    if os.geteuid() != 0:
        try:
            os.execvp("pkexec", ["pkexec", "/usr/bin/lite-upgrade-series8"] + sys.argv[1:])
        except Exception as exc:
            sys.exit(1)

    # Now running as root – safe to create the log file
    _init_log()
    log(f"---------- {APP_NAME} ----------")
    log(f"Target version: {UPGRADE_VERSION}")

    app = LiteUpgradeApp()
    app.run(sys.argv)


if __name__ == "__main__":
    main()
