"""
qqbrowser-skill auto-upgrade module.

Provides the following capabilities:
  - Query PyPI for the latest version
  - Daily version check frequency control
  - Stop browser processes and daemon service
  - Upgrade Python package (pip install --upgrade)
  - Force reinstall QQ Browser
"""

import json
import logging
import os
import subprocess
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Tuple

logger = logging.getLogger(__name__)

# PyPI query URL
PYPI_JSON_URL = "https://pypi.org/pypi/qqbrowser-skill/json"

# Check interval: 24 hours (seconds)
CHECK_INTERVAL_SECONDS = 86400

# Force upgrade threshold: if the latest version was released more than
# this many days ago and the user is still on an older version, browser
# skill commands will be blocked until the user runs 'upgrade'.
FORCE_UPGRADE_THRESHOLD_DAYS = 15

# Upgrade log file name
UPGRADE_LOG_FILE = "upgrade.log"

# Module-level file handler reference for cleanup
_upgrade_file_handler: logging.FileHandler | None = None


def _setup_upgrade_logging(log_dir: str):
    """Add a dedicated file handler to persist upgrade logs to *log_dir*/upgrade.log.

    The handler is attached to the root logger so that all sub-loggers
    (including those in child modules) benefit.
    """
    global _upgrade_file_handler

    try:
        os.makedirs(log_dir, exist_ok=True)
        log_path = os.path.join(log_dir, UPGRADE_LOG_FILE)
        _upgrade_file_handler = logging.FileHandler(log_path, encoding="utf-8")
        _upgrade_file_handler.setFormatter(
            logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
        )
        logging.getLogger().addHandler(_upgrade_file_handler)
        logger.info("Upgrade log file: %s", log_path)
    except Exception as exc:
        print(f"⚠️  Could not create upgrade log file in {log_dir}: {exc}", file=sys.stderr)


def _teardown_upgrade_logging():
    """Remove the upgrade-specific file handler (if any)."""
    global _upgrade_file_handler
    if _upgrade_file_handler is not None:
        _upgrade_file_handler.flush()
        _upgrade_file_handler.close()
        logging.getLogger().removeHandler(_upgrade_file_handler)
        _upgrade_file_handler = None


def _log_and_print(msg: str, level: int = logging.INFO):
    """Print *msg* to stdout AND write it to the upgrade log."""
    print(msg)
    logger.log(level, msg)


# ---------------------------------------------------------------------------
# Version check frequency control
# ---------------------------------------------------------------------------

def _get_check_state_path() -> str:
    """返回版本检查状态文件路径。

    存储在与 daemon state 相同的目录中：
      - Windows: %LOCALAPPDATA%/qqbrowser-skill/upgrade_check.json
      - Unix:    ~/.qqbrowser-skill/upgrade_check.json
    """
    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 os.path.join(state_dir, "upgrade_check.json")


def _read_check_state() -> dict:
    """读取版本检查状态。"""
    path = _get_check_state_path()
    if not os.path.exists(path):
        return {}
    try:
        with open(path, "r") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError):
        return {}


def _write_check_state(state: dict):
    """写入版本检查状态。"""
    path = _get_check_state_path()
    try:
        with open(path, "w") as f:
            json.dump(state, f)
    except OSError as e:
        logger.warning(f"Failed to write version check state file: {e}")


def _should_check_today() -> bool:
    """判断今天是否需要检查更新（每天最多一次）。"""
    state = _read_check_state()
    last_check = state.get("last_check_timestamp", 0)
    now = time.time()
    return (now - last_check) >= CHECK_INTERVAL_SECONDS


def _mark_checked(latest_version: Optional[str] = None, upload_time: Optional[str] = None):
    """标记今天已完成版本检查。"""
    state = _read_check_state()
    state["last_check_timestamp"] = time.time()
    state["last_check_date"] = datetime.now(timezone.utc).isoformat()
    if latest_version:
        state["latest_version"] = latest_version
    if upload_time:
        state["latest_upload_time"] = upload_time
    _write_check_state(state)


# ---------------------------------------------------------------------------
# PyPI version query
# ---------------------------------------------------------------------------

