# Copyright 2024 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.

"""The collection manager for debian:suite-signing-keys collections."""

import re
from typing import Any

from django.db import IntegrityError
from django.db.models import Q
from django.utils import timezone

from debusine.artifacts import SigningKeyArtifact
from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.db.models import Artifact, CollectionItem, User, WorkRequest
from debusine.server.collections.base import (
    CollectionManagerInterface,
    ItemAdditionError,
)


class DebianSuiteSigningKeysManager(CollectionManagerInterface):
    """Manage collection of category debian:suite-signing-keys."""

    COLLECTION_CATEGORY = CollectionCategory.SUITE_SIGNING_KEYS
    VALID_ARTIFACT_CATEGORIES = frozenset({ArtifactCategory.SIGNING_KEY})

    def do_add_artifact(
        self,
        artifact: Artifact,
        *,
        user: User,
        workflow: WorkRequest | None = None,
        variables: dict[str, Any] | None = None,
        name: str | None = None,
        replace: bool = False,
    ) -> CollectionItem:
        """Add the artifact into the managed collection."""
        signing_key_data = SigningKeyArtifact.create_data(artifact.data)
        data: dict[str, Any] = {"purpose": signing_key_data.purpose}
        name = str(signing_key_data.purpose)
        if variables is not None and "source_package_name" in variables:
            data["source_package_name"] = variables["source_package_name"]
            name += f"_{variables['source_package_name']}"

        if replace:
            try:
                old_artifact = (
                    CollectionItem.active_objects.filter(
                        parent_collection=self.collection,
                        child_type=CollectionItem.Types.ARTIFACT,
                        name=name,
                    )
                    .latest("created_at")
                    .artifact
                )
            except CollectionItem.DoesNotExist:
                pass
            else:
                assert old_artifact is not None
                self.remove_artifact(old_artifact, user=user, workflow=workflow)

        try:
            return CollectionItem.objects.create_from_artifact(
                artifact,
                parent_collection=self.collection,
                name=name,
                data=data,
                created_by_user=user,
                created_by_workflow=workflow,
            )
        except IntegrityError as exc:
            raise ItemAdditionError(str(exc))

    def do_remove_artifact(
        self,
        artifact: Artifact,
        *,
        user: User | None = None,
        workflow: WorkRequest | None = None,
    ) -> None:
        """Remove the artifact from the collection."""
        CollectionItem.active_objects.filter(
            artifact=artifact, parent_collection=self.collection
        ).update(
            removed_by_user=user,
            removed_by_workflow=workflow,
            removed_at=timezone.now(),
        )

    def do_lookup(self, query: str) -> CollectionItem | None:
        """
        Return one CollectionItem based on the query.

        :param query: `key:PURPOSE` or `key:PURPOSE_SOURCE`.
        """
        query_filter = Q(
            parent_collection=self.collection,
            child_type=CollectionItem.Types.ARTIFACT,
            category=ArtifactCategory.SIGNING_KEY,
        )

        if m := re.match(r"^key:(.+?)(?:_(.+))?$", query):
            query_filter &= Q(data__purpose=m.group(1))
            if (source_package_name := m.group(2)) is not None:
                try:
                    return CollectionItem.active_objects.get(
                        query_filter
                        & Q(data__source_package_name=source_package_name)
                    )
                except CollectionItem.DoesNotExist:
                    # Fall back to looking for a key not constrained by
                    # source package name.
                    pass
            query_filter &= ~Q(data__has_key="source_package_name")
        else:
            raise LookupError(f'Unexpected lookup format: "{query}"')

        try:
            return CollectionItem.active_objects.get(query_filter)
        except CollectionItem.DoesNotExist:
            return None
