from __future__ import annotations

import contextlib
import enum
from functools import cached_property
from typing import Any, Optional, Type

from attrs import define

from qtgql.codegen.py.bases import _BaseQGraphQLObject
from qtgql.codegen.py.custom_scalars import CustomScalarMap
from qtgql.codegen.py.scalars import BaseCustomScalar, BuiltinScalar
from qtgql.codegen.utils import AntiForwardRef
from qtgql.typingref import TypeHinter


class Kinds(enum.Enum):
    SCALAR = "SCALAR"
    OBJECT = "OBJECT"
    ENUM = "ENUM"
    LIST = "LIST"
    UNION = "UNION"
    NON_NULL = "NON_NULL"
    INTERFACE = "INTERFACE"
    INPUT_OBJECT = "INPUT_OBJECT"


@define(slots=False)
class FieldProperty:
    name: str
    type: GqlTypeHinter
    type_map: dict[str, GqlTypeDefinition]
    enums: "EnumMap"
    scalars: CustomScalarMap
    description: Optional[str] = ""

    @cached_property
    def default_value(self):
        if builtin_scalar := self.type.is_builtin_scalar:
            if builtin_scalar.tp is str:
                return f"'{builtin_scalar.default_value}'"
            return f"{builtin_scalar.default_value}"
        if object_def := self.type.is_object_type:
            return f"{object_def.name}(parent=self)"

        if model_def := self.type.is_model:
            return f"{model_def.model_name}(parent=self, data = [])"

        if custom_scalar := self.type.is_custom_scalar(self.scalars):
            return f"SCALARS.{custom_scalar.__name__}()"

        if enum_def := self.type.is_enum:
            return f"{enum_def.name}(1)"  # 1 is where auto() starts.

        return "None"  # Unions are not supported yet.

    @cached_property
    def deserializer(self) -> str:
        """This gets the dict from graphql and passes the data to init, goes to
        `from_graphql` on the J2 template."""
        # every thing is possibly optional since you can query for only so or so fields.
        default = f"data.get('{self.name}', None)"

        if self.type.is_builtin_scalar:
            return default

        if scalar := self.type.is_custom_scalar(self.scalars):
            return f"SCALARS.{scalar.__name__}.{BaseCustomScalar.from_graphql.__name__}({default})"
        if model_of := self.type.is_model:
            return f"cls.{_BaseQGraphQLObject.deserialize_list_of.__name__}(parent, data, {model_of.model_name}, '{self.name}', {model_of.name})"
        if self.type.is_union():
            return (
                f"cls.{_BaseQGraphQLObject.deserialize_union.__name__}(parent, data, '{self.name}')"
            )

        if gql_type := self.type.is_object_type:
            return f"cls.{_BaseQGraphQLObject.deserialize_optional_child.__name__}(parent, data, {gql_type.name}, '{self.name}')"
        if enum_def := self.type.is_enum:
            return f"{enum_def.name}[data.get('{self.name}', {self.default_value})]"  # graphql enums evaluates to string of the name.
        raise NotImplementedError  # pragma: no cover

    @cached_property
    def annotation(self) -> str:
        """
        :returns: Annotation of the field based on the real type,
        meaning that the private attribute would be of that type.
        this goes for init and the property setter.
        """
        return self.type.annotation(self.scalars)

    @cached_property
    def fget_annotation(self) -> str:
        """This annotates the value that is QML-compatible."""
        if custom_scalar := self.type.is_custom_scalar(self.scalars):
            return TypeHinter.from_annotations(
                custom_scalar.to_qt.__annotations__["return"]
            ).stringify()
        if self.type.is_enum:
            return "int"

        return self.type.annotation(self.scalars)

    @cached_property
    def property_type(self) -> str:
        try:
            # this should raise if it is an inner type.
            ret = GqlTypeHinter.from_string(self.fget_annotation, self.type_map)
            if ret.type in self.type_map.values():
                raise TypeError  # in py3.11 the get_type_hints won't raise so raise brutally
            return self.fget_annotation
        except (TypeError, NameError):
            if self.type.is_union():
                return "QObject"  # graphql doesn't support scalars in Unions ATM. (what about Enums in unions)?
            if self.type.is_enum:
                # QEnum value must be int
                return "int"
            # might be a model, which is also QObject
            assert self.type.is_model or self.type.is_object_type
            return "QObject"

    @cached_property
    def setter_name(self) -> str:
        return self.name + "_setter"

    @cached_property
    def signal_name(self) -> str:
        return self.name + "Changed"

    @cached_property
    def private_name(self) -> str:
        return "_" + self.name

    @property
    def fget(self) -> str:
        if self.type.is_custom_scalar(self.scalars):
            return f"return self.{self.private_name}.{BaseCustomScalar.to_qt.__name__}()"
        if self.type.is_enum:
            return f"return self.{self.private_name}.value"
        return f"return self.{self.private_name}"


