#!/usr/bin/env python3
import argparse
import getpass
import hmac
import json
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path


SAFE_SEGMENT = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
CITY_CHOICES = {"sh": "上海", "sz": "深圳"}
DEFAULT_SERVER = {
    "host": "101.32.248.235",
    "port": 799,
    "user": "campdeploy",
    "key_env": "CAMP_DEPLOY_KEY",
    "default_key": "~/.ssh/camp-deploy-key",
    "remote_root": "/var/www/camps",
    "remote_command": "camp-deploy",
}


def fail(message):
    print(f"camp-admin: {message}", file=sys.stderr)
    return 1


def paths():
    return {
        "config": Path(os.environ.get("CAMP_CONFIG", "/etc/camp-deploy/camps.json")),
        "token": Path(os.environ.get("CAMP_ADMIN_TOKEN_FILE", "/etc/camp-deploy/admin-token")),
        "public_root": Path(os.environ.get("CAMP_CATALOG_PUBLIC_ROOT", "/var/www/camp-list")),
        "deploy_root": Path(os.environ.get("CAMP_DEPLOY_ROOT", "/var/www/camps")),
    }


def read_expected_token(token_path):
    try:
        return token_path.read_text(encoding="utf-8").strip()
    except OSError:
        raise ValueError(f"admin token file not found: {token_path}")


def read_submitted_token():
    if sys.stdin.isatty():
        return getpass.getpass("Camp admin token: ").strip()
    return sys.stdin.readline().strip()


def require_token(token_path):
    expected = read_expected_token(token_path)
    submitted = read_submitted_token()
    if not expected or not hmac.compare_digest(submitted, expected):
        raise PermissionError("invalid admin token")


def load_config(config_path):
    if not config_path.exists():
        return {
            "version": 1,
            "catalog_url": "https://camp-list.sg.superbrain-ai.com/camps.json",
            "server": DEFAULT_SERVER,
            "camps": [],
        }
    with config_path.open("r", encoding="utf-8") as f:
        data = json.load(f)
    data.setdefault("version", 1)
    data.setdefault("catalog_url", "https://camp-list.sg.superbrain-ai.com/camps.json")
    data.setdefault("server", DEFAULT_SERVER)
    data.setdefault("camps", [])
    return data


def atomic_write_json(path, data):
    path.parent.mkdir(parents=True, exist_ok=True)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=str(path.parent), delete=False) as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        f.write("\n")
        tmp_name = f.name
    os.replace(tmp_name, path)


def validate_slug(slug, city):
    if not SAFE_SEGMENT.match(slug):
        raise ValueError("slug must use lowercase letters, digits, and hyphens")
    if not slug.endswith(f"-{city}"):
        raise ValueError(f"slug for {CITY_CHOICES[city]} must end with -{city}")


def publish_catalog(data, public_root):
    public_root.mkdir(parents=True, exist_ok=True)
    atomic_write_json(public_root / "camps.json", data)


def run_root_helper(slug, host):
    if os.environ.get("CAMP_ADMIN_ROOT_NOOP") == "1":
        return
    subprocess.run(["sudo", "/usr/local/bin/camp-admin-root", "ensure-camp", slug, host], check=True)


def add_camp(args):
    p = paths()
    require_token(p["token"])
    validate_slug(args.slug, args.city)
    host = f"{args.slug}.sg.superbrain-ai.com"

    data = load_config(p["config"])
    for camp in data["camps"]:
        if camp.get("slug") == args.slug:
            raise ValueError(f"camp already exists: {args.slug}")
        if camp.get("host") == host:
            raise ValueError(f"camp host already exists: {host}")

    aliases = []
    for alias in args.alias or []:
        alias = alias.strip()
        if alias and alias not in aliases:
            aliases.append(alias)

    city_name = CITY_CHOICES[args.city]
    for alias in (city_name, f"{args.name}{city_name}", args.slug):
        if alias and alias not in aliases:
            aliases.append(alias)

    data["camps"].append(
        {
            "slug": args.slug,
            "name": args.name,
            "city": args.city,
            "host": host,
            "aliases": aliases,
        }
    )
    data["camps"] = sorted(data["camps"], key=lambda camp: camp["slug"])

    atomic_write_json(p["config"], data)
    publish_catalog(data, p["public_root"])
    p["deploy_root"].mkdir(parents=True, exist_ok=True)
    (p["deploy_root"] / args.slug).mkdir(parents=True, exist_ok=True)
    run_root_helper(args.slug, host)
    print(f"added camp: {args.slug} ({args.name})")
    print(f"host: https://{host}/")
    return 0


def list_camps(_args):
    p = paths()
    data = load_config(p["config"])
    for camp in data.get("camps", []):
        print(f"{camp['slug']}\t{camp.get('name', '')}\t{camp.get('host', '')}")
    return 0


def make_parser():
    parser = argparse.ArgumentParser(description="Manage SuperBrain camp deploy catalog.")
    sub = parser.add_subparsers(dest="action", required=True)

    add = sub.add_parser("add", help="Add a camp after reading the admin token from stdin or prompt.")
    add.add_argument("--slug", required=True, help="Camp slug, e.g. ai-art-sh or ai-art-sz.")
    add.add_argument("--name", required=True, help="Human-readable camp name.")
    add.add_argument("--city", choices=sorted(CITY_CHOICES), required=True, help="City suffix and validation scope.")
    add.add_argument("--alias", action="append", help="Optional alias. Can be passed multiple times.")
    add.set_defaults(func=add_camp)

    listing = sub.add_parser("list", help="List configured camps.")
    listing.set_defaults(func=list_camps)
    return parser


def main(argv=None):
    args = make_parser().parse_args(argv)
    try:
        return args.func(args)
    except PermissionError as exc:
        return fail(str(exc))
    except (OSError, ValueError, json.JSONDecodeError, subprocess.CalledProcessError) as exc:
        return fail(str(exc))


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