#!/usr/bin/env python3
"""
Print ports whose latest Monterey x86_64 build failed in the
install-dependencies step because Rust failed to build.

Strategy:
1. Collect every port that lists 'rust' in any dependency.
2. For each port:
   • open /port/<name>/builds/ and grab the newest build number for
     builder “12” or “12_x86_64”;
   • pull that build from Buildbot JSON and check whether the
     install-dependencies step failed with the canonical
     “Failed to build rust …” message.
3. Print the port name as soon as the failure is confirmed.
"""

import html
import json
import re
import subprocess
import sys
import urllib.request
from urllib.error import HTTPError

# ---------------------------------------------------------------------------
ROOT_BB   = "https://build.macports.org"
PORT_SITE = "https://ports.macports.org"
BUILDER   = "ports-12_x86_64-builder"           # Buildbot builder name
UA_HDRS   = {"User-Agent": "mp-rust-check/1.1"}
TIMEOUT   = 15                                  # seconds
RUST_FAIL = re.compile(r"Dependency &#39;rust&#39;.+has previously failed", re.I)

# --------------------------------------------------------------------------1-
def fetch(url: str) -> str:
    """Return response body decoded as UTF-8 (errors ignored)."""
    req = urllib.request.Request(url, headers=UA_HDRS)
    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
        return r.read().decode(errors="ignore")

def jget(url: str) -> dict:
    req = urllib.request.Request(url, headers=UA_HDRS)
    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
        return json.load(r)

# ---------- 1. Ports that depend on Rust -----------------------------------
def rust_dependents() -> set[str]:
    """Return the set of ports that declare any dependency on 'rust'."""
    try:
        out = subprocess.run(
            ["port", "-q", "echo", "depends:rust"],
            check=True, capture_output=True, text=True
        ).stdout
        return {p for p in out.split() if p}
    except (FileNotFoundError, subprocess.CalledProcessError):
        # Fallback: parse rust/details HTML
        page  = fetch(f"{PORT_SITE}/port/rust/details")
        block = re.search(
            r'Ports that depend on "rust".*?Port Health:',
            page, re.S | re.I
        )
        if not block:
            return set()
        ports = re.findall(r'>([A-Za-z0-9_.+-]+)</a>', block.group(0))
        return {html.unescape(p) for p in ports}

# ---------- 2. Latest Monterey build number --------------------------------
def latest_monterey_build(port: str) -> int | None:
    """
    Return the newest build number for Monterey x86_64
    (“ports-12_x86_64-builder”) or None if the port has never been
    built on that builder.
    """
    try:
        page = fetch(f"{PORT_SITE}/port/{port}/builds/")
    except HTTPError:
        return None

    # Find every link like:
    # https://build.macports.org/builders/ports-12_x86_64-builder/builds/134458
    numbers = re.findall(
        rf'/builders/{BUILDER}/builds/(\d+)',
        page,
        re.I,
    )
    if not numbers:
        return None
    # take the largest build number = latest build
    return int(max(numbers, key=int))

# ---------- 3. Did install-dependencies fail because of Rust? --------------
def rust_broke_build(build_no: int) -> bool:
    """True if install-dependencies failed due to Rust."""
    try:
        data = jget(f"{ROOT_BB}/json/builders/{BUILDER}/builds/{build_no}")
    except HTTPError:
        return False

    if data.get("results") == 0:
        return False

    for step in data.get("steps", []):
        if "install-dependencies" not in step["name"] or step["results"] == 0:
            continue
        for _title, rel in step.get("logs", []):
            url = rel if rel.startswith("http") else f"{ROOT_BB}{rel}"
            url = f"{url}?as_text=1&filter=0"
            log = fetch(url)
            if RUST_FAIL.search(log):
                return True
    return False

# ---------- 4. Main --------------------------------------------------------
def main() -> None:
    for port in rust_dependents():
        build_no = latest_monterey_build(port)
        if not build_no:
            continue
        if rust_broke_build(build_no):
            print(port, flush=True)  # immediate output

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit("Interrupted")
