import argparse
import getpass
import importlib.metadata
import ipaddress
import json
import os
import re
import secrets
from datetime import datetime, timezone
from pathlib import Path
import shutil
import sys
import tarfile
import tempfile
from typing import List, Optional
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urljoin
from urllib.request import Request, urlopen

from ._core import generate

try:
    from ._core import NODES_BASE_DOMAIN
except ImportError:
    NODES_BASE_DOMAIN = "nodes.doubleword.ai"

DEFAULT_REPO = "doublewordai/shenron-configs"
DEFAULT_CONFIG_INDEX_ASSET = "configs-index.txt"
DEFAULT_HELM_INDEX_ASSET = "index.yaml"
LEGACY_HELM_ARCHIVE_ASSET = "shenron-helm.tgz"
DEFAULT_HELM_DIR = "shenron-helm"
GITHUB_API = "https://api.github.com"
CF_API = os.environ.get("SHENRON_CF_API_URL", "https://api.cloudflare.com/client/v4")


class CliError(RuntimeError):
    pass


def _normalize_tag(value: str) -> str:
    return value if value.startswith("v") else f"v{value}"


def _tag_to_version(tag: str) -> str:
    return tag[1:] if tag.startswith("v") else tag


def _default_helm_archive_asset(tag: str) -> str:
    return f"shenron-{_tag_to_version(tag)}.tgz"


def _read_text(url: str) -> str:
    req = Request(url, headers={"User-Agent": "shenron-cli"})
    with urlopen(req) as resp:
        return resp.read().decode("utf-8")


def _read_json(url: str) -> dict:
    req = Request(
        url,
        headers={
            "User-Agent": "shenron-cli",
            "Accept": "application/vnd.github+json",
        },
    )
    with urlopen(req) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _get_latest_release_tag(repo: str) -> str:
    payload = _read_json(f"{GITHUB_API}/repos/{repo}/releases/latest")
    tag = payload.get("tag_name")
    if not tag:
        raise CliError(f"could not determine latest release tag for {repo}")
    return tag


def _resolve_release_tag(release: Optional[str], repo: str) -> str:
    if release:
        if release == "latest":
            return _get_latest_release_tag(repo)
        return _normalize_tag(release)

    try:
        package_version = importlib.metadata.version("shenron")
        return _normalize_tag(package_version)
    except importlib.metadata.PackageNotFoundError as exc:
        raise CliError(
            "could not determine installed shenron version; pass --release or use --release latest"
        ) from exc


def _release_download_base(repo: str, tag: str) -> str:
    return f"https://github.com/{repo}/releases/download/{tag}/"


def _as_asset_base_url(value: str) -> str:
    if value.endswith("/"):
        return value
    return f"{value}/"


def _parse_index_entries(index_text: str) -> List[str]:
    entries: List[str] = []
    seen = set()
    for raw in index_text.splitlines():
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        name = line.split()[0]
        if name not in seen:
            entries.append(name)
            seen.add(name)
    return entries


def _parse_config_index(index_text: str) -> List[str]:
    return [
        name
        for name in _parse_index_entries(index_text)
        if name.endswith(".yml") or name.endswith(".yaml")
    ]


def _parse_helm_index(index_text: str) -> List[str]:
    archives: List[str] = []
    seen = set()

    # Support plain newline-delimited indexes containing filenames or URLs.
    for entry in _parse_index_entries(index_text):
        candidate = entry.strip("\"'")
        candidate = candidate.rsplit("/", 1)[-1]
        if candidate.endswith(".tgz") or candidate.endswith(".tar.gz"):
            if candidate not in seen:
                archives.append(candidate)
                seen.add(candidate)

    if archives:
        return archives

    # Support Helm's standard YAML index format by scanning for archive names.
    for match in re.finditer(r"([A-Za-z0-9._-]+\.(?:tgz|tar\.gz))", index_text):
        candidate = match.group(1)
        if candidate not in seen:
            archives.append(candidate)
            seen.add(candidate)
    return archives


def _release_asset_names(repo: str, tag: str) -> List[str]:
    payload = _read_json(f"{GITHUB_API}/repos/{repo}/releases/tags/{tag}")
    assets = payload.get("assets", [])
    names = [asset.get("name", "") for asset in assets if asset.get("name")]
    return sorted(dict.fromkeys(names))


