#!/usr/bin/env python3
"""App Mode launcher using a native desktop window (pywebview)."""

from __future__ import annotations

import argparse
import errno
import inspect
import logging
import socket
import threading
import time
from dataclasses import dataclass

import requests
import uvicorn
from requests.exceptions import RequestException

from app.core.config import settings
from app.main import app as fastapi_app
from app.services import runtime_config
from app.services.app_icon import apply_platform_app_icon, prepare_app_icon_assets
from app.services.ui_events import event_hub
from app.services.voice_runtime import voice_runtime
from voice_client import add_voice_cli_args, args_to_config, run_voice_client

try:
    import webview
except ModuleNotFoundError:  # pragma: no cover - runtime dependency path
    webview = None

LOGGER = logging.getLogger("sentineledge.app_mode")
WINDOW_TITLE = "Orion AI"


@dataclass
class RuntimeState:
    """Mutable runtime state for coordinated shutdown."""

    voice_exit_code: int = 0


def _build_parser() -> argparse.ArgumentParser:
    """Build App Mode CLI parser."""
    parser = argparse.ArgumentParser(
        description=f"Run {settings.app_name} App Mode (backend + voice + native desktop window)",
    )
    parser.add_argument("--host", default="127.0.0.1", help="Backend bind host")
    parser.add_argument("--port", type=int, default=8000, help="Backend bind port")
    parser.add_argument(
        "--ui-url",
        default=None,
        help="Assistant UI URL override (default: /config on first run, otherwise /assistant/ui)",
    )
    parser.add_argument(
        "--startup-timeout",
        type=float,
        default=20.0,
        help="Seconds to wait for backend readiness",
    )
    parser.add_argument("--width", type=int, default=1000, help="Native window width")
    parser.add_argument("--height", type=int, default=800, help="Native window height")
    parser.add_argument("--fullscreen", action="store_true", help="Start native window in fullscreen mode")
    parser.add_argument("--no-resizable", action="store_true", help="Disable native window resizing")

    add_voice_cli_args(parser, default_server="http://127.0.0.1:8000")
    return parser


def _start_backend(host: str, port: int) -> tuple[uvicorn.Server, threading.Thread]:
    """Start uvicorn server in a background thread."""
    config = uvicorn.Config(
        fastapi_app,
        host=host,
        port=port,
        log_level="info",
        reload=False,
    )
    server = uvicorn.Server(config=config)
    thread = threading.Thread(target=server.run, name="app-mode-backend", daemon=True)
    thread.start()
    return server, thread


def _port_is_available(host: str, port: int) -> bool:
    """Return True when host:port can be bound by App Mode backend."""
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            sock.bind((host, port))
    except OSError as exc:
        if exc.errno in {errno.EPERM, errno.EACCES}:
            LOGGER.warning(
                "Port pre-check skipped for %s:%s due to restricted socket permissions (%s).",
                host,
                port,
                exc,
            )
            return True
        return False
    return True


def _wait_for_backend(base_url: str, timeout_seconds: float) -> bool:
    """Wait until backend /health is available."""
    deadline = time.monotonic() + max(timeout_seconds, 1.0)
    endpoint = f"{base_url.rstrip('/')}/health"

    while time.monotonic() < deadline:
        try:
            response = requests.get(endpoint, timeout=1.0)
            if response.status_code == 200:
                return True
        except RequestException:
            pass
        time.sleep(0.2)

    return False


def _effective_server_url(raw_server: str, host: str, port: int) -> str:
    """Resolve default voice target server URL."""
    if raw_server == "http://127.0.0.1:8000" and (host not in {"127.0.0.1", "localhost"} or port != 8000):
        return f"http://127.0.0.1:{port}"
    return raw_server


