"""Core data structures shared across detectors, the engine, and reporting.

A Finding is the atomic unit every detector emits. The Report aggregates findings
plus informational notes (silent-degrade messages, review-needed flags) and any
per-file errors. Everything here is plain data so it serializes cleanly to JSON
and SARIF without pulling in a serialization framework.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional


class Severity(str, Enum):
    """Severity levels, ordered by rank. Inherits str so it serializes as its value."""

    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"

    @property
    def rank(self) -> int:
        return _SEVERITY_RANK[self]

    @classmethod
    def from_name(cls, name: str) -> "Severity":
        return cls(name.lower())


_SEVERITY_RANK = {
    Severity.CRITICAL: 3,
    Severity.HIGH: 2,
    Severity.MEDIUM: 1,
    Severity.LOW: 0,
}


@dataclass
class Finding:
    """One detected item.

    detector:   stable detector id, e.g. "structured.ssn" or "name.frame.caption".
    type:       finding type, e.g. "ssn", "person_name", "tracked_change".
    severity:   Severity level.
    file:       source file path (as given on the command line / walked).
    location:   human locator within the file, e.g. "page 2", "paragraph 5", "line 10",
                or a metadata field path like "docx.core.author".
    span:       (start, end) character offsets within the text unit, when applicable.
    preview:    redacted, safe-to-display snippet (sensitive bytes masked).
    role:       inferred role for name findings (petitioner, testator, ...), else None.
    confidence: optional 0..1 confidence (frame strength + corroboration).
    detail:     free-form extra context (field path, matched raw length, classifier reason).
    """

    detector: str
    type: str
    severity: Severity
    file: str
    location: str = ""
    span: Optional[tuple[int, int]] = None
    preview: str = ""
    role: Optional[str] = None
    confidence: Optional[float] = None
    detail: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        return {
            "detector": self.detector,
            "type": self.type,
            "severity": self.severity.value,
            "file": self.file,
            "location": self.location,
            "span": list(self.span) if self.span is not None else None,
            "preview": self.preview,
            "role": self.role,
            "confidence": self.confidence,
            "detail": self.detail,
        }


@dataclass
class Report:
    """Aggregated scan result."""

    findings: list[Finding] = field(default_factory=list)
    notes: list[str] = field(default_factory=list)
    scanned_files: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)

    def add(self, *findings: Finding) -> None:
        self.findings.extend(findings)

    def note(self, message: str) -> None:
        if message not in self.notes:
            self.notes.append(message)

    def max_severity(self) -> Optional[Severity]:
        if not self.findings:
            return None
        return max((f.severity for f in self.findings), key=lambda s: s.rank)

    def sorted_findings(self) -> list[Finding]:
        """Severity-ranked (critical first), then by file and location."""
        return sorted(
            self.findings,
            key=lambda f: (-f.severity.rank, f.file, f.location),
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "findings": [f.to_dict() for f in self.sorted_findings()],
            "notes": list(self.notes),
            "scanned_files": list(self.scanned_files),
            "errors": list(self.errors),
            "summary": self.severity_counts(),
        }

    def severity_counts(self) -> dict[str, int]:
        counts = {s.value: 0 for s in Severity}
        for f in self.findings:
            counts[f.severity.value] += 1
        return counts


@dataclass
class TextUnit:
    """A unit of extractable text with a human locator."""

    text: str
    locator: str
    # Offset of this unit's text within the document's full_text, so per-unit
    # spans can be mapped to document-global positions when needed.
    base_offset: int = 0


@dataclass
class ExtractedDoc:
    """The result of extracting one file."""

    path: str
    kind: str  # "txt", "docx", "pdf", "image", "eml"
    units: list[TextUnit] = field(default_factory=list)
    full_text: str = ""
    notes: list[str] = field(default_factory=list)
    # Metadata findings are produced during extraction (R-4) and folded into the report.
    metadata_findings: list[Finding] = field(default_factory=list)
