"""
Lance Paychek Safeguard en mode bureau :
serveur local FastAPI + fenetre app ~800x600 (Edge/Chrome),
sinon navigateur par defaut.
(Pas de pywebview / pythonnet.)
"""

from __future__ import annotations

import os
import shutil
import socket
import subprocess
import sys
import threading
import time
import traceback
import webbrowser
from pathlib import Path

# Taille cible de la fenetre app (Chromium --window-size).
_APP_WINDOW_W = 800
_APP_WINDOW_H = 600

# PyInstaller : racine src/ dans le bundle
_SRC = Path(__file__).resolve().parent
if str(_SRC) not in sys.path:
    sys.path.insert(0, str(_SRC))

_server_error: BaseException | None = None


def _ensure_stdio() -> None:
    """PyInstaller windowed (.exe) : sys.stdout/stderr sont None → uvicorn crash."""
    if sys.stdout is not None and sys.stderr is not None:
        return
    try:
        from runtime.paths import logs_dir

        log_file = (logs_dir() / "desktop.log").open("a", encoding="utf-8")
    except Exception:
        log_file = open(os.devnull, "w", encoding="utf-8")
    if sys.stdout is None:
        sys.stdout = log_file
    if sys.stderr is None:
        sys.stderr = log_file


def _log(message: str) -> None:
    try:
        from runtime.paths import logs_dir

        log_path = logs_dir() / "desktop.log"
        stamp = time.strftime("%Y-%m-%d %H:%M:%S")
        with log_path.open("a", encoding="utf-8") as fh:
            fh.write(f"[{stamp}] {message}\n")
    except Exception:
        pass


def _seed_config() -> None:
    from runtime.paths import app_src_dir, bundle_dir, ensure_user_env, project_root

    seed = project_root() / ".env.example"
    if not seed.is_file():
        bundled = bundle_dir()
        if bundled is not None:
            candidate = bundled / ".env.example"
            if candidate.is_file():
                seed = candidate
        if not seed.is_file():
            seed = app_src_dir().parent / ".env.example"
    ensure_user_env(seed_from=seed if seed.is_file() else None)


def _port_in_use(host: str, port: int) -> bool:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.settimeout(0.5)
        return sock.connect_ex((host, port)) == 0


def _wait_for_server(url: str, timeout_s: float = 60.0) -> bool:
    import urllib.error
    import urllib.request

    deadline = time.time() + timeout_s
    while time.time() < deadline:
        if _server_error is not None:
            return False
        try:
            with urllib.request.urlopen(url, timeout=1.0) as resp:
                if resp.status == 200:
                    return True
        except (urllib.error.URLError, TimeoutError, OSError):
            time.sleep(0.2)
    return False


def _run_uvicorn(host: str, port: int) -> None:
    global _server_error
    try:
        import uvicorn

        from main import app

        _log(f"uvicorn start {host}:{port}")
        uvicorn.run(
            app,
            host=host,
            port=port,
            log_level="info",
            access_log=False,
            log_config=None,
        )
    except BaseException as exc:
        _server_error = exc
        _log(f"uvicorn failed: {exc}\n{traceback.format_exc()}")


def _show_windows_message(title: str, message: str) -> None:
    if sys.platform != "win32":
        return
    try:
        import ctypes

        ctypes.windll.user32.MessageBoxW(0, message, title, 0x00000040)
    except Exception:
        pass


def _chromium_exe_candidates() -> list[Path]:
    """Edge d'abord, puis Chrome (chemins Windows usuels + PATH)."""
    ordered: list[Path] = []
    seen: set[str] = set()

    def _add(path: Path | str | None) -> None:
        if not path:
            return
        p = Path(path)
        key = str(p).casefold()
        if key in seen:
            return
        seen.add(key)
        ordered.append(p)

    for name in ("msedge", "chrome"):
        _add(shutil.which(name))

    program_dirs = [
        os.environ.get("PROGRAMFILES", r"C:\Program Files"),
        os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)"),
    ]
    local = os.environ.get("LOCALAPPDATA", "")

    for base in program_dirs:
        if not base:
            continue
        _add(Path(base) / "Microsoft" / "Edge" / "Application" / "msedge.exe")
    for base in program_dirs:
        if not base:
            continue
        _add(Path(base) / "Google" / "Chrome" / "Application" / "chrome.exe")
    if local:
        _add(Path(local) / "Google" / "Chrome" / "Application" / "chrome.exe")
        _add(Path(local) / "Microsoft" / "Edge" / "Application" / "msedge.exe")

    # Reordonne : tous les msedge avant chrome, en gardant l'ordre relatif.
    edges = [p for p in ordered if "msedge" in p.name.casefold()]
    chromes = [p for p in ordered if "chrome" in p.name.casefold()]
    others = [p for p in ordered if p not in edges and p not in chromes]
    return [p for p in (edges + chromes + others) if p.is_file()]


def _chromium_user_data_dir() -> Path:
    """Profil Chromium dedie — evite que Edge/Chrome rende la main a une instance
    deja ouverte (Popen qui exit tout de suite → serveur tue trop tot)."""
    try:
        from runtime.paths import logs_dir

        base = logs_dir().parent
    except Exception:
        base = Path(os.environ.get("LOCALAPPDATA", ".")) / "PaychekCompanion"
    path = base / "chromium-app-profile"
    path.mkdir(parents=True, exist_ok=True)
    return path


