"""
models.py
─────────
Pydantic schemas used by the Fraud Report API.

These are intentionally tolerant: real-world fraud_log.json rows
sometimes miss fields (e.g. `access_key` may be absent in the last
DISQUALIFIED event of a session). All optional fields default to None
so the API never throws a 500 on legacy / malformed rows.

When the data source moves to SQL/NoSQL, only `data_source.py`
changes — these models stay the same, which keeps the OpenAPI contract
and the JS frontend stable.
"""
from __future__ import annotations
from typing import Any, Optional
from pydantic import BaseModel, Field


# ── Raw fraud event row ─────────────────────────────────────────────
class FraudEvent(BaseModel):
    """Mirror of a single row in fraud_log.json."""

    id:           Optional[str]   = None
    candidate_id: Optional[str]   = None
    access_key:   Optional[str]   = None
    event_type:   str             = "UNKNOWN"
    message:      Optional[str]   = ""
    timestamp:    Optional[str]   = None
    round:        int             = 0
    action:       Optional[str]   = "none"
    metadata:     dict[str, Any]  = Field(default_factory=dict)
    created_at:   Optional[str]   = None
    updated_at:   Optional[str]   = None


# ── Stage descriptor (enriched at response time) ────────────────────
class StageInfo(BaseModel):
    round_id:    int
    short_label: str
    full_name:   str
    category:    str
    accent:      str
    known:       bool = True


# ── Single event as returned to the frontend ────────────────────────
class FraudEventOut(FraudEvent):
    """Same row as FraudEvent + derived fields the UI cares about."""

    severity:         str       = "low"   # low | medium | high | critical
    stage:            StageInfo
    detection_source: str       = "client-monitor"
    session_id:       Optional[str] = None
    scenario_id:      Optional[str] = None
    screenshot_url:   Optional[str] = None
    page:             Optional[str] = None
    user_agent:       Optional[str] = None


# ── Per-stage rollup ────────────────────────────────────────────────
class StageSummary(BaseModel):
    stage:        StageInfo
    event_count:  int
    severity_max: str          # highest severity seen in this stage
    actions:      list[str]    # distinct `action` values seen
    flagged:      bool         # any event at all?
    disqualified: bool         # explicit disqualify action present


# ── Report-level summary widgets ────────────────────────────────────
class ReportSummary(BaseModel):
    access_key:       str
    candidate_id:     Optional[str]
    total_events:     int
    by_event_type:    dict[str, int]
    by_severity:      dict[str, int]
    by_round:         dict[str, int]
    sessions_seen:    list[str]
    first_event_at:   Optional[str]
    last_event_at:    Optional[str]
    risk_score:       str          # CLEAN | LOW | MEDIUM | HIGH | CRITICAL
    fraud_status:     str          # PASS | WARN | DISQUALIFIED
    session_status:   str          # ACTIVE | ENDED | DISQUALIFIED
    is_disqualified:  bool


# ── Full report payload ─────────────────────────────────────────────
class FraudReport(BaseModel):
    summary:        ReportSummary
    stage_track:    list[StageSummary]
    events:         list[FraudEventOut]


# ── Lightweight directory entry for the dropdown ────────────────────
class AccessKeyEntry(BaseModel):
    access_key:    str
    candidate_id:  Optional[str] = None
    event_count:   int
    last_event_at: Optional[str] = None
    risk_score:    str           = "CLEAN"


class AccessKeyList(BaseModel):
    count: int
    items: list[AccessKeyEntry]
