from __future__ import annotations

import argparse
import json
import os
import re
import shutil
import socket
import subprocess
import sys
import time
from pathlib import Path
from urllib.parse import urlparse
from urllib import request
from urllib.error import URLError


MAC_SERVICE_LABEL = "com.codexmobile.mac-service"
MAC_TUNNEL_LABEL = "com.codexmobile.mac-tunnel"
DESKTOP_BRIDGE_LABEL = "com.codexmobile.desktop-bridge"
LEGACY_MAC_SERVICE_LABEL = "com.codexbot.mac-service"
LEGACY_MAC_TUNNEL_LABEL = "com.codexbot.mac-tunnel"
LEGACY_DESKTOP_BRIDGE_LABEL = "com.codexbot.desktop-bridge"
DEFAULT_RELAY_API_URL = "https://your-vps-domain.example"
DEFAULT_MAC_SERVICE_PORT = 8765
DEFAULT_VPS_TUNNEL_PORT = 18765
DEFAULT_DESKTOP_BRIDGE_PORT = 8766
DEFAULT_CODEX_APP_PATH = "/Applications/Codex.app"
DEFAULT_CODEX_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex"
RECOMMENDED_CODEX_VERSION = (0, 130, 0)
RECOMMENDED_CODEX_VERSION_LABEL = "0.130.0+；ChatGPT 遠程連接建議使用最新版 Codex App"


def _project_root() -> Path:
    return Path(__file__).resolve().parents[2]


def _gui_domain() -> str:
    return f"gui/{os.getuid()}"


def _run_launchctl(*args: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        ["/bin/launchctl", *args],
        capture_output=True,
        text=True,
        check=False,
    )


def _service_target(label: str) -> str:
    return f"{_gui_domain()}/{label}"


def _launch_agent_path(label: str) -> Path:
    return Path.home() / "Library" / "LaunchAgents" / f"{label}.plist"


def _first_available_label(primary: str, legacy: str) -> str:
    primary_status = _run_launchctl("print", _service_target(primary))
    if primary_status.returncode == 0 or _launch_agent_path(primary).exists():
        return primary
    return legacy


def _service_status(primary: str, legacy: str) -> tuple[subprocess.CompletedProcess[str], str]:
    primary_status = _run_launchctl("print", _service_target(primary))
    if primary_status.returncode == 0:
        return primary_status, primary
    legacy_status = _run_launchctl("print", _service_target(legacy))
    if legacy_status.returncode == 0:
        return legacy_status, legacy
    return primary_status, primary if _launch_agent_path(primary).exists() else legacy


def _load_env_value(key: str) -> str | None:
    env_path = _project_root() / ".env"
    if not env_path.exists():
        return None
    prefix = f"{key}="
    for line in env_path.read_text(encoding="utf-8").splitlines():
        if line.startswith(prefix):
            return line[len(prefix) :].strip()
    return None


def _print_result(title: str, ok: bool, detail: str) -> None:
    status = "OK" if ok else "FAIL"
    print(f"[{status}] {title}: {detail}")


def _codex_bin_path() -> str | None:
    configured = _load_env_value("CODEXMOBILE_LIVE_CODEX_BIN") or _load_env_value("CODEXBOT_LIVE_CODEX_BIN")
    candidates: list[str] = []
    if configured:
        candidates.append(configured)
    candidates.append(DEFAULT_CODEX_APP_BIN)
    resolved = shutil.which("codex")
    if resolved:
        candidates.append(resolved)

    for candidate in candidates:
        if Path(candidate).exists():
            return candidate
        resolved_candidate = shutil.which(candidate)
        if resolved_candidate:
            return resolved_candidate
    return None


def _run_codex(codex_bin: str, *args: str, timeout: float = 4.0) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        [codex_bin, *args],
        capture_output=True,
        text=True,
        check=False,
        timeout=timeout,
    )