def _get_current_version() -> str:
    """获取当前安装的 qqbrowser-skill 版本。"""
    try:
        from importlib.metadata import version
        return version("qqbrowser-skill")
    except Exception:
        return "0.0.0"


def _fetch_latest_version() -> Optional[str]:
    """从 PyPI 查询 qqbrowser-skill 的最新版本号。

    Returns:
        最新版本号字符串，查询失败时返回 None。
    """
    import urllib.request
    import urllib.error

    try:
        req = urllib.request.Request(
            PYPI_JSON_URL,
            headers={"Accept": "application/json", "User-Agent": "qqbrowser-skill-upgrader"},
        )
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return data.get("info", {}).get("version")
    except (urllib.error.URLError, urllib.error.HTTPError, OSError, json.JSONDecodeError, KeyError) as e:
        logger.warning(f"Failed to query PyPI for latest version: {e}")
        return None


def _compare_versions(current: str, latest: str) -> int:
    """比较两个版本号。

    Returns:
        -1 表示 current < latest（有新版本）
         0 表示相同
         1 表示 current > latest
    """
    def _parse(v: str):
        try:
            return tuple(int(x) for x in v.split("."))
        except (ValueError, AttributeError):
            return (0,)

    c = _parse(current)
    l = _parse(latest)

    if c < l:
        return -1
    elif c > l:
        return 1
    return 0


def check_for_update() -> Tuple[bool, str, str]:
    """检查是否有新版本可用。

    Returns:
        (has_update, current_version, latest_version)
    """
    current = _get_current_version()
    latest = _fetch_latest_version()

    if latest is None:
        return False, current, "unknown"

    has_update = _compare_versions(current, latest) < 0
    return has_update, current, latest


def _fetch_latest_version_upload_time() -> Optional[str]:
    """Fetch the upload time of the latest version from PyPI.

    Returns:
        ISO-8601 upload time string (e.g. '2025-06-01T12:00:00'), or None on failure.
    """
    import urllib.request
    import urllib.error

    try:
        req = urllib.request.Request(
            PYPI_JSON_URL,
            headers={"Accept": "application/json", "User-Agent": "qqbrowser-skill-upgrader"},
        )
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            latest_version = data.get("info", {}).get("version")
            if not latest_version:
                return None
            # Get the upload time of the latest version's first file
            releases = data.get("releases", {})
            version_files = releases.get(latest_version, [])
            if version_files:
                return version_files[0].get("upload_time")
            return None
    except (urllib.error.URLError, urllib.error.HTTPError, OSError,
            json.JSONDecodeError, KeyError, IndexError) as e:
        logger.warning(f"Failed to fetch latest version upload time from PyPI: {e}")
        return None


def is_force_upgrade_required() -> Tuple[bool, str]:
    """Check whether the user's version is outdated beyond the force-upgrade threshold.

    Logic:
      1. If no update is available (user is on latest), return False.
      2. Query PyPI for the upload time of the latest version.
      3. If (now - upload_time) > FORCE_UPGRADE_THRESHOLD_DAYS AND user is
         still on an older version, return True.

    Returns:
        (is_required, message) — *message* is a human-readable explanation
        suitable for printing to stderr.
    """
    # First, use cached state to avoid hitting PyPI on every call.
    # The daily_update_check already refreshes the state file once per day.
    state = _read_check_state()
    cached_latest = state.get("latest_version")
    cached_upload_time = state.get("latest_upload_time")

    current = _get_current_version()

    # If no cached data yet, we cannot determine — allow execution.
    if not cached_latest:
        return False, ""

    # If user is already on the latest version, no block.
    if _compare_versions(current, cached_latest) >= 0:
        return False, ""

    # We know there is a newer version; check how old it is.
    if cached_upload_time:
        try:
            upload_dt = datetime.fromisoformat(cached_upload_time.replace("Z", "+00:00"))
            # 确保 upload_dt 是 offset-aware；缓存的时间字符串可能不含时区信息
            if upload_dt.tzinfo is None:
                upload_dt = upload_dt.replace(tzinfo=timezone.utc)
            now_utc = datetime.now(timezone.utc)
            days_since_release = (now_utc - upload_dt).days

            if days_since_release >= FORCE_UPGRADE_THRESHOLD_DAYS:
                msg = (
                    "❌ Browser skill commands are blocked until you upgrade.\n"
                    "   Run: 'qqbrowser-skill upgrade'"
                )
                return True, msg
        except (ValueError, TypeError) as e:
            logger.warning(f"Failed to parse cached upload time '{cached_upload_time}': {e}")

    return False, ""


