import dataclasses as d
import itertools as it
import operator
import typing as t
from functools import singledispatch

from graphql import DocumentNode
from graphql.language import ast

from .tools import camel_to_snake
from .tools import with_last

UNION_WEIGHT, ENUM_WEIGHT, PROTOCOL_WEIGHT, DEFAULT_WEIGHT = range(4)


@d.dataclass(frozen=True)
class PythonType:
    name: str
    import_: t.Optional[str] = None
    child: t.Sequence["PythonType"] = d.field(default_factory=list)
    default: str = ""

    def imports(self) -> t.Iterator[str]:
        for type_ in self.traverse():
            if type_.import_:
                yield f"from {type_.import_} import {type_.name}"

    def traverse(self) -> t.Iterator["PythonType"]:
        yield self
        for child in self.child:
            yield from child.traverse()

    def __str__(self) -> str:
        if self.child:
            return f"{self.name}[{', '.join(map(str, self.child))}]"
        return self.name


@d.dataclass(frozen=True)
class TypedDefinition:
    body: str
    used_types: t.Sequence[PythonType] = d.field(default_factory=list)
    weight: int = DEFAULT_WEIGHT


build_in_types: t.Mapping[str, PythonType] = {
    "ID": PythonType("UUID", "uuid"),
    "String": PythonType("str"),
    "Float": PythonType("Decimal", "decimal"),
    "Int": PythonType("int"),
    "Boolean": PythonType("bool"),
}


@singledispatch
def get_name_node(node: ast.DefinitionNode) -> t.Optional[ast.NameNode]:
    raise NotImplementedError(f"Should never process not node: {node}")


@get_name_node.register(ast.ExecutableDefinitionNode)
@get_name_node.register(ast.TypeDefinitionNode)
def get_name_node_(
    node: t.Union[ast.ExecutableDefinitionNode, ast.TypeDefinitionNode]
) -> t.Optional[ast.NameNode]:
    return node.name


def generate_types(root: DocumentNode) -> t.Iterator[str]:
    defined_types = set(
        map(
            operator.attrgetter("value"),
            filter(
                None, (get_name_node(definition) for definition in root.definitions)
            ),
        )
    )
    yield "# generated by pasiphae, please do not change manually"
    definitions = list(
        filter(
            None,
            (
                process_definition(definition, defined_types)
                for definition in root.definitions
            ),
        )
    )

    all_types = it.chain(*map(operator.attrgetter("used_types"), definitions))
    all_imports = it.chain(*map(operator.methodcaller("imports"), all_types))
    yield from sorted(set(all_imports))

    yield ""
    for definition, is_last in with_last(
        sorted(definitions, key=operator.attrgetter("weight"))
    ):
        if definition:
            yield definition.body
            yield ""
            if not is_last:
                yield ""


@singledispatch
def process_definition(
    definition: ast.DefinitionNode, _defined_types: t.Set[str]
) -> t.Optional[TypedDefinition]:
    raise NotImplementedError(f"Cannot process {definition.__class__}")


@singledispatch
def get_interfaces(
    _definition: t.Union[
        ast.ObjectTypeDefinitionNode, ast.InputObjectTypeDefinitionNode
    ]
) -> str:
    return ""


@get_interfaces.register
def get_interfaces_object(definition: ast.ObjectTypeDefinitionNode) -> str:
    return ", ".join(map(lambda type_: type_.name.value, definition.interfaces))


@process_definition.register(ast.ObjectTypeDefinitionNode)
@process_definition.register(ast.InputObjectTypeDefinitionNode)
def process_object(
    definition: t.Union[
        ast.ObjectTypeDefinitionNode, ast.InputObjectTypeDefinitionNode
    ],
    defined_types: t.Set[str],
) -> t.Optional[TypedDefinition]:
    if definition.name.value in ("Query", "Mutation"):
        return None

    if interfaces := get_interfaces(definition):
        interfaces = f"({interfaces})"

    return process_interface_or_object(
        definition,
        defined_types,
        header=(
            "@dataclass(frozen=True)",
            f"class {definition.name.value}{interfaces}:",
        ),
        weight=DEFAULT_WEIGHT,
        type_=PythonType("dataclass", "dataclasses"),
    )


