"""Pydantic schemas for POST /image-scan/extract.

The schema is deliberately open: the document type is not known in advance, so every
field is optional and malformed pieces are dropped rather than failing the whole scan.
"""

from typing import Any, Optional

from pydantic import BaseModel, ConfigDict, Field, field_validator

from src.models.api.response import ExtractionMetadata


CATEGORY_VISITING_CARD = "visiting_card"
CATEGORY_ARTICLE = "article"
CATEGORY_POSTER = "poster"
CATEGORY_OTHER = "other"

_CATEGORY_WORDS = {
    CATEGORY_VISITING_CARD: ("visiting", "business card", "name card", "contact card"),
    CATEGORY_ARTICLE: ("article", "news", "newspaper", "magazine", "blog", "editorial", "story"),
    CATEGORY_POSTER: ("poster", "post", "flyer", "banner", "advert", "brochure", "pamphlet", "notice board"),
}


def normalize_category(value: Any) -> str:
    """Map whatever label the model used onto one of the four supported categories."""

    text = str(value or "").strip().lower().replace("_", " ").replace("-", " ")
    if not text:
        return CATEGORY_OTHER
    for category, words in _CATEGORY_WORDS.items():
        if text == category.replace("_", " ") or any(word in text for word in words):
            return category
    return CATEGORY_OTHER


def _to_text(value: Any) -> Optional[str]:
    if value is None:
        return None
    text = str(value).strip()
    return text or None


def _to_text_list(value: Any) -> list[str]:
    if not isinstance(value, list):
        return []
    return [text for text in (_to_text(item) for item in value) if text]


class ScanBBox(BaseModel):
    """Logo box, normalized 0..1 relative to the image it was found in."""

    x: float = 0.0
    y: float = 0.0
    w: float = 0.0
    h: float = 0.0


class ScanField(BaseModel):
    """One label/value pair read from the document."""

    model_config = ConfigDict(extra="ignore")

    label: Optional[str] = None
    value: Optional[str] = None

    @field_validator("label", "value", mode="before")
    @classmethod
    def _text(cls, v: Any) -> Optional[str]:
        return _to_text(v)


class ScanTable(BaseModel):
    """A table read from the document."""

    model_config = ConfigDict(extra="ignore")

    title: Optional[str] = None
    headers: list[str] = Field(default_factory=list)
    rows: list[list[str]] = Field(default_factory=list)

    @field_validator("title", mode="before")
    @classmethod
    def _title(cls, v: Any) -> Optional[str]:
        return _to_text(v)

    @field_validator("headers", mode="before")
    @classmethod
    def _headers(cls, v: Any) -> list[str]:
        if not isinstance(v, list):
            return []
        return [str(item).strip() if item is not None else "" for item in v]

    @field_validator("rows", mode="before")
    @classmethod
    def _rows(cls, v: Any) -> list[list[str]]:
        if not isinstance(v, list):
            return []
        rows: list[list[str]] = []
        for row in v:
            if isinstance(row, list):
                rows.append([str(cell).strip() if cell is not None else "" for cell in row])
        return rows


class ScanPoster(BaseModel):
    """A poster/post: what it is about, plus everything else it says."""

    model_config = ConfigDict(extra="ignore")

    subject: Optional[str] = None
    details: list[ScanField] = Field(default_factory=list)

    @field_validator("subject", mode="before")
    @classmethod
    def _subject(cls, v: Any) -> Optional[str]:
        return _to_text(v)

    @field_validator("details", mode="before")
    @classmethod
    def _details(cls, v: Any) -> list[Any]:
        if not isinstance(v, list):
            return []
        return [item for item in v if isinstance(item, dict)]


class ScanLogo(BaseModel):
    """A logo seen on the document, plus the cropped image when it could be cut out."""

    model_config = ConfigDict(extra="ignore")

    brand: Optional[str] = None
    description: Optional[str] = None
    image_index: Optional[int] = None
    bbox: Optional[ScanBBox] = None
    logo_image_base64: Optional[str] = None
    logo_image_mime: Optional[str] = None

    @field_validator("brand", "description", mode="before")
    @classmethod
    def _text(cls, v: Any) -> Optional[str]:
        return _to_text(v)

    @field_validator("image_index", mode="before")
    @classmethod
    def _index(cls, v: Any) -> Optional[int]:
        if isinstance(v, bool):
            return None
        try:
            return int(v)
        except (TypeError, ValueError):
            return None

    @field_validator("bbox", mode="before")
    @classmethod
    def _bbox(cls, v: Any) -> Optional[dict[str, float]]:
        if not isinstance(v, dict):
            return None
        try:
            x, y, w, h = (float(v[key]) for key in ("x", "y", "w", "h"))
        except (KeyError, TypeError, ValueError):
            return None
        x = min(max(x, 0.0), 1.0)
        y = min(max(y, 0.0), 1.0)
        w = min(max(w, 0.0), 1.0 - x)
        h = min(max(h, 0.0), 1.0 - y)
        return {"x": x, "y": y, "w": w, "h": h}