def _configs_from_release_api(repo: str, tag: str) -> List[str]:
    return [
        name
        for name in _release_asset_names(repo, tag)
        if name.endswith(".yml") or name.endswith(".yaml")
    ]


def _load_configs_for_release(repo: str, tag: str, index_url: Optional[str]) -> List[str]:
    if index_url:
        return _parse_config_index(_read_text(index_url))

    base_url = _release_download_base(repo, tag)
    derived_index_url = urljoin(base_url, DEFAULT_CONFIG_INDEX_ASSET)

    try:
        return _parse_config_index(_read_text(derived_index_url))
    except (HTTPError, URLError):
        # Backward compatibility with older releases that predate configs-index.txt.
        return _configs_from_release_api(repo, tag)


def _helm_archives_from_release_api(repo: str, tag: str) -> List[str]:
    assets = _release_asset_names(repo, tag)
    preferred = _default_helm_archive_asset(tag)
    if preferred in assets:
        return [preferred]
    if LEGACY_HELM_ARCHIVE_ASSET in assets:
        return [LEGACY_HELM_ARCHIVE_ASSET]

    tarballs = [name for name in assets if name.endswith(".tgz") or name.endswith(".tar.gz")]
    versioned_shenron = [name for name in tarballs if name.startswith("shenron-") and name.endswith(".tgz")]
    if versioned_shenron:
        return sorted(versioned_shenron)
    helm_tarballs = [name for name in tarballs if "helm" in name.lower()]
    return helm_tarballs or tarballs


def _load_helm_archives_for_release(repo: str, tag: str, index_url: Optional[str]) -> List[str]:
    if index_url:
        return _parse_helm_index(_read_text(index_url))

    base_url = _release_download_base(repo, tag)
    derived_index_url = urljoin(base_url, DEFAULT_HELM_INDEX_ASSET)

    try:
        return _parse_helm_index(_read_text(derived_index_url))
    except (HTTPError, URLError):
        return _helm_archives_from_release_api(repo, tag)


def _select_helm_archive(archives: List[str], requested: Optional[str], tag: str) -> str:
    if requested:
        if requested not in archives:
            available = ", ".join(archives)
            raise CliError(f"helm archive '{requested}' not found in index; available: {available}")
        return requested

    preferred = _default_helm_archive_asset(tag)
    for candidate in (preferred, LEGACY_HELM_ARCHIVE_ASSET):
        if candidate in archives:
            return candidate

    if len(archives) == 1:
        return archives[0]

    available = ", ".join(archives)
    raise CliError(
        f"multiple helm archives found; pass --name to choose one. Available: {available}"
    )


def _interactive_select(options: List[str]) -> str:
    if not options:
        raise CliError("no configs available")
    if not sys.stdin.isatty() or not sys.stdout.isatty():
        raise CliError("interactive selection requires a TTY; use --name in non-interactive environments")

    try:
        import termios
        import tty
    except Exception as exc:
        raise CliError("interactive selection is unsupported on this platform; use --name") from exc

    idx = 0
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)

    def render() -> None:
        sys.stdout.write("\x1b[2J\x1b[H")
        sys.stdout.write(
            "Select a Shenron config (arrow keys, Enter to confirm, q to cancel):\r\n\r\n"
        )
        for i, option in enumerate(options):
            prefix = "> " if i == idx else "  "
            sys.stdout.write(f"{prefix}{option}\r\n")
        sys.stdout.flush()

    try:
        tty.setraw(fd)
        render()
        while True:
            ch = sys.stdin.read(1)
            if ch in ("\r", "\n"):
                sys.stdout.write("\r\n")
                return options[idx]
            if ch in ("q", "Q"):
                raise CliError("selection cancelled")
            if ch == "\x03":
                raise KeyboardInterrupt
            if ch == "\x1b":
                seq = sys.stdin.read(2)
                if seq == "[A":
                    idx = (idx - 1) % len(options)
                    render()
                elif seq == "[B":
                    idx = (idx + 1) % len(options)
                    render()
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old)


def _download_file(url: str, destination: Path) -> None:
    destination.parent.mkdir(parents=True, exist_ok=True)
    req = Request(url, headers={"User-Agent": "shenron-cli"})
    with urlopen(req) as resp, destination.open("wb") as out:
        out.write(resp.read())


