"""VoxCast Server entrypoint.

Puts the repo's src/ on sys.path (the transcription engine + model manager are
shared with the desktop client, unmodified), resolves the data directory, then
runs uvicorn. Works in three shapes:
  - dev:     python compute_server/server_main.py
  - Docker:  PYTHONPATH=/opt/voxcast/src, this file as CMD
  - Windows: a Nuitka console exe wrapping this entrypoint
"""
from __future__ import annotations

import os
import sys
from pathlib import Path


def _bootstrap_paths() -> None:
    here = Path(__file__).resolve().parent
    repo_src = here.parent / "src"
    # compute_server/ MUST rank ahead of src/ on sys.path: both have an `app`
    # (compute_server/app package vs src/app.py) and a `version` module, so the
    # server's must win. Python already puts the script dir (here) at sys.path[0]
    # when run directly; keep it first and only APPEND src (its `paths`/`services`
    # modules are unique names, so lower priority is fine).
    if str(here) in sys.path:
        sys.path.remove(str(here))
    sys.path.insert(0, str(here))
    if repo_src.is_dir() and str(repo_src) not in sys.path:
        sys.path.append(str(repo_src))


def _default_data_dir() -> str:
    if os.name == "nt":
        base = os.environ.get("PROGRAMDATA", r"C:\ProgramData")
        return str(Path(base) / "VoxCast Server")
    return "/var/lib/voxcast"


def _init_config(argv: list[str]) -> int:
    """`--init-config [--host H] [--port P]`: create server.toml with a fresh
    API key, then exit. Called by the Windows installer (admin) so the config
    exists before the first start; a no-op when the file is already there."""
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--init-config", action="store_true")
    parser.add_argument("--host", default="0.0.0.0")
    parser.add_argument("--port", type=int, default=8590)
    parser.add_argument("--api-key", default="")
    parser.add_argument("--model", default="auto")
    args = parser.parse_args(argv)

    from app.auth import init_config_file
    from app.config import config_file_path

    port = args.port if 1 <= args.port <= 65535 else 8590
    path = config_file_path()
    created = init_config_file(path, args.host, port,
                               api_key=args.api_key, model=args.model)
    print(f"server.toml {'created' if created else 'already exists'}: {path}")
    return 0


def _set_model(argv: list[str]) -> int:
    """`--set-model <tier>`: change the speech model in an existing
    server.toml and exit. The new tier is downloaded automatically on the
    next start; restart the service to apply."""
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument("--set-model", dest="model", required=True)
    args = parser.parse_args(argv)

    from app.auth import set_config_model
    from app.config import config_file_path

    path = config_file_path()
    try:
        applied = set_config_model(path, args.model)
    except FileNotFoundError:
        print(f"No server.toml at {path}. Install or start the server first.")
        return 1
    except ValueError as e:
        print(str(e))
        return 2
    print(f"Model set to '{applied}' in {path}.")
    print("Restart the VoxCast Server service to apply "
          "(the model downloads automatically on the next start).")
    return 0


def _resolve_cert_paths(config, data_dir: str) -> tuple[Path, Path]:
    """Where the TLS cert/key live: explicit config paths, else in data_dir."""
    cert = Path(config.tls_cert) if config.tls_cert \
        else Path(data_dir) / "cert.pem"
    key = Path(config.tls_key) if config.tls_key \
        else Path(data_dir) / "key.pem"
    return cert, key


def _gen_cert(argv: list[str]) -> int:
    """`--gen-cert`: (re)generate the self-signed TLS certificate and exit,
    printing its SHA-256 fingerprint. Cross-platform (Windows exe + Unix)."""
    from app.config import ServerConfig
    from app.tls import generate_self_signed

    config = ServerConfig()
    data_dir = config.data_dir or os.environ.get("VOXCAST_DATA_DIR") \
        or _default_data_dir()
    Path(data_dir).mkdir(parents=True, exist_ok=True)
    cert, key = _resolve_cert_paths(config, data_dir)
    import datetime as _dt
    fp = generate_self_signed(cert, key, now=_dt.datetime.now(_dt.timezone.utc))
    # Colon-grouped uppercase, matching what the client shows in its trust
    # dialog, so an operator can compare the two visually without normalizing.
    grouped = ":".join(fp[i:i + 2] for i in range(0, len(fp), 2)).upper()
    print(f"Certificate written to {cert}")
    print(f"SHA-256 fingerprint: {grouped}")
    print("Enable HTTPS: set tls_enabled = true in server.toml, then restart.")
    return 0


