"""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

_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)
            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


def _tray_image():
    from PIL import Image, ImageDraw
    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 _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
        # 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

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