def _extract_tarball(archive_path: Path, destination: Path) -> None:
    destination = destination.resolve()
    destination.mkdir(parents=True, exist_ok=True)

    with tarfile.open(archive_path, "r:*") as archive:
        members = archive.getmembers()
        for member in members:
            member_name = member.name
            if member.issym() or member.islnk():
                raise CliError(f"helm archive contains unsupported link entry: {member_name}")
            if member_name.startswith("/") or member_name.startswith(".."):
                raise CliError(f"unsafe path in helm archive: {member_name}")
            resolved = (destination / member_name).resolve()
            if not (resolved == destination or destination in resolved.parents):
                raise CliError(f"unsafe path in helm archive: {member_name}")

        top_level_parts = {
            Path(member.name).parts[0]
            for member in members
            if member.name not in ("", ".", "./") and Path(member.name).parts
        }

        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_root = Path(tmp_dir)
            archive.extractall(path=tmp_root)

            source_root = tmp_root
            if len(top_level_parts) == 1:
                candidate_root = tmp_root / next(iter(top_level_parts))
                if candidate_root.is_dir():
                    source_root = candidate_root

            for item in source_root.iterdir():
                target = destination / item.name
                if target.exists():
                    if target.is_dir():
                        shutil.rmtree(target)
                    else:
                        target.unlink()
                shutil.move(str(item), str(target))


def _yaml_scalar(value: str) -> str:
    return json.dumps(value)


def _set_config_scalar(config_path: Path, key: str, value: str) -> None:
    lines = config_path.read_text(encoding="utf-8").splitlines()
    updated_lines: list[str] = []
    replaced = False

    for line in lines:
        if line and not line.startswith(" ") and ":" in line:
            current_key = line.split(":", 1)[0]
            if current_key == key:
                updated_lines.append(f"{key}: {_yaml_scalar(value)}")
                replaced = True
                continue
        updated_lines.append(line)

    if not replaced:
        if updated_lines and updated_lines[-1].strip():
            updated_lines.append("")
        updated_lines.append(f"{key}: {_yaml_scalar(value)}")

    config_path.write_text("\n".join(updated_lines) + "\n", encoding="utf-8")


def _apply_config_overrides(
    config_path: Path,
    api_key: Optional[str],
    scouter_api_key: Optional[str],
    scouter_collector_instance: Optional[str],
) -> None:
    overrides = {}
    if api_key is not None:
        overrides["api_key"] = api_key
    if scouter_api_key is not None:
        overrides["scouter_ingest_api_key"] = scouter_api_key
    if scouter_collector_instance is not None:
        overrides["scouter_collector_instance"] = scouter_collector_instance

    if not overrides:
        return

    lines = config_path.read_text(encoding="utf-8").splitlines()
    updated_lines: list[str] = []
    seen = set()

    for line in lines:
        if line and not line.startswith(" ") and ":" in line:
            key = line.split(":", 1)[0]
            if key in overrides:
                updated_lines.append(f"{key}: {_yaml_scalar(overrides[key])}")
                seen.add(key)
                continue
        updated_lines.append(line)

    for key, value in overrides.items():
        if key in seen:
            continue
        if updated_lines and updated_lines[-1].strip():
            updated_lines.append("")
        updated_lines.append(f"{key}: {_yaml_scalar(value)}")

    config_path.write_text("\n".join(updated_lines) + "\n", encoding="utf-8")


# ---------------------------------------------------------------------------
# Cloudflare API helpers
# ---------------------------------------------------------------------------


def _cloudflare_request(
    method: str,
    path: str,
    token: str,
    data: Optional[dict] = None,
) -> dict:
    url = CF_API + path
    payload = json.dumps(data).encode() if data is not None else None
    req = Request(
        url,
        data=payload,
        method=method,
        headers={
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
            "User-Agent": "shenron-cli",
        },
    )
    try:
        with urlopen(req) as resp:
            return json.loads(resp.read().decode())
    except HTTPError as exc:
        body = exc.read().decode(errors="replace")
        raise CliError(f"Cloudflare API error {exc.code} for {method} {path}: {body}") from exc


