# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""
Types used by lookups.

These are mainly used by :py:mod:`debusine.server.collections.lookup`, but
are defined here to reduce import cycles.
"""

from collections.abc import Sequence
from dataclasses import dataclass, field
from typing import Any, Literal, assert_never

from debusine.artifacts.models import CollectionCategory
from debusine.client.models import LookupChildType, LookupResultType
from debusine.db.models.artifacts import Artifact
from debusine.db.models.collections import Collection, CollectionItem
from debusine.db.models.work_requests import WorkRequest
from debusine.db.models.workspaces import Workspace
from debusine.db.permissioncontext import PermissionUser
from debusine.tasks.models import LookupDict, LookupSingle


@dataclass(frozen=True, kw_only=True)
class LookupResult:
    """
    The result of a collection item lookup.

    None of :py:class:`Artifact`, :py:class:`Collection`, or
    :py:class:`CollectionItem` are entirely suitable return types: a lookup
    that returns a bare item needs to return the collection item, but in the
    case of a single-segment lookup there's no collection item available,
    only an artifact or collection.

    The least bad option seems to be to define a custom return type
    returning all the different things callers might need.
    """

    result_type: LookupResultType
    lookup: LookupSingle | LookupDict
    parent_collection_lookup: LookupSingle | None = None
    collection_item: CollectionItem | None = None
    artifact: Artifact | None = None
    collection: Collection | None = None

    @classmethod
    def create(
        cls,
        result_type: LookupResultType,
        lookup: LookupSingle | LookupDict,
        parent_collection_lookup: LookupSingle | None = None,
        collection_item: CollectionItem | None = None,
        artifact: Artifact | None = None,
        collection: Collection | None = None,
    ) -> "LookupResult":
        """Instantiate the right LookupResult subclass based on result type."""
        match result_type:
            case LookupResultType.ARTIFACT:
                return LookupResultArtifact(
                    lookup=lookup,
                    parent_collection_lookup=parent_collection_lookup,
                    collection_item=collection_item,
                    artifact=artifact,
                )
            case LookupResultType.BARE:
                return LookupResultBare(
                    lookup=lookup,
                    parent_collection_lookup=parent_collection_lookup,
                    collection_item=collection_item,
                )
            case LookupResultType.COLLECTION:
                return LookupResultCollection(
                    lookup=lookup,
                    parent_collection_lookup=parent_collection_lookup,
                    collection_item=collection_item,
                    collection=collection,
                )
            case _ as unreachable:
                assert_never(unreachable)

    def reconstruct(
        self, workflow_root: WorkRequest | None = None
    ) -> LookupSingle:
        """
        Reconstruct a lookup matching a given result.

        If the lookup result came directly from a :py:class:`LookupSingle`, then
        this is straightforward: just return the original lookup.

        The remaining cases are when the lookup result was one of many resulting
        from looking up a :py:class:`LookupDict`.  If the lookup result was from
        an item in a collection, then the returned lookup will resolve to the
        item with that name in that collection (which may not be the same as the
        original, if the item has been replaced; this is useful for promises
        that may be replaced by real artifacts).  Otherwise, it will resolve to
        the same artifact or collection, providing that it still exists.
        """
        if isinstance(self.lookup, LookupSingle):
            return self.lookup
        if (
            self.parent_collection_lookup is not None
            and self.collection_item is not None
        ):
            return (
                f"{self.parent_collection_lookup}/"
                f"name:{self.collection_item.name}"
            )
        elif self.artifact is not None:
            return f"{self.artifact.id}@artifacts"
        else:
            assert self.collection is not None
            if (
                workflow_root is not None
                and self.collection.id == workflow_root.internal_collection_id
            ):
                return "internal@collections"
            else:
                return f"{self.collection.id}@collections"


class LookupResultBare(LookupResult):
    """
    A collection item lookup containing bare data.

    Used to assist type annotations.
    """

    result_type: Literal[LookupResultType.BARE]
    parent_collection_lookup: LookupSingle
    collection_item: CollectionItem
    artifact: None
    collection: None

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize with the default result_type."""
        kwargs["result_type"] = LookupResultType.BARE
        return super().__init__(*args, **kwargs)


class LookupResultArtifact(LookupResult):
    """
    A collection item lookup containing an artifact.

    Used to assist type annotations.
    """

    result_type: Literal[LookupResultType.ARTIFACT]
    artifact: Artifact
    collection: None

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize with the default result_type."""
        kwargs["result_type"] = LookupResultType.ARTIFACT
        return super().__init__(*args, **kwargs)


class LookupResultCollection(LookupResult):
    """
    A collection item lookup containing a collection.

    Used to assist type annotations.
    """

    result_type: Literal[LookupResultType.COLLECTION]
    artifact: None
    collection: Collection

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize with the default result_type."""
        kwargs["result_type"] = LookupResultType.COLLECTION
        return super().__init__(*args, **kwargs)


class LookupCacheGroup[Lookup, Result](
    dict[
        tuple[
            Lookup,
            Workspace,
            PermissionUser,
            CollectionCategory | None,
            WorkRequest | None,
            LookupChildType,
        ],
        Result,
    ]
):
    """A cache for a particular kind of lookup."""


@dataclass(slots=True)
class LookupCache:
    """A cache of lookups that have been performed in the current context."""

    single: LookupCacheGroup[LookupSingle, LookupResult] = field(
        default_factory=LookupCacheGroup
    )
    dictionary: LookupCacheGroup[LookupDict, Sequence[LookupResult]] = field(
        default_factory=LookupCacheGroup
    )

    def copy(self) -> "LookupCache":
        """Return a copy of this cache."""
        return LookupCache(
            single=LookupCacheGroup(self.single),
            dictionary=LookupCacheGroup(self.dictionary),
        )
