"""
QQ Browser Skill CLI entry point.

Each browser skill is exposed as a CLI subcommand with typed flags
auto-generated from the SkillParam definitions.

Usage:
    # Start the persistent daemon service (must be running first):
    qqbrowser-skill serve                 # foreground mode
    qqbrowser-skill serve --daemon        # background mode

    # Stop / check daemon status:
    qqbrowser-skill stop
    qqbrowser-skill status

    # List all skills:
    qqbrowser-skill list

    # Execute a skill (sends request to the daemon via RPC):
    qqbrowser-skill browser_go_to_url --url https://www.baidu.com
    qqbrowser-skill browser_click_element --index 5
    qqbrowser-skill browser_input_text --index 3 --text "hello world"
    qqbrowser-skill browser_scroll_by --direction down --pixels 300
    qqbrowser-skill browser_wait --seconds 5
    qqbrowser-skill browser_snapshot

    # Show help for a specific skill:
    qqbrowser-skill browser_find_and_act --help

    # Install QQ Browser dependency:
    qqbrowser-skill install
    qqbrowser-skill install --dry-run
"""
import argparse
import asyncio
import json
import logging
import os
import sys


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


# ---------------------------------------------------------------------------
# Logging setup: stdout with [LOG] prefix + optional file log
# ---------------------------------------------------------------------------

from .constants import DEFAULT_LOG_DIR

_log_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

# 用于追踪 _setup_logging 自己添加的 handler，避免 clear 时误删其他模块的 handler
_managed_handlers: list = []


class _StdoutLogHandler(logging.StreamHandler):
    """A handler that writes log records to stdout with a [LOG] prefix."""

    def __init__(self):
        super().__init__(sys.stdout)

    def emit(self, record):
        try:
            msg = self.format(record)
            self.stream.write(f"[LOG] {msg}\n")
            self.stream.flush()
        except Exception:
            self.handleError(record)


def _setup_logging(log_dir: str = DEFAULT_LOG_DIR):
    """Configure root logger with stdout handler and file handler.

    只清除本函数之前添加的 handler，不影响其他模块（如 installer、upgrader）
    添加的 handler。
    """
    global _managed_handlers
    root = logging.getLogger()
    root.setLevel(logging.INFO)

    # 只移除本函数之前添加的 handler，保留其他模块的 handler
    for h in _managed_handlers:
        root.removeHandler(h)
        try:
            h.close()
        except Exception:
            pass
    _managed_handlers.clear()

    # Handler 1: stdout with [LOG] prefix
    stdout_handler = _StdoutLogHandler()
    stdout_handler.setFormatter(_log_formatter)
    root.addHandler(stdout_handler)
    _managed_handlers.append(stdout_handler)

    # Handler 2: file log
    try:
        os.makedirs(log_dir, exist_ok=True)
        log_file = os.path.join(log_dir, "skill.log")
        file_handler = logging.FileHandler(log_file, encoding="utf-8")
        file_handler.setFormatter(_log_formatter)
        root.addHandler(file_handler)
        _managed_handlers.append(file_handler)
    except Exception as e:
        root.warning(f"Failed to create file log handler at {log_dir}: {e}")


# Apply a basic stdout-only config immediately so early imports can log.
# The file handler is added later in main() once --log-dir is known.
_setup_logging()

from .skill_registry import get_executor, SKILLS, SKILL_MAP, SkillDefinition, SkillParam
from . import vnc_util
from . import installer
from . import upgrader


# ---------------------------------------------------------------------------
# Type mapping: SkillParam.type -> Python type for argparse
# ---------------------------------------------------------------------------

_TYPE_MAP = {
    "string": str,
    "integer": int,
    "number": float,
    "boolean": None,  # handled specially as store_true / store_false
}


def _add_param_to_parser(parser: argparse.ArgumentParser, param: SkillParam):
    """Add a SkillParam as a CLI flag to an argparse parser."""
    flag = f"--{param.name}"
    py_type = _TYPE_MAP.get(param.type)

    if param.type == "boolean":
        # For boolean params, add --flag (store true) and --no-flag (store false)
        if param.required:
            # Required boolean: user must pass --name or --no-name
            group = parser.add_mutually_exclusive_group(required=True)
            group.add_argument(flag, dest=param.name, action="store_true",
                               help=param.description)
            group.add_argument(f"--no-{param.name}", dest=param.name, action="store_false",
                               help=f"Negate: {param.description}")
        else:
            # Optional boolean: default is None (not passed)
            group = parser.add_mutually_exclusive_group(required=False)
            group.add_argument(flag, dest=param.name, action="store_true", default=None,
                               help=param.description)
            group.add_argument(f"--no-{param.name}", dest=param.name, action="store_false",
                               help=f"Negate: {param.description}")
    else:
        kwargs = {
            "type": py_type,
            "help": param.description,
        }
        if param.required:
            kwargs["required"] = True
        else:
            kwargs["default"] = param.default
            kwargs["required"] = False

        if param.enum:
            kwargs["choices"] = param.enum

        parser.add_argument(flag, **kwargs)


