"""Desktop shell — an app window and a tray icon instead of a console.

The server runs in a daemon thread inside this process; the window is a
plain webview onto 127.0.0.1, so there is exactly ONE UI codebase and the
render engine keeps working the way it always has.

Why a tray icon rather than "just a window": this app must keep ingesting
while you play. Closing the window HIDES it — the session keeps recording
marks and chapters — and quitting is a deliberate act from the tray menu.
That is the whole reason the old console-window shape was dangerous.

Everything degrades: no GUI toolkit (headless box, Linux without WebKit)
falls back to opening the default browser, and the app is fully usable
that way — which also keeps the LAN-access story alive.
"""
import os
import socket
import sys
import threading
import time
import webbrowser
from pathlib import Path

_MODE = {"desktop": False}
_window = None
_tray = None
_shutdown = threading.Event()


def is_desktop() -> bool:
    return _MODE["desktop"]


# ---------------------------------------------------------------- single run
def acquire_single_instance(port: int) -> bool:
    """True if we are the only instance. Two copies would fight over the
    port, the DB and the hotkeys — the second should just surface the first."""
    s = socket.socket()
    try:
        s.bind(("127.0.0.1", port))
        s.close()
        return True
    except OSError:
        return False


def _wait_for_server(base: str, timeout: float = 40.0) -> bool:
    import urllib.request
    deadline = time.time() + timeout
    while time.time() < deadline and not _shutdown.is_set():
        try:
            urllib.request.urlopen(base + "/api/health/ping", timeout=2, context=net.context())
            return True
        except Exception:
            time.sleep(0.25)
    return False


def serve_in_thread(port: int, host: str = "127.0.0.1"):
    import uvicorn
    cfg = uvicorn.Config("clipshelf.app:app", host=host, port=port,
                         log_level="warning")
    server = uvicorn.Server(cfg)
    t = threading.Thread(target=server.run, daemon=True)
    t.start()
    return server


# ---------------------------------------------------------------- native bits
def pick_folder():
    """Native folder dialog. None when there's no window (browser mode) —
    the caller falls back to the in-app folder browser."""
    if not _window:
        return None
    try:
        import webview
        res = _window.create_file_dialog(webview.FOLDER_DIALOG)
        if not res:
            return ""
        return res[0] if isinstance(res, (list, tuple)) else str(res)
    except Exception:
        return None


def save_file_as(src, suggested: str = ""):
    """Native save dialog + copy. The window's WebView refuses downloads
    started by the page (an <a download> is inert inside the app), so the
    app has to do the saving itself. Returns the written path, "" if the
    dialog was cancelled, or None in browser mode."""
    import shutil
    from pathlib import Path
    if not _window:
        return None
    try:
        import webview
        res = _window.create_file_dialog(
            webview.SAVE_DIALOG, save_filename=suggested or Path(src).name)
        if not res:
            return ""
        dest = res[0] if isinstance(res, (list, tuple)) else str(res)
        shutil.copy2(src, dest)
        return dest
    except Exception as e:
        print(f"[desktop] save failed ({type(e).__name__}: {e})", flush=True)
        return None


def reveal(path) -> bool:
    """Open the file manager at a file. The second half of "download" on a
    desktop app: knowing where it went."""
    import subprocess
    from pathlib import Path
    p = Path(path)
    try:
        if os.name == "nt":
            subprocess.Popen(["explorer", "/select,", str(p)])
        elif sys.platform == "darwin":
            subprocess.Popen(["open", "-R", str(p)])
        else:
            subprocess.Popen(["xdg-open", str(p.parent)])
        return True
    except Exception:
        return False


ICON_ICO = Path(__file__).parent / "frontend" / "img" / "clipshelf.ico"
ICON_PNG = Path(__file__).parent / "frontend" / "img" / "clipshelf.png"


def _tray_image():
    """The real icon when it shipped; the drawn one if something ate it, so
    a missing file costs a nice icon rather than the whole tray."""
    from PIL import Image, ImageDraw
    try:
        return Image.open(ICON_PNG).convert("RGBA")
    except Exception:
        pass
    img = Image.new("RGBA", (64, 64), (8, 8, 13, 255))
    d = ImageDraw.Draw(img)
    d.ellipse((10, 10, 54, 54), outline=(255, 177, 92, 255), width=5)
    d.line((22, 42, 42, 22), fill=(255, 177, 92, 255), width=5)
    return img


def _claim_taskbar_identity():
    """Windows decides a taskbar button's icon and grouping from the process
    AppUserModelID, and ours defaulted to the interpreter's — which is why
    the button looked like IDLE. Claiming our own ID must happen before any
    window exists."""
    if os.name != "nt":
        return
    try:
        import ctypes
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("Clipshelf.App")
    except Exception as e:
        print(f"[desktop] could not set the taskbar identity ({type(e).__name__})", flush=True)


