"""
Daemon Server - Persistent background service for QQ Browser Skill.

Hosts two servers:
  1. WebSocket Server (port 8765) — browser extension connects here (long-lived).
  2. HTTP RPC Server (port 8766) — CLI processes connect here to send skill requests.

Usage:
    qqbrowser-skill serve                     # foreground mode
    qqbrowser-skill serve --daemon            # background (daemon) mode
    qqbrowser-skill serve --ws-port 8765 --rpc-port 8766
"""

import asyncio
import json
import logging
import os
import signal
import sys
from http import HTTPStatus
from typing import Optional

from .skill_registry import get_executor, SKILL_MAP
from . import vnc_util
from .vnc_proxy import get_vnc_proxy_url
from .constants import DEFAULT_LOG_DIR


def _get_version() -> str:
    """获取当前包版本号。优先从 importlib.metadata 读取，失败则返回 unknown。"""
    try:
        from importlib.metadata import version
        return version("qqbrowser-skill")
    except Exception:
        return "unknown"

logger = logging.getLogger(__name__)

# Default ports
DEFAULT_WS_PORT = 8765
DEFAULT_RPC_PORT = 8766
DEFAULT_RPC_HOST = "127.0.0.1"

# PID / state file location
_STATE_DIR: Optional[str] = None


def _get_state_dir() -> str:
    """Return the directory used for PID/state files.

    On Windows: %LOCALAPPDATA%/qqbrowser-skill  (or %TEMP%)
    On Linux/macOS: ~/.qqbrowser-skill
    """
    global _STATE_DIR
    if _STATE_DIR is not None:
        return _STATE_DIR

    if sys.platform == "win32":
        base = os.environ.get("LOCALAPPDATA", os.environ.get("TEMP", "."))
        _STATE_DIR = os.path.join(base, "qqbrowser-skill")
    else:
        _STATE_DIR = os.path.expanduser("~/.qqbrowser-skill")

    os.makedirs(_STATE_DIR, exist_ok=True)
    return _STATE_DIR


def _state_file_path() -> str:
    return os.path.join(_get_state_dir(), "server.json")


def write_state_file(pid: int, ws_port: int, rpc_port: int):
    """Write the daemon state (PID + ports) to a JSON file."""
    state = {"pid": pid, "ws_port": ws_port, "rpc_port": rpc_port}
    path = _state_file_path()
    with open(path, "w") as f:
        json.dump(state, f)
    logger.info(f"State file written: {path} -> {state}")


def read_state_file() -> Optional[dict]:
    """Read the daemon state file. Returns None if not found or invalid."""
    path = _state_file_path()
    if not os.path.exists(path):
        return None
    try:
        with open(path, "r") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return None


def remove_state_file():
    """Remove the daemon state file."""
    path = _state_file_path()
    try:
        os.remove(path)
    except OSError:
        pass


def is_daemon_running() -> bool:
    """Check if the daemon is currently running by reading state file and checking PID."""
    state = read_state_file()
    if state is None:
        return False

    pid = state.get("pid")
    if pid is None:
        return False

    # Check if the process is alive
    if sys.platform == "win32":
        return _is_pid_alive_windows(pid)
    else:
        return _is_pid_alive_unix(pid)


def _is_pid_alive_unix(pid: int) -> bool:
    try:
        os.kill(pid, 0)
        return True
    except (OSError, ProcessLookupError):
        return False


def _is_pid_alive_windows(pid: int) -> bool:
    try:
        import ctypes
        kernel32 = ctypes.windll.kernel32
        SYNCHRONIZE = 0x00100000
        handle = kernel32.OpenProcess(SYNCHRONIZE, False, pid)
        if handle:
            kernel32.CloseHandle(handle)
            return True
        return False
    except Exception:
        return False


# ---------------------------------------------------------------------------
# HTTP RPC Server (for CLI -> Daemon communication)
# ---------------------------------------------------------------------------