def _parse_codex_version(raw: str) -> tuple[int, int, int] | None:
    match = re.search(r"(\d+)\.(\d+)\.(\d+)", raw)
    if not match:
        return None
    return tuple(int(part) for part in match.groups())


def _codex_cli_status() -> tuple[bool, str, str | None]:
    codex_bin = _codex_bin_path()
    if not codex_bin:
        return False, f"找不到 Codex CLI；建議安裝 Codex {RECOMMENDED_CODEX_VERSION_LABEL}", None

    try:
        result = _run_codex(codex_bin, "--version")
    except (OSError, subprocess.TimeoutExpired) as exc:
        return False, f"無法執行 {codex_bin}: {exc}", codex_bin

    output = (result.stdout or result.stderr or "").strip()
    version = _parse_codex_version(output)
    ok = result.returncode == 0 and version is not None and version >= RECOMMENDED_CODEX_VERSION
    if result.returncode != 0:
        detail = output or "Codex CLI 無回應"
    elif version is None:
        detail = f"{output or '未知版本'}（無法解析版本；建議 {RECOMMENDED_CODEX_VERSION_LABEL}）"
    else:
        detail = f"{output}（建議 {RECOMMENDED_CODEX_VERSION_LABEL}；{codex_bin}）"
    return ok, detail, codex_bin


def _codex_remote_control_status(codex_bin: str | None) -> tuple[bool, str]:
    if not codex_bin:
        return False, "未找到 Codex CLI，暫不能檢查 remote-control"
    try:
        result = _run_codex(codex_bin, "remote-control", "--help")
    except (OSError, subprocess.TimeoutExpired) as exc:
        return False, f"檢查失敗：{exc}"

    output = f"{result.stdout}\n{result.stderr}".strip()
    supported = result.returncode == 0 and "remote-control" in output
    if supported:
        return True, "已支持 codex remote-control；目前仍使用穩定的 app-server 橋接鏈路"
    return False, "暫未支持 codex remote-control；現有橋接鏈路仍可使用"


def _is_macos_process_running(process_name: str) -> bool:
    result = subprocess.run(
        ["/usr/bin/pgrep", "-x", process_name],
        capture_output=True,
        text=True,
        check=False,
    )
    return result.returncode == 0


def _codex_app_status() -> tuple[bool, bool, str]:
    app_path = Path(DEFAULT_CODEX_APP_PATH)
    installed = app_path.exists()
    running = _is_macos_process_running("Codex")
    if installed and running:
        return True, True, f"已安裝並正在運行（{DEFAULT_CODEX_APP_PATH}）"
    if installed:
        return True, False, "已安裝但未運行；官方 ChatGPT 手機連接需要先打開 Codex App"
    return False, False, f"未找到 {DEFAULT_CODEX_APP_PATH}；請先安裝最新版 Codex App"


def _chatgpt_remote_ready_status(codex_app_installed: bool, codex_app_running: bool) -> tuple[bool, str]:
    if codex_app_installed and codex_app_running:
        return True, "基本條件已就緒；請在 Codex App 側邊欄選 Set up Codex mobile，掃 QR 後用 ChatGPT App 完成連接"
    if codex_app_installed:
        return False, "Codex App 已安裝但未運行；打開 Codex App 後再從側邊欄啟動手機連接"
    return False, "需要先安裝並登入最新版 Codex App；官方手機連接流程不從 CLI 或 IDE 啟動"


def _is_port_open(host: str, port: int, timeout: float = 2.0) -> bool:
    try:
        with socket.create_connection((host, port), timeout=timeout):
            return True
    except OSError:
        return False


def _relay_api_url() -> str:
    return (_load_env_value("CODEXMOBILE_PUBLIC_API_URL") or DEFAULT_RELAY_API_URL).rstrip("/")


def _vps_host() -> str:
    configured_host = _load_env_value("CODEXMOBILE_VPS_HOST") or _load_env_value("CODEXBOT_VPS_HOST")
    if configured_host:
        return configured_host
    relay_host = urlparse(_relay_api_url()).hostname
    return relay_host or ""


