# This file was auto-generated by Fern from our API Definition.

import datetime as dt
import enum
import inspect
import sys
import typing
import uuid

import pydantic
import typing_extensions
from .pydantic_utilities import (
    IS_PYDANTIC_V2,
    ModelField,
    UniversalBaseModel,
    get_args,
    get_origin,
    is_literal_type,
    is_union,
    parse_date,
    parse_datetime,
    parse_obj_as,
)
from .serialization import get_field_to_alias_mapping
from pydantic_core import PydanticUndefined


class UnionMetadata:
    discriminant: str

    def __init__(self, *, discriminant: str) -> None:
        self.discriminant = discriminant


Model = typing.TypeVar("Model", bound=pydantic.BaseModel)


def _maybe_resolve_forward_ref(
    type_: typing.Any,
    host: typing.Optional[typing.Type[typing.Any]],
) -> typing.Any:
    """Resolve a ForwardRef using the module where *host* is defined.

    Pydantic v2 + ``from __future__ import annotations`` can leave field
    annotations as ``list[ForwardRef('Block')]`` even after ``model_rebuild``.
    Without resolution, ``construct_type`` sees a ForwardRef (not a class) and
    skips recursive model construction, leaving nested data as raw dicts.
    """
    if host is None or not isinstance(type_, typing.ForwardRef):
        return type_
    mod = sys.modules.get(host.__module__)
    if mod is None:
        return type_
    try:
        return eval(type_.__forward_arg__, vars(mod))  # noqa: S307
    except Exception:
        return type_


class UncheckedBaseModel(UniversalBaseModel):
    if IS_PYDANTIC_V2:
        model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow")  # type: ignore # Pydantic v2
    else:

        class Config:
            extra = pydantic.Extra.allow

    @classmethod
    def model_construct(
        cls: typing.Type["Model"],
        _fields_set: typing.Optional[typing.Set[str]] = None,
        **values: typing.Any,
    ) -> "Model":
        # Fallback construct function to the specified override below.
        return cls.construct(_fields_set=_fields_set, **values)

    # Allow construct to not validate model
    # Implementation taken from: https://github.com/pydantic/pydantic/issues/1168#issuecomment-817742836
    @classmethod
    def construct(
        cls: typing.Type["Model"],
        _fields_set: typing.Optional[typing.Set[str]] = None,
        **values: typing.Any,
    ) -> "Model":
        m = cls.__new__(cls)
        fields_values = {}

        if _fields_set is None:
            _fields_set = set(values.keys())

        fields = _get_model_fields(cls)
        populate_by_name = _get_is_populate_by_name(cls)
        field_aliases = get_field_to_alias_mapping(cls)

        for name, field in fields.items():
            # Key here is only used to pull data from the values dict
            # you should always use the NAME of the field to for field_values, etc.
            # because that's how the object is constructed from a pydantic perspective
            key = field.alias
            if (key is None or field.alias == name) and name in field_aliases:
                key = field_aliases[name]

            if key is None or (key not in values and populate_by_name):  # Added this to allow population by field name
                key = name

            if key in values:
                if IS_PYDANTIC_V2:
                    type_ = field.annotation  # type: ignore # Pydantic v2
                else:
                    type_ = typing.cast(typing.Type, field.outer_type_)  # type: ignore # Pydantic < v1.10.15

                fields_values[name] = (
                    construct_type(object_=values[key], type_=type_, host=cls)
                    if type_ is not None
                    else values[key]
                )
                _fields_set.add(name)
            else:
                default = _get_field_default(field)
                fields_values[name] = default

                # If the default values are non-null act like they've been set
                # This effectively allows exclude_unset to work like exclude_none where
                # the latter passes through intentionally set none values.
                if default != None and default != PydanticUndefined:
                    _fields_set.add(name)

        # Add extras back in
        extras = {}
        pydantic_alias_fields = [field.alias for field in fields.values()]
        internal_alias_fields = list(field_aliases.values())
        for key, value in values.items():
            # If the key is not a field by name, nor an alias to a field, then it's extra
            if (key not in pydantic_alias_fields and key not in internal_alias_fields) and key not in fields:
                if IS_PYDANTIC_V2:
                    extras[key] = value
                else:
                    _fields_set.add(key)
                    fields_values[key] = value

        object.__setattr__(m, "__dict__", fields_values)

        if IS_PYDANTIC_V2:
            object.__setattr__(m, "__pydantic_private__", None)
            object.__setattr__(m, "__pydantic_extra__", extras)
            object.__setattr__(m, "__pydantic_fields_set__", _fields_set)
        else:
            object.__setattr__(m, "__fields_set__", _fields_set)
            m._init_private_attributes()  # type: ignore # Pydantic v1
        return m


