from __future__ import annotations

import dataclasses
import sys
from typing import (
    TYPE_CHECKING,
    Any,
    Optional,
    cast,
)

from strawberry.annotation import StrawberryAnnotation
from strawberry.experimental.pydantic._compat import (
    CompatModelField,
    PydanticCompat,
)
from strawberry.experimental.pydantic.conversion import (
    convert_pydantic_model_to_strawberry_class,
    convert_strawberry_class_to_pydantic_model,
)
from strawberry.experimental.pydantic.exceptions import MissingFieldsListError
from strawberry.experimental.pydantic.fields import replace_types_recursively
from strawberry.experimental.pydantic.utils import (
    DataclassCreationFields,
    ensure_all_auto_fields_in_pydantic,
    get_default_factory_for_field,
    get_private_fields,
)
from strawberry.types.auto import StrawberryAuto
from strawberry.types.cast import get_strawberry_type_cast
from strawberry.types.field import StrawberryField
from strawberry.types.object_type import _process_type, _wrap_dataclass
from strawberry.types.private import is_private
from strawberry.types.type_resolver import _get_fields

if TYPE_CHECKING:
    import builtins
    from collections.abc import Callable, Sequence

    from graphql import GraphQLResolveInfo


def get_type_for_field(field: CompatModelField, is_input: bool, compat: PydanticCompat):  # noqa: ANN201
    outer_type = field.outer_type_

    replaced_type = replace_types_recursively(outer_type, is_input, compat=compat)

    if field.is_v1:
        # only pydantic v1 has this Optional logic
        should_add_optional: bool = field.allow_none
        if should_add_optional:
            return Optional[replaced_type]  # noqa: UP045

    return replaced_type


def _build_dataclass_creation_fields(
    field: CompatModelField,
    is_input: bool,
    existing_fields: dict[str, StrawberryField],
    auto_fields_set: set[str],
    use_pydantic_alias: bool,
    compat: PydanticCompat,
) -> DataclassCreationFields:
    field_type = (
        get_type_for_field(field, is_input, compat=compat)
        if field.name in auto_fields_set
        else existing_fields[field.name].type
    )

    if (
        field.name in existing_fields
        and existing_fields[field.name].base_resolver is not None
    ):
        # if the user has defined a resolver for this field, always use it
        strawberry_field = existing_fields[field.name]
    else:
        # otherwise we build an appropriate strawberry field that resolves it
        existing_field = existing_fields.get(field.name)
        graphql_name = None
        if existing_field and existing_field.graphql_name:
            graphql_name = existing_field.graphql_name
        elif field.has_alias and use_pydantic_alias:
            graphql_name = field.alias

        strawberry_field = StrawberryField(
            python_name=field.name,
            graphql_name=graphql_name,
            # always unset because we use default_factory instead
            default=dataclasses.MISSING,
            default_factory=get_default_factory_for_field(field, compat=compat),
            type_annotation=StrawberryAnnotation.from_annotation(field_type),
            description=field.description,
            deprecation_reason=(
                existing_field.deprecation_reason if existing_field else None
            ),
            permission_classes=(
                existing_field.permission_classes if existing_field else []
            ),
            directives=existing_field.directives if existing_field else (),
            extensions=existing_field.extensions if existing_field else [],
            metadata=existing_field.metadata if existing_field else {},
        )

    return DataclassCreationFields(
        name=field.name,
        field_type=field_type,  # type: ignore
        field=strawberry_field,
    )


if TYPE_CHECKING:
    from strawberry.experimental.pydantic.conversion_types import (
        PydanticModel,
        StrawberryTypeFromPydantic,
    )