def _is_url_ok(url: str, timeout: float = 4.0) -> bool:
    try:
        with request.urlopen(url, timeout=timeout) as response:
            return 200 <= response.status < 500
    except (OSError, URLError):
        return False


def _wait_for_port(host: str, port: int, timeout_seconds: float = 6.0, step_seconds: float = 0.4) -> bool:
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        if _is_port_open(host, port, timeout=step_seconds):
            return True
        time.sleep(step_seconds)
    return _is_port_open(host, port, timeout=step_seconds)


def status_command(args: argparse.Namespace) -> int:
    service, service_label = _service_status(MAC_SERVICE_LABEL, LEGACY_MAC_SERVICE_LABEL)
    tunnel, tunnel_label = _service_status(MAC_TUNNEL_LABEL, LEGACY_MAC_TUNNEL_LABEL)
    bridge, bridge_label = _service_status(DESKTOP_BRIDGE_LABEL, LEGACY_DESKTOP_BRIDGE_LABEL)

    service_ok = service.returncode == 0
    tunnel_ok = tunnel.returncode == 0
    bridge_ok = bridge.returncode == 0
    local_ok = _wait_for_port("127.0.0.1", args.local_port)
    desktop_ok = _wait_for_port("127.0.0.1", args.desktop_port)
    vps_ok = _is_port_open(args.vps_host, 22) if args.vps_host else False
    relay_url = _relay_api_url()
    relay_ok = _is_url_ok(f"{relay_url}/health")
    codex_cli_ok, codex_cli_detail, codex_bin = _codex_cli_status()
    remote_control_ok, remote_control_detail = _codex_remote_control_status(codex_bin)
    codex_app_ok, codex_app_running, codex_app_detail = _codex_app_status()
    chatgpt_remote_ok, chatgpt_remote_detail = _chatgpt_remote_ready_status(codex_app_ok, codex_app_running)

    _print_result("Mac 常駐服務", service_ok, f"已載入（{service_label}）" if service_ok else service.stderr.strip() or "未載入")
    _print_result("Mac 隧道", tunnel_ok, f"已載入（{tunnel_label}）" if tunnel_ok else tunnel.stderr.strip() or "未載入")
    _print_result("Desktop Bridge", bridge_ok, f"已載入（{bridge_label}）" if bridge_ok else bridge.stderr.strip() or "未載入")
    _print_result("本機服務埠", local_ok, f"127.0.0.1:{args.local_port} 可連線" if local_ok else f"127.0.0.1:{args.local_port} 無回應")
    _print_result("桌面橋接埠", desktop_ok, f"127.0.0.1:{args.desktop_port} 可連線" if desktop_ok else f"127.0.0.1:{args.desktop_port} 無回應")
    _print_result(
        "SSH 隧道主機",
        vps_ok,
        f"{args.vps_host}:22 可連線" if vps_ok else ("未配置" if not args.vps_host else f"{args.vps_host}:22 無回應"),
    )
    _print_result("Relay API", relay_ok, f"{relay_url} 可連線" if relay_ok else f"{relay_url} 無回應")
    _print_result("Codex CLI", codex_cli_ok, codex_cli_detail)
    _print_result("Codex Remote Control", remote_control_ok, remote_control_detail)
    _print_result("Codex App", codex_app_ok, codex_app_detail)
    _print_result("ChatGPT Remote Ready", chatgpt_remote_ok, chatgpt_remote_detail)
    return 0 if service_ok and tunnel_ok and bridge_ok and local_ok and desktop_ok else 1


