"""Data models for the agentic extraction loop.

Every step produces structured data. All models support dict serialization
via dataclasses.asdict() for JSON export and later analysis.
"""

from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any


@dataclass
class FieldSchema:
    """A field the agent should extract."""

    name: str
    description: str = ""
    field_type: str = "string"  # string, number, currency, date, boolean, address, list, table


@dataclass
class GoldStandard:
    """Expected extraction values, generated by VLM/LLM."""

    fields: dict[str, Any]
    model_used: str = ""
    mode: str = ""  # "vision" or "text"
    raw_response: str = ""
    generation_time_ms: int = 0
    tokens_used: int = 0
    warnings: list[str] = field(default_factory=list)
    suspect_fields: set[str] = field(default_factory=set)  # fields flagged by preflight — excluded from scoring denominator

    @classmethod
    def from_dict(cls, d: dict[str, Any]) -> GoldStandard:
        """Create a GoldStandard from a plain dict of field values (for manual/stub gold)."""
        return cls(fields=d, model_used="manual", mode="manual")


@dataclass
class FieldComparison:
    """Result of comparing one extracted field to its gold value."""

    field_name: str
    expected: Any
    actual: Any
    match: bool
    match_type: str  # "exact", "normalized", "numeric", "date", "missing", "wrong", "error"
    score: float  # 0.0 or 1.0
    raw_expected: str = ""
    raw_actual: str | None = None
    notes: str = ""


@dataclass
class SelectorProbe:
    """Result of re-running a selector from generated code against the page."""

    selector: str
    match_count: int
    candidates: list[dict] = field(default_factory=list)  # [{text, bbox, size, bold}, ...]
    line_number: int = 0


@dataclass
class LintViolation:
    """A linter rule violation in generated code."""

    rule: str
    line: int
    column: int
    message: str
    severity: str  # "error" or "warning"
    snippet: str = ""


@dataclass
class ExecutionResult:
    """Result of running a generated extraction script."""

    success: bool
    output: dict | None = None
    stdout: str = ""
    stderr: str = ""
    error_type: str | None = None  # "runtime", "timeout", "json_parse", "import", "lint_block"
    error_message: str | None = None
    duration_ms: int = 0
    traceback: str | None = None


@dataclass
class AttemptDiagnostics:
    """Post-execution diagnostic enrichment."""

    failure_category: str | None = None  # NO_MATCH, MULTI_MATCH, PARSE_FAIL, WRONG_VALUE, CRASH, TIMEOUT
    selector_probes: list[SelectorProbe] = field(default_factory=list)
    lessons: list[str] = field(default_factory=list)
    approach_used: str = "unknown"  # anchor_spatial, table, guides, layout, regex, mixed


@dataclass
class ExtractionAttempt:
    """One iteration of the extraction loop."""

    iteration: int
    script: str
    lint_violations: list[LintViolation] = field(default_factory=list)
    execution: ExecutionResult = field(default_factory=lambda: ExecutionResult(success=False))
    field_comparisons: list[FieldComparison] = field(default_factory=list)
    diagnostics: AttemptDiagnostics = field(default_factory=AttemptDiagnostics)
    overall_score: float = 0.0
    generation_time_ms: int = 0
    generation_tokens: int = 0
    total_time_ms: int = 0


@dataclass
class ExtractionSession:
    """Complete record of an extraction session — inputs, attempts, outcome."""

    # Inputs
    pdf_path: str
    page_number: int
    schema: list[FieldSchema]
    to_llm_output: str = ""
    config: dict = field(default_factory=dict)

    # Gold
    gold: GoldStandard = field(default_factory=lambda: GoldStandard(fields={}))

    # Iteration history
    attempts: list[ExtractionAttempt] = field(default_factory=list)

    # Outcome
    best_script: str | None = None
    best_score: float = 0.0
    converged: bool = False
    stop_reason: str = ""  # "converged", "max_iterations", "repeated_failure", "lint_block"

    # Review (optional post-loop step)
    reviewed_script: str | None = None
    review_explanation: str = ""
    review_time_ms: int = 0

    # Telemetry
    total_tokens: int = 0
    total_time_ms: int = 0

    def to_dict(self) -> dict:
        return asdict(self)

    def to_json(self, path: str | Path) -> None:
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "w") as f:
            json.dump(self.to_dict(), f, indent=2, default=str)