def _cf_resolve_zone_id(token: str, base_domain: str) -> str:
    """Walk up the domain hierarchy to find the Cloudflare zone that contains base_domain.

    Cloudflare zones are always root domains (e.g. doubleword.ai), never
    subdomains. Given base_domain="nodes.doubleword.ai" we try:
      nodes.doubleword.ai → not found
      doubleword.ai       → found ✓
    """
    parts = base_domain.split(".")
    for i in range(len(parts) - 1):
        candidate = ".".join(parts[i:])
        resp = _cloudflare_request("GET", f"/zones?name={quote(candidate)}&status=active", token)
        zones = resp.get("result", [])
        if zones:
            return zones[0]["id"]
    raise CliError(
        f"no active Cloudflare zone found for '{base_domain}' or any of its parent domains. "
        f"Ensure the root domain is added to your Cloudflare account."
    )


def _cf_list_dns_records(token: str, zone_id: str, name: str, type_: str = "A") -> List[dict]:
    path = f"/zones/{zone_id}/dns_records?type={type_}&name={quote(name)}"
    resp = _cloudflare_request("GET", path, token)
    return resp.get("result", [])


def _cf_create_dns_record(token: str, zone_id: str, name: str, ip: str) -> dict:
    resp = _cloudflare_request(
        "POST",
        f"/zones/{zone_id}/dns_records",
        token,
        {"type": "A", "name": name, "content": ip, "proxied": False, "ttl": 1},
    )
    result = resp.get("result")
    if not result:
        raise CliError(f"Cloudflare did not return a record after creation: {resp}")
    return result


def _cf_update_dns_record(
    token: str, zone_id: str, record_id: str, name: str, ip: str
) -> dict:
    resp = _cloudflare_request(
        "PUT",
        f"/zones/{zone_id}/dns_records/{record_id}",
        token,
        {"type": "A", "name": name, "content": ip, "proxied": False, "ttl": 1},
    )
    result = resp.get("result")
    if not result:
        raise CliError(f"Cloudflare did not return a record after update: {resp}")
    return result


def _cf_delete_dns_record(token: str, zone_id: str, record_id: str) -> None:
    _cloudflare_request("DELETE", f"/zones/{zone_id}/dns_records/{record_id}", token)


def _assert_nodes_domain(fqdn: str) -> None:
    """Hard guard: reject any CF operation targeting outside *.NODES_BASE_DOMAIN.

    Applied before every create/update/delete call so that even manually crafted
    metadata files cannot trick shenron into touching other DNS records.
    """
    if not fqdn.endswith(f".{NODES_BASE_DOMAIN}"):
        raise CliError(
            f"Security violation: refusing to operate on {fqdn!r}. "
            f"All Cloudflare operations are restricted to *.{NODES_BASE_DOMAIN}. "
            "This restriction is baked into the shenron binary."
        )


# ---------------------------------------------------------------------------
# Endpoint metadata helpers
# ---------------------------------------------------------------------------


def _write_endpoint_metadata(output_dir: Path, metadata: dict) -> Path:
    generated_dir = output_dir / ".generated"
    generated_dir.mkdir(parents=True, exist_ok=True)
    dest = generated_dir / "node_endpoint.json"
    tmp = dest.with_suffix(".tmp")
    tmp.write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
    tmp.replace(dest)
    return dest


