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

"""The collection manager for debian:archive collections."""

import datetime as dt
import re
from typing import Any, override

from debian.debian_support import Version
from django.db import IntegrityError
from django.db.models.fields.json import KT
from django.utils import timezone

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 DebianArchiveManager(CollectionManagerInterface):
    """Manage collection of category debian:archive."""

    COLLECTION_CATEGORY = CollectionCategory.ARCHIVE
    VALID_ARTIFACT_CATEGORIES = frozenset({ArtifactCategory.REPOSITORY_INDEX})

    @override
    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,
        created_at: dt.datetime | None = None,
        replaced_by: CollectionItem | None = None,
    ) -> CollectionItem:
        if "path" not in (variables or {}):
            raise ItemAdditionError(
                f'Adding {artifact.category} to {CollectionCategory.ARCHIVE} '
                f'requires "path"'
            )
        assert variables is not None

        path = variables["path"]
        if path.startswith("dists/"):
            raise ItemAdditionError(
                "Archive-level repository indexes may not have paths under "
                "dists/"
            )
        data = {"path": path}
        name = "index:{path}".format(**data)

        if replace:
            self.remove_items_by_name(
                name=name,
                child_types=[CollectionItem.Types.ARTIFACT],
                user=user,
                workflow=workflow,
            )

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

    @override
    def do_remove_item(
        self,
        item: CollectionItem,
        *,
        user: User | None = None,
        workflow: WorkRequest | None = None,
        remove_associated: bool = False,  # noqa: ARG002
    ) -> None:
        item.removed_by_user = user
        item.removed_by_workflow = workflow
        item.removed_at = timezone.now()
        item.save()

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

        :param query: `source:NAME`, `source-version:NAME_VERSION`,
          `binary:NAME_ARCHITECTURE`,
          `binary-version:NAME_VERSION_ARCHITECTURE`, or `index:PATH`.  If
          more than one possible CollectionItem matches the query (which is
          possible for `source:` and `binary:` queries): return the one with
          the highest version.
        """
        qs = CollectionItem.objects.active().filter(
            child_type=CollectionItem.Types.ARTIFACT
        )
        via_suite: bool | str = True
        sort_by_version = True

        if m := re.match(r"^source:(.+)$", query):
            qs = qs.annotate(package=KT("data__package")).filter(
                category=ArtifactCategory.SOURCE_PACKAGE,
                package=m.group(1),
            )
        elif m := re.match(r"^source-version:(.+?)_(.+)$", query):
            qs = qs.annotate(
                package=KT("data__package"), version=KT("data__version")
            ).filter(
                category=ArtifactCategory.SOURCE_PACKAGE,
                package=m.group(1),
                version=m.group(2),
            )
        elif m := re.match(r"^binary:(.+?)_(.+)$", query):
            qs = qs.annotate(
                package=KT("data__package"),
                architecture=KT("data__architecture"),
            ).filter(
                category=ArtifactCategory.BINARY_PACKAGE,
                package=m.group(1),
                architecture__in={m.group(2), "all"},
            )
        elif m := re.match(r"^binary-version:(.+?)_(.+?)_(.+)$", query):
            qs = qs.annotate(
                package=KT("data__package"),
                version=KT("data__version"),
                architecture=KT("data__architecture"),
            ).filter(
                category=ArtifactCategory.BINARY_PACKAGE,
                package=m.group(1),
                version=m.group(2),
                architecture__in={m.group(3), "all"},
            )
        elif m := re.match(r"^index:dists/([^/]+)/(.+)$", query):
            qs = qs.annotate(path=KT("data__path")).filter(
                child_type=CollectionItem.Types.ARTIFACT,
                category=ArtifactCategory.REPOSITORY_INDEX,
                path=m.group(2),
            )
            via_suite = m.group(1)
            sort_by_version = False
        elif m := re.match(r"^index:(.+)$", query):
            qs = qs.annotate(path=KT("data__path")).filter(
                child_type=CollectionItem.Types.ARTIFACT,
                category=ArtifactCategory.REPOSITORY_INDEX,
                path=m.group(1),
            )
            via_suite = False
            sort_by_version = False
        else:
            raise LookupError(f'Unexpected lookup format: "{query}"')

        # Ideally the sorting would be done in the database, but that
        # requires the debversion extension and installing that requires a
        # superuser, which is cumbersome since it means we can't do it in a
        # Django migration as currently configured.  We don't expect a
        # single suite to contain many active versions of a single package,
        # so doing the sorting in Python should be fine in practice.
        if via_suite:
            suite_qs = self.collection.workspace.collections.exported_suites()
            if isinstance(via_suite, str):
                suite_qs = suite_qs.filter(name=via_suite)
            qs = qs.filter(
                parent_collection__in=suite_qs,
                # Technically redundant, but helps PostgreSQL to use the
                # correct index where available (e.g. db_ci_suite_source_idx).
                parent_category=CollectionCategory.SUITE,
            )
        else:
            qs = qs.filter(
                parent_collection=self.collection,
                # Technically redundant, but helps PostgreSQL to use the
                # correct index where available (e.g.
                # db_ci_archive_index_path_idx).
                parent_category=CollectionCategory.ARCHIVE,
            )
        items = list(qs)
        if sort_by_version:
            items = sorted(
                items, key=lambda item: Version(item.data["version"])
            )
        if not items:
            return None
        return items[-1]
