"""``HumanReportWriter`` — terminal-friendly text rendering.

Format goals:

  - Skim-readable at a glance: header line says how many findings,
    followed by per-severity sections.
  - Each finding shows code + message + remediation + probe id.
  - Closing breakdown by root cause helps with attribution
    ("most failures are PROVIDER_5XX → page their on-call").

No ANSI colors in v0.1 — terminals that don't support them get
garbled output and not every consumer pipes to a TTY. Color support
ships when the CLI's ``--color`` flag does (Phase 1.9 + a future
opt-in feature).
"""

from __future__ import annotations

from io import StringIO
from typing import TYPE_CHECKING

from mgf.apiprobe.core.finding import Severity
from mgf.apiprobe.reporting.base import ReportWriter

if TYPE_CHECKING:
    from mgf.apiprobe.core.finding import Finding
    from mgf.apiprobe.core.report import Report


_SEVERITY_ORDER: tuple[Severity, ...] = (
    Severity.CRITICAL,
    Severity.WARN,
    Severity.INFO,
)

#: In a rollup, list at most this many producing probes inline; the rest
#: collapse to "+N more" so a finding shared by dozens of probes stays scannable.
_MAX_PROBES_IN_ROLLUP = 8


class HumanReportWriter(ReportWriter):
    """Produces a human-readable text report.

    Use as::

        writer = HumanReportWriter()
        print(writer.write(report))
    """

    def write(self, report: Report) -> str:
        out = StringIO()

        # ── Header ────────────────────────────────────────────────────────
        breakdown = report.severity_breakdown()
        finding_count = sum(breakdown.values())
        out.write(
            f"Suite ran {report.probe_count} probe(s) in "
            f"{report.duration_seconds:.2f}s — "
            f"{finding_count} finding(s)"
        )
        if report.transport_errors:
            out.write(f", {report.transport_errors} transport error(s)")
        out.write("\n")

        # ── Per-severity sections ─────────────────────────────────────────
        for severity in _SEVERITY_ORDER:
            findings = [f for f in report.findings if f.severity is severity]
            if not findings:
                continue
            out.write(f"\n{severity.name} — {len(findings)}\n")
            for group in _group_findings(findings):
                _render_group(group, out)

        # ── Root-cause breakdown ──────────────────────────────────────────
        rc_breakdown = report.root_cause_breakdown()
        if rc_breakdown:
            out.write("\nRoot cause breakdown:\n")
            for rc, count in sorted(rc_breakdown.items(), key=lambda x: (-x[1], x[0].name)):
                out.write(f"  {rc.name}: {count}\n")

        return out.getvalue()


def _group_findings(findings: list[Finding]) -> list[list[Finding]]:
    """Group findings that render identically — same ``code`` / ``message`` /
    ``remediation`` / ``threat_refs`` / ``root_cause`` — so the *same* issue
    reported across N probes rolls up to one entry instead of N identical
    blocks (PAPER-03 Approach C). First-occurrence order is preserved; the
    underlying ``Report.findings`` (and every breakdown) is untouched.
    """
    groups: dict[tuple[object, ...], list[Finding]] = {}
    for finding in findings:
        key = (
            finding.code,
            finding.message,
            finding.remediation,
            finding.threat_refs,
            finding.root_cause,
        )
        groups.setdefault(key, []).append(finding)
    return list(groups.values())


def _render_group(group: list[Finding], out: StringIO) -> None:
    """Render one group of identically-rendering findings.

    A lone finding renders exactly as before; a group of N renders a single
    block headed by the count + the distinct producing probes::

        <code> (P probes: p1, p2, ..., +M more)
          <message>
          -> <remediation>
    """
    if len(group) == 1:
        _render_finding(group[0], out)
        return
    first = group[0]
    seen: set[str] = set()
    probes: list[str] = []
    for finding in group:
        if finding.probe_id and finding.probe_id not in seen:
            seen.add(finding.probe_id)
            probes.append(finding.probe_id)
    if probes:
        shown = probes[:_MAX_PROBES_IN_ROLLUP]
        more = len(probes) - len(shown)
        suffix = f", +{more} more" if more else ""
        locus = f"({len(probes)} probes: {', '.join(shown)}{suffix})"
    else:
        locus = f"(x{len(group)})"
    out.write(f"  {first.code} {locus}\n")
    _render_body(first, out)


def _render_finding(finding: Finding, out: StringIO) -> None:
    """Render one finding as a 2-4 line block.

    Layout::

        <code> (probe: <probe_id>)
          <message>
          -> <remediation>
          threat_refs: ...

    Lines are 2-space indented under the code header.
    """
    probe_part = f" (probe: {finding.probe_id})" if finding.probe_id else ""
    out.write(f"  {finding.code}{probe_part}\n")
    _render_body(finding, out)


def _render_body(finding: Finding, out: StringIO) -> None:
    """Render the shared message / remediation / threat_refs lines — the part
    identical between a lone finding and a rollup header."""
    out.write(f"    {finding.message}\n")
    if finding.remediation:
        out.write(f"    -> {finding.remediation}\n")
    if finding.threat_refs:
        out.write(f"    threat_refs: {', '.join(finding.threat_refs)}\n")


__all__ = ["HumanReportWriter"]