def refresh_command(args: argparse.Namespace) -> int:
    steps = [
        ("Mac 常駐服務", _first_available_label(MAC_SERVICE_LABEL, LEGACY_MAC_SERVICE_LABEL)),
        ("Mac 反向隧道", _first_available_label(MAC_TUNNEL_LABEL, LEGACY_MAC_TUNNEL_LABEL)),
        ("Desktop Bridge", _first_available_label(DESKTOP_BRIDGE_LABEL, LEGACY_DESKTOP_BRIDGE_LABEL)),
    ]
    failed = False
    for title, label in steps:
        check = _run_launchctl("print", _service_target(label))
        if check.returncode == 0:
            result = _run_launchctl("kickstart", "-k", _service_target(label))
            ok = result.returncode == 0
            detail = "已重新啟動" if ok else result.stderr.strip() or result.stdout.strip() or "重啟失敗"
        else:
            plist = _launch_agent_path(label)
            if plist.exists():
                result = _run_launchctl("bootstrap", _gui_domain(), str(plist))
                if result.returncode == 0:
                    start_result = _run_launchctl("kickstart", "-k", _service_target(label))
                    ok = start_result.returncode == 0
                    detail = "已重新載入並啟動" if ok else start_result.stderr.strip() or start_result.stdout.strip() or "啟動失敗"
                else:
                    ok = False
                    detail = result.stderr.strip() or result.stdout.strip() or "載入失敗"
            else:
                result = check
                ok = False
                detail = f"找不到啟動檔：{plist}"
        failed = failed or not ok
        _print_result(title, ok, detail)

    local_ok = _wait_for_port("127.0.0.1", args.local_port, timeout_seconds=12.0, step_seconds=0.5)
    _print_result("本機服務埠", local_ok, f"127.0.0.1:{args.local_port} 可連線" if local_ok else f"127.0.0.1:{args.local_port} 無回應")
    desktop_ok = _wait_for_port("127.0.0.1", args.desktop_port, timeout_seconds=12.0, step_seconds=0.5)
    _print_result("桌面橋接埠", desktop_ok, f"127.0.0.1:{args.desktop_port} 可連線" if desktop_ok else f"127.0.0.1:{args.desktop_port} 無回應")

    ports_failed = not (local_ok and desktop_ok)
    if args.vps_host:
        vps_ok = _is_port_open(args.vps_host, 22)
        _print_result("SSH 隧道主機", vps_ok, f"{args.vps_host}:22 可連線" if vps_ok else f"{args.vps_host}:22 無回應")
        failed = failed or not vps_ok
    relay_url = _relay_api_url()
    relay_ok = _is_url_ok(f"{relay_url}/health")
    _print_result("Relay API", relay_ok, f"{relay_url} 可連線" if relay_ok else f"{relay_url} 無回應")

    return 1 if failed or ports_failed else 0


def stop_command(args: argparse.Namespace) -> int:
    labels = [
        MAC_SERVICE_LABEL,
        MAC_TUNNEL_LABEL,
        DESKTOP_BRIDGE_LABEL,
        LEGACY_MAC_SERVICE_LABEL,
        LEGACY_MAC_TUNNEL_LABEL,
        LEGACY_DESKTOP_BRIDGE_LABEL,
    ]
    for label in labels:
        _run_launchctl("bootout", _service_target(label))

    kill_patterns = [
        "codexmobile-mac-service",
        "codexmobile-desktop-bridge",
        "codexbot-mac-service",
        "codexbot-desktop-bridge",
        "127.0.0.1:18765:127.0.0.1:8765",
    ]
    for pattern in kill_patterns:
        subprocess.run(["/usr/bin/pkill", "-f", pattern], capture_output=True, text=True, check=False)

    for port in (args.local_port, args.desktop_port):
        subprocess.run(
            ["/bin/bash", "-lc", f"lsof -ti tcp:{port} | xargs kill -9 2>/dev/null || true"],
            capture_output=True,
            text=True,
            check=False,
        )

    _print_result("背景服務", True, "已停止 Mac 服務、桌面橋接與反向隧道")
    return 0