class SimpleHTTPRPCServer:
    """A minimal async HTTP server for receiving skill execution requests from CLI.

    Uses only Python stdlib (asyncio streams) to avoid extra dependencies.
    Supports:
      POST /execute  — execute a skill
      GET  /health   — health check
      GET  /status   — daemon status (connected clients, etc.)
    """

    def __init__(self, host: str, port: int, executor_func):
        self._host = host
        self._port = port
        self._executor_func = executor_func
        self._server: Optional[asyncio.AbstractServer] = None

    async def start(self):
        self._server = await asyncio.start_server(
            self._handle_connection, self._host, self._port
        )
        logger.info(f"HTTP RPC Server listening on http://{self._host}:{self._port}")

    async def stop(self):
        if self._server:
            self._server.close()
            await self._server.wait_closed()
            logger.info("HTTP RPC Server stopped")

    async def _handle_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
        """Handle a single HTTP connection."""
        try:
            # Read the request line and headers
            request_line = await asyncio.wait_for(reader.readline(), timeout=30)
            if not request_line:
                writer.close()
                return

            request_str = request_line.decode("utf-8", errors="replace").strip()
            parts = request_str.split(" ")
            if len(parts) < 2:
                await self._send_response(writer, 400, {"error": "Bad request"})
                return

            method = parts[0].upper()
            path = parts[1]

            # Read headers
            headers = {}
            while True:
                line = await asyncio.wait_for(reader.readline(), timeout=10)
                line_str = line.decode("utf-8", errors="replace").strip()
                if not line_str:
                    break
                if ":" in line_str:
                    key, _, value = line_str.partition(":")
                    headers[key.strip().lower()] = value.strip()

            # Read body if Content-Length is present
            body = b""
            content_length = int(headers.get("content-length", 0))
            if content_length > 0:
                body = await asyncio.wait_for(reader.readexactly(content_length), timeout=300)

            # Route
            if method == "GET" and path == "/health":
                await self._handle_health(writer)
            elif method == "GET" and path == "/status":
                await self._handle_status(writer)
            elif method == "POST" and path == "/execute":
                await self._handle_execute(writer, body)
            else:
                await self._send_response(writer, 404, {"error": "Not found"})

        except asyncio.TimeoutError:
            logger.warning("HTTP RPC: connection timed out")
            try:
                await self._send_response(writer, 408, {"error": "Request timeout"})
            except Exception:
                pass
        except Exception as e:
            logger.error(f"HTTP RPC: error handling connection: {e}")
            try:
                await self._send_response(writer, 500, {"error": str(e)})
            except Exception:
                pass
        finally:
            try:
                writer.close()
                await writer.wait_closed()
            except Exception:
                pass

    async def _handle_health(self, writer: asyncio.StreamWriter):
        """Health check endpoint."""
        await self._send_response(writer, 200, {"status": "ok"})

    async def _handle_status(self, writer: asyncio.StreamWriter):
        """Status endpoint — returns daemon info."""
        executor = self._executor_func()
        ws_mgr = executor.ws_manager
        status = {
            "status": "running",
            "pid": os.getpid(),
            "ws_server_started": ws_mgr.is_server_started(),
            "connected_clients": len(ws_mgr._connected_clients),
        }
        await self._send_response(writer, 200, status)

    async def _handle_execute(self, writer: asyncio.StreamWriter, body: bytes):
        """Execute a skill request from CLI."""
        try:
            request = json.loads(body.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            await self._send_response(writer, 400, {"error": f"Invalid JSON: {e}"})
            return

        skill_name = request.get("skill_name")
        args = request.get("args", {})
        log_dir = request.get("log_dir", DEFAULT_LOG_DIR)

        if not skill_name:
            await self._send_response(writer, 400, {"error": "Missing 'skill_name'"})
            return

        if skill_name not in SKILL_MAP:
            await self._send_response(writer, 400, {"error": f"Unknown skill: {skill_name}"})
            return

        logger.info(f"RPC execute: skill={skill_name}, args={args}")

        try:
            # Set browser log dir for this request
            vnc_util.set_browser_log_dir(log_dir)

            executor = self._executor_func()
            result = await executor.execute(skill_name, **args)
            await self._send_response(writer, 200, {
                "success": True,
                "result": result.to_dict(),
            })
        except Exception as e:
            logger.error(f"RPC execute error: {e}")
            await self._send_response(writer, 500, {
                "success": False,
                "error": str(e),
            })

    async def _send_response(self, writer: asyncio.StreamWriter, status_code: int, body: dict):
        """Send an HTTP response with JSON body."""
        body_bytes = json.dumps(body, ensure_ascii=False).encode("utf-8")
        status_phrase = HTTPStatus(status_code).phrase
        response = (
            f"HTTP/1.1 {status_code} {status_phrase}\r\n"
            f"Content-Type: application/json; charset=utf-8\r\n"
            f"Content-Length: {len(body_bytes)}\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        ).encode("utf-8") + body_bytes
        writer.write(response)
        await writer.drain()


# ---------------------------------------------------------------------------
# Daemon entry point
# ---------------------------------------------------------------------------

async def run_daemon(ws_port: int = DEFAULT_WS_PORT,
                     rpc_port: int = DEFAULT_RPC_PORT,
                     log_dir: str = DEFAULT_LOG_DIR):
    """Start the persistent daemon service (foreground, blocking).

    This coroutine starts both the WebSocket server and the HTTP RPC server,
    then blocks forever until interrupted.
    """
    executor = get_executor()

    # 1. Start WebSocket server for browser extension
    ws_mgr = executor.ws_manager
    # Override the default port if needed
    await _start_ws_server_on_port(ws_mgr, ws_port)

    # 2. Start HTTP RPC server for CLI
    rpc_server = SimpleHTTPRPCServer(DEFAULT_RPC_HOST, rpc_port, get_executor)
    await rpc_server.start()

    # 3. Write state file
    write_state_file(pid=os.getpid(), ws_port=ws_port, rpc_port=rpc_port)

    logger.info("=" * 60)
    logger.info(f"QQ Browser Skill Daemon is running")
    logger.info(f"  Version          : {_get_version()}")
    logger.info(f"  WebSocket Server : ws://127.0.0.1:{ws_port}  (browser extension)")
    logger.info(f"  HTTP RPC Server  : http://127.0.0.1:{rpc_port}  (CLI requests)")
    logger.info(f"  PID              : {os.getpid()}")
    logger.info(f"  State file       : {_state_file_path()}")
    logger.info("=" * 60)

    # 4. Block forever
    stop_event = asyncio.Event()

    def _signal_handler(sig_num: int):
        sig_name = signal.Signals(sig_num).name
        logger.info(f"Received shutdown signal {sig_name} (signal={sig_num}), stopping daemon...")
        stop_event.set()

    loop = asyncio.get_running_loop()

    if sys.platform != "win32":
        # Unix: use loop.add_signal_handler
        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.add_signal_handler(sig, _signal_handler, sig)
    else:
        # Windows: signal handlers in asyncio are limited;
        # KeyboardInterrupt (Ctrl+C) will be caught by the caller.
        pass

    try:
        await stop_event.wait()
    except (KeyboardInterrupt, asyncio.CancelledError):
        pass
    finally:
        logger.info("Shutting down daemon...")
        await rpc_server.stop()
        await executor.cleanup()
        remove_state_file()
        logger.info("Daemon stopped")


async def _start_ws_server_on_port(ws_mgr, port: int):
    """Start the WebSocket server on the specified port.

    Delegates to WebSocketManager.start_server(port) which handles
    duplicate-start protection, IP allow-list, and all internal state.
    """
    await ws_mgr.start_server(port=port)


def _check_already_running(ws_port: int, rpc_port: int) -> bool:
    """Check if a daemon is already running. If so, print info and return True."""
    if not is_daemon_running():
        # State file exists but process is dead — clean up stale state
        state = read_state_file()
        if state is not None:
            logger.info("Removing stale state file (process is dead)")
            remove_state_file()
        return False

    state = read_state_file()
    pid = state.get("pid", "?")
    existing_ws = state.get("ws_port", "?")
    existing_rpc = state.get("rpc_port", "?")
    print(f"⚠️  Daemon is already running (PID: {pid})")
    print(f"   WebSocket Server : ws://127.0.0.1:{existing_ws}")
    print(f"   HTTP RPC Server  : http://127.0.0.1:{existing_rpc}")
    print(f"   To stop it first: qqbrowser-skill stop")
    return True


def start_daemon_foreground(ws_port: int = DEFAULT_WS_PORT,
                            rpc_port: int = DEFAULT_RPC_PORT,
                            log_dir: str = DEFAULT_LOG_DIR):
    """Start the daemon in the foreground (blocking). Called from CLI."""
    if _check_already_running(ws_port, rpc_port):
        return
    asyncio.run(run_daemon(ws_port=ws_port, rpc_port=rpc_port, log_dir=log_dir))


def start_daemon_background(ws_port: int = DEFAULT_WS_PORT,
                            rpc_port: int = DEFAULT_RPC_PORT,
                            log_dir: str = DEFAULT_LOG_DIR):
    """Start the daemon as a detached background process.

    Works cross-platform:
      - Windows: uses CREATE_NO_WINDOW + DETACHED_PROCESS
      - Unix: uses double-fork or nohup
    """
    import subprocess

    if _check_already_running(ws_port, rpc_port):
        return

    # Build the command to run the daemon in foreground mode in a new process.
    # Use -c with sys.argv override so that relative imports work correctly.
    code = (
        "import sys; "
        f"sys.argv = ['daemon_server', '--ws-port', '{ws_port}', '--rpc-port', '{rpc_port}', '--log-dir', '{log_dir}']; "
        "from x5use.daemon_server import _module_main; _module_main()"
    )
    cmd = [sys.executable, "-c", code]

    if sys.platform == "win32":
        # Windows: detached process
        CREATE_NO_WINDOW = 0x08000000
        DETACHED_PROCESS = 0x00000008
        proc = subprocess.Popen(
            cmd,
            creationflags=DETACHED_PROCESS | CREATE_NO_WINDOW,
            close_fds=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    else:
        # Unix: use nohup-like detachment
        proc = subprocess.Popen(
            cmd,
            start_new_session=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            close_fds=True,
        )

    logger.info(f"Daemon started in background (PID: {proc.pid})")
    print(f"✅ Daemon started in background (PID: {proc.pid})")
    print(f"   WebSocket Server : ws://127.0.0.1:{ws_port}")
    print(f"   HTTP RPC Server  : http://127.0.0.1:{rpc_port}")


def stop_daemon():
    """Stop a running daemon by reading the PID from the state file."""
    state = read_state_file()
    if state is None:
        print("No daemon is running (state file not found).")
        return False

    pid = state.get("pid")
    if pid is None:
        print("Invalid state file (no PID).")
        remove_state_file()
        return False

    if sys.platform == "win32":
        try:
            import ctypes
            kernel32 = ctypes.windll.kernel32
            PROCESS_TERMINATE = 0x0001
            handle = kernel32.OpenProcess(PROCESS_TERMINATE, False, pid)
            if handle:
                kernel32.TerminateProcess(handle, 0)
                kernel32.CloseHandle(handle)
                print(f"✅ Daemon (PID: {pid}) stopped.")
            else:
                print(f"Could not open process {pid}.")
        except Exception as e:
            print(f"Failed to stop daemon: {e}")
    else:
        try:
            os.kill(pid, signal.SIGTERM)
            print(f"✅ Daemon (PID: {pid}) stopped.")
        except ProcessLookupError:
            print(f"Daemon (PID: {pid}) is not running.")
        except PermissionError:
            print(f"Permission denied to stop daemon (PID: {pid}).")

    remove_state_file()
    return True


# ---------------------------------------------------------------------------
# Module entry point (for background process spawning)
# ---------------------------------------------------------------------------

def _module_main():
    """Entry point when invoked as `python -m x5use.daemon_server`."""
    import argparse

    parser = argparse.ArgumentParser(description="QQ Browser Skill Daemon Server")
    parser.add_argument("--ws-port", type=int, default=DEFAULT_WS_PORT)
    parser.add_argument("--rpc-port", type=int, default=DEFAULT_RPC_PORT)
    parser.add_argument("--log-dir", type=str, default=DEFAULT_LOG_DIR)
    args = parser.parse_args()

    # Setup logging
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )
    logging.getLogger(__name__).info(f"qqbrowser-skill daemon version: {_get_version()}")

    # Also log to file
    try:
        os.makedirs(args.log_dir, exist_ok=True)
        fh = logging.FileHandler(
            os.path.join(args.log_dir, "daemon.log"), encoding="utf-8"
        )
        fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
        logging.getLogger().addHandler(fh)
    except Exception:
        pass

    start_daemon_foreground(
        ws_port=args.ws_port,
        rpc_port=args.rpc_port,
        log_dir=args.log_dir,
    )


if __name__ == "__main__":
    _module_main()
