"""Public memorial page schemas (INDL-46).

Response schemas assemble the full memorial page payload from the memorial →
record → plot → section → account chain. Only allowlisted fields are exposed —
no ORM model is serialised directly on this unauthenticated surface.
"""
from __future__ import annotations

from datetime import date, datetime
from typing import List, Optional
from uuid import UUID

from pydantic import BaseModel, EmailStr, Field, ValidationInfo, field_validator

from src.core.utils.sanitize import NullByteError, sanitize_plain_text


class QuickFacts(BaseModel):
    birthplace: Optional[str] = None
    city_of_residence: Optional[str] = None
    occupation: Optional[str] = None
    service_date: Optional[date] = None
    service_location: Optional[str] = None


class PlotCoordinates(BaseModel):
    latitude: Optional[float] = None
    longitude: Optional[float] = None
    has_gps: bool = False
    walking_time_min: Optional[int] = None
    distance_m: Optional[int] = None


class TimelineEntry(BaseModel):
    year: Optional[int] = None
    heading: str
    body: Optional[str] = None


class MemorialPhotoItem(BaseModel):
    id: UUID
    url: str
    caption: Optional[str] = None
    sort_order: int = 0


class FamilyContactInfo(BaseModel):
    name: str
    phone: Optional[str] = None
    email: Optional[str] = None


class CemeteryProfile(BaseModel):
    name: Optional[str] = None
    address: Optional[str] = None
    phone: Optional[str] = None
    email: Optional[str] = None
    hours_grounds: Optional[str] = None
    hours_office: Optional[str] = None


class PublicTributeItem(BaseModel):
    id: UUID
    submitter_name: str
    relationship: Optional[str] = None
    message: str
    photo_url: Optional[str] = None
    created_at: datetime


class PublicMemorialDetail(BaseModel):
    id: UUID
    slug: str
    is_published: bool
    display_name: str
    maiden_name: Optional[str] = None
    date_of_birth: Optional[date] = None
    date_of_death: Optional[date] = None
    age_at_death: Optional[int] = None
    plot_ref: Optional[str] = None
    section_code: Optional[str] = None
    biography: Optional[str] = None
    biography_ai_generated: bool = False
    candle_count: int = 0
    photo_count: int = 0
    tribute_count: int = 0
    qr_code_url: Optional[str] = None
    quick_facts: QuickFacts
    plot_coordinates: Optional[PlotCoordinates] = None
    timeline: List[TimelineEntry] = []
    photos: List[MemorialPhotoItem] = []
    cemetery: CemeteryProfile
    family_contact: Optional[FamilyContactInfo] = None
    allow_tributes: bool = True


class CandleResponse(BaseModel):
    candle_count: int


class PublicTributeSubmit(BaseModel):
    """Field-level validation mirror of the multipart tribute form (AC-32).

    The router accepts multipart/form-data (so an optional photo file can ride
    along); this model validates the text fields before persistence.
    """

    submitter_name: str = Field(..., min_length=2, max_length=200)
    submitter_email: EmailStr
    relationship: Optional[str] = Field(None, max_length=200)
    message: str = Field(..., min_length=1, max_length=500)

    @field_validator("submitter_name", "relationship", "message")
    @classmethod
    def _sanitize_free_text(cls, value: Optional[str], info: ValidationInfo) -> Optional[str]:
        """Strip HTML/control characters and reject NUL bytes.

        Without this a NUL byte in `message` reached psycopg and surfaced as a
        500, and submitted markup was stored raw. Both tribute routes validate
        through this model, so sanitising here covers the multipart form and the
        legacy JSON endpoint together.
        """
        if value is None:
            return None
        try:
            cleaned = sanitize_plain_text(value)
        except NullByteError as exc:
            raise ValueError("Input contains a null byte") from exc
        # min_length ran before this validator, so markup-only input ("<b></b>")
        # would otherwise pass the length check and persist as an empty string.
        if not cleaned:
            if info.field_name in {"submitter_name", "message"}:
                raise ValueError("Field must contain some text")
            # Optional field sanitised down to nothing — store NULL, not "".
            return None
        return cleaned