@process_definition.register
def proess_interface(
    definition: ast.InterfaceTypeDefinitionNode, defined_types: t.Set[str]
) -> TypedDefinition:
    return process_interface_or_object(
        definition,
        defined_types,
        header=(f"class {definition.name.value}(Protocol):",),
        weight=PROTOCOL_WEIGHT,
        type_=PythonType("Protocol", "typing"),
    )


@singledispatch
def has_arguments(
    _field: t.Union[ast.FieldDefinitionNode, ast.InputValueDefinitionNode]
) -> bool:
    raise RuntimeError()


@has_arguments.register
def field_has_arguments(field: ast.FieldDefinitionNode) -> bool:
    return bool(field.arguments)


@has_arguments.register
def input_has_arguments(_field: ast.InputValueDefinitionNode) -> bool:
    return False


def process_interface_or_object(
    definition: t.Union[
        ast.ObjectTypeDefinitionNode,
        ast.InputObjectTypeDefinitionNode,
        ast.InterfaceTypeDefinitionNode,
    ],
    defined_types: t.Set[str],
    header: t.Sequence[str],
    weight: int,
    type_: PythonType,
) -> TypedDefinition:

    process_results: t.Mapping[str, PythonType] = {
        field.name.value: type_node(field.type, defined_types)
        for field in definition.fields
        if not has_arguments(field)
    }

    sorted_process_results = sorted(
        (
            (result.default, camel_to_snake(name), str(result))
            for name, result in process_results.items()
        ),
        key=operator.itemgetter(0),
    )

    body = (
        *(header),
        *(
            f"    {name}: {result}{f' = {default}' if default else ''}"
            for default, name, result in sorted_process_results
        ),
    )
    return TypedDefinition(
        body="\n".join(body),
        used_types=(*(result for result in process_results.values()), type_),
        weight=weight,
    )


@process_definition.register
def process_enum(
    enum: ast.EnumTypeDefinitionNode, _defined_types: t.Set[str]
) -> TypedDefinition:
    body = (
        f"class {enum.name.value}(Enum):",
        *(f'    {field.name.value} = "{field.name.value}"' for field in enum.values),
    )
    return TypedDefinition(
        body="\n".join(body),
        used_types=[PythonType("Enum", "enum")],
        weight=ENUM_WEIGHT,
    )


@process_definition.register
def process_union(
    union: ast.UnionTypeDefinitionNode, defined_types: t.Set[str]
) -> TypedDefinition:
    types = [
        named_type_mode(type_, defined_types, optional=lambda x: x)
        for type_ in union.types
    ]
    return TypedDefinition(
        body=f"{union.name.value} = Union[{', '.join(map(str, types))}]",
        used_types=[*types, PythonType("Union", "typing")],
        weight=UNION_WEIGHT,
    )


@singledispatch
def type_node(node: ast.TypeNode, _defined_types: t.Set[str]) -> PythonType:
    raise NotImplementedError(f"Not implemented {node.__class__}")


def optional(type_: PythonType) -> PythonType:
    return PythonType("Optional", child=[type_], import_="typing", default="None")


@type_node.register
def named_type_mode(
    named_type_node: ast.NamedTypeNode,
    defined_types: t.Set[str],
    optional: t.Callable[[PythonType], PythonType] = optional,
) -> PythonType:
    name = named_type_node.name.value
    if name in defined_types:
        return optional(PythonType(f'"{name}"'))
    try:
        return optional(build_in_types[name])
    except KeyError:
        raise NotImplementedError(f"do not know {name}")


@type_node.register
def not_null_type_node(
    node: ast.NonNullTypeNode, defined_types: t.Set[str]
) -> PythonType:
    processed = type_node(node.type, defined_types)
    assert processed.name == "Optional"
    return processed.child[0]


@type_node.register
def list_type_node(node: ast.ListTypeNode, defined_types: t.Set[str]) -> PythonType:
    processed = type_node(node.type, defined_types)
    return optional(PythonType("Sequence", import_="typing", child=[processed]))
