#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Single installer for CGHEVEN DaVinci Resolve add-on (Windows, macOS; optional Linux).

Run (from this folder):
  Windows:  double-click INSTALL.bat   (auto-installs Python if needed)
  macOS:    python3 cgheven_installer.py

Copies helpers into Blackmagic Fusion script paths the main addon expects.
"""
from __future__ import annotations

import os
import re
import shutil
import sys
import tempfile
import webbrowser
from pathlib import Path

# Windows consoles default to cp1252, which cannot encode the [OK]/[!]/ characters in our
# status messages - a plain print() then raises UnicodeEncodeError and aborts the whole
# (headless) install AFTER files were copied. Force UTF-8 with replacement so output is
# always safe, regardless of the console code page or file redirection.
for _stream in (sys.stdout, sys.stderr):
    try:
        _stream.reconfigure(encoding="utf-8", errors="replace")
    except Exception:
        pass

# All paths relative to this file (add-on root / zip root)
_ADDON_ROOT = Path(__file__).resolve().parent
_MAIN = "CGHEVEN_Asset_Library.py"
_HELPERS = ("cgheven_auth.py", "cgheven_auto_update.py", "cgheven_analytics_helper.py")
_MACOS_DIR = _ADDON_ROOT / "Macos"
# Default brand image (same asset as the main add-on) if assets/ has no file
_BRAND_ICON_URL = "https://api.cgheven.com/uploads/CGHEVEN_Large_e1744786866185_6df63e9aca.png"
_PATREON_LICENSE_URL = "https://www.patreon.com/CGHEVEN/join"

# ── Python dependencies ─────────────────────────────────────────────────────
# Pillow -> thumbnails + window icon (mandatory); certifi -> SSL on locked-down
# machines; imageio -> hover-video preview frames. We install these AT INSTALL
# TIME so the add-on works on a fresh device - but the add-on ALSO self-installs
# on first launch (using DaVinci's own Python), which is the guaranteed path.
# We install into ~/.cgheven_cache/pylibs, the SAME folder the add-on prepends
# to sys.path, so the install is importable regardless of user-site policy.
import subprocess

_CGH_CACHE_ROOT = Path.home() / ".cgheven_cache"
_CGH_LIBS_DIR = _CGH_CACHE_ROOT / "pylibs"
_PIP_DEPS = ("Pillow", "certifi", "imageio")   # Pillow first (thumbnails + icon)


def _dep_python_candidates() -> list[str]:
    """Interpreters to (check / install) deps with. Prefer DaVinci's OWN Python so the
    installed wheels match the ABI of the interpreter that imports them inside Resolve;
    fall back to the Python running this installer."""
    cands: list[str] = []
    if sys.platform == "win32":
        cands += [
            r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Fusion\python\python.exe",
            r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Fusion\python3\python.exe",
        ]
    elif sys.platform == "darwin":
        cands += [
            "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
            "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
            "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
            "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3",
        ]
    cands.append(sys.executable or "")
    seen, out = set(), []
    for p in cands:
        if not p or p in seen:
            continue
        seen.add(p)
        if p == sys.executable or Path(p).exists():
            out.append(p)
    return out


def _py_has_all_deps(py: str) -> bool:
    """Ask the TARGET interpreter (with our libs dir on its path) whether every dep
    actually IMPORTS - catches both absent and broken/mismatched wheels."""
    code = (
        "import sys; sys.path.insert(0, r'%s')\n"
        "mods = ['PIL', 'certifi', 'imageio']\n"
        "ok = True\n"
        "for m in mods:\n"
        "    try:\n"
        "        __import__(m)\n"
        "    except Exception:\n"
        "        ok = False\n"
        "sys.exit(0 if ok else 1)\n" % str(_CGH_LIBS_DIR)
    )
    try:
        r = subprocess.run([py, "-c", code], stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT, timeout=40)
        return r.returncode == 0
    except Exception:
        return False


def _pip_into_libs(py: str, on_progress=None) -> bool:
    """pip install each dep into ~/.cgheven_cache/pylibs with `py`. Each package is
    installed independently (priority order) so a slow/failing imageio doesn't block
    Pillow. Returns True only if ALL deps end up importable by `py`."""
    try:
        _CGH_LIBS_DIR.mkdir(parents=True, exist_ok=True)
    except Exception:
        pass
    base = [py, "-m", "pip", "install", "--disable-pip-version-check", "--no-input",
            "--upgrade", "--target", str(_CGH_LIBS_DIR)]
    bootstrapped = False
    for pkg in _PIP_DEPS:
        if on_progress:
            on_progress(f"Installing {pkg}...")
        done = False
        try:
            r = subprocess.run([*base, pkg], stdout=subprocess.PIPE,
                               stderr=subprocess.STDOUT, timeout=300)
            done = (r.returncode == 0)
        except Exception:
            done = False
        if not done and not bootstrapped:
            # pip may be missing in a stripped Python - bootstrap once, then retry.
            bootstrapped = True
            try:
                subprocess.run([py, "-m", "ensurepip", "--upgrade"],
                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180)
                r = subprocess.run([*base, pkg], stdout=subprocess.PIPE,
                                   stderr=subprocess.STDOUT, timeout=300)
            except Exception:
                pass
    return _py_has_all_deps(py)


def install_python_deps(on_progress=None) -> tuple[bool, str]:
    """Ensure Pillow/certifi/imageio are importable by DaVinci's Python. Best-effort:
    a failure here is NON-fatal because the add-on re-checks and self-installs on first
    launch with its own interpreter. Returns (ok, message) for the result dialog."""
    cands = _dep_python_candidates()
    # 1) Already satisfied for any candidate? Nothing to do.
    for py in cands:
        if _py_has_all_deps(py):
            return True, "[OK] Components already installed (thumbnails & previews ready)."
    # 2) Install with each candidate until one imports cleanly.
    if on_progress:
        on_progress("Checking components...")
    for py in cands:
        if _pip_into_libs(py, on_progress):
            return True, "[OK] Components installed (thumbnails & previews ready)."
    return False, ("[!] Couldn't finish installing image components now - the add-on will "
                   "install them automatically the first time you open it in DaVinci.")

# Match CGHEVEN_Asset_Library C{} theme
_UI = {
    "bg": "#0F0F15",
    "card": "#1A1A26",
    "text": "#FFFFFF",
    "muted": "#8A8A9A",
    "accent": "#277070",
    "accent_h": "#2F8F8F",
    "btn2": "#2C2C3E",
    "link": "#5EC8C8",
    "buy": "#A07020",  # patreon/CTA tone on dark
    "buy_h": "#B88228",
}


def _read_addon_version() -> str:
    try:
        p = _ADDON_ROOT / "CGHEVEN_Asset_Library.py"
        t = p.read_text(encoding="utf-8", errors="ignore")
        m = re.search(r"^ADDON_VER\s*=\s*[\"']([^\"']+)", t, re.M)
        if m:
            return m.group(1).strip()
    except Exception:
        pass
    return "1.0.0"


def _load_installer_logo(master, max_side: int = 72):
    """
    In-window and window icon. Prefer logo/CGHEVEN_logo.png, then assets/installer_icon.png
    or assets/logo.png; else download brand PNG from _BRAND_ICON_URL.
    Returns (PhotoImage|None) or holds ref on master.
    """
    import tkinter as tk

    for rel in ("logo/CGHEVEN_logo.png", "assets/installer_icon.png", "assets/logo.png"):
        p = _ADDON_ROOT / rel
        if p.is_file():
            try:
                from PIL import Image, ImageTk

                im = Image.open(p).convert("RGBA")
                w, h = im.size
                s = max_side / max(w, h)
                if s < 1:
                    im = im.resize(
                        (max(1, int(w * s)), max(1, int(h * s))),
                        Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS,
                    )
                ph = ImageTk.PhotoImage(im, master=master)
                master._cgheven_installer_logo = ph
                return ph
            except Exception:
                try:
                    ph = tk.PhotoImage(master=master, file=str(p))
                    master._cgheven_installer_logo = ph
                    return ph
                except Exception:
                    break
    tmp = Path(tempfile.gettempdir()) / "cgheven_installer_brand.png"
    try:
        import urllib.request

        req = urllib.request.Request(
            _BRAND_ICON_URL,
            headers={"User-Agent": "CGHEVEN-Installer/1.0 (DaVinci Resolve add-on)"},
        )
        with urllib.request.urlopen(req, timeout=10) as r:
            tmp.write_bytes(r.read())
        from PIL import Image, ImageTk

        im = Image.open(tmp).convert("RGBA")
        w, h = im.size
        s = max_side / max(w, h)
        if s < 1:
            im = im.resize(
                (max(1, int(w * s)), max(1, int(h * s))),
                Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS,
            )
        ph = ImageTk.PhotoImage(im, master=master)
        master._cgheven_installer_logo = ph
        return ph
    except Exception:
        if tmp.is_file():
            try:
                ph = tk.PhotoImage(master=master, file=str(tmp))
                master._cgheven_installer_logo = ph
                return ph
            except Exception:
                pass
        return None


def _apply_root_icon_from_photo(root, photo) -> None:
    if not photo:
        return
    try:
        if sys.platform == "win32":
            root.iconphoto(True, photo)
        else:
            root.iconphoto(True, photo)
    except Exception:
        pass


def _os_name() -> str:
    p = sys.platform
    if p == "win32":
        return "windows"
    if p == "darwin":
        return "macos"
    if p.startswith("linux"):
        return "linux"
    return p


def _check_files() -> str | None:
    missing = [f for f in (_MAIN, *_HELPERS) if not (_ADDON_ROOT / f).is_file()]
    if missing:
        return "Missing: " + ", ".join(missing) + f"\n(Folder: {_ADDON_ROOT})"
    if _os_name() == "macos" and not _MACOS_DIR.is_dir():
        return f"Missing folder: {_MACOS_DIR}"
    return None


def _copy_bundled_logo(dest_script_parent: Path) -> None:
    """Copy logo/ next to CGHEVEN_Asset_Library.py (taskbar + UI use bundled PNG)."""
    src = _ADDON_ROOT / "logo"
    if not src.is_dir():
        return
    dst = dest_script_parent / "logo"
    try:
        dst.mkdir(parents=True, exist_ok=True)
        for p in src.iterdir():
            if p.is_file():
                shutil.copy2(p, dst / p.name)
    except OSError:
        pass


# ── Windows (Resolve Fusion: Utility + Modules/cgheven) ─────────────
def _win_appdata_base() -> Path:
    return Path(os.environ.get("APPDATA", "")) / "Blackmagic Design" / "DaVinci Resolve" / "Support" / "Fusion" / "Scripts"


def _win_programdata_bases() -> list[Path]:
    pd = os.environ.get("ProgramData", r"C:\ProgramData")
    return [
        Path(pd) / "Blackmagic Design" / "DaVinci Resolve" / "Fusion" / "Scripts",
        Path(pd) / "Blackmagic Design" / "DaVinci Resolve" / "Support" / "Fusion" / "Scripts",
    ]


def _win_remove_old() -> None:
    """Remove legacy flat copies in Utility/Edit/Comp and Modules\\cgheven."""
    for base in (_win_appdata_base(),) + tuple(_win_programdata_bases()):
        if not base.is_dir():
            continue
        for sub in ("Utility", "Edit", "Comp"):
            for f in (_MAIN, *_HELPERS):
                p = base / sub / f
                if p.is_file():
                    try:
                        p.unlink()
                    except OSError:
                        pass
        mod = base / "Modules" / "cgheven"
        for f in _HELPERS:
            p = mod / f
            if p.is_file():
                try:
                    p.unlink()
                except OSError:
                    pass


def install_windows(on_progress=None) -> tuple[bool, str]:
    err = _check_files()
    if err:
        return False, err
    if on_progress: on_progress("Copying add-on files...")
    _win_remove_old()
    udir = _win_appdata_base() / "Utility"
    mdir = _win_appdata_base() / "Modules" / "cgheven"
    udir.mkdir(parents=True, exist_ok=True)
    mdir.mkdir(parents=True, exist_ok=True)
    try:
        shutil.copy2(_ADDON_ROOT / _MAIN, udir / _MAIN)
        _copy_bundled_logo(udir)
    except OSError as e:
        return False, f"Copy main failed: {e}\n(If you need all-users path, run as Admin.)"
    for h in _HELPERS:
        try:
            shutil.copy2(_ADDON_ROOT / h, mdir / h)
        except OSError as e:
            return False, f"Copy {h} failed: {e}"
    deps_ok, deps_msg = install_python_deps(on_progress=on_progress)
    msg = (
        "Installed.\n\n"
        f"Main:  {udir / _MAIN}\n"
        f"Modules: {mdir}\n\n"
        f"{deps_msg}\n\n"
        "Restart DaVinci Resolve. Workspace > Scripts > Utility."
    )
    return True, msg


def uninstall_windows() -> tuple[bool, str]:
    _win_remove_old()
    return True, "Removed copies from AppData/ProgramData (Utility, Edit, Comp, Modules\\cgheven). Restart Resolve."


# ── macOS (folder Utility/cgheven; Macos launcher) ──────────────────────
def _macos_dest() -> Path:
    return (
        Path.home()
        / "Library"
        / "Application Support"
        / "Blackmagic Design"
        / "DaVinci Resolve"
        / "Fusion"
        / "Scripts"
        / "Utility"
        / "cgheven"
    )


def install_macos(on_progress=None) -> tuple[bool, str]:
    err = _check_files()
    if err:
        return False, err
    if on_progress: on_progress("Copying add-on files...")
    dest = _macos_dest()
    mdest = dest / "Macos"
    dest.mkdir(parents=True, exist_ok=True)
    mdest.mkdir(parents=True, exist_ok=True)
    try:
        for f in (_MAIN, *_HELPERS):
            shutil.copy2(_ADDON_ROOT / f, dest / f)
        _copy_bundled_logo(dest)
        for p in _MACOS_DIR.glob("*.py"):
            if p.is_file():
                shutil.copy2(p, mdest / p.name)
    except OSError as e:
        return False, str(e)
    deps_ok, deps_msg = install_python_deps(on_progress=on_progress)
    launcher = mdest / "CGHEVEN_Asset_Library.py"
    msg = (
        "Installed.\n\n"
        f"{dest}\n\n"
        f"{deps_msg}\n\n"
        "In Resolve: Workspace > Scripts > add this file:\n"
        f"  {launcher}"
    )
    return True, msg


def uninstall_macos() -> tuple[bool, str]:
    d = _macos_dest()
    if d.is_dir():
        try:
            shutil.rmtree(d)
        except OSError as e:
            return False, str(e)
    return True, f"Removed: {d}"


# ── Linux (optional) ───────────────────────────────────────────────────
def _linux_dest() -> Path:
    return (
        Path.home() / ".local" / "share" / "DaVinciResolve" / "Fusion" / "Scripts" / "Utility" / "cgheven"
    )


def install_linux(on_progress=None) -> tuple[bool, str]:
    err = _check_files()
    if err and "Macos" in err and _os_name() == "linux":
        return False, "This Linux layout expects Macos/ in the add-on; copy from a full build."
    if err:
        return False, err
    if on_progress: on_progress("Copying add-on files...")
    dest = _linux_dest()
    m = dest / "Macos"
    dest.mkdir(parents=True, exist_ok=True)
    m.mkdir(parents=True, exist_ok=True)
    try:
        for f in (_MAIN, *_HELPERS):
            shutil.copy2(_ADDON_ROOT / f, dest / f)
        _copy_bundled_logo(dest)
        for p in _MACOS_DIR.glob("*.py"):
            if p.is_file():
                shutil.copy2(p, m / p.name)
    except OSError as e:
        return False, str(e)
    deps_ok, deps_msg = install_python_deps(on_progress=on_progress)
    return True, (f"Installed to:\n{dest}\n\n{deps_msg}\n\n"
                  f"Add in Resolve: {m / 'CGHEVEN_Asset_Library.py'}")


def uninstall_linux() -> tuple[bool, str]:
    d = _linux_dest()
    if d.is_dir():
        try:
            shutil.rmtree(d)
        except OSError as e:
            return False, str(e)
    return True, f"Removed: {d}"


def _other_system_help() -> str:
    return (
        "One add-on pack for all machines.\n\n"
        "- On this PC, use Install / Update in this window.\n"
        "- On another computer, copy the whole add-on folder, then run the same file:\n"
        "  Windows: double-click INSTALL.bat   |   macOS / Linux: python3 cgheven_installer.py\n\n"
        f"Folder to copy:\n  {_ADDON_ROOT}\n"
    )


def _run_tk_gui() -> None:
    import tkinter as tk
    from tkinter import messagebox

    root = tk.Tk()
    ver = _read_addon_version()
    here = _os_name()
    here_l = {"windows": "Windows", "macos": "macOS", "linux": "Linux"}.get(here, here.upper())

    u = _UI
    root.title(f"CGHEVEN Asset Library - v{ver}")
    root.minsize(580, 360)
    root.configure(bg=u["bg"])
    root.resizable(True, True)

    try:
        ph = _load_installer_logo(root, max_side=78)
        _apply_root_icon_from_photo(root, ph)
    except Exception:
        ph = None

    err0 = _check_files()
    uist = tk.NORMAL if not err0 else tk.DISABLED

    outer = tk.Frame(root, bg=u["bg"], padx=22, pady=20)
    outer.pack(fill="both", expand=True)

    if err0:
        bnr = tk.Frame(outer, bg="#2A1A1A", pady=10, padx=12)
        bnr.pack(fill="x", pady=(0, 10))

    def _f(size: int, weight: str = "normal") -> tuple[str, int, str]:
        return ("Segoe UI", size, weight) if sys.platform == "win32" else ("Helvetica Neue", size, weight)

    if err0:
        tk.Label(
            bnr,
            text=err0,
            font=_f(9, "normal"),
            bg="#2A1A1A",
            fg="#FF8888",
            wraplength=500,
            justify="left",
        ).pack(anchor="w")

    main = tk.Frame(outer, bg=u["bg"])
    main.pack(fill="both", expand=True)

    left = tk.Frame(main, bg=u["bg"])
    left.pack(fill="both", expand=True)

    head = tk.Frame(left, bg=u["bg"])
    head.pack(anchor="w", fill="x")

    if ph:
        tk.Label(head, image=ph, bg=u["bg"]).pack(side="left", padx=(0, 16))

    titles = tk.Frame(head, bg=u["bg"])
    titles.pack(side="left", fill="y", anchor="n")
    tk.Label(
        titles,
        text="CGHEVEN Asset Library",
        font=_f(18, "bold"),
        bg=u["bg"],
        fg=u["text"],
    ).pack(anchor="w")
    tk.Label(
        titles,
        text=f"Version {ver}  ·  {here_l}  ·  auto-detect",
        font=_f(10, "normal"),
        bg=u["bg"],
        fg=u["muted"],
    ).pack(anchor="w", pady=(4, 0))

    body = tk.Frame(left, bg=u["bg"])
    body.pack(anchor="w", fill="x", pady=(18, 0))

    blurb = (
        "Connects to your CGHEVEN account inside DaVinci Resolve - browse, preview, and "
        "download VFX/3D assets. This step copies files into Blackmagic Fusion script folders."
    )
    tk.Label(
        body,
        text=blurb,
        font=_f(10, "normal"),
        bg=u["bg"],
        fg=u["text"],
        wraplength=480,
        justify="left",
    ).pack(anchor="w")

    links = tk.Frame(left, bg=u["bg"])
    links.pack(anchor="w", pady=(20, 0))

    def _link(txt: str, url: str) -> None:
        l = tk.Label(links, text=txt, font=_f(10, "normal"), bg=u["bg"], fg=u["link"], cursor="hand2")
        l.pack(side="left", padx=(0, 18))
        l.bind("<Button-1>", lambda e, u=url: webbrowser.open(u))
        l.bind(
            "<Enter>", lambda e, w=l: w.config(fg=u["accent_h"])
        )
        l.bind("<Leave>", lambda e, w=l: w.config(fg=u["link"]))

    _link("cgheven.com", "https://cgheven.com")
    _link("Contact us", "https://cgheven.com/contact")

    def help_other() -> None:
        messagebox.showinfo("Use on another computer", _other_system_help(), parent=root)

    help_row = tk.Frame(left, bg=u["bg"])
    help_row.pack(anchor="w", pady=(10, 0))
    help_l = tk.Label(
        help_row,
        text="Install on another computer...",
        font=_f(9, "normal"),
        bg=u["bg"],
        fg=u["muted"],
        cursor="hand2",
    )
    help_l.pack(anchor="w")
    help_l.bind("<Button-1>", lambda e: help_other())
    help_l.bind("<Enter>", lambda e: help_l.config(fg=u["text"]))
    help_l.bind("<Leave>", lambda e: help_l.config(fg=u["muted"]))

    # Bottom bar: full width, space-between style - Quit -> Buy -> Uninstall | wide Install
    foot = tk.Frame(outer, bg=u["bg"])
    foot.pack(side="bottom", fill="x", pady=(18, 0))
    line = tk.Frame(foot, bg="#2C2C3E", height=1)
    line.pack(fill="x", pady=(0, 10))

    btnbar = tk.Frame(foot, bg=u["bg"])
    btnbar.pack(fill="x", expand=True)
    # Distribute: first 3 columns equal, last (Install) gets extra stretch (wider CTA on the right)
    for c in (0, 1, 2):
        btnbar.grid_columnconfigure(c, weight=1, minsize=84)
    btnbar.grid_columnconfigure(3, weight=3, minsize=200)

    # Progress line under the buttons - shows "Installing Pillow..." etc. so the
    # window doesn't look frozen during the 10-60 s pip can take.
    progress_lbl = tk.Label(
        outer, text="", bg=u["bg"], fg=u["muted"],
        font=_f(9, "normal"), anchor="w", justify="left", wraplength=520,
    )
    progress_lbl.pack(side="bottom", fill="x", pady=(8, 0))

    def _set_progress(text: str) -> None:
        try:
            progress_lbl.configure(text=text)
            progress_lbl.update_idletasks()
        except Exception:
            pass

    def show_result(ok: bool, msg: str) -> None:
        (messagebox.showinfo if ok else messagebox.showerror)("CGHEVEN", msg, parent=root)

    def run_install() -> None:
        # Disable the buttons so the user can't double-launch the install while
        # pip is running, and surface progress in the label below.
        try:
            bt1.configure(state="disabled", text="Installing...")
            bt2.configure(state="disabled")
            root.update_idletasks()
        except Exception:
            pass
        try:
            if here == "windows":
                ok, msg = install_windows(on_progress=_set_progress)
            elif here == "macos":
                ok, msg = install_macos(on_progress=_set_progress)
            elif here == "linux":
                ok, msg = install_linux(on_progress=_set_progress)
            else:
                ok, msg = False, f"Unknown OS: {here}"
        finally:
            try:
                st = tk.NORMAL if not _check_files() else tk.DISABLED
                bt1.configure(state=st, text="Install / Update")
                bt2.configure(state=st)
                _set_progress("")
            except Exception:
                pass
        show_result(ok, msg)

    def run_uninstall() -> None:
        if not messagebox.askyesno(
            "Uninstall",
            "Remove CGHEVEN files from this computer's DaVinci Resolve script folders?",
            parent=root,
        ):
            return
        if here == "windows":
            ok, msg = uninstall_windows()
        elif here == "macos":
            ok, msg = uninstall_macos()
        elif here == "linux":
            ok, msg = uninstall_linux()
        else:
            ok, msg = False, f"Unknown OS: {here}"
        show_result(ok, msg)

    def open_license() -> None:
        webbrowser.open(_PATREON_LICENSE_URL)

    bpad = 6
    bpy = 6
    f_primary = _f(9, "bold")
    f_sec = _f(9, "normal")
    w_small = 10
    w_install = 22  # character width - right column also gets grid weight (extra stretch)
    gpad = {"padx": 4, "pady": 2, "sticky": "ew"}

    bt_quit = tk.Button(
        btnbar,
        text="Quit",
        font=f_sec,
        bg=u["bg"],
        fg=u["muted"],
        activebackground=u["card"],
        activeforeground="white",
        highlightthickness=1,
        highlightbackground="#2C2C3E",
        highlightcolor="#2C2C3E",
        relief="flat",
        cursor="hand2",
        padx=bpad,
        pady=bpy,
        width=w_small,
        command=root.destroy,
    )
    bt_quit.grid(row=0, column=0, **gpad)

    bt_buy = tk.Button(
        btnbar,
        text="Buy License",
        font=f_sec,
        bg=u["buy"],
        fg="white",
        activebackground=u["buy_h"],
        activeforeground="white",
        relief="flat",
        cursor="hand2",
        padx=bpad,
        pady=bpy,
        width=12,
        command=open_license,
    )
    bt_buy.grid(row=0, column=1, **gpad)

    bt2 = tk.Button(
        btnbar,
        text="Uninstall",
        font=f_sec,
        bg=u["btn2"],
        fg=u["text"],
        activebackground="#3A3A50",
        activeforeground="white",
        disabledforeground="#6A6A7A",
        relief="flat",
        cursor="hand2",
        padx=bpad,
        pady=bpy,
        width=w_small,
        state=uist,
        command=run_uninstall,
    )
    bt2.grid(row=0, column=2, **gpad)

    bt1 = tk.Button(
        btnbar,
        text="Install / Update",
        font=f_primary,
        bg=u["accent"],
        fg="white",
        activebackground=u["accent_h"],
        activeforeground="white",
        disabledforeground="#5A5A6A",
        relief="flat",
        cursor="hand2",
        padx=14,
        pady=8,
        width=w_install,
        state=uist,
        command=run_install,
    )
    bt1.grid(row=0, column=3, padx=4, pady=2, sticky="ew")

    # center window
    try:
        root.update_idletasks()
        rw = root.winfo_width()
        rh = root.winfo_height()
        sw = root.winfo_screenwidth()
        sh = root.winfo_screenheight()
        root.geometry(f"+{(sw - rw) // 2}+{(sh - rh) // 2}")
    except Exception:
        pass

    root.mainloop()


def _run_cli() -> int:
    do_un = any(a in ("-u", "--uninstall", "uninstall") for a in sys.argv[1:])
    print("CGHEVEN -", "uninstall" if do_un else "install", f"on {_os_name()}")
    if not do_un:
        e = _check_files()
        if e:
            print("Error:", e, file=sys.stderr)
            return 1
    if do_un:
        if _os_name() == "windows":
            ok, msg = uninstall_windows()
        elif _os_name() == "macos":
            ok, msg = uninstall_macos()
        elif _os_name() == "linux":
            ok, msg = uninstall_linux()
        else:
            ok, msg = False, "Unknown OS"
    else:
        if _os_name() == "windows":
            ok, msg = install_windows()
        elif _os_name() == "macos":
            ok, msg = install_macos()
        elif _os_name() == "linux":
            ok, msg = install_linux()
        else:
            ok, msg = False, "Unknown OS"
    print(msg)
    return 0 if ok else 1


def main() -> int:
    head = os.environ.get("CGHEVEN_INSTALL_HEADLESS", "").strip().lower() in ("1", "true", "yes")
    if head or any(x in sys.argv for x in ("--headless", "--no-gui", "-u", "--uninstall")):
        return _run_cli()
    try:
        _run_tk_gui()
        return 0
    except Exception:
        return _run_cli()


if __name__ == "__main__":
    raise SystemExit(main())
