"""Chrome daemon manager: persistent Chrome for Token Factory.

Manages a persistent Chrome instance that background processes (e.g., OpenClaw
LaunchAgent) can use for BotGuard token generation. The Token Factory's
find_existing_chrome() will discover it automatically.

Usage:
    gemcli chrome start    # Launch persistent Chrome (from interactive terminal)
    gemcli chrome stop     # Kill the persistent Chrome
    gemcli chrome status   # Check health and auth status
"""

from __future__ import annotations

import json
import os
import signal
import subprocess
import time
from dataclasses import dataclass
from pathlib import Path

import httpx
from loguru import logger

from .cdp import (
    _clear_port_map,
    _write_port_map,
    find_available_port,
    find_existing_gemcli_chrome,
    find_or_create_gemini_page,
    get_chrome_path,
    get_debugger_url,
    get_page_cookies,
)
from .constants import CONFIG_DIR_NAME, COOKIE_1PSID
from .exceptions import TokenFactoryError

DAEMON_FILE = "chrome-daemon.json"
GEMINI_APP_URL = "https://gemini.google.com/app"


@dataclass
class ChromeDaemonInfo:
    """State of the persistent Chrome daemon."""

    pid: int
    port: int
    profile_name: str
    started_at: float
    headless: bool
    chrome_profile_path: str

    def to_dict(self) -> dict:
        return {
            "pid": self.pid,
            "port": self.port,
            "profile_name": self.profile_name,
            "started_at": self.started_at,
            "headless": self.headless,
            "chrome_profile_path": self.chrome_profile_path,
        }

    @classmethod
    def from_dict(cls, data: dict) -> ChromeDaemonInfo:
        return cls(
            pid=data["pid"],
            port=data["port"],
            profile_name=data["profile_name"],
            started_at=data["started_at"],
            headless=data["headless"],
            chrome_profile_path=data["chrome_profile_path"],
        )


def _daemon_file_path() -> Path:
    """Return path to the daemon state file."""
    return Path.home() / CONFIG_DIR_NAME / DAEMON_FILE


def load_daemon_info() -> ChromeDaemonInfo | None:
    """Load daemon info from disk. Returns None if file doesn't exist."""
    path = _daemon_file_path()
    if not path.exists():
        return None
    try:
        data = json.loads(path.read_text())
        return ChromeDaemonInfo.from_dict(data)
    except (json.JSONDecodeError, KeyError, OSError) as e:
        logger.debug(f"Failed to load daemon info: {e}")
        return None


def save_daemon_info(info: ChromeDaemonInfo) -> None:
    """Save daemon info to disk."""
    path = _daemon_file_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(info.to_dict(), indent=2))


def clear_daemon_info() -> None:
    """Remove the daemon file."""
    path = _daemon_file_path()
    if path.exists():
        path.unlink()


def is_process_alive(pid: int) -> bool:
    """Check if a process with the given PID is running.

    Uses os.kill(pid, 0) to check existence, then verifies the process
    is actually Chrome (not a recycled PID) via ps on macOS/Linux.
    """
    try:
        os.kill(pid, 0)
    except OSError:
        return False

    # Verify it's actually Chrome (prevent killing a recycled PID)
    try:
        result = subprocess.run(
            ["ps", "-p", str(pid), "-o", "comm="],
            capture_output=True, text=True, timeout=5,
        )
        comm = result.stdout.strip().lower()
        return "chrome" in comm or "chromium" in comm
    except Exception:
        # If ps fails, trust os.kill result
        return True


def is_chrome_responsive(port: int) -> bool:
    """Check if Chrome is responding to CDP on the given port."""
    try:
        resp = httpx.get(f"http://localhost:{port}/json/version", timeout=2)
        return resp.status_code == 200
    except Exception:
        return False


def is_gemini_authenticated(port: int) -> bool:
    """Check if the Chrome instance has valid Gemini auth cookies.

    Finds a Gemini page and checks for the __Secure-1PSID cookie.
    """
    try:
        page = find_or_create_gemini_page(port)
        if not page:
            return False
        ws_url = page.get("webSocketDebuggerUrl")
        if not ws_url:
            # Try to get it from the debugger URL
            ws_url = get_debugger_url(port)
        if not ws_url:
            return False
        cookies = get_page_cookies(ws_url)
        return any(c.get("name") == COOKIE_1PSID for c in cookies)
    except Exception as e:
        logger.debug(f"Auth check failed: {e}")
        return False


def check_daemon_health() -> tuple[ChromeDaemonInfo | None, str]:
    """Full health check of the persistent Chrome daemon.

    Returns:
        (info, status) where status is one of:
        - "running": PID alive AND CDP responding
        - "stale": PID file exists but process is dead (auto-cleaned)
        - "unresponsive": PID alive but CDP not responding
        - "not_running": no daemon file
    """
    info = load_daemon_info()
    if info is None:
        return None, "not_running"

    if not is_process_alive(info.pid):
        logger.debug(f"Chrome daemon PID {info.pid} is dead, cleaning up")
        clear_daemon_info()
        return info, "stale"

    if not is_chrome_responsive(info.port):
        return info, "unresponsive"

    return info, "running"


