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

"""
Database interaction for worker tasks on the server.

This DBFieldResolver extension is used when Task code is run server-side with
database access.
"""

from collections.abc import Collection as AbcCollection
from typing import Any, TYPE_CHECKING, assert_never, overload, override

from django.conf import settings

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.client.models import LookupChildType, RelationType
from debusine.db.models.artifacts import Artifact
from debusine.db.models.collections import Collection
from debusine.db.permissioncontext import PermissionContext
from debusine.db.tasks.models import (
    DBArtifactInfo,
    DBBaseTaskInputArtifact,
    DBInputArtifactMultiple,
    DBInputCollectionSingle,
)
from debusine.tasks import (
    BaseTaskWithExecutor,
    TaskConfigError,
    ensure_collection_category,
)
from debusine.tasks.executors import ExecutorImageCategory
from debusine.tasks.inputs import inputs
from debusine.tasks.inputs.resolver import FieldResolver
from debusine.tasks.models import (
    BackendType,
    ExtraDebusineRepository,
    ExtraExternalRepository,
    ExtraRepository,
    InputCollectionSingle,
    LookupMultiple,
    LookupSingle,
)

if TYPE_CHECKING:
    from debusine.db.tasks import DBTask


class DBFieldResolver(FieldResolver):
    """Implementation of database interaction in worker tasks."""

    def __init__(self, task: "DBTask[Any, Any]") -> None:
        """Construct a :py:class:`DBFieldResolver`."""
        self.task = task
        self.work_request = task.work_request

    @overload
    def lookup_single_artifact(
        self,
        lookup: LookupSingle,
        *,
        default_category: CollectionCategory | None = None,
        label: str,
    ) -> DBArtifactInfo: ...

    @overload
    def lookup_single_artifact(
        self,
        lookup: None,
        *,
        default_category: CollectionCategory | None = None,
        label: str,
    ) -> None: ...

    @override
    def lookup_single_artifact(
        self,
        lookup: LookupSingle | None,
        *,
        default_category: CollectionCategory | None = None,
        label: str,
    ) -> DBArtifactInfo | None:
        return (
            None
            if lookup is None
            else DBArtifactInfo.from_artifact(
                self.work_request.lookup_single(
                    lookup=lookup,
                    default_category=default_category,
                    expect_type=LookupChildType.ARTIFACT,
                ).artifact,
                label=label,
                lookup=lookup,
            )
        )

    @override
    def lookup_multiple_artifacts(
        self,
        lookup: LookupMultiple,
        *,
        default_category: CollectionCategory | None = None,
        label: str,
    ) -> DBInputArtifactMultiple:
        artifacts = [
            DBBaseTaskInputArtifact.from_artifact(result.artifact)
            for result in self.work_request.lookup_multiple(
                lookup=lookup,
                default_category=default_category,
                expect_type=LookupChildType.ARTIFACT,
            )
        ]
        return DBInputArtifactMultiple(
            lookup=lookup, label=label, artifacts=artifacts
        )

    @override
    def find_related_artifacts(
        self,
        artifact_ids: AbcCollection[int],
        *,
        target_category: ArtifactCategory,
        relation_type: RelationType = RelationType.RELATES_TO,
        label: str,
    ) -> DBInputArtifactMultiple:
        artifacts = [
            DBBaseTaskInputArtifact.from_artifact(artifact)
            for artifact in Artifact.objects.filter(
                targeted_by__artifact__in=artifact_ids,
                category=target_category,
                targeted_by__type=relation_type,
            ).order_by("id")
        ]
        # TODO: can we do better for a lookup value here?
        return DBInputArtifactMultiple(
            lookup=[], label=label, artifacts=artifacts
        )

    @override
    def get_server_setting(self, setting: str) -> str:
        value = getattr(settings, setting)
        assert isinstance(value, str)
        return value

    @override
    def get_fallback_architecture(self) -> str:
        return self.work_request.get_fallback_architecture()

    @override
    def export_suite_signing_keys(self, suite_id: int) -> str | None:
        # Import here to prevent circular imports
        from debusine.server.collections import DebianSuiteManager

        suite = Collection.objects.get(id=suite_id)
        assert isinstance(suite.manager, DebianSuiteManager)
        return suite.manager.export_signing_keys()

    @override
    def lookup_environment(
        self,
        lookup: LookupSingle,
        *,
        architecture: str | None = None,
        backend: BackendType | None = None,
        default_category: CollectionCategory | None = None,
        image_category: "ExecutorImageCategory | None" = None,
        try_variant: str | None = None,
        label: str,
    ) -> DBArtifactInfo:
        return (
            None
            if lookup is None
            else DBArtifactInfo.from_artifact(
                self.work_request.lookup_environment(
                    lookup,
                    architecture=architecture,
                    backend=backend,
                    default_category=default_category,
                    image_category=image_category,
                    try_variant=try_variant,
                ).artifact,
                lookup=lookup,
                label=label,
            )
        )

    @override
    def resolve_environmentinput(
        self, field: inputs.EnvironmentInput
    ) -> DBArtifactInfo:
        # Prevent import loop
        from debusine.db.tasks import DBWorkerProxyTask

        assert isinstance(self.task, DBWorkerProxyTask)
        external_task = self.task.task
        assert isinstance(external_task, BaseTaskWithExecutor)
        lookup = self.get_field(field)
        assert lookup is not None
        info = self.lookup_environment(
            lookup,
            architecture=external_task.build_architecture,
            backend=external_task.backend if field.set_backend else None,
            default_category=CollectionCategory.ENVIRONMENTS,
            image_category=field.image_category,
            try_variant=self.task.name if field.try_variant else None,
            label=field.field,
        )
        field.check_category(info)
        return info

    @override
    def resolve_optionalenvironmentinput(
        self, field: inputs.OptionalEnvironmentInput
    ) -> DBArtifactInfo | None:
        # Prevent import loop
        from debusine.db.tasks import DBWorkerProxyTask

        assert isinstance(self.task, DBWorkerProxyTask)
        external_task = self.task.task
        assert isinstance(external_task, BaseTaskWithExecutor)
        lookup = self.get_field(field)
        if lookup is None:
            return None
        info = self.lookup_environment(
            lookup,
            architecture=external_task.build_architecture,
            backend=external_task.backend,
            default_category=CollectionCategory.ENVIRONMENTS,
            image_category=None,
            try_variant=self.task.name,
            label=field.field,
        )
        field.check_category(info)
        return info

    @override
    def resolve_suitearchiveinput(
        self, field: inputs.SuiteArchiveInput
    ) -> tuple[DBInputCollectionSingle, DBInputCollectionSingle | None]:
        # Import here to prevent circular imports.
        from debusine.server.collections.lookup import lookup_single

        lookup = self.get_field(field)
        assert lookup is not None

        suite_result = self.work_request.lookup_single(
            lookup=lookup,
            expect_type=LookupChildType.COLLECTION,
        )
        suite = DBInputCollectionSingle.from_collection(
            suite_result.collection, lookup=lookup, label=field.field
        )

        ensure_collection_category(
            configuration_key=field.field,
            category=suite.category,
            expected=CollectionCategory.SUITE,
        )

        archive_lookup = f"_@{CollectionCategory.ARCHIVE}"
        archive: InputCollectionSingle | None
        try:
            archive_result = lookup_single(
                archive_lookup,
                workspace=suite_result.collection.workspace,
                pc=PermissionContext.for_work_request(
                    work_request=self.work_request
                ),
                expect_type=LookupChildType.COLLECTION,
            )
        except KeyError:
            archive = None
        else:
            archive = (
                DBInputCollectionSingle.from_collection(
                    archive_result.collection,
                    lookup=archive_lookup,
                    label=field.field,
                )
                if archive_result is not None
                else None
            )

        return suite, archive

    def _resolve_extra_repository(
        self,
        extra_repository: ExtraRepository,
    ) -> ExtraExternalRepository:
        """Resolve a single extra repository into the external form."""
        # Import here to prevent circular imports
        from debusine.server.collections import DebianSuiteManager

        match extra_repository:
            case ExtraDebusineRepository():
                debusine_fqdn = settings.DEBUSINE_DEBIAN_ARCHIVE_PRIMARY_FQDN
                suite = self.work_request.lookup_single(
                    extra_repository.suite,
                    expect_type=LookupChildType.COLLECTION,
                ).collection
                assert isinstance(suite.manager, DebianSuiteManager)
                components = extra_repository.components
                if components is None:
                    if "components" not in suite.data:
                        raise TaskConfigError(
                            f"'components' not set for {extra_repository.suite}"
                        )
                    components = suite.data["components"]
                return ExtraExternalRepository(
                    # Ideally we'd use HTTPS, but that might require
                    # certificate setup within environments, and we know that
                    # our own repositories are signed, so HTTP should be good
                    # enough to use from workers for now.
                    url=(
                        f"http://{debusine_fqdn}"
                        f"/{suite.workspace.scope.name}/{suite.workspace.name}"
                    ),
                    suite=suite.name,
                    components=components,
                    signing_key=suite.manager.export_signing_keys(),
                )
            case ExtraExternalRepository():
                return extra_repository
            case _ as unreachable:
                assert_never(unreachable)

    @override
    def resolve_extrarepositoriesinput(
        self, field: inputs.ExtraRepositoriesInput
    ) -> list[ExtraExternalRepository] | None:
        extra_repositories = self.get_field(field)
        if extra_repositories is None:
            return None
        return [
            self._resolve_extra_repository(extra_repository)
            for extra_repository in extra_repositories
        ]

    @override
    def get_field[T](self, field: inputs.DataFieldInput[T, Any]) -> T | None:
        try:
            value: Any = field.getter(self.task.data)
        except AttributeError:
            raise TaskConfigError(
                f"Task input attribute {field.field!r} not found"
            )
        if value is None:
            return None
        if not isinstance(value, field.lookup_type):
            raise TaskConfigError(
                f"Task input attribute {field.field!r} is {value!r}"
                f" which is not of type {field.lookup_type}"
            )
        return value