def _validate_collection_items_compatible(collection: typing.Any, target_type: typing.Type[typing.Any]) -> bool:
    """
    Validate that all items in a collection are compatible with the target type.

    Args:
        collection: The collection to validate (list, set, or dict values)
        target_type: The target type to validate against

    Returns:
        True if all items are compatible, False otherwise
    """
    if inspect.isclass(target_type) and issubclass(target_type, pydantic.BaseModel):
        for item in collection:
            try:
                # Try to validate the item against the target type
                if isinstance(item, dict):
                    parse_obj_as(target_type, item)
                else:
                    # If it's not a dict, it might already be the right type
                    if not isinstance(item, target_type):
                        return False
            except Exception:
                return False
    return True


def _get_literal_field_value(
    inner_type: typing.Type[typing.Any], field_name: str, field: typing.Any, object_: typing.Any
) -> typing.Any:
    """Get the value of a Literal field from *object_*, checking both alias and field name."""
    name_or_alias = get_field_to_alias_mapping(inner_type).get(field_name, field_name)
    if isinstance(object_, dict):
        return object_.get(name_or_alias)
    return getattr(object_, name_or_alias, None)


def _literal_fields_match_strict(inner_type: typing.Type[typing.Any], object_: typing.Any) -> bool:
    """Return True iff every Literal-typed field in *inner_type* is **present** in
    *object_* and its value equals the field's declared default.

    This prevents models whose fields are all optional (e.g. ``FigureDetails``)
    from vacuously matching inputs that don't carry the discriminant key at all
    (e.g. ``{}`` for text blocks).  For types with no Literal fields this
    returns True unconditionally.
    """
    fields = _get_model_fields(inner_type)
    for field_name, field in fields.items():
        if IS_PYDANTIC_V2:
            field_type = field.annotation  # type: ignore # Pydantic v2
        else:
            field_type = field.outer_type_  # type: ignore # Pydantic v1

        if is_literal_type(field_type):  # type: ignore[arg-type]
            field_default = _get_field_default(field)
            object_value = _get_literal_field_value(inner_type, field_name, field, object_)
            if field_default != object_value:
                return False
    return True