@define(slots=False)
class GqlTypeDefinition:
    kind: Kinds
    name: str
    fields: list[FieldProperty]
    docstring: Optional[str] = ""

    @cached_property
    def model_name(self) -> str:
        return "Q" + self.name + "Model"


@define
class EnumValue:
    """encapsulates enumValues from introspection, maps to an Enum member."""

    name: str
    description: str = ""


@define
class GqlEnumDefinition:
    name: str
    members: list[EnumValue]


EnumMap = dict[str, "GqlEnumDefinition"]


class GqlTypeHinter(TypeHinter):
    def __init__(
        self,
        type: Any,  # noqa: A003
        of_type: tuple["GqlTypeHinter", ...] = (),
    ):
        self.type = type
        self.of_type: tuple["GqlTypeHinter", ...] = of_type

    @property
    def is_object_type(self) -> Optional[GqlTypeDefinition]:
        with contextlib.suppress(TypeError):
            if issubclass(self.type, AntiForwardRef):
                ret = self.type.resolve()
                if isinstance(ret, GqlTypeDefinition):
                    return ret

    @property
    def is_model(self) -> Optional[GqlTypeDefinition]:
        if self.is_list():
            # enums are not supported in lists yet (valid graphql spec though)
            return self.of_type[0].is_object_type

    @property
    def is_enum(self) -> Optional["GqlEnumDefinition"]:
        with contextlib.suppress(TypeError):
            if issubclass(self.type, AntiForwardRef):
                if isinstance(self.type.resolve(), GqlEnumDefinition):
                    return self.type.resolve()

    @property
    def is_builtin_scalar(self) -> Optional[BuiltinScalar]:
        if isinstance(self.type, BuiltinScalar):
            return self.type

    def is_custom_scalar(self, scalars: CustomScalarMap) -> Optional[Type[BaseCustomScalar]]:
        if self.type in scalars.values():
            return self.type

    def annotation(self, scalars: CustomScalarMap) -> str:
        """
        :returns: Annotation of the field based on the real type,
        meaning that the private attribute would be of that type.
        this goes for init and the property setter.
        """
        # int, str, float etc...
        if builtin_scalar := self.is_builtin_scalar:
            return builtin_scalar.tp.__name__

        if scalar := self.is_custom_scalar(scalars):
            return f"SCALARS.{scalar.__name__}"
        if gql_enum := self.is_enum:
            return gql_enum.name
        # handle Optional, Union, List etc...
        # removing redundant prefixes.
        if model_def := self.is_model:
            return model_def.model_name
        if object_def := self.is_object_type:
            return object_def.name
        if self.is_union():
            return "Union[" + ",".join((th.annotation(scalars) for th in self.of_type)) + "]"
        raise NotImplementedError  # pragma no cover

    def as_annotation(self, object_map=None):  # pragma: no cover
        raise NotImplementedError("not safe to call on this type")
