"""``Report`` — the aggregated output of a :class:`Suite` run.

See: ``DESIGN.md`` §3.7 (Report + writers).

A Report is a frozen dataclass of findings + run metadata. It does
not know how to render itself — the writers in
:mod:`mgf.apiprobe.reporting` consume Report and emit text / JSON /
JUnit / etc. (Currently console + JSON ship; the rest are future
adds.)

Helpers on :class:`Report` cover the common consumer questions:
"did anything fail?" / "what's the worst severity?" / "how do
findings break down by root cause?". Anything richer belongs in a
writer or a custom analysis layer.
"""

from __future__ import annotations

from collections import Counter
from dataclasses import dataclass
from typing import TYPE_CHECKING

from mgf.apiprobe.core.finding import RootCause, Severity, severity_at_least

if TYPE_CHECKING:
    from datetime import datetime

    from mgf.apiprobe.core.finding import Finding


@dataclass(frozen=True, slots=True)
class Report:
    """One suite run's output.

    Findings are insertion-ordered (per-probe order in the suite,
    then per-check order in :data:`mgf.apiprobe.checks.DEFAULT_CHECKS`).
    Stable ordering matters for diffing reports across runs, which
    is what the v0.4 drift-detection feature consumes.
    """

    findings: tuple[Finding, ...]
    """All findings emitted during the run."""

    started_at: datetime
    """Wall-clock time the suite started. UTC, timezone-aware."""

    finished_at: datetime
    """Wall-clock time the suite finished. UTC, timezone-aware."""

    probe_count: int
    """Number of probes the suite executed."""

    transport_errors: int = 0
    """Number of probes that failed at the transport layer (DNS, TLS,
    timeout). These produce a Finding with ``RootCause.NETWORK`` AND
    are counted here so reports surface "we couldn't even talk to the
    provider" separately from "the response was wrong."
    """

    @property
    def duration_seconds(self) -> float:
        """Wall-clock duration of the run."""
        return (self.finished_at - self.started_at).total_seconds()

    @property
    def has_critical(self) -> bool:
        """True iff any finding has :attr:`Severity.CRITICAL`."""
        return any(f.severity is Severity.CRITICAL for f in self.findings)

    def has_findings_meeting(self, threshold: Severity) -> bool:
        """True iff any finding's severity is at or above ``threshold``.

        Used by the suite-exit-code translator: ``True`` → suite exits
        non-zero. The threshold defaults to ``Severity.CRITICAL`` per
        :class:`~mgf.apiprobe.config.ApiprobeSettings.fail_on_severity`.
        """
        return any(severity_at_least(f.severity, threshold) for f in self.findings)

    def severity_breakdown(self) -> dict[Severity, int]:
        """Count findings by severity. Always returns all 3 keys."""
        counts = Counter(f.severity for f in self.findings)
        return {s: counts.get(s, 0) for s in Severity}

    def root_cause_breakdown(self) -> dict[RootCause, int]:
        """Count findings by root cause. Returns only non-zero keys."""
        return dict(Counter(f.root_cause for f in self.findings))

    def by_probe(self) -> dict[str | None, tuple[Finding, ...]]:
        """Group findings by probe id.

        Probes without a probe_id share the ``None`` key. Useful for
        report writers that group output per-probe.
        """
        groups: dict[str | None, list[Finding]] = {}
        for f in self.findings:
            groups.setdefault(f.probe_id, []).append(f)
        return {k: tuple(v) for k, v in groups.items()}


__all__ = ["Report"]
