from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import asdict, dataclass
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast

from typing_extensions import Self

from cognite.client.data_classes._base import (
    CogniteFilter,
    CogniteObject,
    CogniteResourceList,
)
from cognite.client.data_classes.data_modeling._validation import validate_data_modeling_identifier
from cognite.client.data_classes.data_modeling.core import DataModelingResource
from cognite.client.data_classes.data_modeling.data_types import (
    DirectRelation,
    DirectRelationReference,
    PropertyType,
)
from cognite.client.data_classes.data_modeling.ids import ContainerId, ViewId
from cognite.client.data_classes.filters import Filter
from cognite.client.utils._text import (
    convert_all_keys_to_camel_case_recursive,
)

if TYPE_CHECKING:
    from cognite.client import CogniteClient


class ViewCore(DataModelingResource):
    def __init__(
        self,
        space: str,
        external_id: str,
        version: str,
        description: str | None = None,
        name: str | None = None,
        filter: Filter | None = None,
        implements: list[ViewId] | None = None,
        **_: Any,
    ) -> None:
        self.space = space
        self.external_id = external_id
        self.description = description
        self.name = name
        self.filter = filter
        self.implements = implements
        self.version = version

    @classmethod
    def _load(cls, resource: dict, cognite_client: CogniteClient | None = None) -> Self:
        if "implements" in resource:
            resource["implements"] = [ViewId.load(v) for v in resource["implements"]] or None
        if "filter" in resource:
            resource["filter"] = Filter.load(resource["filter"])

        return super()._load(resource)

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output = super().dump(camel_case)

        if self.implements:
            output["implements"] = [v.dump(camel_case) for v in self.implements]
        if self.filter:
            output["filter"] = self.filter.dump()

        return output

    def as_id(self) -> ViewId:
        return ViewId(
            space=self.space,
            external_id=self.external_id,
            version=self.version,
        )


class ViewApply(ViewCore):
    """A group of properties. Write only version.

    Args:
        space (str): The workspace for the view, a unique identifier for the space.
        external_id (str): Combined with the space is the unique identifier of the view.
        version (str): DMS version.
        description (str | None): Textual description of the view
        name (str | None): Human readable name for the view.
        filter (Filter | None): A filter Domain Specific Language (DSL) used to create advanced filter queries.
        implements (list[ViewId] | None): References to the views from where this view will inherit properties and edges.
        properties (dict[str, ViewPropertyApply] | None): No description.
    """

    def __init__(
        self,
        space: str,
        external_id: str,
        version: str,
        description: str | None = None,
        name: str | None = None,
        filter: Filter | None = None,
        implements: list[ViewId] | None = None,
        properties: dict[str, ViewPropertyApply] | None = None,
    ) -> None:
        validate_data_modeling_identifier(space, external_id)
        super().__init__(space, external_id, version, description, name, filter, implements)
        self.properties = properties

    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        properties = (
            {k: ViewPropertyApply.load(v) for k, v in resource["properties"].items()}
            if "properties" in resource
            else None
        )
        implements = [ViewId.load(v) for v in resource["implements"]] if "implements" in resource else None
        filter = Filter.load(resource["filter"]) if "filter" in resource else None
        return cls(
            space=resource["space"],
            external_id=resource["externalId"],
            version=resource["version"],
            description=resource.get("description"),
            name=resource.get("name"),
            filter=filter,
            implements=implements,
            properties=properties,
        )

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output = super().dump(camel_case)
        if "properties" in output:
            output["properties"] = {k: v.dump(camel_case) for k, v in output["properties"].items()}

        return output