def main() -> int:
    """App Mode entrypoint."""
    logging.basicConfig(level=getattr(logging, settings.log_level, logging.INFO))
    args = _build_parser().parse_args()
    launch_started = time.monotonic()

    def _phase(name: str, **fields: object) -> None:
        detail = " ".join(f"{key}={value}" for key, value in fields.items())
        LOGGER.info(
            "app_mode.phase ts_ms=%s phase=%s elapsed_ms=%s %s",
            int(time.time() * 1000),
            name,
            int(max((time.monotonic() - launch_started) * 1000.0, 0.0)),
            detail,
        )

    if webview is None:
        LOGGER.error("pywebview is not installed. Install dependencies with: pip install -r requirements.txt")
        return 1

    stop_event = threading.Event()
    runtime = RuntimeState()
    voice_thread: threading.Thread | None = None

    if not _port_is_available(args.host, args.port):
        LOGGER.error(
            "Backend port %s on %s is already in use. Stop the running server or choose a different --port.",
            args.port,
            args.host,
        )
        return 1

    LOGGER.info("App Mode starting (single-process backend+voice, hub_id=%s)", id(event_hub))
    _phase("startup_begin", host=args.host, port=args.port)
    server, backend_thread = _start_backend(host=args.host, port=args.port)
    _phase("backend_thread_started", thread_alive=backend_thread.is_alive())

    try:
        local_server = f"http://127.0.0.1:{args.port}"
        if not _wait_for_backend(local_server, args.startup_timeout):
            LOGGER.error("Backend did not become ready within %.1f seconds", args.startup_timeout)
            _phase("backend_ready_timeout", timeout_seconds=args.startup_timeout)
            return 1
        _phase("backend_ready", url=local_server)

        default_path = runtime_config.app_mode_initial_path()
        ui_url = args.ui_url or f"http://127.0.0.1:{args.port}{default_path}"
        initial_mode = runtime_config.UI_MODE_CONFIG if "/config" in ui_url else runtime_config.UI_MODE_ASSISTANT
        runtime_config.set_app_ui_mode(initial_mode)
        _phase("ui_url_resolved", url=ui_url, initial_mode=initial_mode)

        args.server = _effective_server_url(args.server, args.host, args.port)
        voice_config = args_to_config(args)

        def _voice_worker() -> None:
            """Run voice loop while app window is active."""
            try:
                runtime.voice_exit_code = run_voice_client(
                    config=voice_config,
                    event_callback=event_hub.broadcast,
                    stop_event=stop_event,
                )
            except Exception:  # pragma: no cover - defensive runtime path
                LOGGER.exception("Voice loop crashed")
                voice_runtime.set_worker_active(False, error="voice loop crashed")
                runtime.voice_exit_code = 1

        voice_thread_lock = threading.Lock()

        def _start_voice_thread() -> None:
            """Start voice loop on-demand after UI thread is initialized."""
            nonlocal voice_thread
            with voice_thread_lock:
                if voice_thread is not None and voice_thread.is_alive():
                    return
                voice_thread = threading.Thread(target=_voice_worker, name="app-mode-voice", daemon=True)
                voice_thread.start()
                _phase("voice_thread_started", thread_alive=voice_thread.is_alive(), stt_backend=voice_config.stt_backend)

        _phase("voice_thread_deferred", stt_backend=voice_config.stt_backend)

        LOGGER.info("Launching native Assistant window: %s", ui_url)
        _phase("window_prepare_begin", title=WINDOW_TITLE)
        icon_assets = prepare_app_icon_assets()
        if apply_platform_app_icon(icon_assets.source_png):
            LOGGER.info("Applied macOS dock icon from %s", icon_assets.source_png)
            _phase("dock_icon_applied", source=icon_assets.source_png)

        def _on_window_closed() -> None:
            LOGGER.info("Native window closed; stopping voice and backend")
            stop_event.set()
            server.should_exit = True

        create_window_kwargs = {
            "title": WINDOW_TITLE,
            "url": ui_url,
            "width": args.width,
            "height": args.height,
            "resizable": not args.no_resizable,
            "fullscreen": bool(args.fullscreen),
            "background_color": "#000000",
        }
        create_signature = inspect.signature(webview.create_window)
        if "icon" in create_signature.parameters and icon_assets.window_icon is not None:
            create_window_kwargs["icon"] = str(icon_assets.window_icon)

        _phase("window_create_begin")
        window = webview.create_window(
            **create_window_kwargs,
        )
        _phase("window_created")
        window.events.closed += _on_window_closed

        # Must run on the main thread.
        start_kwargs: dict[str, object] = {}
        start_signature = inspect.signature(webview.start)
        if "func" in start_signature.parameters:
            start_kwargs["func"] = _start_voice_thread
        else:
            _start_voice_thread()
        if "icon" in start_signature.parameters and icon_assets.start_icon is not None:
            start_kwargs["icon"] = str(icon_assets.start_icon)
        _phase("webview_start_begin")
        webview.start(**start_kwargs)
        _phase("webview_start_end")
    except KeyboardInterrupt:
        LOGGER.info("App Mode interrupted by keyboard")
        _phase("keyboard_interrupt")
    finally:
        stop_event.set()
        server.should_exit = True
        _phase("shutdown_begin")
        if voice_thread is not None:
            voice_thread.join(timeout=10)
        backend_thread.join(timeout=10)
        LOGGER.info("App Mode shutdown complete")
        _phase("shutdown_complete", voice_exit_code=runtime.voice_exit_code)

    return runtime.voice_exit_code


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