def _run_endpoint_setup(argv: List[str]) -> int:
    parser = argparse.ArgumentParser(
        prog="shenron endpoint setup",
        description="Register this node with a DNS subdomain via Cloudflare.",
    )
    parser.add_argument(
        "--slug",
        default=None,
        help="Subdomain label for this node (auto-generated if not provided).",
    )
    parser.add_argument(
        "--public-ip",
        default=None,
        help="Public IPv4 address of this node (prompted if not provided).",
    )
    parser.add_argument(
        "--cf-token",
        default=None,
        help="Cloudflare API token (or set CLOUDFLARE_API_TOKEN env var; prompted if neither).",
    )
    parser.add_argument(
        "--reset",
        action="store_true",
        help="Overwrite an existing DNS record if it points to a different IP.",
    )
    parser.add_argument(
        "--output-dir",
        default=".",
        help="Directory where .generated/ will be written (default: current directory).",
    )

    args = parser.parse_args(argv)
    output_dir = Path(args.output_dir).resolve()
    metadata_path = output_dir / ".generated" / "node_endpoint.json"

    # Short-circuit if already configured (without --reset)
    if metadata_path.exists() and not args.reset:
        existing = json.loads(metadata_path.read_text(encoding="utf-8"))
        fqdn = existing.get("fqdn", "?")
        print(f"Endpoint already configured: {fqdn}")
        print(f"  Metadata: {metadata_path}")
        print("Pass --reset to reconfigure.")
        return 0

    # Resolve public IP (before any prompts for secrets)
    public_ip = args.public_ip or ""
    if not public_ip:
        try:
            public_ip = input("Public IPv4 address of this node: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nAborted.", file=sys.stderr)
            return 1
    try:
        ipaddress.IPv4Address(public_ip)
    except ValueError as exc:
        raise CliError(f"invalid IPv4 address: {public_ip!r}") from exc

    # Resolve slug (validate before hitting network)
    slug = args.slug or f"node-{secrets.token_hex(4)}"
    if not re.match(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", slug):
        raise CliError(
            f"invalid slug {slug!r}: must be lowercase alphanumeric with optional hyphens, "
            "no leading/trailing hyphen"
        )

    fqdn = f"{slug}.{NODES_BASE_DOMAIN}"
    _assert_nodes_domain(fqdn)  # belt-and-suspenders guard
    print(f"Registering {fqdn} → {public_ip} ...")

    # Resolve Cloudflare token (only now, after cheap validation passes)
    cf_token = args.cf_token or os.environ.get("CLOUDFLARE_API_TOKEN", "")
    if not cf_token:
        try:
            cf_token = getpass.getpass("Cloudflare API token: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nAborted.", file=sys.stderr)
            return 1
    if not cf_token:
        raise CliError("Cloudflare API token is required")

    # Resolve zone ID (always auto-resolved from NODES_BASE_DOMAIN)
    print(f"Resolving zone ID for {NODES_BASE_DOMAIN} ...")
    zone_id = _cf_resolve_zone_id(cf_token, NODES_BASE_DOMAIN)
    print(f"  Zone ID: {zone_id}")

    # Check / create / update DNS A record
    records = _cf_list_dns_records(cf_token, zone_id, fqdn)
    record_id: str
    if records:
        existing_record = records[0]
        existing_ip = existing_record.get("content", "")
        record_id = existing_record["id"]
        if existing_ip == public_ip:
            print(f"  DNS A record already exists and matches: {fqdn} → {public_ip}")
        elif args.reset:
            print(f"  Updating DNS A record: {fqdn} → {public_ip} (was {existing_ip})")
            updated = _cf_update_dns_record(cf_token, zone_id, record_id, fqdn, public_ip)
            record_id = updated["id"]
        else:
            raise CliError(
                f"DNS A record for {fqdn} already points to {existing_ip!r} (not {public_ip!r}). "
                "Pass --reset to update it."
            )
    else:
        print(f"  Creating DNS A record: {fqdn} → {public_ip}")
        created = _cf_create_dns_record(cf_token, zone_id, fqdn, public_ip)
        record_id = created["id"]

    # Write metadata
    metadata: dict = {
        "fqdn": fqdn,
        "subdomain_label": slug,
        "base_domain": NODES_BASE_DOMAIN,
        "public_ip": public_ip,
        "cloudflare_zone_id": zone_id,
        "cloudflare_record_id": record_id,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    dest = _write_endpoint_metadata(output_dir, metadata)
    print(f"\nEndpoint configured: https://{fqdn}/v1")
    print(f"  Metadata written to: {dest}")
    print("\nRun 'shenron <config.yml>' to regenerate deployment files with HTTPS enabled.")
    return 0


def _run_endpoint_teardown(argv: List[str]) -> int:
    parser = argparse.ArgumentParser(
        prog="shenron endpoint teardown",
        description="Delete this node's DNS record from Cloudflare and revert to HTTP-only mode.",
    )
    parser.add_argument(
        "--cf-token",
        default=None,
        help="Cloudflare API token (or set CLOUDFLARE_API_TOKEN env var; prompted if neither).",
    )
    parser.add_argument(
        "--output-dir",
        default=".",
        help="Directory where .generated/ lives (default: current directory).",
    )
    parser.add_argument(
        "--yes",
        action="store_true",
        help="Skip confirmation prompt.",
    )

    args = parser.parse_args(argv)
    output_dir = Path(args.output_dir).resolve()
    metadata_path = output_dir / ".generated" / "node_endpoint.json"

    if not metadata_path.exists():
        raise CliError(f"no endpoint metadata found at {metadata_path}; nothing to tear down.")

    metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
    fqdn = metadata["fqdn"]
    zone_id = metadata["cloudflare_zone_id"]
    record_id = metadata["cloudflare_record_id"]

    # Hard security guard — refuse to delete records outside *.nodes.doubleword.ai
    _assert_nodes_domain(fqdn)

    if not args.yes:
        try:
            answer = input(f"Delete DNS A record '{fqdn}'? [y/N] ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            print("\nAborted.", file=sys.stderr)
            return 1
        if answer != "y":
            print("Aborted.")
            return 0

    # Resolve CF token (after confirmation so we don't prompt then abort)
    cf_token = args.cf_token or os.environ.get("CLOUDFLARE_API_TOKEN", "")
    if not cf_token:
        try:
            cf_token = getpass.getpass("Cloudflare API token: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nAborted.", file=sys.stderr)
            return 1
    if not cf_token:
        raise CliError("Cloudflare API token is required")

    print(f"Deleting DNS A record: {fqdn} ...")
    _cf_delete_dns_record(cf_token, zone_id, record_id)
    print(f"  Deleted record {record_id}")

    metadata_path.unlink()
    print(f"  Removed metadata: {metadata_path}")
    print(f"\nEndpoint torn down.")
    print("Run 'shenron <config.yml>' to regenerate deployment files in HTTP-only mode.")
    return 0


def _run_endpoint(argv: List[str]) -> int:
    sub = argv[0] if argv else ""
    if sub == "setup":
        return _run_endpoint_setup(argv[1:])
    if sub == "teardown":
        return _run_endpoint_teardown(argv[1:])
    print("usage: shenron endpoint <setup|teardown> [options]", file=sys.stderr)
    print("       shenron endpoint setup --help", file=sys.stderr)
    print("       shenron endpoint teardown --help", file=sys.stderr)
    return 1


# ---------------------------------------------------------------------------
# Generate
# ---------------------------------------------------------------------------


def _run_generate(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(
        prog="shenron",
        description="Generate Shenron docker-compose deployment files from a YAML config.",
    )
    parser.add_argument(
        "target",
        nargs="?",
        default=".",
        help="Config file path or directory containing one config YAML (default: current directory).",
    )
    parser.add_argument(
        "--output-dir",
        default=None,
        help="Directory where files are written (default: target directory).",
    )

    args = parser.parse_args(argv)

    generated = generate(args.target, args.output_dir)
    print("Generated files:")
    for path in generated:
        print(path)
    return 0


def _run_get(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(
        prog="shenron get",
        description="Download release configs or helm charts for Shenron.",
    )
    parser.add_argument(
        "--release",
        default=None,
        help="Release tag (e.g. v0.6.3 or 0.6.3). Use 'latest' for newest release and shenron_version=latest. Default: installed shenron version.",
    )
    parser.add_argument(
        "--repo",
        default=DEFAULT_REPO,
        help=f"GitHub repo in owner/name form (default: {DEFAULT_REPO}).",
    )
    parser.add_argument(
        "--name",
        default=None,
        help="Asset filename to download directly.",
    )
    parser.add_argument(
        "--helm",
        action="store_true",
        help=f"Download helm chart assets instead of config files (default output directory: {DEFAULT_HELM_DIR}).",
    )
    parser.add_argument(
        "--api-key",
        default=None,
        help="Override api_key in the downloaded config.",
    )
    parser.add_argument(
        "--scouter-api-key",
        default=None,
        help="Override scouter_ingest_api_key in the downloaded config.",
    )
    parser.add_argument(
        "--scouter-colector-instance",
        "--scouter-collector-instance",
        dest="scouter_collector_instance",
        default=None,
        help="Override scouter_collector_instance in the downloaded config.",
    )
    parser.add_argument(
        "--dir",
        default=None,
        help=f"Output directory (default: current directory for configs, {DEFAULT_HELM_DIR} for --helm).",
    )
    parser.add_argument(
        "--force",
        action="store_true",
        help="Overwrite config files and allow writing into non-empty --helm output directories.",
    )
    parser.add_argument(
        "--index-url",
        default=None,
        help=argparse.SUPPRESS,
    )
    parser.add_argument(
        "--base-url",
        default=None,
        help=argparse.SUPPRESS,
    )

    args = parser.parse_args(argv)

    use_latest_tag = args.release == "latest"
    release_tag = _resolve_release_tag(args.release, args.repo)
    base_url = _as_asset_base_url(args.base_url or _release_download_base(args.repo, release_tag))

    if args.helm:
        if args.api_key is not None or args.scouter_api_key is not None or args.scouter_collector_instance is not None:
            raise CliError("config override flags are only supported when downloading configs")

        try:
            helm_archives = _load_helm_archives_for_release(args.repo, release_tag, args.index_url)
        except (HTTPError, URLError) as exc:
            raise CliError(f"failed to load helm index for release {release_tag}: {exc}") from exc

        if not helm_archives and args.release is None and args.index_url is None:
            latest_tag = _get_latest_release_tag(args.repo)
            if latest_tag != release_tag:
                release_tag = latest_tag
                base_url = _as_asset_base_url(args.base_url or _release_download_base(args.repo, release_tag))
                helm_archives = _load_helm_archives_for_release(args.repo, release_tag, args.index_url)

        if not helm_archives:
            raise CliError(f"no helm chart archives found for release {release_tag}")

        selected_archive = _select_helm_archive(helm_archives, args.name, release_tag)
        out_dir = Path(args.dir or DEFAULT_HELM_DIR).resolve()
        if out_dir.exists():
            if not out_dir.is_dir():
                raise CliError(f"helm output path exists and is not a directory: {out_dir}")
            if any(out_dir.iterdir()) and not args.force:
                raise CliError(
                    f"helm output directory is not empty: {out_dir} (pass --force to overwrite files)"
                )

        with tempfile.TemporaryDirectory() as tmp_dir:
            archive_path = Path(tmp_dir) / selected_archive
            archive_url = urljoin(base_url, selected_archive)
            try:
                _download_file(archive_url, archive_path)
            except (HTTPError, URLError) as exc:
                raise CliError(f"failed to download helm chart archive from {archive_url}: {exc}") from exc
            _extract_tarball(archive_path, out_dir)

        print(f"Downloaded helm chart archive: {selected_archive}")
        print(f"Extracted helm chart to: {out_dir}")
        return 0

    try:
        configs = _load_configs_for_release(args.repo, release_tag, args.index_url)
    except (HTTPError, URLError) as exc:
        raise CliError(f"failed to load config index for release {release_tag}: {exc}") from exc

    if not configs and args.release is None and args.index_url is None:
        latest_tag = _get_latest_release_tag(args.repo)
        if latest_tag != release_tag:
            release_tag = latest_tag
            base_url = _as_asset_base_url(args.base_url or _release_download_base(args.repo, release_tag))
            configs = _load_configs_for_release(args.repo, release_tag, args.index_url)

    if not configs:
        raise CliError(f"no config entries found for release {release_tag}")

    selected = args.name
    if selected is None:
        selected = _interactive_select(configs)
    elif selected not in configs:
        available = ", ".join(configs)
        raise CliError(f"config '{selected}' not found in index; available: {available}")

    out_dir = Path(args.dir or ".").resolve()
    out_dir.mkdir(parents=True, exist_ok=True)
    config_path = out_dir / selected
    if config_path.exists() and not args.force:
        raise CliError(f"config already exists: {config_path} (pass --force to overwrite)")

    config_url = urljoin(base_url, selected)
    try:
        _download_file(config_url, config_path)
    except (HTTPError, URLError) as exc:
        raise CliError(f"failed to download config from {config_url}: {exc}") from exc

    if use_latest_tag:
        _set_config_scalar(config_path, "shenron_version", "latest")
    _apply_config_overrides(
        config_path,
        api_key=args.api_key,
        scouter_api_key=args.scouter_api_key,
        scouter_collector_instance=args.scouter_collector_instance,
    )

    generated = generate(str(config_path), str(out_dir))

    print(f"Downloaded config: {config_path}")
    print("Generated files:")
    for path in generated:
        print(path)
    return 0


def main() -> int:
    try:
        if len(sys.argv) > 1 and sys.argv[1] == "get":
            return _run_get(sys.argv[2:])
        if len(sys.argv) > 1 and sys.argv[1] == "endpoint":
            return _run_endpoint(sys.argv[2:])
        return _run_generate(sys.argv[1:])
    except Exception as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1


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