class View(ViewCore):
    """A group of properties. Read only version.

    Args:
        space (str): The workspace for the view, a unique identifier for the space.
        external_id (str): Combined with the space is the unique identifier of the view.
        version (str): DMS version.
        properties (dict[str, MappedProperty | ConnectionDefinition]): View with included properties and expected edges, indexed by a unique space-local identifier.
        last_updated_time (int): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.
        created_time (int): The number of milliseconds since 00:00:00 Thursday, 1 January 1970, Coordinated Universal Time (UTC), minus leap seconds.
        description (str | None): Textual description of the view
        name (str | None): Human readable name for the view.
        filter (Filter | None): A filter Domain Specific Language (DSL) used to create advanced filter queries.
        implements (list[ViewId] | None): References to the views from where this view will inherit properties and edges.
        writable (bool): Whether the view supports write operations.
        used_for (Literal["node", "edge", "all"]): Does this view apply to nodes, edges or both.
        is_global (bool): Whether this is a global container, i.e., one of the out-of-the-box models.
        **_ (Any): No description.
    """

    def __init__(
        self,
        space: str,
        external_id: str,
        version: str,
        properties: dict[str, MappedProperty | ConnectionDefinition],
        last_updated_time: int,
        created_time: int,
        description: str | None = None,
        name: str | None = None,
        filter: Filter | None = None,
        implements: list[ViewId] | None = None,
        writable: bool = False,
        used_for: Literal["node", "edge", "all"] = "node",
        is_global: bool = False,
        **_: Any,
    ) -> None:
        super().__init__(
            space,
            external_id,
            version,
            description,
            name,
            filter,
            implements,
        )
        self.writable = writable
        self.used_for = used_for
        self.is_global = is_global
        self.properties = properties
        self.last_updated_time = last_updated_time
        self.created_time = created_time

    @classmethod
    def _load(cls, resource: dict, cognite_client: CogniteClient | None = None) -> View:
        if "properties" in resource and isinstance(resource["properties"], dict):
            resource["properties"] = {k: ViewProperty.load(v) for k, v in resource["properties"].items()} or None

        return super()._load(resource, cognite_client)

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output = super().dump(camel_case)
        if "properties" in output:
            output["properties"] = {k: v.dump(camel_case) for k, v in output["properties"].items()}

        return output

    def as_apply(self) -> ViewApply:
        """Convert to a view applies.

        Returns:
            ViewApply: The view apply.
        """
        properties: dict[str, ViewPropertyApply] | None = None
        if self.properties:
            for k, v in self.properties.items():
                if isinstance(v, (MappedProperty, SingleHopConnectionDefinition)):
                    if properties is None:
                        properties = {}
                    properties[k] = v.as_apply()
                else:
                    raise NotImplementedError(f"Unsupported conversion to apply for property type {type(v)}")

        return ViewApply(
            space=self.space,
            external_id=self.external_id,
            version=self.version,
            description=self.description,
            name=self.name,
            filter=self.filter,
            implements=self.implements,
            properties=properties,
        )


class ViewList(CogniteResourceList[View]):
    _RESOURCE = View

    def as_apply(self) -> ViewApplyList:
        """Convert to a view an apply list.

        Returns:
            ViewApplyList: The view apply list.
        """
        return ViewApplyList(resources=[v.as_apply() for v in self])

    def as_ids(self) -> list[ViewId]:
        """Returns the list of ViewIds

        Returns:
            list[ViewId]: The list of ViewIds
        """
        return [v.as_id() for v in self]


class ViewApplyList(CogniteResourceList[ViewApply]):
    _RESOURCE = ViewApply

    def as_ids(self) -> list[ViewId]:
        """Returns the list of ViewIds

        Returns:
            list[ViewId]: The list of ViewIds
        """
        return [v.as_id() for v in self]


class ViewFilter(CogniteFilter):
    """Represent the filer arguments for the list endpoint.

    Args:
        space (str | None): The space to query
        include_inherited_properties (bool): Whether to include properties inherited from views this view implements.
        all_versions (bool): Whether to return all versions. If false, only the newest version is returned, which is determined based on the 'createdTime' field.
        include_global (bool): Whether to include global views.
    """

    def __init__(
        self,
        space: str | None = None,
        include_inherited_properties: bool = True,
        all_versions: bool = False,
        include_global: bool = False,
    ) -> None:
        self.space = space
        self.include_inherited_properties = include_inherited_properties
        self.all_versions = all_versions
        self.include_global = include_global


class ViewProperty(CogniteObject, ABC):
    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        if "direction" in resource:
            return cast(Self, SingleHopConnectionDefinition.load(resource))
        else:
            return cast(Self, MappedProperty.load(resource))

    @abstractmethod
    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        raise NotImplementedError


class ViewPropertyApply(CogniteObject, ABC):
    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        if "direction" in resource:
            return cast(Self, SingleHopConnectionDefinitionApply.load(resource))
        else:
            return cast(Self, MappedPropertyApply.load(resource))

    @abstractmethod
    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        raise NotImplementedError


@dataclass
class MappedPropertyApply(ViewPropertyApply):
    container: ContainerId
    container_property_identifier: str
    name: str | None = None
    description: str | None = None
    source: ViewId | None = None

    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        return cls(
            container=ContainerId.load(resource["container"]),
            container_property_identifier=resource["containerPropertyIdentifier"],
            name=resource.get("name"),
            description=resource.get("description"),
            source=ViewId.load(resource["source"]) if "source" in resource else None,
        )

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output: dict[str, Any] = {
            "container": self.container.dump(camel_case, include_type=True),
            (
                "containerPropertyIdentifier" if camel_case else "container_property_identifier"
            ): self.container_property_identifier,
        }
        if self.name is not None:
            output["name"] = self.name
        if self.description is not None:
            output["description"] = self.description
        if self.source is not None:
            output["source"] = self.source.dump(camel_case, include_type=True)

        return output


