from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Sequence

from infrahub_sdk.utils import str_to_bool

from infrahub.core.constants import DiffAction, RelationshipCardinality
from infrahub.core.constants.database import DatabaseEdgeType

from .models import (
    AttributeChangelog,
    NodeChangelog,
    RelationshipCardinalityManyChangelog,
    RelationshipCardinalityOneChangelog,
    RelationshipPeerChangelog,
)

if TYPE_CHECKING:
    from infrahub.core.branch import Branch
    from infrahub.core.diff.model.path import (
        EnrichedDiffAttribute,
        EnrichedDiffNode,
        EnrichedDiffRelationship,
        EnrichedDiffRoot,
    )
    from infrahub.core.schema import MainSchemaTypes
    from infrahub.database import InfrahubDatabase


@dataclass
class NodeInDiff:
    node_id: str
    kind: str
    label: str


class DiffChangelogCollector:
    def __init__(self, diff: EnrichedDiffRoot, branch: Branch, db: InfrahubDatabase) -> None:
        self._diff = diff
        self._branch = branch
        self._db = db
        self._diff_nodes: dict[str, NodeInDiff]

    def _populate_diff_nodes(self) -> None:
        self._diff_nodes = {
            node.uuid: NodeInDiff(node_id=node.uuid, kind=node.kind, label=node.label) for node in self._diff.nodes
        }

    def get_node(self, node_id: str) -> NodeInDiff:
        return self._diff_nodes[node_id]

    def get_peer_kind(self, peer_id: str, node_kind: str, relationship_name: str) -> str:
        """If the peer kind doesn't exist in the diff use the peer kind from the schema"""
        try:
            return self.get_node(node_id=peer_id).kind
        except KeyError:
            schema = self._db.schema.get(node_kind, branch=self._branch, duplicate=False)
            rel_schema = schema.get_relationship(name=relationship_name)
            return rel_schema.peer

    def _process_node(self, node: EnrichedDiffNode) -> NodeChangelog:
        node_changelog = NodeChangelog(node_id=node.uuid, node_kind=node.kind, display_label=node.label)
        schema = self._db.schema.get(node_changelog.node_kind, branch=self._branch, duplicate=False)
        for attribute in node.attributes:
            self._process_node_attribute(node=node_changelog, attribute=attribute, schema=schema)

        for relationship in node.relationships:
            self._process_node_relationship(node=node_changelog, relationship=relationship)

        return node_changelog

    def _process_node_attribute(
        self, node: NodeChangelog, attribute: EnrichedDiffAttribute, schema: MainSchemaTypes
    ) -> None:
        try:
            schema_attribute = schema.get_attribute(name=attribute.name)
            attribute_kind = schema_attribute.kind
        except ValueError:
            # This would currently happen if there has been a schema migration as part of the merge
            # then we don't have access to the attribute kind
            attribute_kind = "n/a"

        changelog_attribute = AttributeChangelog(name=attribute.name, kind=attribute_kind)
        for attr_property in attribute.properties:
            match attr_property.property_type:
                case DatabaseEdgeType.HAS_VALUE:
                    # TODO deserialize correct value type from string
                    changelog_attribute.value = attr_property.new_value
                    changelog_attribute.value_previous = attr_property.previous_value
                case DatabaseEdgeType.IS_PROTECTED:
                    changelog_attribute.add_property(
                        name="is_protected",
                        value_current=self._convert_string_boolean_value(value=attr_property.new_value),
                        value_previous=self._convert_string_boolean_value(value=attr_property.previous_value),
                    )
                case DatabaseEdgeType.IS_VISIBLE:
                    changelog_attribute.add_property(
                        name="is_visible",
                        value_current=self._convert_string_boolean_value(value=attr_property.new_value),
                        value_previous=self._convert_string_boolean_value(value=attr_property.previous_value),
                    )
                case DatabaseEdgeType.HAS_SOURCE:
                    changelog_attribute.add_property(
                        name="source",
                        value_current=attr_property.new_value,
                        value_previous=attr_property.previous_value,
                    )
                case DatabaseEdgeType.HAS_OWNER:
                    changelog_attribute.add_property(
                        name="owner",
                        value_current=attr_property.new_value,
                        value_previous=attr_property.previous_value,
                    )

        node.attributes[attribute.name] = changelog_attribute

    def _process_node_relationship(self, node: NodeChangelog, relationship: EnrichedDiffRelationship) -> None:
        match relationship.cardinality:
            case RelationshipCardinality.ONE:
                self._process_node_cardinality_one_relationship(node=node, relationship=relationship)

            case RelationshipCardinality.MANY:
                self._process_node_cardinality_many_relationship(node=node, relationship=relationship)

    def _process_node_cardinality_one_relationship(
        self, node: NodeChangelog, relationship: EnrichedDiffRelationship
    ) -> None:
        changelog_rel = RelationshipCardinalityOneChangelog(name=relationship.name)
        for entry in relationship.relationships:
            for rel_prop in entry.properties:
                match rel_prop.property_type:
                    case DatabaseEdgeType.IS_RELATED:
                        if rel_prop.new_value:
                            changelog_rel.peer_id = rel_prop.new_value
                            changelog_rel.peer_kind = self.get_peer_kind(
                                peer_id=rel_prop.new_value,
                                node_kind=node.node_kind,
                                relationship_name=relationship.name,
                            )
                        if rel_prop.previous_value:
                            changelog_rel.peer_id_previous = rel_prop.previous_value
                            changelog_rel.peer_kind_previous = self.get_peer_kind(
                                peer_id=rel_prop.previous_value,
                                node_kind=node.node_kind,
                                relationship_name=relationship.name,
                            )
                    case DatabaseEdgeType.IS_PROTECTED:
                        changelog_rel.add_property(
                            name="is_protected",
                            value_current=self._convert_string_boolean_value(value=rel_prop.new_value),
                            value_previous=self._convert_string_boolean_value(value=rel_prop.previous_value),
                        )
                    case DatabaseEdgeType.IS_VISIBLE:
                        changelog_rel.add_property(
                            name="is_visible",
                            value_current=self._convert_string_boolean_value(value=rel_prop.new_value),
                            value_previous=self._convert_string_boolean_value(value=rel_prop.previous_value),
                        )
                    case DatabaseEdgeType.HAS_OWNER:
                        changelog_rel.add_property(
                            name="owner",
                            value_current=rel_prop.new_value,
                            value_previous=rel_prop.previous_value,
                        )
                    case DatabaseEdgeType.HAS_SOURCE:
                        changelog_rel.add_property(
                            name="source",
                            value_current=rel_prop.new_value,
                            value_previous=rel_prop.previous_value,
                        )

        node.add_relationship(relationship=changelog_rel)

    def _convert_string_boolean_value(self, value: str | None) -> bool | None:
        """Convert string based boolean for is_protected and is_visible."""
        if value is not None:
            return str_to_bool(value)

        return None

    def _process_node_cardinality_many_relationship(
        self, node: NodeChangelog, relationship: EnrichedDiffRelationship
    ) -> None:
        changelog_rel = RelationshipCardinalityManyChangelog(name=relationship.name)
        for peer in relationship.relationships:
            peer_log = RelationshipPeerChangelog(
                peer_id=peer.peer_id,
                peer_kind=self.get_peer_kind(
                    peer_id=peer.peer_id, node_kind=node.node_kind, relationship_name=relationship.name
                ),
                peer_status=peer.action,
            )
            for peer_prop in peer.properties:
                match peer_prop.property_type:
                    case DatabaseEdgeType.IS_VISIBLE:
                        peer_log.add_property(
                            name="is_visible",
                            value_current=self._convert_string_boolean_value(value=peer_prop.new_value),
                            value_previous=self._convert_string_boolean_value(value=peer_prop.previous_value),
                        )
                    case DatabaseEdgeType.IS_PROTECTED:
                        peer_log.add_property(
                            name="is_protected",
                            value_current=self._convert_string_boolean_value(value=peer_prop.new_value),
                            value_previous=self._convert_string_boolean_value(value=peer_prop.previous_value),
                        )
                    case DatabaseEdgeType.HAS_OWNER:
                        peer_log.add_property(
                            name="owner",
                            value_current=peer_prop.new_value,
                            value_previous=peer_prop.previous_value,
                        )
                    case DatabaseEdgeType.HAS_SOURCE:
                        peer_log.add_property(
                            name="source",
                            value_current=peer_prop.new_value,
                            value_previous=peer_prop.previous_value,
                        )

            changelog_rel.peers.append(peer_log)

        node.add_relationship(relationship=changelog_rel)

    def collect_changelogs(self) -> Sequence[tuple[DiffAction, NodeChangelog]]:
        self._populate_diff_nodes()
        changelogs = [
            (node.action, self._process_node(node=node))
            for node in self._diff.nodes
            if node.action != DiffAction.UNCHANGED
        ]
        return changelogs