def type(
    model: builtins.type[PydanticModel],
    *,
    name: str | None = None,
    is_input: bool = False,
    is_interface: bool = False,
    description: str | None = None,
    directives: Sequence[object] | None = (),
    all_fields: bool = False,
    include_computed: bool = False,
    use_pydantic_alias: bool = True,
) -> Callable[..., builtins.type[StrawberryTypeFromPydantic[PydanticModel]]]:
    def wrap(cls: Any) -> builtins.type[StrawberryTypeFromPydantic[PydanticModel]]:
        compat = PydanticCompat.from_model(model)
        model_fields = compat.get_model_fields(model, include_computed=include_computed)

        existing_fields = getattr(cls, "__annotations__", {})

        # Fields explicitly marked as Private should be excluded from pydantic
        # field processing - they should be handled as private fields instead
        private_field_names = {
            name for name, type_ in existing_fields.items() if is_private(type_)
        }

        # Fields with explicit (non-auto) type annotations should use their
        # explicit type rather than auto-deriving from pydantic
        explicitly_typed_fields = {
            name
            for name, type_ in existing_fields.items()
            if not isinstance(type_, StrawberryAuto) and name not in private_field_names
        }

        # these are the fields that matched a field name in the pydantic model
        # and should copy their alias from the pydantic model
        # Private fields are excluded since they shouldn't be exposed in the schema
        fields_set = {
            name
            for name, _ in existing_fields.items()
            if name in model_fields and name not in private_field_names
        }
        # these are the fields that were marked with strawberry.auto and
        # should copy their type from the pydantic model
        auto_fields_set = {
            name
            for name, type_ in existing_fields.items()
            if isinstance(type_, StrawberryAuto)
        }

        if all_fields:
            # all_fields adds all pydantic model fields, but respects explicit definitions:
            # - Private fields are excluded (handled separately as private fields)
            # - Explicitly typed fields keep their type (not auto-derived from pydantic)
            fields_set = set(model_fields.keys()) - private_field_names
            # Only auto-derive types for fields not explicitly typed
            auto_fields_set = (
                set(model_fields.keys()) - private_field_names - explicitly_typed_fields
            )

        if not fields_set:
            raise MissingFieldsListError(cls)

        ensure_all_auto_fields_in_pydantic(
            model=model,
            auto_fields=auto_fields_set,
            cls_name=cls.__name__,
            include_computed=include_computed,
        )

        wrapped = _wrap_dataclass(cls)
        extra_strawberry_fields = _get_fields(wrapped, {})
        extra_fields = cast("list[dataclasses.Field]", extra_strawberry_fields)
        private_fields = get_private_fields(wrapped)

        extra_fields_dict = {field.name: field for field in extra_strawberry_fields}

        all_model_fields: list[DataclassCreationFields] = [
            _build_dataclass_creation_fields(
                field,
                is_input,
                extra_fields_dict,
                auto_fields_set,
                use_pydantic_alias,
                compat=compat,
            )
            for field_name, field in model_fields.items()
            if field_name in fields_set
        ]

        all_model_fields = [
            DataclassCreationFields(
                name=field.name,
                field_type=field.type,  # type: ignore
                field=field,
            )
            for field in extra_fields + private_fields
            if field.name not in fields_set
        ] + all_model_fields

        # Implicitly define `is_type_of` to support interfaces/unions that use
        # pydantic objects (not the corresponding strawberry type)
        @classmethod  # type: ignore
        def is_type_of(cls: builtins.type, obj: Any, _info: GraphQLResolveInfo) -> bool:
            if (type_cast := get_strawberry_type_cast(obj)) is not None:
                return type_cast is cls

            return isinstance(obj, (cls, model))

        namespace = {"is_type_of": is_type_of}
        # We need to tell the difference between a from_pydantic method that is
        # inherited from a base class and one that is defined by the user in the
        # decorated class. We want to override the method only if it is
        # inherited. To tell the difference, we compare the class name to the
        # fully qualified name of the method, which will end in <class>.from_pydantic
        has_custom_from_pydantic = hasattr(
            cls, "from_pydantic"
        ) and cls.from_pydantic.__qualname__.endswith(f"{cls.__name__}.from_pydantic")
        has_custom_to_pydantic = hasattr(
            cls, "to_pydantic"
        ) and cls.to_pydantic.__qualname__.endswith(f"{cls.__name__}.to_pydantic")

        if has_custom_from_pydantic:
            namespace["from_pydantic"] = cls.from_pydantic
        if has_custom_to_pydantic:
            namespace["to_pydantic"] = cls.to_pydantic

        if hasattr(cls, "resolve_reference"):
            namespace["resolve_reference"] = cls.resolve_reference

        kwargs: dict[str, object] = {}

        # Python 3.10.1 introduces the kw_only param to `make_dataclass`.
        # If we're on an older version then generate our own custom init function
        # Note: Python 3.10.0 added the `kw_only` param to dataclasses, it was
        # just missed from the `make_dataclass` function:
        # https://github.com/python/cpython/issues/89961
        if sys.version_info >= (3, 10, 1):
            kwargs["kw_only"] = dataclasses.MISSING

        cls = dataclasses.make_dataclass(
            cls.__name__,
            [field.to_tuple() for field in all_model_fields],
            bases=cls.__bases__,
            namespace=namespace,
            **kwargs,  # type: ignore
        )

        _process_type(
            cls,
            name=name,
            is_input=is_input,
            is_interface=is_interface,
            description=description,
            directives=directives,
        )

        if is_input:
            model._strawberry_input_type = cls  # type: ignore
        else:
            model._strawberry_type = cls  # type: ignore
        cls._pydantic_type = model

        def from_pydantic_default(
            instance: PydanticModel, extra: dict[str, Any] | None = None
        ) -> StrawberryTypeFromPydantic[PydanticModel]:
            ret = convert_pydantic_model_to_strawberry_class(
                cls=cls, model_instance=instance, extra=extra
            )
            ret._original_model = instance
            return ret

        def to_pydantic_default(self: Any, **kwargs: Any) -> PydanticModel:
            instance_kwargs = {
                f.name: convert_strawberry_class_to_pydantic_model(
                    getattr(self, f.name)
                )
                for f in dataclasses.fields(self)
            }
            instance_kwargs.update(kwargs)
            return model(**instance_kwargs)

        if not has_custom_from_pydantic:
            cls.from_pydantic = staticmethod(from_pydantic_default)
        if not has_custom_to_pydantic:
            cls.to_pydantic = to_pydantic_default

        return cls

    return wrap


