#!/usr/bin/env python3
"""Install the Pay-Switch Agent plugin as a local ``payagent`` command.

The installer is intentionally dependency-free. When it is executed from inside
the repository it copies the local plugin directory. When it is executed through
the public Pay-Switch one-liner, it downloads a PayAgent plugin archive and
extracts only ``plugins/pay-switch-agent``.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import platform
import shutil
import site
import stat
import subprocess
import tempfile
import urllib.request
import zipfile
from pathlib import Path
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

REPO_ZIP_URL = "https://payswitch.clawhunt.store/api/payagent/plugin.zip"
ARCHIVE_PLUGIN_PREFIX = "payswitch-main/plugins/pay-switch-agent/"
DEFAULT_CONFIG_URL = "https://payswitch.clawhunt.store/api/payagent/config"
DEFAULT_PANEL_URL = "https://payswitch.clawhunt.store"
DEFAULT_BOOTSTRAP_URL = "https://payswitch.clawhunt.store/api/payagent/bootstrap"
TOKEN_KEYCHAIN_SERVICE = "pay-switch-agent"
TOKEN_KEYCHAIN_ACCOUNT_ENV = "PAY_SWITCH_AGENT_TOKEN_ACCOUNT"
DEFAULT_PLUGIN_ID = "pay-switch-agent"
DEFAULT_DEVICE_ID = "local-device"


def user_base_bin() -> Path:
    if os.name == "nt":
        return Path(site.getuserbase()) / "Scripts"
    return Path.home() / ".local" / "bin"


def default_install_dir() -> Path:
    return Path.home() / ".payagent" / "pay-switch-agent"


def current_platform_tag() -> str:
    system = platform.system().lower() or "unknown"
    machine = platform.machine().lower()
    machine = machine.replace("x86_64", "amd64").replace("aarch64", "arm64")
    return f"{system}-{machine}"


def payagent_package_url(url: str) -> str:
    parsed = urlsplit(url)
    if parsed.scheme not in {"http", "https"}:
        return url
    if not parsed.path.endswith("/api/payagent/plugin.zip"):
        return url
    query_pairs = parse_qsl(parsed.query, keep_blank_values=True)
    if any(key == "platform" for key, _value in query_pairs):
        return url
    query = urlencode([*query_pairs, ("platform", current_platform_tag())])
    return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, query, parsed.fragment))


def is_plugin_root(path: Path) -> bool:
    return (
        (path / ".codex-plugin" / "plugin.json").exists()
        and (path / "scripts" / "payswitch_agent.py").exists()
        and (path / "flows" / "ai-media-payment-chain.json").exists()
    )


def local_plugin_root() -> Path | None:
    file_name = globals().get("__file__")
    if not file_name:
        return None
    here = Path(file_name).resolve().parent
    return here if is_plugin_root(here) else None


def copytree_clean(source: Path, target: Path, *, force: bool) -> None:
    if target.exists():
        if not force:
            raise SystemExit(
                f"{target} already exists. Re-run with --force to replace it."
            )
        shutil.rmtree(target)
    ignore = shutil.ignore_patterns(
        "__pycache__",
        "*.pyc",
        ".pytest_cache",
        "dist",
        "build",
        "*.egg-info",
    )
    shutil.copytree(source, target, ignore=ignore)


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def verify_package_checksums(root: Path) -> None:
    checksum_path = root / "checksums.sha256"
    if not checksum_path.exists():
        return
    for raw in checksum_path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        expected, _, rel_path = line.partition("  ")
        if not expected or not rel_path:
            raise SystemExit(f"Invalid checksum line in {checksum_path}: {raw}")
        candidate = (root / rel_path).resolve()
        if not str(candidate).startswith(str(root.resolve()) + os.sep):
            raise SystemExit(f"Checksum path escapes package root: {rel_path}")
        if not candidate.exists() or not candidate.is_file():
            raise SystemExit(f"Checksum file missing: {rel_path}")
        actual = sha256_file(candidate)
        if actual != expected:
            raise SystemExit(f"Checksum mismatch for {rel_path}")


def restore_package_executables(root: Path) -> None:
    if os.name == "nt":
        return
    manifest_path = root / "package-manifest.json"
    if not manifest_path.exists():
        return
    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise SystemExit(f"Invalid package manifest: {exc}") from exc
    root_resolved = root.resolve()
    executable_paths = {
        str(manifest.get("protected_binary") or "").strip(),
        str(manifest.get("protected_sidecar_binary") or "").strip(),
        str(manifest.get("superclaw_sidecar") or "").strip(),
    }
    for relative_path in sorted(path for path in executable_paths if path):
        candidate = (root / relative_path).resolve()
        if not str(candidate).startswith(str(root_resolved) + os.sep):
            raise SystemExit(f"Executable path escapes package root: {relative_path}")
        if not candidate.exists() or not candidate.is_file():
            raise SystemExit(f"Executable file missing: {relative_path}")
        candidate.chmod(
            candidate.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
        )


def extract_plugin_archive(archive: Path, extracted: Path) -> None:
    extracted.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(archive) as zf:
        names = zf.namelist()
        if ".codex-plugin/plugin.json" in names and "install.py" in names:
            members = names
            prefix = ""
        else:
            members = [name for name in names if name.startswith(ARCHIVE_PLUGIN_PREFIX)]
            prefix = ARCHIVE_PLUGIN_PREFIX
        if not members:
            raise SystemExit("Plugin archive did not contain a Pay-Switch Agent plugin")
        for name in members:
            rel = name.removeprefix(prefix)
            if not rel:
                continue
            destination = extracted / rel
            if not str(destination.resolve()).startswith(str(extracted.resolve()) + os.sep):
                raise SystemExit(f"Plugin archive path escapes target: {name}")
            if name.endswith("/"):
                destination.mkdir(parents=True, exist_ok=True)
                continue
            destination.parent.mkdir(parents=True, exist_ok=True)
            with zf.open(name) as src, destination.open("wb") as dst:
                shutil.copyfileobj(src, dst)


def download_plugin(target: Path, *, force: bool, repo_zip_url: str) -> None:
    with tempfile.TemporaryDirectory(prefix="payagent-install-") as tmp:
        tmp_path = Path(tmp)
        archive = tmp_path / "payswitch-main.zip"
        urllib.request.urlretrieve(payagent_package_url(repo_zip_url), archive)
        extracted = tmp_path / "plugin"
        extract_plugin_archive(archive, extracted)
        copytree_clean(extracted, target, force=force)


def install_plugin(
    source: Path | None,
    target: Path,
    *,
    force: bool,
    repo_zip_url: str,
) -> str:
    if source is not None:
        source = source.resolve()
        if not is_plugin_root(source):
            raise SystemExit(f"{source} is not a Pay-Switch Agent plugin root")
        verify_package_checksums(source)
        copytree_clean(source, target, force=force)
        verify_package_checksums(target)
        restore_package_executables(target)
        return "local"

    local = local_plugin_root()
    if local is not None:
        verify_package_checksums(local)
        copytree_clean(local, target, force=force)
        verify_package_checksums(target)
        restore_package_executables(target)
        return "local"

    download_plugin(target, force=force, repo_zip_url=repo_zip_url)
    verify_package_checksums(target)
    restore_package_executables(target)
    return "archive"


def request_bootstrap_token(bootstrap_url: str) -> dict[str, str]:
    if not bootstrap_url:
        return {}
    req = urllib.request.Request(
        bootstrap_url,
        data=b"{}",
        headers={"Accept": "application/json", "Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            payload = json.loads(resp.read().decode("utf-8"))
    except Exception as exc:
        print(f"bootstrap: skipped ({exc})")
        return {}
    if not isinstance(payload, dict):
        return {}
    token = str(payload.get("token") or "").strip()
    if not token:
        return {}
    return {
        "PAY_SWITCH_AGENT_TOKEN": token,
        "PAY_SWITCH_ALLOW_LIVE": "1",
    }


def merge_env_lines(existing: list[str], updates: dict[str, str]) -> list[str]:
    if not updates:
        return existing
    seen: set[str] = set()
    merged: list[str] = []
    for line in existing:
        key = (
            line.split("=", 1)[0].strip()
            if "=" in line and not line.lstrip().startswith("#")
            else ""
        )
        if key in updates:
            merged.append(f"{key}={updates[key]}")
            seen.add(key)
        else:
            merged.append(line)
    for key, value in updates.items():
        if key not in seen:
            merged.append(f"{key}={value}")
    return merged


def write_env(
    home: Path,
    *,
    config_url: str,
    panel_url: str,
    bootstrap_env: dict[str, str],
) -> Path:
    home.mkdir(parents=True, exist_ok=True)
    env_path = home / "payagent.env"
    if env_path.exists():
        lines = env_path.read_text(encoding="utf-8").splitlines()
        env_path.write_text(
            "\n".join(merge_env_lines(lines, bootstrap_env)) + "\n",
            encoding="utf-8",
        )
        try:
            env_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
        except OSError:
            pass
        return env_path
    lines = [
        f"PAY_SWITCH_CONFIG_URL={config_url}",
        f"PAY_SWITCH_PANEL_URL={panel_url}",
    ]
    if bootstrap_env:
        lines.extend(
            [
                "PAY_SWITCH_AGENT_TOKEN=" + bootstrap_env["PAY_SWITCH_AGENT_TOKEN"],
                "PAY_SWITCH_ALLOW_LIVE=1",
            ]
        )
    else:
        lines.extend(
            [
                "# PAY_SWITCH_AGENT_TOKEN=<auto-provisioned by installer when available>",
                "# PAY_SWITCH_ALLOW_LIVE=1",
            ]
        )
    env_path.write_text("\n".join([*lines, ""]), encoding="utf-8")
    try:
        env_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
    except OSError:
        pass
    return env_path


def read_env_file(path: Path) -> dict[str, str]:
    if not path.exists():
        return {}
    values: dict[str, str] = {}
    for raw in path.read_text(encoding="utf-8").splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        if key.strip():
            values[key.strip()] = value.strip().strip("\"'")
    return values


def keychain_account(env_values: dict[str, str]) -> str:
    return env_values.get(TOKEN_KEYCHAIN_ACCOUNT_ENV) or (
        f"{env_values.get('PAY_SWITCH_PLUGIN_ID') or DEFAULT_PLUGIN_ID}:"
        f"{env_values.get('PAY_SWITCH_DEVICE_ID') or DEFAULT_DEVICE_ID}"
    )


def delete_macos_keychain_token(account: str) -> None:
    if platform.system() != "Darwin":
        return
    try:
        subprocess.run(
            [
                "security",
                "delete-generic-password",
                "-s",
                TOKEN_KEYCHAIN_SERVICE,
                "-a",
                account,
            ],
            check=False,
            capture_output=True,
            text=True,
        )
    except OSError:
        return


def run_installed_auth_revoke(install_dir: Path, env_path: Path) -> int:
    core = install_dir / "bin" / ("payagent-core.exe" if os.name == "nt" else "payagent-core")
    script = install_dir / "scripts" / "payswitch_agent.py"
    env = os.environ.copy()
    if env_path.exists():
        env.update(read_env_file(env_path))
    if core.exists():
        cmd = [str(core), "auth", "revoke", "--json"]
    elif script.exists():
        python = shutil.which("python3") or shutil.which("python") or "python"
        cmd = [python, str(script), "auth", "revoke", "--json"]
    else:
        return 127
    result = subprocess.run(cmd, check=False, env=env, capture_output=True, text=True)
    return result.returncode


def unlink_if_exists(path: Path) -> bool:
    if not path.exists():
        return False
    path.unlink()
    return True


def uninstall_plugin(
    install_dir: Path,
    bin_dir: Path,
    *,
    env_path: Path,
    revoke: bool,
    remove_env: bool,
) -> dict[str, object]:
    env_values = read_env_file(env_path)
    revoke_exit = run_installed_auth_revoke(install_dir, env_path) if revoke else None
    account = keychain_account(env_values)
    delete_macos_keychain_token(account)
    removed_wrappers = []
    for name in ("payagent", "payagent.cmd", "payagent.ps1"):
        wrapper = bin_dir / name
        if unlink_if_exists(wrapper):
            removed_wrappers.append(str(wrapper))
    removed_plugin = False
    if install_dir.exists():
        shutil.rmtree(install_dir)
        removed_plugin = True
    removed_env = False
    if remove_env and env_path.exists():
        env_path.unlink()
        removed_env = True
    return {
        "removed_plugin": removed_plugin,
        "removed_wrappers": removed_wrappers,
        "removed_env": removed_env,
        "credential_account": account,
        "revoke_exit": revoke_exit,
    }


def shell_quote(value: str) -> str:
    return "'" + value.replace("'", "'\"'\"'") + "'"


def write_unix_wrapper(path: Path, install_dir: Path, env_path: Path) -> None:
    content = f"""#!/usr/bin/env sh
