"""This module contains classes representing the API classes generated by the SDK generator."""

from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal, cast

from cognite.client.data_classes import data_modeling as dm

from cognite.pygen import config as pygen_config
from cognite.pygen.utils.text import create_name

from .data_classes import DataClass, EdgeDataClass, NodeDataClass
from .fields import BaseConnectionField, CDFExternalField
from .filter_methods import FilterMethod, FilterParameter

if TYPE_CHECKING:
    from cognite.pygen._core.generators import APIGenerator


@dataclass(frozen=True)
class APIClass:
    """This is the base class for all API classes.

    Args:
        parent_attribute: The name of the attribute in the client class that this API class is attached.
        name: The name of the API class.
        file_name: The name of the file where the API class is located.

    """

    parent_attribute: str
    name: str
    file_name: str


@dataclass(frozen=True)
class NodeAPIClass(APIClass):
    is_edge_class: bool
    view_id: dm.ViewId

    @classmethod
    def from_view(
        cls, view_id: dm.ViewId, base_name: str, is_edge_class: bool, api_class: pygen_config.APIClassNaming
    ) -> NodeAPIClass:
        file_name = create_name(base_name, api_class.file_name)
        class_name = create_name(base_name, api_class.name)
        return cls(
            parent_attribute=create_name(base_name, api_class.client_attribute),
            name=f"{class_name}API",
            file_name=file_name,
            is_edge_class=is_edge_class,
            view_id=view_id,
        )


@dataclass(frozen=True)
class QueryAPIClass(APIClass):
    data_class: DataClass

    @classmethod
    def create(cls, data_class: DataClass, base_name: str, api_class: pygen_config.APIClassNaming) -> QueryAPIClass:
        file_name = create_name(base_name, api_class.file_name)
        class_name = create_name(base_name, api_class.name)
        parent_attribute = create_name(base_name, api_class.client_attribute)
        return cls(
            parent_attribute=f"{parent_attribute}_query",  # Not used.
            name=f"{class_name}QueryAPI",
            file_name=f"{file_name}_query",
            data_class=data_class,
        )


@dataclass(frozen=True)
class EdgeAPIClass(APIClass):
    start_class: DataClass
    end_class: DataClass
    edge_class: DataClass | None
    field_name: str
    type: dm.DirectRelationReference
    direction: Literal["outwards", "inwards"]
    filter_method: FilterMethod
    doc_name: str
    query: QueryAPIClass
    end_view_id: dm.ViewId
    end_filter_method: FilterMethod
    field: BaseConnectionField
    has_default_instance_space: bool

    @property
    def has_edge_class(self) -> bool:
        return self.edge_class is not None

    def filter_parameters(
        self,
        include_nodes: bool = True,
        include_end_node: bool = False,
        suffix_edge: bool = False,
        case: Literal["signature", "docs", "filter_call"] = "signature",
    ) -> Iterator[FilterParameter]:
        if include_nodes:
            nodes: dict[str, FilterParameter] = {}
            type_ = "dm.NodeId | list[dm.NodeId]"
            if self.has_default_instance_space:
                type_ = f"str | list[str] | {type_}"
            nodes["from"] = FilterParameter(
                name=f"from_{self.start_class.variable}",
                type_=type_,
                description=f"ID of the source { self.start_class.doc_name}.",
                default=None,
            )
            if self.has_default_instance_space:
                nodes["from_space"] = FilterParameter(
                    name=f"from_{self.start_class.variable}_space",
                    type_="str",
                    description=f"Location of the {self.start_class.doc_list_name}.",
                    default="DEFAULT_INSTANCE_SPACE",
                    is_nullable=False,
                )
            nodes["to"] = FilterParameter(
                name=f"to_{self.end_class.variable}",
                type_=type_,
                description=f"ID of the target { self.end_class.doc_name}.",
                default=None,
            )
            if self.has_default_instance_space:
                nodes["to_space"] = FilterParameter(
                    name=f"to_{self.end_class.variable}_space",
                    type_="str",
                    description=f"Location of the {self.end_class.doc_list_name}.",
                    default="DEFAULT_INSTANCE_SPACE",
                    is_nullable=False,
                )

            if case == "filter_call" and self.direction == "inwards":
                if self.has_default_instance_space:
                    yield from [nodes["to"], nodes["to_space"], nodes["from"], nodes["from_space"]]
                else:
                    yield from [nodes["to"], nodes["from"]]
            else:
                yield from nodes.values()

        if include_end_node:
            yield from self.end_filter_method.parameters
        if suffix_edge:
            yield from [param.copy(name=f"{param.name}_edge") for param in self.filter_method.parameters]
        else:
            yield from self.filter_method.parameters

    @classmethod
    def from_fields(
        cls,
        field: BaseConnectionField,
        data_class: DataClass,
        base_name: str,
        query_class_by_view_id: dict[dm.ViewId, QueryAPIClass],
        api_generator_by_view_id: dict[dm.ViewId, APIGenerator],
        has_default_instance_space: bool,
        pygen_config: pygen_config.PygenConfig,
    ) -> EdgeAPIClass:
        if field.is_direct_relation:
            raise ValueError("Bug in Pygen: Expected only Edge Connection Fields")
        api_class = pygen_config.naming.api_class
        base_name = f"{base_name}_{field.name}"
        file_name = create_name(base_name, api_class.file_name)
        class_name = create_name(base_name, api_class.name)
        parent_attribute = create_name(field.name, api_class.client_attribute)

        # This is always true for Edge Connection Fields
        end_class = cast(NodeDataClass, field.destination_class)

        edge_class: EdgeDataClass | None = None
        if field.edge_class:
            edge_class = field.edge_class
            filter_method = FilterMethod.from_fields(
                edge_class.fields, pygen_config.filtering, has_default_instance_space, is_edge_class=True
            )
        else:  #  NodeDataClass
            filter_method = FilterMethod.from_fields(
                [], pygen_config.filtering, has_default_instance_space, is_edge_class=False
            )

        return cls(
            parent_attribute=f"{parent_attribute}_edge",
            name=f"{class_name}API",
            file_name=file_name,
            edge_class=edge_class,
            field_name=field.name,
            # This is always true for Edge Connection Fields
            type=field.edge_type,  # type: ignore[arg-type]
            start_class=data_class,
            end_class=end_class,
            filter_method=filter_method,
            doc_name=create_name(field.name, api_class.doc_name, python_variable=False),
            query=query_class_by_view_id[end_class.view_id],
            direction=field.edge_direction,
            end_view_id=end_class.view_id,
            end_filter_method=api_generator_by_view_id[end_class.view_id].list_method,
            field=field,
            has_default_instance_space=has_default_instance_space,
        )


