#!/usr/bin/env python3
"""Verify an ``mgf-*`` release is live on the Codeberg PyPI registry (PKG-05).

This is the CI ``verify-codeberg`` step's logic (it replaces the old
``verify-pypi`` step). Per ``PACKAGE_DISTRIBUTION.md`` PKG-05, a release is
"live" only when the Codeberg REST API returns HTTP 200 for the exact
name+version — reading a local ``dist/`` wheel is never proof of publication,
and ``pypi.org`` is never queried for an ``mgf-*`` package (PKG-05 / PKG-11).

Two checks, in increasing order of authority:

1. **REST version record** — poll
   ``…/api/v1/packages/magogi-admin/pypi/<name>/<version>`` until it returns
   ``200`` (the canonical PKG-05 gate).
2. **SHA256 cross-check** — read the published wheel's hash from the PEP 503
   simple index and compare it to the locally built ``dist/`` wheel. A
   mismatch fails the step; if a remote hash can't be parsed, the REST ``200``
   above already proved publication, so it only warns.

Like ``check_sbom.py``, the logic lives in a committed stdlib-only script
rather than an inline ``.woodpecker.yml`` heredoc: inline multi-line shell does
not survive ``woodpecker-cli exec`` (``scripts/ci-local.sh``) faithfully, and
``python:3.12-slim`` ships no ``curl`` — ``urllib`` is always present.

Usage::

    CB_USER=<codeberg-user> CB_TOKEN=<read-or-write:package> \
        python scripts/verify_codeberg.py mgf-common 0.45.0
"""

from __future__ import annotations

import base64
import hashlib
import os
import re
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]

# The federation registry — fixed endpoints (PACKAGE_DISTRIBUTION.md §3, PKG-02).
BASE = "https://codeberg.org"
OWNER = "magogi-admin"

POLL_ATTEMPTS = 6
POLL_INTERVAL_S = 15  # 6 × 15s ≈ 90s, matching the old verify-pypi headroom
HTTP_TIMEOUT_S = 30


def _fail(msg: str) -> None:
    sys.stderr.write(f"verify-codeberg: {msg}\n")
    raise SystemExit(1)


def _normalized(name: str) -> str:
    """PEP 503 normalized project name (used in the simple-index path)."""
    return re.sub(r"[-_.]+", "-", name).lower()


def _rest_status(url: str, token: str) -> int:
    """GET ``url`` with token auth; return the HTTP status (0 on a network error)."""
    req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
    try:
        with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S) as resp:  # noqa: S310 — fixed https literal
            return int(resp.status)
    except urllib.error.HTTPError as exc:
        return int(exc.code)
    except urllib.error.URLError:
        return 0


def _simple_index(url: str, user: str, token: str) -> str | None:
    """Fetch the PEP 503 simple-index page with basic auth, or ``None`` on error."""
    basic = base64.b64encode(f"{user}:{token}".encode()).decode("ascii")
    req = urllib.request.Request(url, headers={"Authorization": f"Basic {basic}"})
    try:
        with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT_S) as resp:  # noqa: S310 — fixed https literal
            return resp.read().decode("utf-8", "replace")
    except (urllib.error.HTTPError, urllib.error.URLError):
        return None


def main(argv: list[str]) -> int:
    if len(argv) != 2:
        _fail("usage: verify_codeberg.py <package-name> <version>")
    name, version = argv

    user = os.environ.get("CB_USER", "").strip()
    token = os.environ.get("CB_TOKEN", "").strip()
    if not token:
        _fail("CB_TOKEN is unset — a read:package (or write:package) Codeberg token is required (PKG-03).")

    norm = _normalized(name)
    rest_url = f"{BASE}/api/v1/packages/{OWNER}/pypi/{name}/{version}"

    # 1. REST version record — the canonical PKG-05 gate.
    live = False
    for attempt in range(1, POLL_ATTEMPTS + 1):
        status = _rest_status(rest_url, token)
        if status == 200:
            print(f"OK: {name} {version} live on Codeberg (REST 200, attempt {attempt}).")
            print(f"    {BASE}/{OWNER}/-/packages/pypi/{norm}/{version}")
            live = True
            break
        if status in (401, 403):
            _fail(
                f"REST returned {status} — the token lacks package read access on "
                f"{OWNER} (PKG-03 scope), not a propagation delay."
            )
        print(f"Codeberg hasn't surfaced {name} {version} yet (HTTP {status}, attempt {attempt}/{POLL_ATTEMPTS})...")
        if attempt < POLL_ATTEMPTS:
            time.sleep(POLL_INTERVAL_S)

    if not live:
        _fail(f"{name} {version} did not surface on Codeberg within "
              f"~{POLL_ATTEMPTS * POLL_INTERVAL_S}s. Check the publish step.")

    # 2. SHA256 cross-check (defense-in-depth; REST 200 already proved publication).
    wheel = ROOT / "dist" / f"{name.replace('-', '_')}-{version}-py3-none-any.whl"
    if not wheel.is_file():
        print(f"NOTE: {wheel.name} not in dist/ — skipping the SHA256 cross-check.")
        return 0
    local_sha = hashlib.sha256(wheel.read_bytes()).hexdigest()

    index = _simple_index(f"{BASE}/api/packages/{OWNER}/pypi/simple/{norm}/", user, token)
    remote_sha = None
    if index is not None:
        m = re.search(re.escape(wheel.name) + r"#sha256=([0-9a-f]{64})", index)
        if m:
            remote_sha = m.group(1)

    if remote_sha is None:
        print("NOTE: could not read a remote SHA256 from the simple index; "
              "REST 200 already proved publication.")
        return 0
    if local_sha != remote_sha:
        _fail(f"SHA256 mismatch — local={local_sha} remote={remote_sha}")
    print(f"OK: Codeberg wheel SHA256 matches the local build ({local_sha}).")
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
