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

"""Signing task to sign a repository index."""

import shutil
from pathlib import Path
from typing import override

from debusine.artifacts import RepositoryIndex
from debusine.artifacts.models import (
    ArtifactCategory,
    WorkRequestResults,
)
from debusine.assets import AssetCategory, KeyPurpose
from debusine.client.models import AssetPermissionCheckResponse, RelationType
from debusine.signing.db.models import Key, sign
from debusine.signing.models import SigningMode
from debusine.signing.tasks import BaseSigningTask
from debusine.signing.tasks.models import (
    SignRepositoryIndexData,
    SignRepositoryIndexDynamicData,
)
from debusine.tasks.inputs import SingleInput, SuiteArchiveInput


class SignRepositoryIndex(
    BaseSigningTask[SignRepositoryIndexData, SignRepositoryIndexDynamicData]
):
    """Task that signs a repository index using a key."""

    TASK_VERSION = 1

    unsigned = SingleInput(categories=[ArtifactCategory.REPOSITORY_INDEX])
    suite_archive = SuiteArchiveInput(field="suite_collection")

    @override
    def post_init(self) -> None:
        super().post_init()
        self._file_path: str | None = None
        self._keys: list[Key] = []
        self._permission_check: AssetPermissionCheckResponse | None = None
        self._signed_path: Path | None = None

    @override
    def build_dynamic_data(self) -> SignRepositoryIndexDynamicData:
        """Resolve lookups for this task."""
        suite, archive = self.suite_archive
        signing_keys: list[str] | None = None
        if suite.data.get("signing_keys") is not None:
            signing_keys = suite.data["signing_keys"]
        else:
            if archive is not None:
                signing_keys = archive.data.get("signing_keys")

        return SignRepositoryIndexDynamicData(
            signing_keys=signing_keys or [], unsigned_id=self.unsigned.id
        )

    @override
    def fetch_input(self, destination: Path) -> bool:
        """Fetch artifacts and database objects needed by the task."""
        assert self.debusine is not None
        assert self.dynamic_data is not None
        assert self.work_request_id is not None
        assert self.workspace_name is not None

        if not self.dynamic_data.signing_keys:
            self.append_to_log_file(
                "fetch_input.log",
                [
                    f"No signing keys are configured for "
                    f"{self.data.suite_collection}."
                ],
            )
            return False

        unsigned = self.fetch_artifact(
            self.dynamic_data.unsigned_id, destination
        )
        # debian:repository-index artifacts always contain exactly one file.
        assert len(unsigned.files) == 1
        [self._file_path] = unsigned.files.keys()

        self._keys = []
        for key in self.dynamic_data.signing_keys:
            asset_slug = f"{KeyPurpose.OPENPGP_REPOSITORY}:{key}"
            permission_check = self.debusine.asset_permission_check(
                asset_category=AssetCategory.SIGNING_KEY,
                asset_slug=asset_slug,
                permission_name="sign_repository_with",
                artifact_id=self.dynamic_data.unsigned_id,
                work_request_id=self.work_request_id,
                workspace=self.workspace_name,
            )
            if not permission_check.has_permission:
                self.append_to_log_file(
                    "fetch_input.log",
                    [
                        f"User {permission_check.username} does not have "
                        f"permission to sign {self.dynamic_data.unsigned_id} "
                        f"with {asset_slug}."
                    ],
                )
                return False
            if (
                self._permission_check is not None
                and permission_check != self._permission_check
            ):
                self.append_to_log_file(
                    "fetch_input.log",
                    [
                        f"Mismatching permission-check responses for "
                        f"different signing keys: "
                        f"{permission_check} != {self._permission_check}"
                    ],
                )
                return False
            self._permission_check = permission_check

            try:
                self._keys.append(
                    Key.objects.get(
                        purpose=KeyPurpose.OPENPGP_REPOSITORY, fingerprint=key
                    )
                )
            except Key.DoesNotExist:
                self.append_to_log_file(
                    "fetch_input.log",
                    [f"Signing key {asset_slug} does not exist"],
                )
                return False

        return True

    @override
    def configure_for_execution(
        self,
        download_directory: Path,  # noqa: U100
    ) -> bool:
        """Configure task variables."""
        # Nothing to do here; fetch_input does it all.
        return True

    @override
    def prepare_to_run(
        self, download_directory: Path, execute_directory: Path
    ) -> None:
        """Copy downloaded files into the execution directory."""
        shutil.copytree(download_directory, execute_directory / "input")

    @override
    def run(self, execute_directory: Path) -> WorkRequestResults:
        """Sign the requested file."""
        assert self.work_request_id is not None
        assert self._file_path is not None
        assert self._permission_check is not None

        with self.open_debug_log_file("cmd-output.log", mode="wb") as log_file:
            data_path = execute_directory / "input" / self._file_path
            signed_path = execute_directory / "output" / self.data.signed_name
            signed_path.parent.mkdir(parents=True, exist_ok=True)
            try:
                sign(
                    self._keys,
                    data_path,
                    signed_path,
                    self.data.mode,
                    self.work_request_id,
                    username=self._permission_check.username,
                    user_id=self._permission_check.user_id,
                    resource=self._permission_check.resource,
                    log_file=log_file,
                )
            except Exception as e:
                log_file.write(f"Signing failed: {e}\n".encode())
                raise
            else:
                self._signed_path = signed_path

        return WorkRequestResults.SUCCESS

    @override
    def upload_artifacts(
        self,
        execute_directory: Path,
        *,
        execution_result: WorkRequestResults,
    ) -> None:
        """Upload artifacts for the task."""
        assert self.dynamic_data is not None
        assert self.work_request_id is not None
        assert self.debusine is not None
        assert self._signed_path is not None
        # run() always either returns SUCCESS or raises an exception; in the
        # latter case we won't get here.
        assert execution_result == WorkRequestResults.SUCCESS

        content_type: str | None
        match self.data.mode:
            case SigningMode.DETACHED:
                content_type = "application/pgp-signature; charset=utf-8"
            case SigningMode.CLEAR:
                # This is unregistered in
                # https://www.iana.org/assignments/media-types/media-types.xhtml,
                # but it's what file(1) claims.
                content_type = "text/PGP; charset=utf-8"
            case _:  # pragma: no cover
                # Checked by SignRepositoryIndexData.validate_mode.
                raise AssertionError(f"Unexpected mode: {self.data.mode}")

        signed_artifact = RepositoryIndex.create(
            file=self._signed_path,
            path=self.data.signed_name,
            content_type=content_type,
        )
        uploaded_signed_artifact = self.debusine.upload_artifact(
            signed_artifact,
            workspace=self.workspace_name,
            work_request=self.work_request_id,
        )
        self.debusine.relation_create(
            uploaded_signed_artifact.id,
            self.dynamic_data.unsigned_id,
            RelationType.RELATES_TO,
        )