def _post_local_json(port: int, path: str, payload: dict, token: str) -> dict:
    body = json.dumps(payload).encode("utf-8")
    req = request.Request(
        f"http://127.0.0.1:{port}{path}",
        data=body,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with request.urlopen(req, timeout=5) as response:
        return json.loads(response.read().decode("utf-8"))


def reply_command(args: argparse.Namespace) -> int:
    text = " ".join(args.text).strip()
    if not text:
        print("請提供要回覆到桌面橋接的內容。", file=sys.stderr)
        return 2

    bridge_token = (
        _load_env_value("CODEXMOBILE_DESKTOP_BRIDGE_TOKEN")
        or _load_env_value("CODEXBOT_DESKTOP_BRIDGE_TOKEN")
        or _load_env_value("CODEXMOBILE_MAC_SERVICE_TOKEN")
        or _load_env_value("CODEXBOT_MAC_SERVICE_TOKEN")
    )
    service_token = _load_env_value("CODEXMOBILE_MAC_SERVICE_TOKEN") or _load_env_value("CODEXBOT_MAC_SERVICE_TOKEN")
    if not bridge_token or not service_token:
        print("找不到桌面橋接或 Mac 服務 token，請檢查 .env。", file=sys.stderr)
        return 2

    try:
        _post_local_json(args.desktop_port, "/reply", {"text": text}, bridge_token)
        _print_result("桌面橋接回覆", True, "已送出到手機端消息流")
    except Exception as exc:
        _print_result("桌面橋接回覆", False, str(exc))
        return 1

    try:
        state = _post_local_json(
            args.local_port,
            "/task/chat_entry",
            {"author": "assistant", "text": f"[桌面協作] {text}"},
            service_token,
        )
        title = state.get("state", {}).get("title") or "目前任務"
        _print_result("項目聊天同步", True, f"已寫入 {title}")
    except Exception as exc:
        _print_result("項目聊天同步", False, str(exc))
        return 1

    return 0


def print_shortcut_hint() -> None:
    root = _project_root()
    print()
    print("建議做法：")
    print(f"1. 在 Mac 上執行 `cd {root} && source .venv/bin/activate && codexmobile-mac-client refresh`")
    print("2. 或把這條命令包成 Automator / 捷徑，換網後點一下刷新。")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="CodexMobile Mac 網路切換後的本地服務/隧道刷新工具")
    parser.add_argument("--vps-host", default=_vps_host(), help="VPS 主機地址")
    parser.add_argument("--local-port", type=int, default=DEFAULT_MAC_SERVICE_PORT, help="Mac 本地服務埠")
    parser.add_argument("--remote-port", type=int, default=DEFAULT_VPS_TUNNEL_PORT, help="VPS 反向隧道埠")
    parser.add_argument("--desktop-port", type=int, default=DEFAULT_DESKTOP_BRIDGE_PORT, help="桌面橋接服務埠")

    subparsers = parser.add_subparsers(dest="command", required=True)
    subparsers.add_parser("status", help="查看 Mac 服務與隧道狀態")
    subparsers.add_parser("refresh", help="重啟 Mac 服務與隧道，適合換網後執行")
    subparsers.add_parser("stop", help="停止 Mac 服務、桌面橋接與反向隧道")
    reply_parser = subparsers.add_parser("reply", help="回覆桌面橋接訊息，並同步寫入目前項目聊天")
    reply_parser.add_argument("text", nargs="+", help="要回覆給手機端的內容")
    return parser


def main() -> None:
    parser = build_parser()
    args = parser.parse_args()

    if args.command == "status":
        code = status_command(args)
    elif args.command == "refresh":
        code = refresh_command(args)
    elif args.command == "stop":
        code = stop_command(args)
    elif args.command == "reply":
        code = reply_command(args)
    else:
        parser.error(f"Unknown command: {args.command}")
        return

    print_shortcut_hint()
    raise SystemExit(code)


if __name__ == "__main__":
    main()