def _apply_window_icon(title: str):
    """Hang the real icon on the window itself.

    The AppUserModelID fixes grouping, but the button still draws whatever
    icon the window carries, and a pywebview window carries none. Found by
    title rather than through the backend's native handle, which differs
    between WebView2, GTK and Qt."""
    if os.name != "nt" or not ICON_ICO.exists():
        return
    try:
        import ctypes
        u32 = ctypes.windll.user32
        hwnd = u32.FindWindowW(None, title)
        if not hwnd:
            return
        IMAGE_ICON, LR_LOADFROMFILE, LR_DEFAULTSIZE = 1, 0x10, 0x40
        WM_SETICON, ICON_SMALL, ICON_BIG = 0x0080, 0, 1
        for which, cx in ((ICON_SMALL, 16), (ICON_BIG, 32)):
            h = u32.LoadImageW(None, str(ICON_ICO), IMAGE_ICON, cx, cx,
                               LR_LOADFROMFILE | LR_DEFAULTSIZE)
            if h:
                u32.SendMessageW(hwnd, WM_SETICON, which, h)
    except Exception as e:
        print(f"[desktop] could not set the window icon ({type(e).__name__})", flush=True)


def _start_tray(base: str):
    """Tray icon: show the window, open in browser, quit. Best effort —
    a machine without a system tray still runs fine."""
    global _tray
    try:
        import pystray
    except Exception as e:
        print(f"[desktop] no tray ({type(e).__name__}) — window only", flush=True)
        return

    def show(_i=None, _it=None):
        try:
            if _window:
                _window.show()
        except Exception:
            pass

    def open_browser(_i=None, _it=None):
        webbrowser.open(base)

    def quit_app(icon=None, _it=None):
        _shutdown.set()
        try:
            if icon:
                icon.stop()
        except Exception:
            pass
        try:
            if _window:
                _window.destroy()
        except Exception:
            pass

    menu = pystray.Menu(
        pystray.MenuItem("Open", show, default=True),
        pystray.MenuItem("Open in browser", open_browser),
        pystray.MenuItem("Quit", quit_app))
    try:
        _tray = pystray.Icon("clipshelf", _tray_image(), "Clipshelf", menu)
        threading.Thread(target=_tray.run, daemon=True).start()
    except Exception as e:
        print(f"[desktop] tray unavailable ({type(e).__name__})", flush=True)


def _stop_tray():
    try:
        if _tray:
            _tray.stop()
    except Exception:
        pass


def relaunch(delay: float = 0.8):
    """Start a detached copy of the launcher, then quit this one. The new
    process applies any staged update before importing the app."""
    import subprocess
    from clipshelf.config import ROOT
    cmd = [sys.executable, str(ROOT / "run.py")] + [a for a in sys.argv[1:] if a != "--selftest"]
    kw = {"cwd": str(ROOT), "close_fds": True}
    if os.name == "nt":
        kw["creationflags"] = 0x00000008 | 0x08000000   # DETACHED | NO_WINDOW
    else:
        kw["start_new_session"] = True
    time.sleep(delay)                       # let the HTTP response go out first
    try:
        subprocess.Popen(cmd, **kw)
    except Exception as e:
        print(f"[desktop] could not relaunch: {e}", flush=True)
        return
    shutdown()


def shutdown():
    """Quit the app the way the tray Quit does."""
    _shutdown.set()
    _stop_tray()
    try:
        if _window:
            _window.destroy()
    except Exception:
        pass
    threading.Timer(1.5, lambda: os._exit(0)).start()


# ---------------------------------------------------------------- entry
def run(port: int, title: str = "Clipshelf") -> bool:
    """Open the app window. Returns False when no GUI backend exists, so
    the launcher can fall back to the browser."""
    global _window
    try:
        import webview
        _claim_taskbar_identity()      # before any window exists, or it does nothing
        # webview.initialize() picks and loads a platform backend (WebView2
        # on Windows, GTK/Qt elsewhere) and raises if none is usable.
        # NOTE: webview.guilib is a VARIABLE that is None until this runs —
        # calling it as a module is how this probe was wrong before.
        webview.initialize()
    except Exception as e:
        print(f"[desktop] no window toolkit ({type(e).__name__}: {e}) — using your browser",
              flush=True)
        return False

    base = f"http://127.0.0.1:{port}"
    if not _wait_for_server(base):
        print("[desktop] server didn't start", flush=True)
        return False

    _MODE["desktop"] = True
    _window = webview.create_window(title, base, width=1400, height=900,
                                    min_size=(900, 600), confirm_close=False)

    def on_closing():
        """Hide instead of exit — ingestion must survive a stray window close.
        Quit lives in the tray menu (and is unconditional if there's no tray)."""
        if _tray is None or _shutdown.is_set():
            return True
        try:
            _window.hide()
        except Exception:
            return True
        return False

    try:
        _window.events.closing += on_closing
    except Exception:
        pass
    try:
        _window.events.shown += lambda: _apply_window_icon(title)
    except Exception:
        pass

    _start_tray(base)
    try:
        webview.start()
    finally:
        _shutdown.set()
        _stop_tray()
    return True