# ---------------------------------------------------------------------------
# Daily update check (called on serve startup)
# ---------------------------------------------------------------------------

def daily_update_check():
    """每日一次的更新检查。如果有新版本，打印提示信息。

    仅在距离上次检查超过 24 小时时执行检查。
    此函数不会阻塞或执行升级操作，仅打印提示。
    """
    if not _should_check_today():
        return

    try:
        has_update, current, latest = check_for_update()

        if has_update:
            # Also fetch and cache the upload time for force-upgrade checks
            upload_time = _fetch_latest_version_upload_time()
            _mark_checked(latest, upload_time=upload_time)

            print(f"\n⚠️  A new version of qqbrowser-skill is available: {current} → {latest}")
            print(f"   Run 'qqbrowser-skill upgrade' to upgrade\n")
            logger.info(f"New version available: {current} -> {latest}")
        else:
            _mark_checked(latest)
    except Exception as e:
        logger.warning(f"Daily update check failed: {e}")
        # 即使检查失败也标记为已检查，避免反复重试
        _mark_checked()


# ---------------------------------------------------------------------------
# Stop browser and daemon service
# ---------------------------------------------------------------------------

def _stop_daemon_service(_daemon_server=None) -> bool:
    """停止 qqbrowser-skill daemon 服务。

    Args:
        _daemon_server: Pre-imported daemon_server module to avoid loading
                        new code after pip upgrade.

    Returns:
        True 表示成功停止或本来就没有在运行。
    """
    if _daemon_server is None:
        from . import daemon_server as _daemon_server

    daemon_server = _daemon_server

    if not daemon_server.is_daemon_running():
        _log_and_print("ℹ️  Daemon service is not running, skipping stop step")
        return True

    _log_and_print("🛑 Stopping daemon service...")
    success = daemon_server.stop_daemon()
    if success:
        # Wait for process to fully exit
        time.sleep(1)
        if not daemon_server.is_daemon_running():
            _log_and_print("✅ Daemon service stopped")
            return True
        else:
            _log_and_print("⚠️  Daemon service may not have fully exited, waiting...")
            time.sleep(2)
            return not daemon_server.is_daemon_running()
    else:
        _log_and_print("⚠️  Failed to stop daemon service, continuing upgrade...", logging.WARNING)
        return True


def _stop_browser_processes() -> bool:
    """停止所有 QQ Browser 进程。

    Returns:
        True 表示成功停止或没有正在运行的浏览器进程。
    """
    from .constants import BROWSER_PROCESS_NAME

    _log_and_print(f"🛑 Stopping QQ Browser processes ({BROWSER_PROCESS_NAME})...")

    if sys.platform == "win32":
        return _stop_browser_windows(BROWSER_PROCESS_NAME)
    else:
        return _stop_browser_unix(BROWSER_PROCESS_NAME)


