import types
from datetime import datetime, time as Time
from decimal import Decimal
from typing import Any, TypeVar, get_args, get_origin

from pydantic import BaseModel

from ..models import OcrField
from ..parse_ocr.parser import OcrParser

ModelT = TypeVar("ModelT", bound=BaseModel)


def _list_item_model(annotation: Any) -> type[BaseModel] | None:
    if get_origin(annotation) is list:
        args = get_args(annotation)
        if args and isinstance(args[0], type) and issubclass(args[0], BaseModel):
            return args[0]
    return None


def _is_plain_dict_annotation(annotation: Any) -> bool:
    """True for `dict[str, str] | None`-shaped fields (e.g. the *_column_frequencies
    fields) -- these are plain read-only transcriptions, not `OcrField`-wrapped leaves,
    so they need to bypass _convert_leaf's OcrField-construction path entirely."""
    if get_origin(annotation) is types.UnionType:
        annotation = next(
            (a for a in get_args(annotation) if a is not type(None)), annotation
        )
    return get_origin(annotation) is dict


def _convert_plain_dict(raw_value: Any) -> dict[str, str] | None:
    """Builds a `dict[str, str]` for a target field like *_column_frequencies. The raw
    Gemini-side value is a list of {column, frequency} pair objects (see
    GeminiColumnFrequency) rather than an actual dict -- Gemini's structured-output
    schema rejects open/map types (`additionalProperties`) outright, so the raw mirror
    model can't use `dict[str, str]` itself. A plain dict is also accepted here for
    robustness (e.g. direct unit-test construction)."""
    pairs: list[tuple[Any, Any]]
    if isinstance(raw_value, dict):
        pairs = list(raw_value.items())
    elif isinstance(raw_value, list):
        pairs = [
            (getattr(item, "column", None), getattr(item, "frequency", None))
            if isinstance(item, BaseModel)
            else (item.get("column"), item.get("frequency"))
            for item in raw_value
            if isinstance(item, (BaseModel, dict))
        ]
    else:
        return None

    cleaned = {
        str(key): str(value).strip()
        for key, value in pairs
        if key and value is not None and str(value).strip()
    }
    return cleaned or None


def _coerce_value(parser: OcrParser, raw_text: str, value_type: Any) -> Any:
    if value_type is Decimal:
        return OcrParser.decimal_from_text(raw_text)
    if value_type is Time:
        return parser.parse_time(raw_text)
    if value_type is datetime:
        return parser.parse_date(raw_text)
    return raw_text


def _convert_leaf(
    parser: OcrParser, raw_value: Any, target_cls: type[BaseModel], field_name: str
) -> OcrField[Any] | None:
    if raw_value is None:
        return None
    raw_text = str(raw_value).strip()
    if not raw_text:
        return None

    value_type = OcrParser.get_field_value_type(target_cls, field_name)
    field_spec = OcrParser.get_field_spec(target_cls, field_name)
    value = _coerce_value(parser, raw_text, value_type)

    return OcrField(
        raw_text=raw_text,
        value=value,
        normalized_text=str(value) if value is not None else None,
        ocr_spec=field_spec.ocr_spec if field_spec is not None else None,
        ocr_default=field_spec.ocr_default if field_spec is not None else None,
    )


def convert_model(
    parser: OcrParser,
    raw_model: BaseModel,
    target_cls: type[ModelT],
    source_table: str | None = None,
) -> ModelT:
    """Converts a Gemini 'raw' (all-str) mirror model instance into the fully typed
    target model (OcrField[T] leaves), reusing the same reflection utilities and
    OcrField spec-validation the Document AI path relies on.
    """
    field_values: dict[str, Any] = {}
    for field_name, field_info in target_cls.model_fields.items():
        # default=None: some target models have derived fields (e.g. WTPLeftPageOCR.log_rows)
        # with no corresponding attribute on the raw Gemini mirror; the caller fills those in
        # separately after conversion.
        raw_value = getattr(raw_model, field_name, None)
        item_cls = _list_item_model(field_info.annotation)
        if item_cls is not None:
            field_values[field_name] = [
                convert_model(parser, item, item_cls, source_table=field_name)
                for item in (raw_value or [])
            ]
        elif _is_plain_dict_annotation(field_info.annotation):
            field_values[field_name] = _convert_plain_dict(raw_value)
        else:
            field_values[field_name] = _convert_leaf(parser, raw_value, target_cls, field_name)

    OcrParser.apply_required_fields(field_values, target_cls)
    if "source_table" in target_cls.model_fields:
        field_values["source_table"] = source_table

    return target_cls(**field_values)
