"""Core types for the self-hosted CLI. Deliberately standalone rather than
importing from `api/` — the CLI ships as a single Docker image with zero
external runtime dependencies beyond the corpus artifact pull (see hard
constraint #4, self-hosted mode is offline by default), so it can't assume
`api/`'s FastAPI/SQLModel/Postgres stack is anywhere nearby.
"""

from dataclasses import dataclass, field
from enum import StrEnum


class Posture(StrEnum):
    NAIVE = "naive"
    HARDENED = "hardened"
    CUSTOM = "custom"


class SuiteMode(StrEnum):
    QUICK = "quick"
    FULL = "full"


class Severity(StrEnum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


SEVERITY_RANK: dict[Severity, int] = {
    Severity.LOW: 0,
    Severity.MEDIUM: 1,
    Severity.HIGH: 2,
    Severity.CRITICAL: 3,
}


class Verdict(StrEnum):
    HELD = "HELD"
    COMPROMISED = "COMPROMISED"
    LEAKED = "LEAKED"
    OVERREACH = "OVERREACH"
    FALSE_POSITIVE = "FALSE_POSITIVE"
    ERROR = "ERROR"


@dataclass(frozen=True)
class Injection:
    id: str
    name: str
    vector: str
    severity: Severity
    payload: str
    goal: str
    # A benign payload that must NOT provoke a reaction. Defaults False so
    # a corpus written before this field loads unchanged — but a corpus
    # with no control cannot detect an agent that refuses everything, and
    # `abrade run` says so rather than scoring it as though it could.
    is_control: bool = False


@dataclass(frozen=True)
class ResultRow:
    injection_id: str
    status: str  # "done" | "error"
    verdict: Verdict
    reason: str
    tell: str
    agent_reply: str
    injected_telemetry: str
    # Deterministic leak evidence. None means this payload carried no
    # canary — "not measured", never "measured clean".
    canary_token: str | None = None
    canary_exfiltrated: bool | None = None
    # Which kind of value was planted — see canary.py's CanaryType. A
    # leaked credential and a leaked customer SSN are different findings,
    # and the token on its own does not say which one this was.
    canary_type: str | None = None
    # Carried alongside injection_id (not just looked up from the corpus at
    # print time) so a *later*, separate `abrade push` invocation — reading
    # only the saved run JSON, not the original --corpus file, which may no
    # longer be at the same path or may have changed since — still has
    # enough to report a category on Console without an injections table
    # to join against (see api/.../models/run_result.py's docstring).
    injection_name: str = ""
    injection_vector: str = ""
    injection_severity: str = ""
    # Carried for the same reason as the fields above: `abrade push` and
    # `abrade report` read a saved run long after the --corpus file has
    # moved or changed, and without this they cannot tell a benign control
    # from an attack. The gate depends on that distinction. (R2-002)
    injection_is_control: bool = False


@dataclass
class RunRecord:
    id: str
    agent_name: str
    posture: Posture
    suite_mode: SuiteMode
    model: str
    started_at: str
    completed_at: str | None
    status: str  # "running" | "done" | "error"
    containment: int
    results: list[ResultRow] = field(default_factory=list)