def _stop_browser_unix(process_name: str) -> bool:
    """Unix 平台下停止浏览器进程。

    注意：使用 pkill -f 时会匹配所有命令行包含 process_name 的进程，
    这可能误杀当前正在执行升级的 Python 进程（例如 qqbrowser-skill upgrade）。
    因此需要通过 --ignore-ancestors (如果可用) 或手动过滤排除自身进程。
    """
    my_pid = str(os.getpid())

    def _find_browser_pids() -> list:
        """查找浏览器进程 PID，排除当前进程及其子进程。"""
        try:
            result = subprocess.run(
                ["pgrep", "-f", process_name],
                capture_output=True, text=True
            )
            if result.returncode != 0:
                return []
            pids = [p.strip() for p in result.stdout.strip().splitlines() if p.strip()]
            # 排除当前进程自身及 pgrep 产生的短暂进程
            excluded = {my_pid}
            return [p for p in pids if p not in excluded]
        except FileNotFoundError:
            return []

    try:
        target_pids = _find_browser_pids()

        if not target_pids:
            _log_and_print("ℹ️  No running QQ Browser processes found")
            return True

        # 使用 kill 对特定 PID 发送 SIGTERM，避免 pkill -f 误杀自身
        _log_and_print(f"   Sending SIGTERM to browser PIDs: {', '.join(target_pids)}")
        for pid in target_pids:
            try:
                os.kill(int(pid), 15)  # SIGTERM
            except (ProcessLookupError, PermissionError) as e:
                logger.warning(f"Failed to send SIGTERM to PID {pid}: {e}")

        _log_and_print("   Termination signal sent, waiting for browser to exit...")
        time.sleep(2)

        # 检查是否还有残留进程
        remaining_pids = _find_browser_pids()
        if not remaining_pids:
            _log_and_print("✅ All QQ Browser processes stopped")
            return True
        else:
            # 对残留进程发送 SIGKILL 强制终止
            _log_and_print("⚠️  Browser processes still running, force killing...", logging.WARNING)
            for pid in remaining_pids:
                try:
                    os.kill(int(pid), 9)  # SIGKILL
                except (ProcessLookupError, PermissionError) as e:
                    logger.warning(f"Failed to send SIGKILL to PID {pid}: {e}")
            time.sleep(1)
            _log_and_print("✅ QQ Browser processes force killed")
            return True
    except Exception as e:
        logger.warning(f"Failed to stop browser processes: {e}")
        # 回退到 killall（killall 按进程名匹配，不会匹配 python 进程）
        try:
            subprocess.run(["killall", process_name], capture_output=True)
            time.sleep(2)
            _log_and_print("✅ QQ Browser processes stopped")
            return True
        except FileNotFoundError:
            logger.warning("killall is not available")
            return False


