"""Run one Android compiler with bounded private output and fixed failure signals.

A signal alone does not locate a failure. ``signals":["dependency_resolution",
"tls_verification"]`` was the WHOLE record of a 15-minute Android build on task
1183 (run 34362765661): the session could not tell which artifact would not
resolve or whose certificate was rejected, so its only move was to run the same
build again. Child output stays unpublished — it carries the store proxy's
credentials, and GitHub masks only the exact registered secret — so the detail
is EXTRACTED and SANITIZED instead: bare hostnames and artifact coordinates,
never a URL, never userinfo, never a free-text line.
"""
import argparse
import json
import re
import subprocess
import sys
import threading
import time

LIMIT = 65536
#: At most this many extracted facts per kind — a bounded record, not a log.
MAX_DETAILS = 8
#: A host in a URL, with any ``user:password@`` userinfo dropped by the pattern
#: itself so a credential can never reach the capture group.
_URL_HOST = re.compile(r"\bhttps?://(?:[^/@\s]*@)?([A-Za-z0-9.-]+\.[A-Za-z]{2,})")
#: Gradle/Maven coordinates: group:artifact:version, which carry no secrets.
_COORDINATE = re.compile(r"\b([A-Za-z][\w.-]*(?::[\w.+-]+){2})\b")
#: Hosts a certificate complaint names, e.g. "No subject alternative DNS name
#: matching pub.dev found" or "unable to find valid certification path to <host>".
_CERT_HOST = re.compile(r"(?:matching|for|to)\s+([a-z0-9-]+(?:\.[a-z0-9-]+)+)", re.I)
SIGNALS = {
    "tls_verification": ("PKIX path building failed", "CERTIFICATE_VERIFY_FAILED", "certificate verify failed"),
    "dependency_resolution": ("Could not resolve all files", "Could not resolve all artifacts", "Could not GET '"),
    "connection_timeout": ("Connect timed out", "Read timed out", "Connection timed out"),
    "kotlin_compilation": ("Compilation error. See log for more details", "Kotlin compilation failed"),
    "dart_compilation": ("Target kernel_snapshot_program failed", "Target kernel_snapshot failed"),
    "android_resources": ("Android resource linking failed", "Android resource compilation failed"),
    "r8_missing_classes": ("Missing classes detected while running R8",),
    "duplicate_classes": ("Duplicate class ",),
    "manifest_merge": ("Manifest merger failed",),
    "signing_configuration": ("Keystore was tampered with", "Keystore file ", "Failed to read key "),
    "out_of_memory": ("java.lang.OutOfMemoryError", "Java heap space", "GC overhead limit exceeded"),
    "gradle_daemon_lost": ("Gradle build daemon disappeared unexpectedly",),
    "disk_full": ("No space left on device",),
}


def signals(output: bytes) -> list[str]:
    text = output.decode("utf-8", errors="replace")
    return sorted(name for name, patterns in SIGNALS.items() if any(pattern in text for pattern in patterns))


def _bounded(values) -> list[str]:
    """Deduplicate, order and cap — a record the session can act on."""
    seen = []
    for value in values:
        if value not in seen:
            seen.append(value)
        if len(seen) >= MAX_DETAILS:
            break
    return sorted(seen)


def details(output: bytes, found: list[str]) -> dict:
    """Sanitized facts that LOCATE the signals, extracted from the private tail.

    Only pattern captures leave this function: hostnames and dependency
    coordinates. The raw text, any URL, and anything shaped like userinfo stay
    in the buffer that is discarded with the process.
    """
    text = output.decode("utf-8", errors="replace")
    out = {}
    if "dependency_resolution" in found or "tls_verification" in found:
        hosts = _bounded(_URL_HOST.findall(text))
        if "tls_verification" in found:
            hosts = _bounded(hosts + _CERT_HOST.findall(text))
        if hosts:
            out["hosts"] = hosts
    if "dependency_resolution" in found:
        coordinates = _bounded(_COORDINATE.findall(text))
        if coordinates:
            out["artifacts"] = coordinates
    return out


def emit(title: str, level: str, **fields) -> None:
    print(f"::{level} title={title}::" + json.dumps(fields, separators=(",", ":")), flush=True)


def drain(stream, tail: bytearray) -> None:
    while chunk := stream.read1(8192):
        tail[:] = (tail + chunk)[-LIMIT:]


def run(phase: str, command: list[str], *, interval: float = 60) -> int:
    started, tail = time.monotonic(), bytearray()
    try:
        child = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    except OSError:
        emit("android_build_failed", "error", schema="gowalk-cicd/android-build-failed.v1",
             phase=phase, exit_code=127, signals=["command_start_failed"])
        return 127
    reader = threading.Thread(target=drain, args=(child.stdout, tail), daemon=True)
    reader.start()
    while True:
        try:
            code = child.wait(timeout=interval)
            break
        except subprocess.TimeoutExpired:
            emit("android_build_progress", "notice", schema="gowalk-cicd/android-build-progress.v1",
                 phase=phase, elapsed_seconds=int(time.monotonic() - started))
    # A descendant retaining stdout must not hold a finished compiler's result indefinitely.
    reader.join(timeout=1)
    if not reader.is_alive():
        child.stdout.close()
    if code:
        retained = bytes(tail)
        found = signals(retained)
        emit("android_build_failed", "error", schema="gowalk-cicd/android-build-failed.v1",
             phase=phase, exit_code=code, signals=found, **details(retained, found))
    return code if code >= 0 else 128 - code


def main(argv: list[str]) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--phase", required=True, choices=("flutter_bundle", "gradle_bundle"))
    parser.add_argument("command", nargs=argparse.REMAINDER)
    args = parser.parse_args(argv)
    command = args.command[1:] if args.command[:1] == ["--"] else args.command
    if not command:
        parser.error("a build command is required")
    return run(args.phase, command)


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