def _convert_undiscriminated_union_type(
    union_type: typing.Type[typing.Any],
    object_: typing.Any,
    host: typing.Optional[typing.Type[typing.Any]] = None,
) -> typing.Any:
    inner_types = get_args(union_type)
    if typing.Any in inner_types:
        return object_

    # When any union member carries a Literal discriminant field, require the
    # discriminant key to be present AND matching before accepting a candidate.
    # This prevents models with all-optional fields (e.g. FigureDetails) from
    # greedily matching inputs that belong to a different variant or to a
    # plain-dict fallback (e.g. EmptyBlockDetails = Dict[str, Any]).
    has_literal_discriminant = any(
        inspect.isclass(t)
        and issubclass(t, pydantic.BaseModel)
        and any(
            is_literal_type(
                f.annotation if IS_PYDANTIC_V2 else f.outer_type_  # type: ignore
            )
            for f in _get_model_fields(t).values()
        )
        for t in inner_types
    )

    for inner_type in inner_types:
        # Handle lists of objects that need parsing
        if get_origin(inner_type) is list and isinstance(object_, list):
            list_inner_type = _maybe_resolve_forward_ref(get_args(inner_type)[0], host)
            try:
                if inspect.isclass(list_inner_type) and issubclass(list_inner_type, pydantic.BaseModel):
                    if _validate_collection_items_compatible(object_, list_inner_type):
                        parsed_list = [parse_obj_as(object_=item, type_=list_inner_type) for item in object_]
                        return parsed_list
            except Exception:
                pass

        try:
            if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel):
                if has_literal_discriminant and not _literal_fields_match_strict(inner_type, object_):
                    continue
                return parse_obj_as(inner_type, object_)
        except Exception:
            continue

    # First pass: try types where all literal fields match the object's values.
    for inner_type in inner_types:
        if inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel):
            if has_literal_discriminant:
                if not _literal_fields_match_strict(inner_type, object_):
                    continue
            else:
                # Legacy lenient check: skip only when a Literal value is
                # present but doesn't match (allows absent-discriminant inputs).
                fields = _get_model_fields(inner_type)
                literal_fields_match = True
                for field_name, field in fields.items():
                    if IS_PYDANTIC_V2:
                        field_type = field.annotation  # type: ignore # Pydantic v2
                    else:
                        field_type = field.outer_type_  # type: ignore # Pydantic v1

                    if is_literal_type(field_type):  # type: ignore[arg-type]
                        field_default = _get_field_default(field)
                        object_value = _get_literal_field_value(inner_type, field_name, field, object_)
                        if object_value is not None and field_default != object_value:
                            literal_fields_match = False
                            break

                if not literal_fields_match:
                    continue

            try:
                return construct_type(object_=object_, type_=inner_type, host=host)
            except Exception:
                continue

    # Second pass: if no literal matches, return the first successful cast.
    # When a Literal discriminant is present, skip Pydantic models whose
    # discriminant doesn't match so that plain-dict fallback types are reached.
    for inner_type in inner_types:
        try:
            if has_literal_discriminant and inspect.isclass(inner_type) and issubclass(inner_type, pydantic.BaseModel):
                if not _literal_fields_match_strict(inner_type, object_):
                    continue
            return construct_type(object_=object_, type_=inner_type, host=host)
        except Exception:
            continue


def _convert_union_type(
    type_: typing.Type[typing.Any],
    object_: typing.Any,
    host: typing.Optional[typing.Type[typing.Any]] = None,
) -> typing.Any:
    base_type = get_origin(type_) or type_
    union_type = type_
    if base_type == typing_extensions.Annotated:  # type: ignore[comparison-overlap]
        union_type = get_args(type_)[0]
        annotated_metadata = get_args(type_)[1:]
        for metadata in annotated_metadata:
            if isinstance(metadata, UnionMetadata):
                try:
                    # Cast to the correct type, based on the discriminant
                    for inner_type in get_args(union_type):
                        try:
                            objects_discriminant = getattr(object_, metadata.discriminant)
                        except:
                            objects_discriminant = object_[metadata.discriminant]
                        if inner_type.__fields__[metadata.discriminant].default == objects_discriminant:
                            return construct_type(object_=object_, type_=inner_type, host=host)
                except Exception:
                    # Allow to fall through to our regular union handling
                    pass
    return _convert_undiscriminated_union_type(union_type, object_, host)


