#!/usr/bin/env python3
"""PKG-08 — Codeberg package retention + quota report for the mgf-* registry.

Mechanises PACKAGE_DISTRIBUTION.md **PKG-08** (retention is mandatory): report
the magogi-admin PyPI registry's storage vs quota and list prune candidates
(versions older than the current + previous minor of each package). Read-only
by default; ``--prune --yes`` actually deletes superseded versions.

Token: parsed from ``$CODEBERG_PACKAGE_TOKEN`` or the chmod-600 token file
(``--token-file``) — never passed via chat/argv (PKG-03 / REL-04). Stdlib-only.

Usage::

    python scripts/package_retention.py                 # quota + prune-candidate report
    python scripts/package_retention.py --prune          # dry-run: show what WOULD be deleted
    python scripts/package_retention.py --prune --yes     # actually delete superseded versions
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request
from collections import defaultdict
from pathlib import Path

OWNER = "magogi-admin"
API = "https://codeberg.org/api/v1"


def _token(token_file: str | None) -> str:
    raw = os.environ.get("CODEBERG_PACKAGE_TOKEN", "")
    if not raw and token_file:
        raw = Path(token_file).read_text(encoding="utf-8")
    m = re.search(r"\b([0-9a-fA-F]{40})\b", raw)
    if not m:
        sys.exit("error: no 40-hex Codeberg token in $CODEBERG_PACKAGE_TOKEN or --token-file")
    return m.group(1)


def _get(path: str, token: str) -> object:
    req = urllib.request.Request(f"{API}{path}", headers={"Authorization": f"token {token}"})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read().decode())


def _ver_key(v: str) -> tuple[int, ...]:
    return tuple(int(n) for n in re.findall(r"\d+", v)) or (0,)


def _superseded(versions: list[str]) -> list[str]:
    """Versions older than the current + previous MINOR (the retention floor)."""
    minors = sorted({_ver_key(v)[:2] for v in versions}, reverse=True)
    keep = set(minors[:2])  # current + previous minor
    return sorted(v for v in versions if _ver_key(v)[:2] not in keep)


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description="PKG-08 retention + quota report")
    ap.add_argument("--token-file", default=None)
    ap.add_argument("--prune", action="store_true", help="prune superseded versions")
    ap.add_argument("--yes", action="store_true", help="actually delete (else dry-run)")
    args = ap.parse_args(argv)
    token = _token(args.token_file)

    quota = _get("/user/quota", token)
    used = quota.get("used", {}) if isinstance(quota, dict) else {}
    print(f"Quota: {json.dumps(used)}" if used else "Quota: (no usage reported)")

    pkgs = _get(f"/packages/{OWNER}?type=pypi&limit=1000", token)
    assert isinstance(pkgs, list)
    by_name: dict[str, list[str]] = defaultdict(list)
    for p in pkgs:
        assert isinstance(p, dict)
        by_name[str(p.get("name"))].append(str(p.get("version")))

    total_candidates = 0
    for name in sorted(by_name):
        cands = _superseded(by_name[name])
        if not cands:
            continue
        total_candidates += len(cands)
        print(
            f"\n{name}: {len(by_name[name])} versions; prune candidates (< current+previous minor): {', '.join(cands)}"
        )
        if args.prune:
            for v in cands:
                if not args.yes:
                    print(f"  DRY-RUN would delete {name} {v}")
                    continue
                try:
                    req = urllib.request.Request(
                        f"{API}/packages/{OWNER}/pypi/{name}/{v}",
                        method="DELETE",
                        headers={"Authorization": f"token {token}"},
                    )
                    urllib.request.urlopen(req, timeout=30)
                    print(f"  deleted {name} {v}")
                except urllib.error.HTTPError as exc:
                    print(f"  FAILED {name} {v}: {exc}")
    print(
        f"\nTotal prune candidates: {total_candidates}"
        f"{' (dry-run — pass --yes to delete)' if args.prune and not args.yes else ''}"
    )
    return 0


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