def _resolve_data_dir(config) -> str:
    data_dir = config.data_dir or os.environ.get("VOXCAST_DATA_DIR") \
        or _default_data_dir()
    Path(data_dir).mkdir(parents=True, exist_ok=True)
    return data_dir


def _prompts_cli(argv: list[str]) -> int:
    """Manage server-side enhancement prompts (company-global AI modes).
    Cross-platform: same CLI on the Windows exe and Unix.
      --list-prompts
      --add-prompt "Name" "System prompt text..."  [--id my-id]
      --set-prompt "Name" "..."  (alias of --add-prompt)
      --remove-prompt <id>
    """
    import argparse

    from app import prompts as pm
    from app.config import ServerConfig

    parser = argparse.ArgumentParser(prog="VoxCastServer")
    g = parser.add_mutually_exclusive_group(required=True)
    g.add_argument("--list-prompts", action="store_true")
    g.add_argument("--add-prompt", nargs=2, metavar=("NAME", "PROMPT"))
    g.add_argument("--set-prompt", nargs=2, metavar=("NAME", "PROMPT"))
    g.add_argument("--remove-prompt", metavar="ID")
    parser.add_argument("--id", default="")
    args = parser.parse_args(argv)

    data_dir = _resolve_data_dir(ServerConfig())

    if args.list_prompts:
        rows = pm.load_prompts(data_dir)
        if not rows:
            print("No server prompts defined.")
        for e in rows:
            print(f"[{e['id']}] {e['name']}")
            print(f"    {e['prompt']}")
        return 0

    pair = args.add_prompt or args.set_prompt
    if pair:
        try:
            entry = pm.add_prompt(data_dir, pair[0], pair[1], id=args.id)
        except ValueError as e:
            print(str(e))
            return 2
        print(f"Saved prompt [{entry['id']}] {entry['name']}.")
        print("Clients see it after their next connection; no restart needed.")
        return 0

    if args.remove_prompt:
        ok = pm.remove_prompt(data_dir, args.remove_prompt)
        print("Removed." if ok else f"No prompt with id '{args.remove_prompt}'.")
        return 0 if ok else 1
    return 0


def _update_cli(argv: list[str]) -> int:
    """`--update [--check]`: check getvoxcast for a newer server build and, unless
    --check, download + verify + launch the installer (which stops the service,
    replaces files and restarts). Windows only for the exe path; on Unix it just
    reports (pull the new tarball/image instead)."""
    import argparse
    import hashlib
    import subprocess
    import tempfile

    import httpx

    from version import SERVER_VERSION

    parser = argparse.ArgumentParser()
    parser.add_argument("--update", action="store_true")
    parser.add_argument("--check", action="store_true")
    args = parser.parse_args(argv)

    url = "https://getvoxcast.com/api/v1/update/latest"
    try:
        with httpx.Client(timeout=httpx.Timeout(15.0, connect=5.0),
                          follow_redirects=True) as c:
            r = c.get(url, params={"channel": "stable", "variant": "server"})
        r.raise_for_status()
        data = r.json()
    except Exception as e:  # noqa: BLE001
        print(f"Update check failed: {e}")
        return 1

    remote = str(data.get("version") or "")
    if not data.get("available") or not remote or remote == SERVER_VERSION:
        print(f"VoxCast Server is up to date (v{SERVER_VERSION}).")
        return 0
    print(f"Update available: v{remote} (running v{SERVER_VERSION}).")
    if args.check:
        return 0

    dl_url = str(data.get("url") or "")
    sha = str(data.get("sha256") or "").lower()
    if not dl_url or not sha:
        print("Update manifest incomplete; download manually from getvoxcast.com.")
        return 1
    if os.name != "nt":
        print("On Linux, update by pulling the new server package or Docker image.")
        return 0

    dest = Path(tempfile.gettempdir()) / f"VoxCast-Server-{remote}-Setup.exe"
    print(f"Downloading {dl_url} ...")
    hasher = hashlib.sha256()
    try:
        with httpx.Client(timeout=httpx.Timeout(600.0, connect=10.0),
                          follow_redirects=False) as c:
            with c.stream("GET", dl_url) as resp:
                resp.raise_for_status()
                with dest.open("wb") as f:
                    for chunk in resp.iter_bytes(1024 * 256):
                        f.write(chunk)
                        hasher.update(chunk)
    except Exception as e:  # noqa: BLE001
        print(f"Download failed: {e}")
        dest.unlink(missing_ok=True)
        return 1
    if hasher.hexdigest().lower() != sha:
        print("Checksum mismatch; refusing to run the installer.")
        dest.unlink(missing_ok=True)
        return 1
    print("Verified. Launching installer (the service will restart)...")
    subprocess.Popen([str(dest), "/VERYSILENT", "/NORESTART"])
    return 0


