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

"""Task to use blhc in debusine."""

from pathlib import Path
from typing import override

from debian.debian_support import Version

import debusine.tasks.tags as ttags
import debusine.worker.tags as wtags
from debusine import utils
from debusine.artifacts import BlhcArtifact
from debusine.artifacts.models import (
    ArtifactCategory,
    DebianPackageBuildLog,
    WorkRequestResults,
    get_architectures,
    get_source_package_name,
    get_source_package_version,
)
from debusine.client.models import RelationType
from debusine.tasks import BaseTaskWithExecutor, RunCommandTask
from debusine.tasks.inputs import EnvironmentInput, SingleInput, Stage
from debusine.tasks.models import BlhcData, BlhcDynamicData


class Blhc(
    RunCommandTask[BlhcData, BlhcDynamicData],
    BaseTaskWithExecutor[BlhcData, BlhcDynamicData],
):
    """Task to use blhc (build-log hardening check) in debusine."""

    TASK_VERSION = 1

    CAPTURE_OUTPUT_FILENAME = "blhc.txt"

    input_artifact = SingleInput(
        field="input.artifact",
        categories=[ArtifactCategory.PACKAGE_BUILD_LOG],
        stage=Stage.PENDING,
    )
    environment = EnvironmentInput(stage=Stage.PENDING)

    @override
    def post_init(self) -> None:
        super().post_init()

        # list of build logs to be checked
        self._blhc_target: Path | None = None

    @override
    def compute_scheduler_tags(
        self,
    ) -> tuple[ttags.TaskProvidedTags, wtags.TaskRequiredTags]:
        provided, required = super().compute_scheduler_tags()

        assert isinstance(self.input_artifact.data, DebianPackageBuildLog)
        provided.add(
            ttags.ProvenanceProvided.SYSTEM,
            {
                ttags.SOURCE_PACKAGE_PREFIX
                + get_source_package_name(self.input_artifact.data)
            },
        )
        return provided, required

    @override
    def get_subject(self) -> str | None:
        assert isinstance(self.input_artifact.data, DebianPackageBuildLog)
        return get_source_package_name(self.input_artifact.data)

    @override
    def compute_dynamic_data(self) -> BlhcDynamicData:
        """
        Resolve artifact lookups for this task.

        :subject: source package name out of the analyzed build log
        :parameter_summary: source package name and version (arch)
        """
        input_artifact = self.input_artifact
        assert isinstance(input_artifact.data, DebianPackageBuildLog)
        package_name = get_source_package_name(input_artifact.data)
        package_version = get_source_package_version(input_artifact.data)
        architectures = " ".join(sorted(get_architectures(input_artifact.data)))

        return BlhcDynamicData(
            environment_id=self.environment.artifact_id,
            input_artifact_id=input_artifact.artifact_id,
            # We don't need the dose-distcheck analysis, and it's
            # potentially complex.
            input_artifact_data=input_artifact.data.model_copy(
                update={"bd_uninstallable": None}
            ),
            subject=self.get_subject(),
            parameter_summary=f"{package_name}_{package_version} "
            f"({architectures})",
        )

    def _blhc_supports_debian_option(self) -> bool:
        """
        Check whether the environment's blhc supports ``--debian``.

        This was added in https://bugs.debian.org/845339, and we want to use
        it if it's available, but if it isn't then it's better to omit that
        option than to fail.
        """
        return self.get_package_version("blhc") >= Version(
            "0.07+20170817+gita232d32"
        )

    @override
    def _cmdline(self) -> list[str]:
        """
        Build the blhc command line.

        Use configuration of self.data and self.blhc_target.
        """
        cmd = ["blhc"]
        if self._blhc_supports_debian_option():
            cmd.append("--debian")

        if extra_flags := self.data.extra_flags:
            # we already checked that the flags are sane...
            cmd += extra_flags

        cmd.append(str(self._blhc_target))

        return cmd

    @override
    def fetch_input(self, destination: Path) -> bool:
        """Download the required artifacts."""
        assert self.dynamic_data

        self.fetch_artifact(self.dynamic_data.input_artifact_id, destination)

        return True

    @override
    def configure_for_execution(self, download_directory: Path) -> bool:
        """
        Find .build files in the input artifacts.

        Set self._blhc_target to the relevant file.

        :param download_directory: where to search the files
        :return: True if valid files were found
        """
        self._prepare_executor_instance()

        if self.executor_instance is None:
            raise AssertionError("self.executor_instance cannot be None")

        self.executor_instance.run(
            ["apt-get", "update"], run_as_root=True, check=True
        )
        self.executor_instance.run(
            ["apt-get", "--yes", "install", "blhc"],
            run_as_root=True,
            check=True,
        )

        build_files = utils.find_files_suffixes(download_directory, [".build"])

        if len(build_files) == 0:
            list_of_files = sorted(map(str, download_directory.iterdir()))
            self.append_to_log_file(
                "configure_for_execution.log",
                [f"No *.build files to be analyzed. Files: {list_of_files}"],
            )
            return False

        self._blhc_target = build_files[0]

        return True

    @override
    def upload_artifacts(
        self,
        exec_directory: Path,
        *,
        execution_result: WorkRequestResults,  # noqa: U100
    ) -> None:
        """Upload the BlhcArtifact with the files and relationships."""
        assert self.dynamic_data
        if not self.debusine:
            raise AssertionError("self.debusine not set")

        blhc_file = exec_directory / self.CAPTURE_OUTPUT_FILENAME

        blhc_artifact = BlhcArtifact.create(
            blhc_output=blhc_file,
            build_log=self.dynamic_data.input_artifact_data,
        )

        uploaded = self.debusine.upload_artifact(
            blhc_artifact,
            workspace=self.workspace_name,
            work_request=self.work_request_id,
        )

        for source_artifact_id in self._source_artifacts_ids:
            self.debusine.relation_create(
                uploaded.id, source_artifact_id, RelationType.RELATES_TO
            )

    @override
    def task_result(
        self,
        returncode: int | None,
        execute_directory: Path,  # noqa: U100
    ) -> WorkRequestResults:
        """
        Evaluate task output and return success.

        We don't actually check the output, but use the return code of
        blhc.
        """
        if returncode in [0, 1]:
            return WorkRequestResults.SUCCESS
        return WorkRequestResults.FAILURE