def _stop_browser_windows(process_name: str) -> bool:
    """Windows 平台下停止浏览器进程。"""
    try:
        result = subprocess.run(
            ["taskkill", "/F", "/IM", process_name],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            _log_and_print("✅ QQ Browser processes stopped")
            time.sleep(1)
            return True
        elif "not found" in result.stderr.lower() or result.returncode == 128:
            _log_and_print("ℹ️  No running QQ Browser processes found")
            return True
        else:
            logger.warning(f"taskkill returned: {result.returncode}, stderr: {result.stderr}")
            return True
    except FileNotFoundError:
        logger.warning("taskkill is not available")
        return False


# ---------------------------------------------------------------------------
# Upgrade execution
# ---------------------------------------------------------------------------

def _upgrade_pip_package() -> bool:
    """使用 pipx 升级 qqbrowser-skill 包。

    Returns:
        True 表示升级成功。
    """
    _log_and_print("📦 Upgrading qqbrowser-skill via pipx...")
    try:
        result = subprocess.run(
            ["pipx", "upgrade", "qqbrowser-skill"],
            capture_output=True, text=True
        )
        # Always log the full pipx output for troubleshooting
        if result.stdout:
            logger.info("[pipx stdout] %s", result.stdout.rstrip())
        if result.stderr:
            logger.info("[pipx stderr] %s", result.stderr.rstrip())

        if result.returncode == 0:
            _log_and_print("✅ Python package upgraded successfully via pipx")
            if result.stdout:
                # Print key installation info to terminal
                for line in result.stdout.splitlines():
                    if "upgraded" in line.lower() or "already" in line.lower():
                        _log_and_print(f"   {line.strip()}")
            return True
        else:
            _log_and_print(f"❌ pipx upgrade failed (exit code: {result.returncode})", logging.ERROR)
            if result.stderr:
                _log_and_print(f"   Error: {result.stderr.strip()}", logging.ERROR)
            return False
    except FileNotFoundError:
        _log_and_print("❌ pipx not found. Please install pipx first: https://pipx.pypa.io/", logging.ERROR)
        return False
    except Exception as e:
        _log_and_print(f"❌ pipx upgrade error: {e}", logging.ERROR)
        return False


def _force_reinstall_browser(log_dir: str, _installer=None) -> bool:
    """强制重新安装 QQ Browser（跳过已安装检查）。

    Args:
        log_dir: Log directory path.
        _installer: Pre-imported installer module to avoid loading
                    new code after pip upgrade.

    Returns:
        True 表示安装成功。
    """
    if _installer is None:
        from . import installer as _installer
    installer = _installer

    _log_and_print("📦 Reinstalling QQ Browser...")
    try:
        success = installer.install(dry_run=False, log_dir=log_dir, force=True)
        if success:
            _log_and_print("✅ QQ Browser installed successfully")
        else:
            _log_and_print("❌ QQ Browser installation failed", logging.ERROR)
        return success
    except Exception as e:
        _log_and_print(f"❌ QQ Browser installation error: {e}", logging.ERROR)
        return False


# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------

def upgrade(log_dir: str, skip_browser: bool = False) -> bool:
    """执行完整的升级流程。

    流程：
      1. 查询 PyPI 最新版本
      2. 如果有新版本：
         a. 停止 daemon 服务
         b. 停止 QQ Browser 进程
         c. 升级 Python 包
         d. 重装 QQ Browser（除非 skip_browser=True）
      3. 提示用户重新启动服务

    Args:
        log_dir: 日志目录。
        skip_browser: 如果为 True，跳过浏览器重装步骤。

    Returns:
        True 表示整个升级流程成功。
    """
    # Pre-import modules that will be used AFTER pip upgrade (Step 3/4).
    # This ensures we use the currently-loaded (old) code throughout the
    # entire upgrade process, avoiding incompatibility if the new version
    # changes function signatures or module structure.
    from . import daemon_server as _daemon_server
    from . import installer as _installer

    # Set up dedicated upgrade log file
    _setup_upgrade_logging(log_dir)

    try:
        _log_and_print("🔍 Checking for the latest version of qqbrowser-skill...")
        has_update, current, latest = check_for_update()

        if not has_update:
            _log_and_print(f"✅ Already up to date: {current}")
            _mark_checked(latest)
            return True

        _log_and_print(f"📢 New version available: {current} → {latest}")
        _log_and_print("")

        # Step 1: Stop daemon service
        _log_and_print("=" * 50)
        _log_and_print("Step 1/4: Stop daemon service")
        _log_and_print("=" * 50)
        _stop_daemon_service(_daemon_server=_daemon_server)
        _log_and_print("")

        # Step 2: Stop browser processes
        _log_and_print("=" * 50)
        _log_and_print("Step 2/4: Stop QQ Browser processes")
        _log_and_print("=" * 50)
        _stop_browser_processes()
        _log_and_print("")

        # Step 3: Upgrade Python package
        _log_and_print("=" * 50)
        _log_and_print("Step 3/4: Upgrade qqbrowser-skill Python package")
        _log_and_print("=" * 50)
        pkg_success = _upgrade_pip_package()
        if not pkg_success:
            _log_and_print("\n❌ Python package upgrade failed, aborting upgrade", logging.ERROR)
            return False
        _log_and_print("")

        # Step 4: Reinstall browser
        _log_and_print("=" * 50)
        _log_and_print("Step 4/4: Reinstall QQ Browser")
        _log_and_print("=" * 50)
        if skip_browser:
            _log_and_print("ℹ️  Skipped browser reinstallation (--skip-browser)")
        else:
            browser_success = _force_reinstall_browser(log_dir, _installer=_installer)
            if not browser_success:
                _log_and_print("\n⚠️  QQ Browser installation failed, but Python package was upgraded successfully", logging.WARNING)
                _log_and_print("   You can retry later with 'qqbrowser-skill install --force'")
        _log_and_print("")

        # Mark as checked
        _mark_checked(latest)

        # Completion message
        _log_and_print("=" * 50)
        _log_and_print("✅ Upgrade complete!")
        _log_and_print("=" * 50)
        new_version = _get_current_version()
        _log_and_print(f"   Current version: {new_version}")
        _log_and_print(f"   Run 'qqbrowser-skill serve --daemon' to restart the service")
        _log_and_print("")

        return True
    finally:
        # Always clean up the upgrade file handler
        _teardown_upgrade_logging()
