"""Update GitHub Actions SHA pins in workflow files.

Reads the action registry from .github/action-pins.yaml, resolves each
action's tag to a full commit SHA via the GitHub REST API, and updates
all .github/workflows/*.yaml files in-place.

Usage:
    python scripts/update_action_pins.py

Environment variables:
    GITHUB_TOKEN  Optional Bearer token for GitHub API auth (avoids rate limits).

Exit codes:
    0  Success (changes made or no changes needed)
    1  Error
"""

from __future__ import annotations

import json
import os
import re
import sys
import urllib.request
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
PINS_FILE = REPO_ROOT / ".github" / "action-pins.yaml"
WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows"

# Matches lines like:  actions/checkout: { tag: "v6" }
PINS_RE = re.compile(
    r"^\s*(?P<action>[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+)\s*:\s*\{\s*tag\s*:\s*\"(?P<tag>[^\"]+)\"\s*\}",
)

# Matches uses: lines like:  uses: actions/checkout@abc123  # v6
USES_RE = re.compile(
    r"(?P<prefix>uses:\s*)(?P<action>[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+)@(?P<sha>[0-9a-fA-F]{40})(?P<suffix>\s*#.*)?",
)


def github_get(url: str) -> dict:
    """GET a GitHub API URL and return parsed JSON."""
    headers = {"Accept": "application/vnd.github+json"}
    token = os.environ.get("GITHUB_TOKEN", "")
    if token:
        headers["Authorization"] = f"Bearer {token}"
    req = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read().decode())


def resolve_tag_sha(action: str, tag: str) -> str:
    """Resolve an action's tag (or branch) to the underlying commit SHA."""
    owner_repo = action
    # Try as a tag ref first, then fall back to branch ref
    for ref_prefix in ("tags", "heads"):
        url = f"https://api.github.com/repos/{owner_repo}/git/ref/{ref_prefix}/{tag}"
        try:
            data = github_get(url)
        except urllib.error.HTTPError:
            continue

        obj = data["object"]
        if obj["type"] == "commit":
            return obj["sha"]
        elif obj["type"] == "tag":
            # Annotated tag — dereference to commit
            tag_data = github_get(obj["url"])
            return tag_data["object"]["sha"]
        else:
            raise SystemExit(
                f"ERROR: Unexpected object type {obj['type']} for {action}@{tag}"
            )

    raise SystemExit(f"ERROR: Could not resolve ref {tag} for {action}")


def parse_pins(path: Path) -> dict[str, str]:
    """Parse action-pins.yaml and return {action: tag} mapping."""
    pins: dict[str, str] = {}
    for line in path.read_text().splitlines():
        m = PINS_RE.match(line)
        if m:
            pins[m.group("action")] = m.group("tag")
    return pins


def update_workflows(resolved: dict[str, str], tag_map: dict[str, str]) -> int:
    """Update all workflow files, returning count of changes made."""
    changes = 0
    for wf in sorted(WORKFLOWS_DIR.glob("*.yaml")):
        lines = wf.read_text().splitlines(keepends=True)
        new_lines: list[str] = []
        for line in lines:
            m = USES_RE.search(line)
            if m:
                action = m.group("action")
                old_sha = m.group("sha")
                if action in resolved and old_sha != resolved[action]:
                    tag = tag_map[action]
                    new_line = USES_RE.sub(
                        rf"\g<prefix>{action}@{resolved[action]}  # {tag}",
                        line,
                    )
                    new_lines.append(new_line)
                    changes += 1
                    continue
            new_lines.append(line)
        wf.write_text("".join(new_lines))
    return changes


def main() -> None:
    if not PINS_FILE.exists():
        print(f"ERROR: {PINS_FILE} not found", file=sys.stderr)
        sys.exit(1)

    pins = parse_pins(PINS_FILE)
    if not pins:
        print("ERROR: No actions found in action-pins.yaml", file=sys.stderr)
        sys.exit(1)

    resolved: dict[str, str] = {}
    for action, tag in sorted(pins.items()):
        sha = resolve_tag_sha(action, tag)
        resolved[action] = sha

    # Collect current SHAs from workflows for diff reporting
    current_shas: dict[str, set[str]] = {}
    for wf in sorted(WORKFLOWS_DIR.glob("*.yaml")):
        for line in wf.read_text().splitlines():
            m = USES_RE.search(line)
            if m:
                action = m.group("action")
                current_shas.setdefault(action, set()).add(m.group("sha"))

    # Report and update
    for action in sorted(resolved):
        new_sha = resolved[action]
        old_shas = current_shas.get(action, set())
        if not old_shas:
            print(f"{action}: NOT USED in workflows")
        elif old_shas == {new_sha}:
            print(f"{action}: UP-TO-DATE")
        else:
            for old in sorted(old_shas):
                if old != new_sha:
                    print(f"{action}: UPDATED {old} → {new_sha}")

    changes = update_workflows(resolved, pins)
    if changes:
        print(f"\n{changes} pin(s) updated.")
    else:
        print("\nAll pins up-to-date.")


if __name__ == "__main__":
    main()