def start_chrome_daemon(
    profile_name: str,
    chrome_profile_path: str,
    headless: bool = True,
    port: int | None = None,
) -> ChromeDaemonInfo:
    """Start Chrome as a persistent background daemon.

    Must be run from an interactive terminal (macOS security requirement).
    Chrome is detached from the terminal and survives terminal close.

    Uses dynamic port selection and port-to-profile mapping to coexist
    with other CDP consumers (e.g., Antigravity browser, NLM CLI).

    Args:
        profile_name: Auth profile name.
        chrome_profile_path: Chrome user-data-dir path.
        headless: If True (default), run headless. If False, show Chrome window.
        port: Specific CDP port. If None, auto-selects an available port.

    Returns:
        ChromeDaemonInfo with PID, port, etc.

    Raises:
        TokenFactoryError: If Chrome can't be started.
    """
    # Check if our daemon is already running
    existing_info, status = check_daemon_health()
    if status == "running":
        raise TokenFactoryError(
            f"Chrome daemon is already running (PID {existing_info.pid}, "
            f"port {existing_info.port}). Stop it first: gemcli chrome stop"
        )

    # Try to reuse an existing gemcli Chrome instance for this profile
    existing_port, debugger_url = find_existing_gemcli_chrome(
        profile_name=profile_name
    )
    if existing_port and debugger_url:
        logger.info(
            f"Reusing existing Chrome on port {existing_port} "
            f"for profile '{profile_name}'"
        )
        # We didn't launch it, but track it as our daemon
        info = ChromeDaemonInfo(
            pid=0,  # Unknown PID for reused instance
            port=existing_port,
            profile_name=profile_name,
            started_at=time.time(),
            headless=headless,
            chrome_profile_path=chrome_profile_path,
        )
        save_daemon_info(info)
        return info

    # Find Chrome binary
    chrome_path = get_chrome_path()
    if not chrome_path:
        raise TokenFactoryError(
            "Chrome not found. Install Google Chrome to use persistent Chrome mode."
        )

    # Find available port — dynamic selection, no global blocking
    if port is not None:
        # Verify the specified port is available
        import socket
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.bind(("localhost", port))
        except OSError:
            raise TokenFactoryError(f"Port {port} is already in use.")
    else:
        port = find_available_port()

    # Ensure chrome profile directory exists
    profile_dir = Path(chrome_profile_path)
    profile_dir.mkdir(parents=True, exist_ok=True)

    # Build Chrome args (same flags as Token Factory)
    args = [
        chrome_path,
        f"--remote-debugging-port={port}",
        "--no-first-run",
        "--no-default-browser-check",
        "--disable-extensions",
        f"--user-data-dir={profile_dir}",
        "--remote-allow-origins=*",
    ]
    if headless:
        args.append("--headless")
    args.append(GEMINI_APP_URL)

    # Launch Chrome detached from terminal
    try:
        process = subprocess.Popen(
            args,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            stdin=subprocess.DEVNULL,
            start_new_session=True,
        )
    except Exception as e:
        raise TokenFactoryError(f"Failed to launch Chrome: {e}")

    # Wait for Chrome to start responding
    logger.debug(f"Chrome launched (PID {process.pid}), waiting for CDP on port {port}")
    responsive = False
    for _ in range(8):  # up to ~4 seconds
        time.sleep(0.5)
        if is_chrome_responsive(port):
            responsive = True
            break

    if not responsive:
        # Chrome started but not responding — try to clean up
        try:
            process.terminate()
            process.wait(timeout=5)
        except Exception:
            try:
                process.kill()
            except Exception:
                pass
        raise TokenFactoryError(
            f"Chrome launched (PID {process.pid}) but failed to respond "
            f"on port {port} within 4 seconds."
        )

    # Save daemon info and register in port map
    info = ChromeDaemonInfo(
        pid=process.pid,
        port=port,
        profile_name=profile_name,
        started_at=time.time(),
        headless=headless,
        chrome_profile_path=chrome_profile_path,
    )
    save_daemon_info(info)
    _write_port_map(port, profile_name, process.pid)

    logger.info(f"Chrome daemon started: PID {info.pid}, port {info.port}")
    return info


def stop_chrome_daemon() -> bool:
    """Stop the persistent Chrome daemon.

    Only kills the exact PID from our daemon file. Never scans for
    random Chrome processes.

    Returns:
        True if a daemon was stopped, False if none was running.
    """
    info = load_daemon_info()
    if info is None:
        return False

    pid = info.pid

    if not is_process_alive(pid):
        logger.debug(f"Chrome daemon PID {pid} already dead, cleaning up")
        clear_daemon_info()
        _clear_port_map(info.port)
        return True

    # Graceful shutdown
    logger.debug(f"Sending SIGTERM to Chrome daemon PID {pid}")
    try:
        os.kill(pid, signal.SIGTERM)
    except OSError as e:
        logger.debug(f"SIGTERM failed: {e}")
        clear_daemon_info()
        _clear_port_map(info.port)
        return False

    # Wait for process to exit
    for _ in range(10):  # up to 5 seconds
        time.sleep(0.5)
        if not is_process_alive(pid):
            clear_daemon_info()
            _clear_port_map(info.port)
            logger.info(f"Chrome daemon PID {pid} stopped gracefully")
            return True

    # Force kill
    logger.debug(f"Chrome daemon PID {pid} did not exit, sending SIGKILL")
    try:
        os.kill(pid, signal.SIGKILL)
        time.sleep(0.5)
    except OSError:
        pass

    clear_daemon_info()
    _clear_port_map(info.port)
    logger.info(f"Chrome daemon PID {pid} force-killed")
    return True
