#!/usr/bin/env python3
"""后台启动后端与前端。"""

from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request
import webbrowser
from pathlib import Path

ROOT = Path(__file__).resolve().parent
BACKEND = ROOT / "backend"
FRONTEND = ROOT / "frontend"
RUN_DIR = ROOT / ".run"
PID_FILE = RUN_DIR / "pids.json"
LOG_FILE = RUN_DIR / "launch.log"
LOCK_FILE = RUN_DIR / "launcher.lock"

CREATE_NO_WINDOW = 0x08000000 if sys.platform == "win32" else 0
APP_URL = "http://127.0.0.1:5173"


def log(msg: str) -> None:
    RUN_DIR.mkdir(exist_ok=True)
    line = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n"
    with LOG_FILE.open("a", encoding="utf-8") as f:
        f.write(line)


def show_error(message: str) -> None:
    log(f"ERROR: {message}")
    if sys.platform == "win32":
        try:
            import ctypes

            ctypes.windll.user32.MessageBoxW(  # type: ignore[attr-defined]
                0, message, "DeepSeek 对话智能体 - 启动失败", 0x10
            )
        except Exception:
            pass


def child_env() -> dict[str, str]:
    env = os.environ.copy()
    for node_dir in [
        Path(r"C:\Program Files\nodejs"),
        Path.home() / "AppData" / "Roaming" / "npm",
    ]:
        if node_dir.exists():
            env["PATH"] = str(node_dir) + os.pathsep + env.get("PATH", "")
    return env


def ensure_env() -> None:
    env_path = ROOT / ".env"
    example = ROOT / ".env.example"
    if not env_path.exists() and example.exists():
        shutil.copy(example, env_path)


def hidden_popen(cmd: str, cwd: Path) -> subprocess.Popen:
    return subprocess.Popen(
        cmd,
        cwd=str(cwd),
        shell=True,
        env=child_env(),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
        creationflags=CREATE_NO_WINDOW,
    )


def check_urls(urls: list[str], timeout: float = 2) -> bool:
    for url in urls:
        try:
            with urllib.request.urlopen(url, timeout=timeout) as resp:
                if resp.status == 200:
                    return True
        except (urllib.error.URLError, TimeoutError, OSError):
            continue
    return False


def wait_urls(urls: list[str], timeout: float = 60) -> bool:
    deadline = time.time() + timeout
    while time.time() < deadline:
        if check_urls(urls, timeout=2):
            return True
        time.sleep(0.5)
    return False


def services_ready() -> bool:
    return check_urls(
        ["http://127.0.0.1:8000/health", "http://localhost:8000/health"]
    ) and check_urls(["http://127.0.0.1:5173", "http://localhost:5173"])


def save_pids(backend_pid: int, frontend_pid: int) -> None:
    RUN_DIR.mkdir(exist_ok=True)
    PID_FILE.write_text(
        json.dumps({"backend": backend_pid, "frontend": frontend_pid}),
        encoding="utf-8",
    )


def find_npm() -> str | None:
    env = child_env()
    npm = shutil.which("npm", path=env.get("PATH"))
    if npm:
        return npm
    for candidate in [
        Path(r"C:\Program Files\nodejs\npm.cmd"),
        Path.home() / "AppData" / "Roaming" / "npm" / "npm.cmd",
    ]:
        if candidate.exists():
            return str(candidate)
    return None


def stop_existing() -> None:
    stop_script = ROOT / "stop.py"
    if stop_script.exists():
        subprocess.run(
            [sys.executable, str(stop_script), "--quiet"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            creationflags=CREATE_NO_WINDOW,
        )
        time.sleep(2)


def main() -> None:
    RUN_DIR.mkdir(exist_ok=True)
    log("=" * 40)
    log(f"开始启动 (python={sys.executable})")

    if LOCK_FILE.exists():
        if services_ready():
            log("服务已在运行，直接打开浏览器")
            webbrowser.open(APP_URL)
            return
        age = time.time() - LOCK_FILE.stat().st_mtime
        if age < 120:
            log("等待上一次启动完成...")
            for _ in range(20):
                time.sleep(1)
                if services_ready():
                    log("服务已就绪，打开浏览器")
                    webbrowser.open(APP_URL)
                    return
            show_error("启动超时，请运行「诊断.bat」查看原因。")
            return
        LOCK_FILE.unlink(missing_ok=True)
    LOCK_FILE.write_text(str(os.getpid()), encoding="utf-8")

    try:
        ensure_env()

        if services_ready():
            log("服务已在运行，直接打开浏览器")
            webbrowser.open(APP_URL)
            return

        stop_existing()

        log("安装/检查 Python 依赖...")
        pip_result = subprocess.run(
            [sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "-q"],
            cwd=BACKEND,
            env=child_env(),
            capture_output=True,
            text=True,
            creationflags=CREATE_NO_WINDOW,
        )
        if pip_result.returncode != 0:
            show_error(
                "Python 依赖安装失败。\n\n"
                f"{pip_result.stderr[-500:]}\n\n详情见：{LOG_FILE}"
            )
            return

        if not (FRONTEND / "node_modules").exists():
            npm = find_npm()
            if not npm:
                show_error(
                    "未找到 Node.js / npm。\n\n请先安装：https://nodejs.org"
                )
                return
            log("安装前端依赖...")
            result = subprocess.run(
                f'"{npm}" install',
                cwd=FRONTEND,
                shell=True,
                env=child_env(),
                capture_output=True,
                text=True,
                creationflags=CREATE_NO_WINDOW,
            )
            if result.returncode != 0:
                show_error(
                    "前端依赖安装失败。\n\n"
                    f"{result.stderr[-500:]}\n\n详情见：{LOG_FILE}"
                )
                return

        log("启动后端...")
        backend = hidden_popen(
            f'"{sys.executable}" -m uvicorn main:app --host 127.0.0.1 --port 8000',
            BACKEND,
        )
        if not wait_urls(
            ["http://127.0.0.1:8000/health", "http://localhost:8000/health"],
            timeout=60,
        ):
            backend.terminate()
            show_error(
                "后端启动失败（8000 端口）。\n\n"
                f"请查看日志：{LOG_FILE}"
            )
            return

        npm = find_npm()
        if not npm:
            backend.terminate()
            show_error("未找到 npm，请安装 Node.js：https://nodejs.org")
            return

        log("启动前端...")
        frontend = hidden_popen(f'"{npm}" run dev -- --host 127.0.0.1', FRONTEND)
        if not wait_urls(
            ["http://127.0.0.1:5173", "http://localhost:5173"],
            timeout=90,
        ):
            frontend.terminate()
            backend.terminate()
            show_error(
                "前端启动失败（5173 端口）。\n\n"
                f"请查看日志：{LOG_FILE}"
            )
            return

        save_pids(backend.pid, frontend.pid)
        log("启动成功，打开浏览器")
        webbrowser.open(APP_URL)

    except Exception as exc:
        log(f"异常: {exc}")
        show_error(f"启动异常：{exc}\n\n详情见：{LOG_FILE}")
    finally:
        LOCK_FILE.unlink(missing_ok=True)


if __name__ == "__main__":
    main()
