"""Container object for BORO boxology models."""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime

from .audit_entries import AuditEntries
from .layouts import Layouts
from .model_metadatas import ModelMetadatas
from .objects import Associations, ObjectHashes, ObjectHashLike, Objects
from .objects.stereotypes import stereotypes_include


@dataclass(slots=True)
class BoxologyModels:
    """Aggregates BORO objects, associations, and layout data."""

    id: ObjectHashes
    name: str
    description: str
    dialect_id: str
    objects: dict[ObjectHashes, Objects] = field(default_factory=dict)
    associations: dict[ObjectHashes, Associations] = field(default_factory=dict)
    layout: dict[ObjectHashes, Layouts] = field(default_factory=dict)
    metadata: ModelMetadatas = field(
        default_factory=lambda: ModelMetadatas(
            version="1.0.0",
            created_at=datetime.now(tz=UTC),
            updated_at=datetime.now(tz=UTC),
            created_by="system",
        ),
    )
    audit_log: list[AuditEntries] = field(default_factory=list)
    _object_id_lookup: dict[str, ObjectHashes] = field(
        init=False,
        repr=False,
        default_factory=dict,
    )
    _association_id_lookup: dict[str, ObjectHashes] = field(
        init=False,
        repr=False,
        default_factory=dict,
    )

    def __post_init__(self) -> None:
        self.id = self._coerce_hash(self.id)

        raw_objects = list(dict(self.objects).values())
        self.objects = {self._coerce_hash(obj.id): obj for obj in raw_objects}
        self._object_id_lookup = {str(obj_id): obj_id for obj_id in self.objects.keys()}

        raw_associations = list(dict(self.associations).values())
        self.associations = {
            self._coerce_hash(association.id): association
            for association in raw_associations
        }
        self._association_id_lookup = {
            str(association_id): association_id
            for association_id in self.associations.keys()
        }

        raw_layout = dict(self.layout)
        resolved_layout: dict[ObjectHashes, Layouts] = {}
        for identifier, layout in raw_layout.items():
            resolved_layout[self._resolve_layout_identifier(identifier)] = layout
        self.layout = resolved_layout

    @staticmethod
    def _coerce_hash(identifier: ObjectHashLike) -> ObjectHashes:
        if isinstance(identifier, ObjectHashes):
            return identifier
        return ObjectHashes.from_hash(str(identifier))

    def _lookup_object_hash(self, identifier: ObjectHashLike) -> ObjectHashes:
        if isinstance(identifier, ObjectHashes):
            return identifier
        key = str(identifier)
        cached = self._object_id_lookup.get(key)
        if cached is not None:
            return cached
        hashed = ObjectHashes.from_string(key)
        self._object_id_lookup[key] = hashed
        return hashed

    def _lookup_association_hash(self, identifier: ObjectHashLike) -> ObjectHashes:
        if isinstance(identifier, ObjectHashes):
            return identifier
        key = str(identifier)
        cached = self._association_id_lookup.get(key)
        if cached is not None:
            return cached
        hashed = ObjectHashes.from_string(key)
        self._association_id_lookup[key] = hashed
        return hashed

    def _resolve_layout_identifier(self, identifier: ObjectHashLike) -> ObjectHashes:
        return self._lookup_object_hash(identifier)

    def get_object(self, object_id: ObjectHashLike) -> Objects:
        """Return an object by identifier."""
        try:
            return self.objects[self._lookup_object_hash(object_id)]
        except KeyError as error:
            msg = f"Object '{object_id}' was not found."
            raise KeyError(msg) from error

    def get_association(self, association_id: ObjectHashLike) -> Associations:
        """Return an association by identifier."""
        try:
            return self.associations[self._lookup_association_hash(association_id)]
        except KeyError as error:
            msg = f"Association '{association_id}' was not found."
            raise KeyError(msg) from error

    def get_objects_by_stereotype(
        self,
        stereotype: str,
    ) -> list[Objects]:
        """Return objects that include the provided stereotype."""
        return [
            obj
            for obj in self.objects.values()
            if stereotypes_include(obj.stereotypes, stereotype)
        ]

    def get_associations_by_stereotype(
        self,
        stereotype: str,
    ) -> list[Associations]:
        """Return associations tagged with the provided stereotype."""
        return [
            association
            for association in self.associations.values()
            if stereotypes_include(association.stereotypes, stereotype)
        ]