class ScannedDocument(BaseModel):
    """One distinct document found in the uploaded images."""

    model_config = ConfigDict(extra="ignore")

    image_indexes: list[int] = Field(default_factory=list)
    category: str = CATEGORY_OTHER
    document_type: Optional[str] = None
    issuer: Optional[str] = None
    title: Optional[str] = None
    summary: Optional[str] = None
    language: Optional[str] = None
    fields: list[ScanField] = Field(default_factory=list)
    tables: list[ScanTable] = Field(default_factory=list)
    full_text: Optional[str] = None
    card_data: Optional[dict[str, str]] = None
    poster: Optional[ScanPoster] = None
    logos: list[ScanLogo] = Field(default_factory=list)

    @field_validator("category", mode="before")
    @classmethod
    def _category(cls, v: Any) -> str:
        return normalize_category(v)

    @field_validator("card_data", mode="before")
    @classmethod
    def _card_data(cls, v: Any) -> Optional[dict[str, str]]:
        if not isinstance(v, dict):
            return None
        cleaned: dict[str, str] = {}
        for key, value in v.items():
            if isinstance(value, list):
                value = ", ".join(str(item).strip() for item in value if item is not None)
            text = _to_text(value)
            name = _to_text(key)
            if name and text:
                cleaned[name] = text
        return cleaned or None

    @field_validator("poster", mode="before")
    @classmethod
    def _object_or_none(cls, v: Any) -> Optional[dict[str, Any]]:
        return v if isinstance(v, dict) else None

    @field_validator("document_type", "issuer", "title", "summary", "language", "full_text", mode="before")
    @classmethod
    def _text(cls, v: Any) -> Optional[str]:
        return _to_text(v)

    @field_validator("image_indexes", mode="before")
    @classmethod
    def _indexes(cls, v: Any) -> list[int]:
        if not isinstance(v, list):
            return []
        indexes: list[int] = []
        for item in v:
            if isinstance(item, bool):
                continue
            try:
                indexes.append(int(item))
            except (TypeError, ValueError):
                continue
        return indexes

    @field_validator("fields", "tables", "logos", mode="before")
    @classmethod
    def _object_lists(cls, v: Any) -> list[Any]:
        if not isinstance(v, list):
            return []
        return [item for item in v if isinstance(item, dict)]


class ImageScanLLMOutput(BaseModel):
    """Shape of the JSON the model returns."""

    model_config = ConfigDict(extra="ignore")

    documents: list[ScannedDocument] = Field(default_factory=list)
    warnings: list[str] = Field(default_factory=list)

    @field_validator("documents", mode="before")
    @classmethod
    def _documents(cls, v: Any) -> list[Any]:
        if not isinstance(v, list):
            return []
        return [item for item in v if isinstance(item, dict)]

    @field_validator("warnings", mode="before")
    @classmethod
    def _warnings(cls, v: Any) -> list[str]:
        return _to_text_list(v)


class ScanDocumentMetadata(BaseModel):
    """Everything about a document apart from its headline keys, kept together under `metadata`."""

    image_indexes: list[int] = Field(default_factory=list)
    title: Optional[str] = None
    summary: Optional[str] = None
    language: Optional[str] = None
    fields: list[ScanField] = Field(default_factory=list)
    tables: list[ScanTable] = Field(default_factory=list)
    # For an article this is the whole article as plain text.
    full_text: Optional[str] = None
    logos: list[ScanLogo] = Field(default_factory=list)


class ScanDocumentOut(BaseModel):
    """One document as the API returns it."""

    category: str = CATEGORY_OTHER
    document_type: Optional[str] = None
    issuer: Optional[str] = None
    card_data: Optional[dict[str, str]] = None
    poster: Optional[ScanPoster] = None
    metadata: ScanDocumentMetadata


class ImageScanResponse(BaseModel):
    """API response: the documents found, plus request-level info and model usage."""

    scan_id: Optional[str] = None
    image_count: int
    documents: list[ScanDocumentOut] = Field(default_factory=list)
    warnings: list[str] = Field(default_factory=list)
    usage: ExtractionMetadata
