from __future__ import annotations

from dataclasses import dataclass
from decimal import Decimal
from typing import Annotated, Any, Generic, Literal, TypeVar
from pydantic import BaseModel, ConfigDict, Field, model_validator

T = TypeVar("T")
S = TypeVar("S")


class NormalizedValue(BaseModel):
    """Document AI normalized value payload for extracted entity fields."""

    text: str | None = None
    integer_value: int | None = None
    float_value: float | None = None

    model_config = ConfigDict(extra="allow")


class RawEntity(BaseModel):
    """Raw Document AI entity, including nested property entities."""

    type: str
    mention_text: str = ""
    normalized_value: NormalizedValue | None = None
    confidence: float
    id: str | None = None
    properties: list[RawEntity] = Field(default_factory=list)


class RawOcrDocument(BaseModel):
    """OCR response shape returned by this library before domain conversion."""

    project_id: str
    location: str
    processor_id: str
    source_file: str
    ocr_text: str
    entities: list[RawEntity]
    entities_by_type: dict[str, list[RawEntity]] = Field(default_factory=dict)

class RangeSpec(BaseModel, Generic[S]):
    """Validates that a numeric value falls within [min, max]."""

    model_config = ConfigDict(frozen=True)

    type: Literal["range"] = "range"
    min: S | None = None
    max: S | None = None

    def check(self, value: Any) -> bool:
        if value is None:
            return False
        try:
            numeric = Decimal(str(value))
            if self.min is not None and numeric < Decimal(str(self.min)):
                return False
            if self.max is not None and numeric > Decimal(str(self.max)):
                return False
            return True
        except Exception:
            return False


class OptionSpec(BaseModel, Generic[S]):
    """Validates that a value is one of a declared set of options."""
    model_config = ConfigDict(frozen=True)

    type: Literal["options"] = "options"
    options: tuple[S, ...]

    def check(self, value: Any) -> bool:
        if value is None:
            return False
        return value in self.options


OcrSpec = Annotated[RangeSpec | OptionSpec, Field(discriminator="type")]


@dataclass(frozen=True)
class FieldSpec:
    """Annotated metadata for declaring validation specs on log entry model fields."""

    ocr_spec: RangeSpec | OptionSpec | None = None
    ocr_default: str | None = None
    # A required field that ends up with no value at all (no entity/property emitted by
    # the OCR provider for a blank cell) gets a synthesized flagged OcrField placeholder
    # instead of silently staying `None` -- see OcrParser.apply_required_fields.
    required: bool = False


class OcrField(BaseModel, Generic[T]):
    """A parsed OCR value with the original text and extraction metadata."""

    raw_text: str
    value: T | None = None
    confidence: float | None = None
    entity_id: str | None = None
    normalized_text: str | None = None
    flag: bool = False
    ocr_spec: OcrSpec | None = None
    ocr_default: str | None = None

    @model_validator(mode="after")
    def _apply_spec(self) -> OcrField[T]:
        if self.ocr_spec is None:
            return self
        if self.value is not None:
            spec_meta = getattr(type(self.ocr_spec), "__pydantic_generic_metadata__", {})
            spec_type_args = spec_meta.get("args", ())
            if spec_type_args and not isinstance(self.value, spec_type_args[0]):
                raise ValueError(
                    f"{type(self.ocr_spec).__qualname__} expects "
                    f"{spec_type_args[0].__name__} but got {type(self.value).__name__}"
                )
        if not self.ocr_spec.check(self.value):
            if isinstance(self.ocr_spec, OptionSpec) and self.ocr_default is not None:
                # A default resolves the mismatch (e.g. "Nod" -> "Nm"), so this isn't something
                # a reviewer needs to look at — flagging it would just be noise.
                self.normalized_text = self.ocr_default
            else:
                self.flag = True
                if self.ocr_default is not None:
                    self.normalized_text = self.ocr_default
        return self


class OCRResult(BaseModel, Generic[T]):
    """Structured OCR output with timing metadata."""

    result: T
    time_elapsed: float