def construct_type(
    *,
    type_: typing.Type[typing.Any],
    object_: typing.Any,
    host: typing.Optional[typing.Type[typing.Any]] = None,
) -> typing.Any:
    """
    Here we are essentially creating the same `construct` method in spirit as the above, but for all types, not just
    Pydantic models.
    The idea is to essentially attempt to coerce object_ to type_ (recursively)
    """
    # Short circuit when dealing with optionals, don't try to coerces None to a type
    if object_ is None:
        return None

    base_type = get_origin(type_) or type_
    is_annotated = base_type == typing_extensions.Annotated  # type: ignore[comparison-overlap]
    maybe_annotation_members = get_args(type_)
    is_annotated_union = is_annotated and is_union(get_origin(maybe_annotation_members[0]))

    if base_type == typing.Any:  # type: ignore[comparison-overlap]
        return object_

    if base_type == dict:
        if not isinstance(object_, typing.Mapping):
            return object_

        key_type, items_type = get_args(type_)
        key_type = _maybe_resolve_forward_ref(key_type, host)
        items_type = _maybe_resolve_forward_ref(items_type, host)
        d = {
            construct_type(object_=key, type_=key_type, host=host): construct_type(
                object_=item, type_=items_type, host=host
            )
            for key, item in object_.items()
        }
        return d

    if base_type == list:
        if not isinstance(object_, list):
            return object_

        inner_type = _maybe_resolve_forward_ref(get_args(type_)[0], host)
        return [construct_type(object_=entry, type_=inner_type, host=host) for entry in object_]

    if base_type == set:
        if not isinstance(object_, set) and not isinstance(object_, list):
            return object_

        inner_type = _maybe_resolve_forward_ref(get_args(type_)[0], host)
        return {construct_type(object_=entry, type_=inner_type, host=host) for entry in object_}

    if is_union(base_type) or is_annotated_union:
        return _convert_union_type(type_, object_, host)

    # Cannot do an `issubclass` with a literal type, let's also just confirm we have a class before this call
    if (
        object_ is not None
        and not is_literal_type(type_)
        and (
            (inspect.isclass(base_type) and issubclass(base_type, pydantic.BaseModel))
            or (
                is_annotated
                and inspect.isclass(maybe_annotation_members[0])
                and issubclass(maybe_annotation_members[0], pydantic.BaseModel)
            )
        )
    ):
        if IS_PYDANTIC_V2:
            return type_.model_construct(**object_)
        else:
            return type_.construct(**object_)

    if base_type == dt.datetime:
        try:
            return parse_datetime(object_)
        except Exception:
            return object_

    if base_type == dt.date:
        try:
            return parse_date(object_)
        except Exception:
            return object_

    if base_type == uuid.UUID:
        try:
            return uuid.UUID(object_)
        except Exception:
            return object_

    if base_type == int:
        try:
            return int(object_)
        except Exception:
            return object_

    if base_type == bool:
        try:
            if isinstance(object_, str):
                stringified_object = object_.lower()
                return stringified_object == "true" or stringified_object == "1"

            return bool(object_)
        except Exception:
            return object_

    # Until the Fern generator is updated to handle enums correctly, we need this code.
    # It's here to handle the case where the object is an enum value, and we need to convert it to the enum type.
    # However, this code will get removed by the Fern generator.
    # If you are reviewing a PR and you see this code being removed, please flag it as a potential issue.
    if inspect.isclass(base_type) and issubclass(base_type, enum.Enum):
        try:
            return base_type(object_)
        except (ValueError, KeyError):
            return object_

    return object_


def _get_is_populate_by_name(model: typing.Type["Model"]) -> bool:
    if IS_PYDANTIC_V2:
        return model.model_config.get("populate_by_name", False)  # type: ignore # Pydantic v2
    return model.__config__.allow_population_by_field_name  # type: ignore # Pydantic v1


PydanticField = typing.Union[ModelField, pydantic.fields.FieldInfo]


# Pydantic V1 swapped the typing of __fields__'s values from ModelField to FieldInfo
# And so we try to handle both V1 cases, as well as V2 (FieldInfo from model.model_fields)
def _get_model_fields(
    model: typing.Type["Model"],
) -> typing.Mapping[str, PydanticField]:
    if IS_PYDANTIC_V2:
        return model.model_fields  # type: ignore # Pydantic v2
    else:
        return model.__fields__  # type: ignore # Pydantic v1


def _get_field_default(field: PydanticField) -> typing.Any:
    try:
        value = field.get_default()  # type: ignore # Pydantic < v1.10.15
    except:
        value = field.default
    if IS_PYDANTIC_V2:
        from pydantic_core import PydanticUndefined

        if value == PydanticUndefined:
            return None
        return value
    return value