def _open_app_window(url: str) -> subprocess.Popen | None:
    """
    Ouvre une fenetre dediee Chromium (~800x600) via --app=...
    Retourne le process navigateur (pour arreter le serveur a la fermeture).
    """
    if sys.platform != "win32":
        return None

    profile = _chromium_user_data_dir()
    args_tail = [
        f"--user-data-dir={profile}",
        f"--app={url}",
        f"--window-size={_APP_WINDOW_W},{_APP_WINDOW_H}",
        "--new-window",
        "--no-first-run",
        "--no-default-browser-check",
    ]
    for exe in _chromium_exe_candidates():
        try:
            proc = subprocess.Popen(
                [str(exe), *args_tail],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                close_fds=True,
            )
            _log(
                f"app window via {exe} size={_APP_WINDOW_W}x{_APP_WINDOW_H} "
                f"pid={proc.pid} profile={profile}"
            )
            return proc
        except OSError as exc:
            _log(f"app window failed ({exe}): {exc}")
    return None


def _open_browser(url: str) -> subprocess.Popen | None:
    """
    Prefere Edge/Chrome en mode app 800x600 ; sinon navigateur par defaut.
    Retourne le process Chromium si possible.
    """
    proc = _open_app_window(url)
    if proc is not None:
        # Profil deja pris / hand-off → process peut mourir tout de suite.
        time.sleep(0.6)
        if proc.poll() is None:
            return proc
        _log(f"app window process exited early (code={proc.returncode}) — fallback")

    _log("fallback: open URL via shell / default browser")
    if sys.platform == "win32":
        try:
            os.startfile(url)  # type: ignore[attr-defined]
            _log(f"os.startfile({url})")
            return None
        except OSError as exc:
            _log(f"os.startfile failed: {exc}")

    opened = webbrowser.open(url)
    _log(f"webbrowser.open({url}) -> {opened}")
    if not opened:
        _show_windows_message(
            "Paychek Safeguard",
            f"Ouvre manuellement ton navigateur sur :\n{url}\n\n"
            f"Pour une fenetre fixe {_APP_WINDOW_W}x{_APP_WINDOW_H}, "
            "installe Microsoft Edge ou Google Chrome.",
        )
    return None


def _pid_file_path() -> Path | None:
    """runtime/safeguard.pid a cote du kit portable (parent de src/)."""
    try:
        from runtime.paths import project_root

        runtime = project_root() / "runtime"
        if runtime.is_dir():
            return runtime / "safeguard.pid"
    except Exception:
        pass
    return None


def _write_pid_file() -> None:
    path = _pid_file_path()
    if path is None:
        return
    try:
        path.write_text(str(os.getpid()), encoding="utf-8")
        _log(f"pid file {path} -> {os.getpid()}")
    except Exception as exc:
        _log(f"pid file write failed: {exc}")


def _clear_pid_file() -> None:
    path = _pid_file_path()
    if path is None or not path.is_file():
        return
    try:
        path.unlink()
    except Exception:
        pass


def main() -> None:
    os.environ.setdefault("PAYCHEK_DESKTOP", "1")
    _ensure_stdio()
    _seed_config()
    _write_pid_file()

    try:
        from config import settings
        from runtime.paths import is_frozen, logs_dir

        host = "127.0.0.1"
        port = int(settings.port)
        url = f"http://{host}:{port}/"
        health = f"{url}health"

        _log(f"launch frozen={is_frozen()} url={url} mode=app-window")

        reused = False
        server: threading.Thread | None = None
        if _port_in_use(host, port):
            if _wait_for_server(health, timeout_s=3.0):
                _log("reusing existing server on port")
                reused = True
            else:
                msg = (
                    f"Le port {port} est deja utilise, mais Paychek ne repond pas.\n"
                    "Close any leftover Safeguard, or run _technique\\STOP.bat, then retry."
                )
                _log(msg)
                raise RuntimeError(msg)
        else:
            # Daemon: when the UI window closes, main exits and the process stops.
            server = threading.Thread(
                target=_run_uvicorn, args=(host, port), daemon=True, name="uvicorn"
            )
            server.start()

            timeout = 90.0 if is_frozen() else 30.0
            if not _wait_for_server(health, timeout_s=timeout):
                detail = str(_server_error) if _server_error else "timeout"
                msg = (
                    f"Paychek Safeguard n'a pas demarre sur {url}\n"
                    f"Detail: {detail}\n"
                    f"Log: {logs_dir() / 'desktop.log'}"
                )
                _log(msg)
                raise RuntimeError(msg)

        browser_proc = _open_browser(url)

        if reused:
            # Instance deja en cours : on a juste rouvert le navigateur.
            _log("reopen done — leaving existing server process running")
            return

        # Le serveur doit rester vivant pour l'addon NinjaTrader même si
        # l'utilisateur ferme la fenêtre UI. Arrêt explicite : STOP.bat.
        if browser_proc is not None:
            _log(
                f"UI opened (browser pid={browser_proc.pid}) — "
                "server stays up until STOP.bat (UI close does not kill server)"
            )
        else:
            _log("server running — close via _technique\\STOP.bat when done")

        assert server is not None
        while server.is_alive():
            server.join(timeout=1.0)
            if _server_error is not None:
                raise RuntimeError(f"Serveur arrete: {_server_error}")

    except Exception as exc:
        _log(f"main failed: {exc}\n{traceback.format_exc()}")
        _show_windows_message(
            "Paychek Safeguard - Error",
            f"{exc}\n\nSee logs in %LOCALAPPDATA%\\PaychekCompanion\\logs\\desktop.log",
        )
        raise
    finally:
        # Ne pas effacer le pid du serveur existant quand on ne fait que rouvrir l'UI.
        if not locals().get("reused"):
            _clear_pid_file()


if __name__ == "__main__":
    main()
