#!/usr/bin/env python3
"""pi-ignore installer — installs the `~/.pi/bin/pi` wrapper + verifies the
monkey-patch can take effect. Idempotent; safe to re-run after pi upgrades.

Usage:
  python3 install.py            install or refresh (idempotent)
  python3 install.py --check    verify only
  python3 install.py --uninstall
  python3 install.py --fix      re-install after a pi upgrade
  python3 install.py --quiet    silent install (npm postinstall friendly)
"""

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
PATCH = ROOT / "extensions" / "pi-ignore" / "patch.mjs"
ARBITER = ROOT / "extensions" / "pi-ignore" / "arbiter.mjs"
PI_BIN_DIR = Path.home() / ".pi" / "bin"
WRAPPER = PI_BIN_DIR / "pi"
REAL_RUNNER = ROOT / ".pi-real-runner.js"
VERSION_FILE = ROOT / ".pi-version.json"
EXT_LINK = Path.home() / ".pi" / "agent" / "extensions" / "pi-ignore"
EXT_SRC = ROOT / "extensions" / "pi-ignore"


def sh(cmd: list[str], **kw) -> subprocess.CompletedProcess:
    return subprocess.run(cmd, capture_output=True, text=True, **kw)


def find_pi_pkg_root() -> Path | None:
    """Locate @earendil-works/pi-coding-agent package root."""
    candidates = []
    for cmd in (["pnpm", "root", "-g"], ["npm", "root", "-g"]):
        r = sh(cmd)
        if r.returncode == 0 and r.stdout.strip():
            candidates.append(Path(r.stdout.strip()))
    # `which pi` shim (pnpm/npm shims are shell scripts with a resolve line)
    r = sh(["which", "pi"])
    if r.returncode == 0:
        shim = Path(r.stdout.strip())
        candidates.append(shim.parent)  # e.g. ~/Library/pnpm (symlink farm)
    for base in candidates:
        if not base.exists():
            continue
        for p in base.rglob("pi-coding-agent"):
            if p.is_dir() and (p / "dist" / "cli.js").exists():
                return p
    return None


def pi_version(pkg_root: Path) -> str:
    pkg = pkg_root / "package.json"
    try:
        return json.loads(pkg.read_text(encoding="utf-8")).get("version", "unknown")
    except Exception:
        return "unknown"


def write_wrapper(pkg_root: Path) -> None:
    cli = pkg_root / "dist" / "cli.js"
    PI_BIN_DIR.mkdir(parents=True, exist_ok=True)
    body = f"""#!/bin/sh
# pi-ignore wrapper — managed by {ROOT}/install.py, re-run to regenerate.
# Preloads the pi-ignore patch so .piignore rules apply to every pi session.
export PI_IGNORE_PI_CLI="{cli}"
export PI_IGNORE_PATCH="{PATCH}"
exec node --import "$PI_IGNORE_PATCH" "$PI_IGNORE_PI_CLI" "$@"
"""
    WRAPPER.write_text(body, encoding="utf-8")
    os.chmod(WRAPPER, 0o755)


def write_runner_ref(pkg_root: Path) -> None:
    runner = pkg_root / "dist" / "core" / "extensions" / "runner.js"
    REL = ROOT
    REAL_RUNNER.write_text(
        f'export {{ ExtensionRunner }} from "{runner}";\n', encoding="utf-8"
    )


def write_version(pkg_root: Path) -> None:
    VERSION_FILE.write_text(
        json.dumps({"version": pi_version(pkg_root), "pkgRoot": str(pkg_root)}),
        encoding="utf-8",
    )


def verify() -> list[str]:
    """Run the real preload path and check the patch takes effect. Returns issues."""
    issues = []
    if not PATCH.exists():
        return [f"missing {PATCH}"]
    script = (
        "import { healthCheck } from "
        + json.dumps("file://" + str(ARBITER.resolve()))
        + "; const h = await healthCheck(); "
        + "console.log(JSON.stringify({ok: h.ok, issues: h.issues, version: h.version}));"
    )
    env = os.environ
    r = sh(
        ["node", "--import", str(PATCH), "--input-type=module", "-e", script],
        env=env,
    )
    if r.returncode != 0:
        issues.append(f"node preload failed: {r.stderr.strip()[:300]}")
    else:
        try:
            out = json.loads(r.stdout.strip().splitlines()[-1])
            if not out.get("ok"):
                issues.extend(out.get("issues", []))
        except Exception as e:
            issues.append(f"verify parse failed: {e}")
    return issues


