# FILE: src/apps/public/schemas/records.py
"""
Schemas for the public (unauthenticated) records search + AI-search endpoints
(INDL-48).

`PublicRecordSummaryResponse` is intentionally a SEPARATE, minimal schema from
`src.apps.records.schemas.responses.RecordSummaryResponse` — that shared
schema backs authenticated staff endpoints and legitimately carries
`date_of_birth`/`date_of_death` (full dates), `gender`, `visibility_config`,
`status`, timestamps, etc. Reusing it for an unauthenticated public surface
would violate the INDL-48 security requirements (OWASP API3 / SEC-05 / S-08),
which require the public payload to expose an explicit, reviewed allowlist
and nothing else — in particular, no full dates of birth/death, no
address/contact/financial fields.

`maiden_name`/`occupation`/`city_of_residence` were added to the allowlist
after the initial delivery to match the reference prototype's result-card
design — conventional genealogy-style fields (comparable to what FindAGrave
displays publicly), not sensitive PII. This was a deliberate, reviewed
expansion of the allowlist, not scope creep.

`extra="forbid"` on both models is a defence-in-depth belt-and-suspenders
control: if a future field is ever added to `Record` and accidentally passed
into these models, construction fails loudly instead of silently leaking it.
"""
from typing import Literal, Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field

_SECTION_CODE_PATTERN = r"^[A-Za-z0-9\-]{1,20}$"


class PublicRecordSummaryResponse(BaseModel):
    """The only fields ever returned by /api/public/records and
    /api/public/records/ai-search. See SEC-05 / S-08 in the INDL-48 PRD."""

    model_config = ConfigDict(extra="forbid")

    id: UUID
    first_name: str
    last_name: str
    maiden_name: Optional[str] = None
    year_of_birth: Optional[int] = None
    year_of_death: Optional[int] = None
    plot_ref: Optional[str] = None
    section_code: Optional[str] = None
    interment_type: Optional[str] = None
    memorial_slug: Optional[str] = None
    occupation: Optional[str] = None
    city_of_residence: Optional[str] = None


class AISearchRequest(BaseModel):
    """Raw request body for POST /api/public/records/ai-search. Length/blank
    validation is performed manually in the router so the exact PRD error
    messages ("Query is required" / "Query must be 500 characters or fewer")
    are returned instead of a generic Pydantic validation error."""

    query: str = Field(..., max_length=2000)


class AISearchExtraction(BaseModel):
    """Strict schema Claude's tool-use response must validate against before
    any field is used to build a DB query (OWASP A08 / API10). Unexpected
    fields are dropped by raising instead of silently passing them through."""

    model_config = ConfigDict(extra="forbid")

    first_name: Optional[str] = Field(None, max_length=100)
    last_name: Optional[str] = Field(None, max_length=100)
    year_of_birth_approx: Optional[int] = Field(None, ge=1800, le=2100)
    year_of_death_approx: Optional[int] = Field(None, ge=1800, le=2100)
    # Server-side re-validation against the same allowlist enforced by the
    # Claude tool-use JSON schema — the LLM's adherence to its own tool schema
    # must never be the only gate (OWASP A08 / V5.1.3). An out-of-allowlist
    # value from Claude fails Pydantic validation here and the whole
    # extraction is treated as unusable by the caller.
    interment_type: Optional[Literal["burial", "cremation_interred", "pre_need"]] = None
    section_code: Optional[str] = Field(None, max_length=20, pattern=_SECTION_CODE_PATTERN)
    gender: Optional[str] = Field(None, max_length=30)