def _build_skill_args(skill: SkillDefinition, parsed: argparse.Namespace) -> dict:
    """Extract skill arguments from parsed CLI namespace."""
    args = {}
    for param in skill.params:
        value = getattr(parsed, param.name, None)
        if value is not None:
            args[param.name] = value
        elif param.required:
            # Should not happen if argparse is configured correctly
            args[param.name] = param.default
    return args


# ---------------------------------------------------------------------------
# List subcommand
# ---------------------------------------------------------------------------

def list_skills():
    """Print all available skills to stdout."""
    print("\n=== Available Browser Skills ===\n")
    for skill in SKILLS:
        params_str = ""
        if skill.params:
            parts = []
            for p in skill.params:
                req = "required" if p.required else f"optional, default={p.default}"
                enum_str = f", choices={p.enum}" if p.enum else ""
                parts.append(f"      --{p.name} ({p.type}, {req}{enum_str}): {p.description}")
            params_str = "\n" + "\n".join(parts)
        print(f"  {skill.name}: {skill.description}{params_str}\n")
    print(f"Total: {len(SKILLS)} skills\n")
    print("Usage: qqbrowser-skill <skill_name> [--param value ...]")
    print("       qqbrowser-skill <skill_name> --help")


# ---------------------------------------------------------------------------
# Execution (via RPC to daemon)
# ---------------------------------------------------------------------------

async def run_skill_via_rpc(skill_name: str, args: dict, log_dir: str = DEFAULT_LOG_DIR):
    """Execute a skill by sending an RPC request to the running daemon.

    The daemon hosts the persistent WebSocket server and browser connection.
    CLI processes are lightweight clients that delegate execution to the daemon.
    """
    from . import rpc_client
    from .daemon_server import read_state_file, DEFAULT_RPC_PORT

    # Determine RPC port from state file
    rpc_port = DEFAULT_RPC_PORT
    state = read_state_file()
    if state and "rpc_port" in state:
        rpc_port = state["rpc_port"]

    result = await rpc_client.execute_skill(
        skill_name=skill_name,
        args=args,
        log_dir=log_dir,
        port=rpc_port,
    )

    if result.get("success"):
        result_json = json.dumps(result["result"], ensure_ascii=False, indent=2)
        print(f"[RESULT] {result_json}")
    else:
        error_msg = result.get("error", "Unknown error")
        print(f"[ERROR] {error_msg}", file=sys.stderr)
        sys.exit(1)


async def run_skill(skill_name: str, args: dict, log_dir: str = DEFAULT_LOG_DIR):
    """Execute a single skill directly (used by MCP server / in-process callers).

    NOTE: This function intentionally does NOT call executor.cleanup().
    The WebSocket server and connections are kept alive across calls so that
    long-lived callers (e.g. MCP server) can reuse the same connection.
    For CLI one-shot usage, cleanup is handled by the caller (main()).
    """
    vnc_util.set_browser_log_dir(log_dir)
    executor = get_executor()
    result = await executor.execute(skill_name, **args)
    result_json = json.dumps(result.to_dict(), ensure_ascii=False, indent=2)
    # Mark result output so callers can distinguish it from log lines
    print(f"[RESULT] {result_json}")


# ---------------------------------------------------------------------------
# Main: build subcommand parser dynamically from skill definitions
# ---------------------------------------------------------------------------

