#!/usr/bin/env python3
from typing import Dict, List

from graphql import GraphQLSchema

from .query_parser import (
    ParsedField,
    ParsedObject,
    ParsedOperation,
    ParsedQuery,
    ParsedVariableDefinition,
)
from .utils_codegen import (
    CodeChunk,
    camel_case_to_lower_case,
)


class DataclassesRenderer:
    def __init__(self, schema: GraphQLSchema) -> None:
        self.schema = schema

    def render(
        self, parsed_query: ParsedQuery, fragment_name_to_importpath: Dict[str, str]
    ) -> str:
        buffer = CodeChunk()
        buffer.write("#!/usr/bin/env python3")
        buffer.write("# @" + "generated AUTOGENERATED file. Do not Change!")
        buffer.write("")
        buffer.write("from dataclasses import dataclass")
        buffer.write("from datetime import datetime")
        buffer.write("from gql_client.runtime.datetime_utils import DATETIME_FIELD")
        buffer.write("from gql_client.runtime.graphql_client import GraphqlClient")
        buffer.write("from gql_client.runtime.client import OperationException")
        buffer.write("from gql_client.runtime.reporter import FailedOperationException")
        buffer.write("from functools import partial")
        buffer.write("from numbers import Number")
        buffer.write("from typing import Any, Callable, List, Mapping, Optional, Dict")
        buffer.write("from time import perf_counter")
        buffer.write("from dataclasses_json import DataClassJsonMixin")
        buffer.write("")
        for fragment_name in sorted(set(parsed_query.used_fragments)):
            importpath = fragment_name_to_importpath[fragment_name]
            buffer.write(
                f"from {importpath} import {fragment_name}, QUERY as {fragment_name}Query"
            )
        enum_names = set()
        for enum in parsed_query.enums:
            enum_names.add(enum.name)
        if enum_names:
            buffer.write("from gql_client.runtime.enum_utils import enum_field")
            for enum_name in sorted(enum_names):
                buffer.write(
                    f"from ..enum.{camel_case_to_lower_case(enum_name)} import {enum_name}"
                )
            buffer.write("")
        input_object_names = set()
        for input_object in parsed_query.input_objects:
            input_object_names.add(input_object.name)
        if input_object_names:
            for input_object_name in sorted(input_object_names):
                buffer.write(
                    f"from ..input.{camel_case_to_lower_case(input_object_name)} "
                    f"import {input_object_name}"
                )
            buffer.write("")

        sorted_objects = sorted(
            parsed_query.objects,
            key=lambda obj: 1 if isinstance(obj, ParsedOperation) else 0,
        )
        for obj in sorted_objects:
            buffer.write("")
            if isinstance(obj, ParsedObject):
                self.__render_object(parsed_query, buffer, obj)
            elif isinstance(obj, ParsedOperation):
                self.__render_operation(parsed_query, buffer, obj)

        if parsed_query.fragment_objects:
            if parsed_query.used_fragments:
                queries = [
                    f"{fragment_name}Query"
                    for fragment_name in sorted(set(parsed_query.used_fragments))
                ]
                buffer.write(f'QUERY: List[str] = {" + ".join(queries)} + ["""')
            else:
                buffer.write('QUERY: List[str] = ["""')
            buffer.write(parsed_query.query)
            buffer.write('"""]')
            buffer.write("")

        for fragment_obj in parsed_query.fragment_objects:
            self.__render_fragment(parsed_query, buffer, fragment_obj)

        return str(buffer)

    def render_enums(self, parsed_query: ParsedQuery) -> Dict[str, str]:
        result = {}

        for enum in parsed_query.enums + parsed_query.internal_enums:
            buffer = CodeChunk()
            buffer.write("#!/usr/bin/env python3")
            buffer.write("# @" + "generated AUTOGENERATED file. Do not Change!")
            buffer.write("")
            buffer.write("from enum import Enum")
            buffer.write("")
            with buffer.write_block(f"class {enum.name}(Enum):"):
                for value_name, value in enum.values.items():
                    if isinstance(value, str):
                        value = f'"{value}"'

                    buffer.write(f"{value_name} = {value}")
                buffer.write('MISSING_ENUM = ""')
                buffer.write("")
                buffer.write("@classmethod")
                with buffer.write_block(
                    f'def _missing_(cls, value: object) -> "{enum.name}":'
                ):
                    buffer.write("return cls.MISSING_ENUM")
            buffer.write("")
            result[enum.name] = str(buffer)

        return result

    def render_input_objects(self, parsed_query: ParsedQuery) -> Dict[str, str]:
        result = {}

        for input_object in parsed_query.input_objects + parsed_query.internal_inputs:
            buffer = CodeChunk()
            buffer.write("#!/usr/bin/env python3")
            buffer.write("# @" + "generated AUTOGENERATED file. Do not Change!")
            buffer.write("")
            buffer.write("from dataclasses import dataclass")
            buffer.write("from datetime import datetime")
            buffer.write("from functools import partial")
            buffer.write("from gql_client.runtime.datetime_utils import DATETIME_FIELD")
            buffer.write("from numbers import Number")
            buffer.write("from typing import Any, Callable, List, Mapping, Optional")
            buffer.write("")
            buffer.write("from dataclasses_json import DataClassJsonMixin")
            buffer.write("")
            enum_names = set()
            for enum in input_object.input_enums:
                enum_names.add(enum.name)
            if enum_names:
                buffer.write("from gql_client.runtime.enum_utils import enum_field")
                for enum_name in sorted(enum_names):
                    buffer.write(
                        f"from ..enum.{camel_case_to_lower_case(enum_name)} import {enum_name}"
                    )
                buffer.write("")
            input_object_names = set()
            for input_dep in input_object.inputs:
                input_object_names.add(input_dep.name)
            for input_object_name in sorted(input_object_names):
                buffer.write(
                    f"from ..input.{camel_case_to_lower_case(input_object_name)} "
                    f"import {input_object_name}"
                )

            self.__render_object(parsed_query, buffer, input_object, True)
            buffer.write("")
            result[input_object.name] = str(buffer)

        return result

    def __render_object(
        self,
        parsed_query: ParsedQuery,
        buffer: CodeChunk,
        obj: ParsedObject,
        is_input: bool = False,
    ) -> None:
        class_parents = (
            "(DataClassJsonMixin)" if not obj.parents else f'({", ".join(obj.parents)})'
        )

        buffer.write("@dataclass")
        with buffer.write_block(f"class {obj.name}{class_parents}:"):
            # render child objects
            children_names = set()
            for child_object in obj.children:
                if child_object.name not in children_names:
                    self.__render_object(parsed_query, buffer, child_object, is_input)
                children_names.add(child_object.name)

            # render fields
            sorted_fields = self.__sort_fields(parsed_query, obj.fields, is_input)
            for field in sorted_fields:
                self.__render_field(parsed_query, buffer, field, is_input)

            # pass if not children or fields
            if not (obj.children or obj.fields):
                buffer.write("pass")

        buffer.write("")

    def __render_fragment(
        self, parsed_query: ParsedQuery, buffer: CodeChunk, obj: ParsedObject
    ) -> None:
        class_parents = (
            "(DataClassJsonMixin)" if not obj.parents else f'({", ".join(obj.parents)})'
        )

        buffer.write("@dataclass")
        with buffer.write_block(f"class {obj.name}{class_parents}:"):

            # render child objects
            children_names = set()
            for child_object in obj.children:
                if child_object.name not in children_names:
                    self.__render_object(parsed_query, buffer, child_object)
                children_names.add(child_object.name)

            # render fields
            sorted_fields = self.__sort_fields(parsed_query, obj.fields)
            for field in sorted_fields:
                self.__render_field(parsed_query, buffer, field)

        buffer.write("")

    def __render_operation(
        self, parsed_query: ParsedQuery, buffer: CodeChunk, parsed_op: ParsedOperation
    ) -> None:
        if len(parsed_query.used_fragments):
            queries = [
                f"{fragment_name}Query"
                for fragment_name in sorted(set(parsed_query.used_fragments))
            ]
            buffer.write(f'QUERY: List[str] = {" + ".join(queries)} + ["""')
        else:
            buffer.write('QUERY: List[str] = ["""')
        buffer.write(parsed_query.query)
        buffer.write('"""]')
        buffer.write("")
        buffer.write("@dataclass")
        with buffer.write_block(f"class {parsed_op.name}(DataClassJsonMixin):"):
            # Render children
            for child_object in parsed_op.children:
                self.__render_object(parsed_query, buffer, child_object)

            # operation fields
            buffer.write(f"data: {parsed_op.name}Data")
            buffer.write("")

            # Execution functions
            if parsed_op.variables:
                vars_args = ", " + ", ".join(
                    [
                        self.__render_variable_definition(var)
                        for var in parsed_op.variables
                    ]
                )
                variables_dict = (
                    "{"
                    + ", ".join(
                        f'"{var.name}": {var.name}' for var in parsed_op.variables
                    )
                    + "}"
                )
            else:
                vars_args = ""
                variables_dict = "{}"

            buffer.write("@classmethod")
            buffer.write("# fmt: off")
            assert len(parsed_op.children) == 1
            child = parsed_op.children[0]
            assert len(child.fields) == 1
            query = child.fields[0]
            query_name = query.name
            query_result_type = f"{query.type}"
            if query_result_type not in ("int", "str", "bool"):
                query_result_type = f"{parsed_op.name}Data.{query.type}"
            if query.nullable:
                query_result_type = f"Optional[{query_result_type}]"
            if query.is_list:
                query_result_type = f"List[{query_result_type}]"
            with buffer.write_block(
                f"def execute(cls, client: GraphqlClient{vars_args})"
                f" -> {query_result_type}:"
            ):
                buffer.write("# fmt: off")
                buffer.write(f"variables: Dict[str, Any] = {variables_dict}")
                with buffer.write_block("try:"):
                    buffer.write("network_start = perf_counter()")
                    buffer.write(
                        "response_text = client.call(''.join(set(QUERY)), "
                        "variables=variables)"
                    )
                    buffer.write("decode_start = perf_counter()")
                    buffer.write("res = cls.from_json(response_text).data")
                    buffer.write("decode_time = perf_counter() - decode_start")
                    buffer.write("network_time = decode_start - network_start")
                    buffer.write(
                        f'client.reporter.log_successful_operation("{parsed_op.name}", variables, network_time, decode_time)'
                    )
                    buffer.write(f"return res.{query_name}")
                with buffer.write_block("except OperationException as e:"):
                    with buffer.write_block("raise FailedOperationException("):
                        buffer.write("client.reporter,")
                        buffer.write("e.err_msg,")
                        buffer.write(f'"{parsed_op.name}",')
                        buffer.write("variables,")
                    buffer.write(")")
            buffer.write("")

    @staticmethod
    def __sort_fields(
        parsed_query: ParsedQuery, fields: List[ParsedField], is_input: bool = False
    ) -> List[ParsedField]:
        enum_names = [e.name for e in parsed_query.enums + parsed_query.internal_enums]

        def sort_key(field) -> int:
            if field.type in enum_names or field.type == "DateTime":
                return 1
            if field.nullable and is_input:
                return 2
            return 0

        return sorted(fields, key=sort_key)

    @staticmethod
    def __render_variable_definition(var: ParsedVariableDefinition):
        var_type = var.type

        if var_type == "DateTime":
            var_type = "datetime"
        elif var_type == "Cursor":
            var_type = "str"

        if var.is_list:
            return f"{var.name}: List[{var_type}] = []"

        if not var.nullable:
            return f"{var.name}: {var_type}"

        return f'{var.name}: Optional[{var_type}] = {var.default_value or "None"}'

    @staticmethod
    def __render_field(
        parsed_query: ParsedQuery,
        buffer: CodeChunk,
        field: ParsedField,
        is_input: bool = False,
    ) -> None:
        enum_names = [e.name for e in parsed_query.enums + parsed_query.internal_enums]
        is_enum = field.type in enum_names
        suffix = ""
        field_type = field.type
        if field.is_list:
            field_type = f"List[{field_type}]"

        if is_enum:
            suffix = f" = enum_field({field_type})"

        if field_type == "DateTime":
            suffix = " = DATETIME_FIELD"
            field_type = "datetime"

        if field.nullable:
            if is_input:
                suffix = f" = {field.default_value}"
            buffer.write(f"{field.name}: Optional[{field_type}]{suffix}")
        else:
            buffer.write(f"{field.name}: {field_type}{suffix}")
