#!/usr/bin/env python3
"""Turn CI logs into a concise, locally generated triage report."""

from __future__ import annotations

import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class Rule:
    key: str
    title: str
    pattern: re.Pattern[str]
    action: str


RULES = (
    Rule("oom", "Memory exhaustion", re.compile(r"out of memory|oomkilled|exit code 137|killed process", re.I),
         "Reduce parallelism or memory use; confirm the runner memory limit."),
    Rule("disk", "Disk exhaustion", re.compile(r"no space left on device|enospc", re.I),
         "Remove unnecessary caches/artifacts and inspect runner disk usage."),
    Rule("auth", "Authentication or authorization", re.compile(r"authorization\s*[:=]|unauthorized|forbidden|permission denied|status code 401|status code 403", re.I),
         "Verify the least-privilege credential, its scope, and secret injection."),
    Rule("network", "Network or DNS failure", re.compile(r"timed? out|econnreset|temporary failure in name resolution|could not resolve host", re.I),
         "Retry only transient operations and verify DNS/egress availability."),
    Rule("dependency", "Dependency resolution", re.compile(r"could not find a version|unable to resolve dependency|eresolve|no matching distribution", re.I),
         "Pin a compatible dependency set and regenerate the lockfile."),
    Rule("test", "Test failure", re.compile(r"\bassert(?:ion)?(?:error| failed)|\bfailed tests?\b|\btests? failed\b", re.I),
         "Reproduce the first failing test locally and isolate the smallest cause."),
    Rule("missing", "Missing file or command", re.compile(r"no such file or directory|command not found|module not found|modulenotfounderror", re.I),
         "Check checkout paths, generated assets, and installed build dependencies."),
)

SECRET = re.compile(
    r"(?i)((?:token|password|passwd|secret|api[_-]?key|authorization)\s*[:=]\s*)([^\s,;]+)"
)


def redact(line: str) -> str:
    return SECRET.sub(r"\1[REDACTED]", line)


def analyze(text: str, source: str) -> str:
    lines = [redact(line.rstrip()) for line in text.splitlines()]
    findings: list[tuple[Rule, list[tuple[int, str]]]] = []
    for rule in RULES:
        matches = [(number, line[:240]) for number, line in enumerate(lines, 1) if rule.pattern.search(line)]
        if matches:
            findings.append((rule, matches[:3]))

    output = [
        "# CI Rescue report",
        "",
        f"- Source: `{source}`",
        f"- Lines analyzed: {len(lines)}",
        f"- Failure classes found: {len(findings)}",
        "",
    ]
    if not findings:
        output += [
            "## Result",
            "",
            "No known failure signature was found. Inspect the first non-zero command and rerun it with debug logging.",
        ]
        return "\n".join(output) + "\n"

    output += ["## Prioritized findings", ""]
    for index, (rule, matches) in enumerate(findings, 1):
        output += [
            f"### {index}. {rule.title} (`{rule.key}`)",
            "",
            f"Recommended next action: {rule.action}",
            "",
            "Evidence:",
            "",
        ]
        output += [f"- Line {number}: `{line}`" for number, line in matches]
        output.append("")
    return "\n".join(output)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("log", nargs="?", help="CI log path; omit or use - for stdin")
    parser.add_argument("-o", "--output", help="write the Markdown report to this path")
    args = parser.parse_args()
    if not args.log or args.log == "-":
        text, source = sys.stdin.read(), "stdin"
    else:
        path = Path(args.log)
        try:
            text, source = path.read_text(errors="replace"), str(path)
        except OSError as exc:
            parser.error(str(exc))
    report = analyze(text, source)
    if args.output:
        Path(args.output).write_text(report)
    else:
        sys.stdout.write(report)
    return 0


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