@dataclass
class MappedProperty(ViewProperty):
    container: ContainerId
    container_property_identifier: str
    type: PropertyType
    nullable: bool
    auto_increment: bool
    source: ViewId | None = None
    default_value: str | int | dict | None = None
    name: str | None = None
    description: str | None = None

    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        type_ = resource["type"]
        source = type_.pop("source", None) or resource.get("source")

        return cls(
            container=ContainerId.load(resource["container"]),
            container_property_identifier=resource["containerPropertyIdentifier"],
            type=PropertyType.load(type_),
            nullable=resource["nullable"],
            auto_increment=resource["autoIncrement"],
            source=ViewId.load(source) if source else None,
            default_value=resource.get("defaultValue"),
            name=resource.get("name"),
            description=resource.get("description"),
        )

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output = asdict(self)
        output["type"] = self.type.dump(camel_case)
        if self.source and isinstance(self.type, DirectRelation):
            output["type"]["source"] = output.pop("source", None)
        if camel_case:
            return convert_all_keys_to_camel_case_recursive(output)
        return output

    def as_apply(self) -> MappedPropertyApply:
        return MappedPropertyApply(
            container=self.container,
            container_property_identifier=self.container_property_identifier,
            name=self.name,
            description=self.description,
            source=self.source,
        )


@dataclass
class ConnectionDefinition(ViewProperty):
    ...


@dataclass
class SingleHopConnectionDefinition(ConnectionDefinition):
    type: DirectRelationReference
    source: ViewId
    name: str | None = None
    description: str | None = None
    edge_source: ViewId | None = None
    direction: Literal["outwards", "inwards"] = "outwards"
    connection_type: Literal["multiEdgeConnection"] = "multiEdgeConnection"

    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        instance = cls(
            type=DirectRelationReference.load(resource["type"]),
            source=ViewId.load(resource["source"]),
            name=resource.get("name"),
            description=resource.get("description"),
            edge_source=(edge_source := resource.get("edgeSource")) and ViewId.load(edge_source),
        )
        if "direction" in resource:
            instance.direction = resource["direction"]
        if "connectionType" in resource:
            instance.connection_type = resource["connectionType"]
        return instance

    def dump(self, camel_case: bool = True) -> dict[str, Any]:
        output = asdict(self)

        if self.type:
            output["type"] = self.type.dump(camel_case)

        if self.source:
            output["source"] = self.source.dump(camel_case)

        if self.edge_source:
            output["edge_source"] = self.edge_source.dump(camel_case)

        if self.connection_type is not None:
            output["connection_type"] = self.connection_type

        return convert_all_keys_to_camel_case_recursive(output) if camel_case else output

    def as_apply(self) -> SingleHopConnectionDefinitionApply:
        return SingleHopConnectionDefinitionApply(
            type=self.type,
            source=self.source,
            name=self.name,
            description=self.description,
            edge_source=self.edge_source,
            direction=self.direction,
        )


@dataclass
class ConnectionDefinitionApply(ViewPropertyApply):
    ...


T_ConnectionDefinitionApply = TypeVar("T_ConnectionDefinitionApply", bound=ConnectionDefinitionApply)


@dataclass
class SingleHopConnectionDefinitionApply(ConnectionDefinitionApply):
    type: DirectRelationReference
    source: ViewId
    name: str | None = None
    description: str | None = None
    edge_source: ViewId | None = None
    direction: Literal["outwards", "inwards"] = "outwards"
    connection_type: Literal["multiEdgeConnection"] = "multiEdgeConnection"

    @classmethod
    def _load(cls, resource: dict[str, Any], cognite_client: CogniteClient | None = None) -> Self:
        instance = cls(
            type=DirectRelationReference.load(resource["type"]),
            source=ViewId.load(resource["source"]),
            name=resource.get("name"),
            description=resource.get("description"),
            edge_source=(edge_source := resource.get("edgeSource")) and ViewId.load(edge_source),
        )
        if "direction" in resource:
            instance.direction = resource["direction"]
        if "connectionType" in resource:
            instance.connection_type = resource["connectionType"]
        return instance

    def dump(self, camel_case: bool = True) -> dict:
        output: dict[str, Any] = {
            "type": self.type.dump(camel_case),
            "source": self.source.dump(camel_case, include_type=True),
            "direction": self.direction,
        }
        if self.name is not None:
            output["name"] = self.name
        if self.description is not None:
            output["description"] = self.description
        if self.edge_source is not None:
            output[("edgeSource" if camel_case else "edge_source")] = self.edge_source.dump(
                camel_case, include_type=True
            )
        if self.connection_type is not None:
            output[("connectionType" if camel_case else "connection_type")] = self.connection_type

        return output