def path_note() -> str | None:
    """Note when the wrapper is not ahead of `pi` on PATH (optional now).

    The extension self-loads the patch at startup, so the wrapper is just
    redundant insurance (preloads the patch even before extensions load).
    No PATH changes are required for pi-ignore to work.
    """
    cur = sh(["which", "pi"])
    if cur.returncode != 0:
        return None  # pi not on PATH (nothing wraps)
    if WRAPPER.exists() and Path(cur.stdout.strip()) == WRAPPER:
        return None
    return (
        f"  • 提示：`which pi` 是 {cur.stdout.strip()}（wrapper 在 {WRAPPER}）。"
        "扩展已自载补丁，无需改 PATH；"
        f"如需 wrapper 双保险，把 {PI_BIN_DIR} 放到 PATH 最前。"
    )


def link_extension() -> None:
    """Symlink the pi extension into ~/.pi/agent/extensions/ (global)."""
    EXT_LINK.parent.mkdir(parents=True, exist_ok=True)
    if EXT_LINK.is_symlink() and os.path.realpath(EXT_LINK) == str(EXT_SRC.resolve()):
        return
    if EXT_LINK.exists() or EXT_LINK.is_symlink():
        EXT_LINK.unlink()  # stale or wrong target
    EXT_LINK.symlink_to(EXT_SRC, target_is_directory=True)


def install() -> int:
    quiet = "--quiet" in sys.argv
    pkg_root = find_pi_pkg_root()
    if not pkg_root:
        if not quiet:
            print("✗ 找不到 pi-coding-agent 包（pnpm/npm root -g 与 which pi 均失败）")
        return 1
    write_wrapper(pkg_root)
    write_runner_ref(pkg_root)
    write_version(pkg_root)
    link_extension()
    issues = verify()
    if not quiet:
        print(f"✓ wrapper: {WRAPPER}")
        print(f"✓ patch:   {PATCH}")
        print(f"✓ 扩展:    {EXT_LINK} → {EXT_SRC}")
        print(f"✓ pi 版本: {pi_version(pkg_root)} ({pkg_root})")
    if issues:
        if not quiet:
            print("⚠️  patch 验证未通过（fail-open：pi 正常，规则不生效）：")
            for i in issues:
                print(f"   • {i}")
            print(f"   • 修复: python3 {ROOT / 'install.py'} --fix")
        return 2
    if not quiet:
        print("✓ patch 验证通过：health check OK")
    note = path_note()
    if note and not quiet:
        print(note)
    return 0 if not issues else 2


def uninstall() -> int:
    removed = []
    for p in (WRAPPER, REAL_RUNNER, VERSION_FILE, EXT_LINK):
        if p.exists():
            p.unlink()
            removed.append(p)
    if removed:
        print("✓ 已移除: " + ", ".join(str(p) for p in removed))
    else:
        print("（无已安装内容）")
    note = path_note()
    if note:
        print("提示：如需彻底恢复，从 shell rc 中移除 ~/.pi/bin 的 PATH 行。")
    return 0


def main() -> int:
    args = sys.argv[1:]
    if "--uninstall" in args:
        return uninstall()
    if "--check" in args:
        pkg_root = find_pi_pkg_root()
        if not pkg_root:
            print("✗ 找不到 pi-coding-agent")
            return 1
        issues = verify()
        if issues:
            print("⚠️  patch 未生效：")
            for i in issues:
                print(f"   • {i}")
            return 2
        print(f"✓ patch 生效（pi {pi_version(pkg_root)}）")
        return 0
    return install()


if __name__ == "__main__":
    sys.exit(main())