PAYAGENT_PLUGIN_DIR={shell_quote(str(install_dir))}
PAYAGENT_ENV={shell_quote(str(env_path))}
export PAYAGENT_PLUGIN_DIR
export PAYAGENT_ENV
if [ -f "$PAYAGENT_ENV" ]; then
  set -a
  . "$PAYAGENT_ENV"
  set +a
fi
if [ -x "$PAYAGENT_PLUGIN_DIR/bin/payagent-core" ]; then
  exec "$PAYAGENT_PLUGIN_DIR/bin/payagent-core" "$@"
fi
if command -v python3 >/dev/null 2>&1; then
  exec python3 "$PAYAGENT_PLUGIN_DIR/scripts/payswitch_agent.py" "$@"
fi
exec python "$PAYAGENT_PLUGIN_DIR/scripts/payswitch_agent.py" "$@"
"""
    path.write_text(content, encoding="utf-8")
    path.chmod(path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)


def write_windows_wrapper(path: Path, install_dir: Path, env_path: Path) -> None:
    content = f"""@echo off
set "PAYAGENT_PLUGIN_DIR={install_dir}"
set "PAYAGENT_ENV={env_path}"
if exist "%PAYAGENT_ENV%" (
  for /f "usebackq tokens=1,* delims==" %%A in ("%PAYAGENT_ENV%") do (
    echo %%A| findstr /b "#" >nul
    if errorlevel 1 set "%%A=%%B"
  )
)
if exist "%PAYAGENT_PLUGIN_DIR%\\bin\\payagent-core.exe" (
  "%PAYAGENT_PLUGIN_DIR%\\bin\\payagent-core.exe" %*
) else (
  python "%PAYAGENT_PLUGIN_DIR%\\scripts\\payswitch_agent.py" %*
)
"""
    path.write_text(content, encoding="utf-8")


def write_wrappers(bin_dir: Path, install_dir: Path, env_path: Path) -> list[Path]:
    bin_dir.mkdir(parents=True, exist_ok=True)
    wrappers: list[Path] = []
    if os.name == "nt":
        cmd = bin_dir / "payagent.cmd"
        write_windows_wrapper(cmd, install_dir, env_path)
        wrappers.append(cmd)
        ps1 = bin_dir / "payagent.ps1"
        install_dir_ps = str(install_dir).replace("'", "''")
        env_path_ps = str(env_path).replace("'", "''")
        ps1.write_text(
            "\n".join(
                [
                    f"$env:PAYAGENT_PLUGIN_DIR = '{install_dir_ps}'",
                    f"$env:PAYAGENT_ENV = '{env_path_ps}'",
                    "if (Test-Path $env:PAYAGENT_ENV) {",
                    "  Get-Content $env:PAYAGENT_ENV | ForEach-Object {",
                    "    if ($_ -and -not $_.StartsWith('#') -and $_.Contains('=')) {",
                    "      $k,$v = $_.Split('=',2)",
                    "      [Environment]::SetEnvironmentVariable($k, $v, 'Process')",
                    "    }",
                    "  }",
                    "}",
                    "$core = Join-Path $env:PAYAGENT_PLUGIN_DIR 'bin\\payagent-core.exe'",
                    "if (Test-Path $core) { & $core @args }",
                    (
                        "else { python "
                        "\"$env:PAYAGENT_PLUGIN_DIR\\scripts\\payswitch_agent.py\" @args }"
                    ),
                    "",
                ]
            ),
            encoding="utf-8",
        )
        wrappers.append(ps1)
    else:
        wrapper = bin_dir / "payagent"
        write_unix_wrapper(wrapper, install_dir, env_path)
        wrappers.append(wrapper)
    return wrappers


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Install the Pay-Switch Agent plugin")
    parser.add_argument(
        "--uninstall",
        action="store_true",
        help="Remove the installed PayAgent plugin and local credential references",
    )
    parser.add_argument(
        "--revoke",
        action="store_true",
        help="During --uninstall, call payagent auth revoke before deleting local files",
    )
    parser.add_argument(
        "--remove-env",
        action="store_true",
        help="During --uninstall, also remove ~/.payagent/payagent.env",
    )
    parser.add_argument(
        "--source",
        type=Path,
        help="Copy from a local plugin root instead of GitHub",
    )
    parser.add_argument("--install-dir", type=Path, default=default_install_dir())
    parser.add_argument("--bin-dir", type=Path, default=user_base_bin())
    parser.add_argument(
        "--config-url",
        default=os.environ.get("PAY_SWITCH_CONFIG_URL", DEFAULT_CONFIG_URL),
    )
    parser.add_argument(
        "--panel-url",
        default=os.environ.get("PAY_SWITCH_PANEL_URL", DEFAULT_PANEL_URL),
    )
    parser.add_argument(
        "--bootstrap-url",
        default=os.environ.get("PAY_SWITCH_BOOTSTRAP_URL", DEFAULT_BOOTSTRAP_URL),
        help="Optional Pay-Switch bootstrap endpoint for a managed client token",
    )
    parser.add_argument("--repo-zip-url", default=REPO_ZIP_URL)
    parser.add_argument(
        "--force",
        action="store_true",
        help="Replace an existing installed plugin directory",
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    install_dir = args.install_dir.expanduser().resolve()
    bin_dir = args.bin_dir.expanduser().resolve()
    payagent_home = install_dir.parent
    env_path = payagent_home / "payagent.env"

    if args.uninstall:
        result = uninstall_plugin(
            install_dir,
            bin_dir,
            env_path=env_path,
            revoke=args.revoke,
            remove_env=args.remove_env,
        )
        print("PayAgent uninstalled")
        print(f"plugin_removed: {result['removed_plugin']}")
        print(f"wrappers_removed: {len(result['removed_wrappers'])}")
        print(f"env_removed: {result['removed_env']}")
        print(f"credential_account: {result['credential_account']}")
        if args.revoke:
            print(f"revoke_exit: {result['revoke_exit']}")
        return 0

    source_kind = install_plugin(
        args.source,
        install_dir,
        force=args.force,
        repo_zip_url=args.repo_zip_url,
    )
    bootstrap_env = request_bootstrap_token(str(args.bootstrap_url or "").strip())
    env_path = write_env(
        payagent_home,
        config_url=args.config_url,
        panel_url=args.panel_url,
        bootstrap_env=bootstrap_env,
    )
    wrappers = write_wrappers(bin_dir, install_dir, env_path)

    print("PayAgent installed")
    print(f"source: {source_kind}")
    print(f"plugin: {install_dir}")
    print(f"env: {env_path}")
    print(f"token: {'auto-provisioned' if bootstrap_env else 'not configured'}")
    for wrapper in wrappers:
        print(f"command: {wrapper}")
    if str(bin_dir) not in os.environ.get("PATH", ""):
        print(f"PATH hint: add {bin_dir} to PATH, or run the command path above directly.")
    print("")
    print("Next:")
    print("  payagent secret init")
    print("  payagent probe")
    print(
        "  "
        'payagent submit --payee jimeng --amount 50 '
        '--currency CNY '
        '--reason "Jimeng points top-up for image and video generation" '
        "--method alipay_qr --execute"
    )
    print(
        "  "
        'payagent submit --payee deepseek --product api-key --amount 10 '
        '--currency CNY --reason "DeepSeek API key purchase/top-up" '
        "--method alipay_qr --request-api-key --execute"
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