def input(
    model: builtins.type[PydanticModel],
    *,
    name: str | None = None,
    is_interface: bool = False,
    description: str | None = None,
    directives: Sequence[object] | None = (),
    all_fields: bool = False,
    use_pydantic_alias: bool = True,
) -> Callable[..., builtins.type[StrawberryTypeFromPydantic[PydanticModel]]]:
    """Convenience decorator for creating an input type from a Pydantic model.

    Equal to `partial(type, is_input=True)`

    See https://github.com/strawberry-graphql/strawberry/issues/1830.
    """
    return type(
        model=model,
        name=name,
        is_input=True,
        is_interface=is_interface,
        description=description,
        directives=directives,
        all_fields=all_fields,
        use_pydantic_alias=use_pydantic_alias,
    )


def interface(
    model: builtins.type[PydanticModel],
    *,
    name: str | None = None,
    is_input: bool = False,
    description: str | None = None,
    directives: Sequence[object] | None = (),
    all_fields: bool = False,
    use_pydantic_alias: bool = True,
) -> Callable[..., builtins.type[StrawberryTypeFromPydantic[PydanticModel]]]:
    """Convenience decorator for creating an interface type from a Pydantic model.

    Equal to `partial(type, is_interface=True)`

    See https://github.com/strawberry-graphql/strawberry/issues/1830.
    """
    return type(
        model=model,
        name=name,
        is_input=is_input,
        is_interface=True,
        description=description,
        directives=directives,
        all_fields=all_fields,
        use_pydantic_alias=use_pydantic_alias,
    )
