# FILE: src/apps/pages/schemas/page_content.py
from __future__ import annotations

import re
from datetime import datetime
from typing import Dict, List, Literal, Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator

_PHONE_RE = re.compile(r"^(\(\d{3}\)\s?\d{3}-\d{4}|\+1\d{10})$")
_POSTAL_RE = re.compile(r"^[A-Za-z]\d[A-Za-z]\s?\d[A-Za-z]\d$")
_HTML_TAG_RE = re.compile(r"<[^>]*>")
_PROVINCES = ("AB", "BC", "MB", "NB", "NL", "NS", "NT", "NU", "ON", "PE", "QC", "SK", "YT")

PAGE_SLUGS = ("global", "find", "availability", "map", "contact")


def _strip_html(v: Optional[str]) -> Optional[str]:
    """Reject CMS text containing HTML tags (stored-XSS defense — PRD A03)."""
    if v is None:
        return v
    if _HTML_TAG_RE.search(v):
        raise ValueError("HTML tags are not allowed in this field")
    return v


class GlobalContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    phone_main: str
    phone_afterhours: Optional[str] = None
    email: str = Field(max_length=255)
    address_street: str = Field(min_length=5, max_length=255)
    address_city: str = Field(min_length=2, max_length=100)
    address_province: Literal[*_PROVINCES]  # type: ignore[valid-type]
    address_postal: str
    hours_grounds: str = Field(min_length=5, max_length=100)
    hours_office_weekday: str = Field(min_length=5, max_length=100)
    hours_office_saturday: Optional[str] = Field(default=None, max_length=100)
    total_hectares: float
    lat: float
    lng: float

    @model_validator(mode="before")
    @classmethod
    def _drop_legacy_founding_year(cls, data: object) -> object:
        # founding_year moved to accounts.established_year (Settings > Cemetery Profile) —
        # this quietly discards the key from rows/payloads saved before the move instead
        # of failing extra="forbid" validation on old stored JSONB.
        if isinstance(data, dict) and "founding_year" in data:
            data = {k: v for k, v in data.items() if k != "founding_year"}
        return data

    @field_validator("phone_main", "phone_afterhours")
    @classmethod
    def validate_phone(cls, v: Optional[str]) -> Optional[str]:
        if v is None or v == "":
            return v
        if not _PHONE_RE.match(v):
            raise ValueError("Please enter a valid phone number")
        return v

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        # Reuse EmailStr's validation without changing the field type (kept as str for stable JSONB shape)
        from pydantic import TypeAdapter
        TypeAdapter(EmailStr).validate_python(v)
        return v

    @field_validator("address_city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        if not all(ch.isalpha() or ch in " '-" for ch in v):
            raise ValueError("Please enter a city")
        return v

    @field_validator("address_postal")
    @classmethod
    def validate_postal(cls, v: str) -> str:
        if not _POSTAL_RE.match(v):
            raise ValueError("Please enter a valid Canadian postal code")
        v = v.upper().replace(" ", "")
        return f"{v[:3]} {v[3:]}"

    @field_validator("total_hectares")
    @classmethod
    def validate_hectares(cls, v: float) -> float:
        if v <= 0 or v > 9999.9:
            raise ValueError("Please enter total hectares")
        return v

    @field_validator("lat")
    @classmethod
    def validate_lat(cls, v: float) -> float:
        if v < -90 or v > 90:
            raise ValueError("Please enter a valid latitude (-90 to 90)")
        return v

    @field_validator("lng")
    @classmethod
    def validate_lng(cls, v: float) -> float:
        if v < -180 or v > 180:
            raise ValueError("Please enter a valid longitude (-180 to 180)")
        return v


class FindContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    hero_headline: str = Field(min_length=2, max_length=100)
    hero_subtitle: str = Field(min_length=10, max_length=500)
    context_bar_text: Optional[str] = Field(default=None, max_length=100)
    footer_note: Optional[str] = Field(default=None, max_length=500)
    ai_chips: List[str] = Field(default_factory=list, max_length=5)
    find_card1_title: Optional[str] = Field(default=None, max_length=60)
    find_card1_description: Optional[str] = Field(default=None, max_length=200)
    find_card2_title: Optional[str] = Field(default=None, max_length=60)
    find_card2_description: Optional[str] = Field(default=None, max_length=200)
    find_card3_title: Optional[str] = Field(default=None, max_length=60)
    find_card3_description: Optional[str] = Field(default=None, max_length=200)

    _strip = field_validator(
        "hero_headline", "hero_subtitle", "context_bar_text", "footer_note",
        "find_card1_title", "find_card1_description",
        "find_card2_title", "find_card2_description",
        "find_card3_title", "find_card3_description",
    )(_strip_html)

    @field_validator("ai_chips")
    @classmethod
    def validate_ai_chips(cls, v: List[str]) -> List[str]:
        if len(v) > 5:
            raise ValueError("A maximum of 5 chips is allowed")
        for chip in v:
            _strip_html(chip)
            if len(chip) < 10 or len(chip) > 200:
                raise ValueError("Each chip must be 10-200 characters")
        return v


class AvailabilityContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    hero_headline: str = Field(min_length=2, max_length=100)
    hero_lead: str = Field(min_length=10, max_length=500)
    no_checkout_notice: Optional[str] = Field(default=None, max_length=300)

    _strip = field_validator("hero_headline", "hero_lead", "no_checkout_notice")(_strip_html)


class SectionPin(BaseModel):
    model_config = ConfigDict(extra="forbid")

    x: float
    y: float

    @field_validator("x", "y")
    @classmethod
    def clamp(cls, v: float) -> float:
        if v < 0 or v > 100:
            raise ValueError("Pin coordinates must be 0-100")
        return v


_S3_URL_RE = re.compile(r"^https://[a-zA-Z0-9.\-]*amazonaws\.com/")


class MapSectionCard(BaseModel):
    model_config = ConfigDict(extra="forbid")

    code: str = Field(min_length=1, max_length=5)
    name: str = Field(min_length=2, max_length=100)
    established_year: Optional[int] = None
    description: Optional[str] = Field(default=None, max_length=400)
    plot_types: Optional[str] = Field(default=None, max_length=200)
    availability_label: Optional[str] = Field(default=None, max_length=100)
    notable_features: Optional[str] = Field(default=None, max_length=200)

    _strip = field_validator(
        "code", "name", "description", "plot_types", "availability_label", "notable_features",
    )(_strip_html)

    @field_validator("established_year")
    @classmethod
    def validate_established_year(cls, v: Optional[int]) -> Optional[int]:
        if v is None:
            return v
        if v < 1600 or v > datetime.now().year:
            raise ValueError("Please enter a valid founding year")
        return v


class MapContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    hero_eyebrow: str = Field(min_length=2, max_length=50)
    hero_headline: str = Field(min_length=2, max_length=100)
    hero_subtitle: str = Field(min_length=10, max_length=500)
    map_image_url: Optional[str] = None
    map_pdf_en_url: Optional[str] = None
    map_pdf_fr_url: Optional[str] = None
    section_pins: Dict[str, SectionPin] = Field(default_factory=dict)
    parking_note: Optional[str] = Field(default=None, max_length=300)
    printable_map_description: Optional[str] = Field(default=None, max_length=300)
    printable_map_status_note: Optional[str] = Field(default=None, max_length=300)
    sections: List[MapSectionCard] = Field(default_factory=list, max_length=6)

    _strip = field_validator(
        "hero_eyebrow", "hero_headline", "hero_subtitle",
        "parking_note", "printable_map_description", "printable_map_status_note",
    )(_strip_html)

    @field_validator("map_image_url", "map_pdf_en_url", "map_pdf_fr_url")
    @classmethod
    def validate_s3_url(cls, v: Optional[str]) -> Optional[str]:
        # Only the platform's own S3/CloudFront host is ever allowed here (PRD A10/API7 SSRF defense)
        if v is None or v == "":
            return None
        if not (_S3_URL_RE.match(v) or v.startswith("https://d") and "cloudfront.net" in v):
            raise ValueError("File URL must point to the platform's storage")
        return v


class ContactContent(BaseModel):
    model_config = ConfigDict(extra="forbid")

    hero_headline: str = Field(min_length=2, max_length=100)
    hero_subtitle: str = Field(min_length=10, max_length=500)
    gate1_description: Optional[str] = Field(default=None, max_length=300)
    gate2_description: Optional[str] = Field(default=None, max_length=300)
    transit_route: Optional[str] = Field(default=None, max_length=100)
    transit_stop: Optional[str] = Field(default=None, max_length=200)
    transit_walk_time: Optional[str] = Field(default=None, max_length=100)
    parking_note: Optional[str] = Field(default=None, max_length=300)
    accessibility_note: Optional[str] = Field(default=None, max_length=300)
    email_reply_time: Optional[str] = Field(default=None, max_length=100)

    _strip = field_validator(
        "hero_headline", "hero_subtitle", "gate1_description", "gate2_description",
        "transit_route", "transit_stop", "transit_walk_time", "parking_note",
        "accessibility_note", "email_reply_time",
    )(_strip_html)


SLUG_SCHEMAS = {
    "global": GlobalContent,
    "find": FindContent,
    "availability": AvailabilityContent,
    "map": MapContent,
    "contact": ContactContent,
}


class PageContentOut(BaseModel):
    model_config = ConfigDict(from_attributes=True)

    page_slug: str
    content: dict
    updated_at: Optional[datetime] = None
    updated_by: Optional[UUID] = None
    updated_by_name: Optional[str] = None