def main():
    # 在 Windows 上强制将 stdout/stderr 设置为 UTF-8 编码，避免中文输出乱码
    if sys.platform == "win32":
        import io
        if hasattr(sys.stdout, "buffer"):
            sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
        if hasattr(sys.stderr, "buffer"):
            sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")

    parser = argparse.ArgumentParser(
        prog="qqbrowser-skill",
        description="QQ Browser Skill CLI — control QQ browser from the command line.",
        usage="qqbrowser-skill <command> [options]",
    )
    parser.add_argument(
        "--log-dir",
        type=str,
        default=DEFAULT_LOG_DIR,
        help=f"Log directory for QQ browser & skill log files (default: {DEFAULT_LOG_DIR})"
    )

    subparsers = parser.add_subparsers(dest="command", help="Command or skill name")

    # 'serve' subcommand — start the persistent daemon service
    serve_parser = subparsers.add_parser(
        "serve",
        help="Start the persistent daemon service (WebSocket + RPC servers)",
        description="Start the persistent daemon service that maintains the WebSocket "
                    "connection to the browser extension and accepts skill requests via RPC.",
    )
    serve_parser.add_argument(
        "--daemon", "-d",
        action="store_true",
        default=False,
        help="Run in background (daemon) mode",
    )
    serve_parser.add_argument(
        "--ws-port",
        type=int,
        default=8765,
        help="WebSocket server port for browser extension (default: 8765)",
    )
    serve_parser.add_argument(
        "--rpc-port",
        type=int,
        default=8766,
        help="HTTP RPC server port for CLI requests (default: 8766)",
    )

    # 'stop' subcommand — stop the running daemon
    subparsers.add_parser(
        "stop",
        help="Stop the running daemon service",
    )

    # 'status' subcommand — check daemon status
    subparsers.add_parser(
        "status",
        help="Check the daemon service status",
    )

    # 'list' subcommand
    subparsers.add_parser("list", help="List all available browser skills")

    # 'debug' subcommand — collect debug info and logs
    debug_parser = subparsers.add_parser(
        "debug",
        help="Collect debug information and log files into a tar.gz archive",
        description="Collect system info, daemon status, and all log files, "
                    "then package them into a .tar.gz archive for troubleshooting.",
    )
    debug_parser.add_argument(
        "--output", "-o",
        type=str,
        default=None,
        help="Output file path for the debug archive (default: auto-generated in /tmp)",
    )

    # 'install' subcommand — install QQ Browser dependency
    install_parser = subparsers.add_parser(
        "install",
        help="Install QQ Browser and system dependencies",
        description="Download and install the QQ Browser package for the current platform (Linux/macOS/Windows).",
    )
    install_parser.add_argument(
        "--dry-run",
        action="store_true",
        default=False,
        help="Show what would be done without actually installing",
    )
    install_parser.add_argument(
        "--force",
        action="store_true",
        default=False,
        help="Force reinstall even if QQ Browser is already installed",
    )

    # One subcommand per skill
    for skill in SKILLS:
        sub = subparsers.add_parser(
            skill.name,
            help=skill.description,
            description=skill.description,
        )
        for param in skill.params:
            _add_param_to_parser(sub, param)

    args = parser.parse_args()

    # Re-setup logging with the user-specified log directory (adds file handler)
    _setup_logging(args.log_dir)

    logger = logging.getLogger(__name__)
    logger.info(f">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> QQBrowserSkill RUN COMMAND >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
    logger.info(f"QQBrowserSkill version: {_get_version()}")
    logger.info(f"Command: {args.command}")
    logger.info(f"Log directory: {args.log_dir}")
    logger.debug(f"Parsed arguments: {vars(args)}")

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    if args.command == "list":
        logger.info("Listing all available skills")
        list_skills()
        sys.exit(0)

    if args.command == "debug":
        logger.info("Running debug info collection")
        from .debug_collector import collect_debug_info
        output_path = collect_debug_info(args.log_dir, args.output)
        print(f"\n✅ Debug archive created: {output_path}")
        sys.exit(0)

    if args.command == "install":
        logger.info(f"Running install (dry_run={args.dry_run}, force={args.force})")
        success = installer.install(dry_run=args.dry_run, log_dir=args.log_dir, force=args.force)
        logger.info(f"Install finished, success={success}")
        sys.exit(0 if success else 1)

    if args.command == "stop":
        logger.info("Stopping daemon")
        from . import daemon_server
        daemon_server.stop_daemon()
        sys.exit(0)

    if args.command == "status":
        logger.info("Checking daemon status")
        _show_status()
        # Periodic update check — status is a natural time to notify the user
        upgrader.periodic_update_check()
        sys.exit(0)

    # Post-upgrade hook: 检测版本变更并自动执行更新后逻辑（重装浏览器等）。
    # 必须在浏览器安装检查和 serve 版本检查之前执行，
    # 否则升级后首次运行时浏览器版本检查会提前退出，导致 hook 永远无法执行。
    upgrader.run_post_upgrade_hook(args.log_dir)

    # 除 install、list、stop、status 外的所有命令，先检查浏览器是否已安装
    if not installer.is_installed():
        logger.error("QQ Browser is not installed")
        print("❌ QQ Browser is not installed.", file=sys.stderr)
        print("   Please run 'qqbrowser-skill install' first to install QQ Browser.", file=sys.stderr)
        sys.exit(1)

    # serve 命令：检查浏览器是否需要更新
    if args.command == "serve":
        needs_upgrade, current_version, _ = installer.needs_upgrade()
        logger.info(f"QQ Browser needs upgrade: {needs_upgrade}")
        if needs_upgrade:
            logger.error(f"QQ Browser needs update (current: {current_version})")
            if current_version:
                print(f"❌ QQ Browser version {current_version} needs update.", file=sys.stderr)
            else:
                print("❌ QQ Browser is not installed.", file=sys.stderr)
            print("   Please run 'qqbrowser-skill install' first.", file=sys.stderr)
            sys.exit(1)

        # 检查并关闭正在运行的浏览器进程
        installer.close_browser_process()

    # Periodic update check before executing browser-related commands (serve + skills).
    # This runs at most once every 10 minutes (controlled by state file),
    # so the network overhead is negligible for most invocations.
    upgrader.periodic_update_check()

    # Force upgrade check: block browser-related commands (serve + skills) if the
    # user's version is outdated beyond the threshold (FORCE_UPGRADE_THRESHOLD_DAYS).
    force_required, force_msg = upgrader.is_force_upgrade_required()
    if force_required:
        logger.error("Force upgrade required, blocking skill execution")
        print(force_msg, file=sys.stderr)
        sys.exit(1)

    if args.command == "serve":
        from . import daemon_server
        if args.daemon:
            logger.info(f"Starting daemon in background mode (ws_port={args.ws_port}, rpc_port={args.rpc_port})")
            daemon_server.start_daemon_background(
                ws_port=args.ws_port,
                rpc_port=args.rpc_port,
                log_dir=args.log_dir,
            )
        else:
            logger.info(f"Starting daemon in foreground mode (ws_port={args.ws_port}, rpc_port={args.rpc_port})")
            daemon_server.start_daemon_foreground(
                ws_port=args.ws_port,
                rpc_port=args.rpc_port,
                log_dir=args.log_dir,
            )
        sys.exit(0)

    # Execute skill via RPC to daemon
    skill = SKILL_MAP.get(args.command)
    if not skill:
        logger.error(f"Unknown skill: {args.command}")
        print(f"Unknown skill: {args.command}", file=sys.stderr)
        sys.exit(1)

    skill_args = _build_skill_args(skill, args)
    logger.info(f"Executing skill '{skill.name}' via RPC with args: {skill_args}")
    try:
        asyncio.run(run_skill_via_rpc(skill.name, skill_args, args.log_dir))
        logger.info(f"Skill '{skill.name}' executed successfully")
    except Exception as e:
        # 导入放在这里避免循环引用问题
        from .rpc_client import DaemonNotRunningError, RPCError
        if isinstance(e, DaemonNotRunningError):
            logger.error("Daemon is not running")
            print("❌ Daemon is not running.", file=sys.stderr)
            print("   Please run 'qqbrowser-skill serve --daemon' first to start the daemon service.", file=sys.stderr)
            sys.exit(1)
        elif isinstance(e, RPCError):
            logger.error(f"RPC call failed: {e}")
            print(f"❌ RPC call failed: {e}", file=sys.stderr)
            sys.exit(1)
        else:
            logger.exception(f"Unexpected error while executing skill '{skill.name}'")
            raise


def _show_status():
    """Show the current daemon status.

    综合使用状态文件、进程扫描和端口探测来判断 daemon 状态。
    """
    from . import daemon_server
    from . import rpc_client

    info = daemon_server.get_daemon_info()
    if info is None:
        # 所有检测手段都未发现 daemon
        # 清理可能残留的状态文件
        if daemon_server.read_state_file() is not None:
            daemon_server.remove_state_file()
        print("❌ Daemon is not running.")
        print("   Start it with: qqbrowser-skill serve")
        return

    pid = info.get("pid", "?")
    ws_port = info.get("ws_port", "?")
    rpc_port = info.get("rpc_port", "?")

    # 如果状态文件不存在但进程/端口检测到了 daemon，给出提示
    state = daemon_server.read_state_file()
    source_hint = ""
    if state is None:
        source_hint = " (detected via process scan, state file missing)"

    print(f"✅ Daemon is running{source_hint}")
    print(f"   PID              : {pid}")
    print(f"   WebSocket Server : ws://127.0.0.1:{ws_port}")
    print(f"   HTTP RPC Server  : http://127.0.0.1:{rpc_port}")

    # Try to get detailed status via RPC
    try:
        status = asyncio.run(rpc_client.get_status(port=int(rpc_port)))
        clients = status.get("connected_clients", 0)
        print(f"   Connected clients: {clients}")
    except Exception:
        print("   (Could not retrieve detailed status via RPC)")


if __name__ == "__main__":
    main()