def main() -> None:
    _bootstrap_paths()

    if "--init-config" in sys.argv[1:]:
        raise SystemExit(_init_config(sys.argv[1:]))
    if "--set-model" in sys.argv[1:]:
        raise SystemExit(_set_model(sys.argv[1:]))
    if "--gen-cert" in sys.argv[1:]:
        raise SystemExit(_gen_cert(sys.argv[1:]))
    _PROMPT_FLAGS = ("--list-prompts", "--add-prompt", "--set-prompt",
                     "--remove-prompt")
    if any(f in sys.argv[1:] for f in _PROMPT_FLAGS):
        raise SystemExit(_prompts_cli(sys.argv[1:]))
    if "--update" in sys.argv[1:]:
        raise SystemExit(_update_cli(sys.argv[1:]))

    from loguru import logger

    from app.config import ServerConfig

    config = ServerConfig()

    # Resolve the data dir (models, uploads, cache) and make paths.data_dir()
    # honor it process-wide before the engine touches disk.
    data_dir = config.data_dir or os.environ.get("VOXCAST_DATA_DIR") or _default_data_dir()
    os.environ["VOXCAST_DATA_DIR"] = data_dir
    Path(data_dir).mkdir(parents=True, exist_ok=True)

    logger.remove()
    logger.add(sys.stderr, level=config.log_level,
               format="{time:YYYY-MM-DD HH:mm:ss} | {level: <7} | {message}")

    from version import SERVER_VERSION
    logger.info("VoxCast Server {} starting (data_dir={})", SERVER_VERSION, data_dir)

    import uvicorn

    from app.main import create_app

    # TLS: resolve (and auto-generate a self-signed) cert before create_app so
    # the discovery reply can advertise the right scheme.
    ssl_kwargs: dict = {}
    if config.tls_enabled:
        import datetime as _dt

        from app.tls import ensure_cert
        cert, key = _resolve_cert_paths(config, data_dir)
        fp = ensure_cert(cert, key, now=_dt.datetime.now(_dt.timezone.utc))
        config.tls_cert = str(cert)
        config.tls_key = str(key)
        ssl_kwargs = {"ssl_certfile": str(cert), "ssl_keyfile": str(key)}
        logger.info("HTTPS enabled (self-signed SHA-256 {})", fp)

    app = create_app(config)
    # proxy_headers trusts X-Forwarded-For only from the local reverse proxy
    # (the hosted deployment terminates TLS at nginx on loopback).
    uvicorn.run(app, host=config.host, port=config.port,
                log_level=config.log_level.lower(), ws_max_size=64 * 1024 * 1024,
                proxy_headers=True, forwarded_allow_ips="127.0.0.1",
                **ssl_kwargs)


if __name__ == "__main__":
    main()