@dataclass(frozen=True)
class TimeSeriesAPIClass(APIClass):
    query_class: str
    prop_name: str
    variable: str

    @classmethod
    def from_field(
        cls, field: CDFExternalField, base_name: str, api_class: pygen_config.APIClassNaming
    ) -> TimeSeriesAPIClass:
        base_name = f"{base_name}_{field.name}"
        file_name = create_name(base_name, api_class.file_name)
        class_name = create_name(base_name, api_class.name)
        parent_attribute = create_name(field.name, api_class.client_attribute)
        variable = create_name(field.name, api_class.variable)

        return cls(
            parent_attribute=f"{parent_attribute}",
            name=f"{class_name}API",
            file_name=f"{file_name}",
            query_class=f"{class_name}Query",
            prop_name=field.prop_name,
            variable=variable,
        )


@dataclass(frozen=True)
class MultiAPIClass:
    """This represents a set of APIs which are generated from a single data model.

    The motivation for having this class is the case when you want to create one SDK for multiple data models.

    Args:
        sub_apis_by_view_id: A dictionary mapping view IDs to the corresponding API classes.
        parent_attribute: The name of the attribute in the client class that this API class is attached
        name: The name of the API class.
        model: The data model that the APIs are generated from.
    """

    sub_apis_by_view_id: dict[dm.ViewId, APIClass]
    parent_attribute: str
    name: str
    model: dm.DataModel[dm.View]

    @property
    def model_id(self) -> dm.DataModelId:
        return self.model.as_id()

    @classmethod
    def from_data_model(
        cls,
        data_model: dm.DataModel[dm.View],
        api_class_by_view_id: dict[dm.ViewId, APIClass],
        multi_api_class: pygen_config.MultiAPIClassNaming,
    ) -> MultiAPIClass:
        sub_apis: dict[dm.ViewId, APIClass] = {}
        for view in sorted(data_model.views, key=lambda v: (v.name or v.external_id, v.space, v.version)):
            if view.as_id() in api_class_by_view_id:
                sub_apis[view.as_id()] = api_class_by_view_id[view.as_id()]

        data_model_name = data_model.name or data_model.external_id

        return cls(
            sub_apis_by_view_id=sub_apis,
            parent_attribute=create_name(data_model_name, multi_api_class.client_attribute),
            name=f"{create_name(data_model_name, multi_api_class.name)}APIs",
            model=data_model,
        )
