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

"""qa workflow."""

from django.db.models import Q, QuerySet

from debusine.artifacts.models import TaskTypes
from debusine.db.models import WorkRequest
from debusine.server.workflows import workflow_utils
from debusine.server.workflows.models import (
    AutopkgtestWorkflowData,
    BlhcWorkflowData,
    DebDiffWorkflowData,
    LintianWorkflowData,
    PiupartsWorkflowData,
    QAFailOn,
    QAWorkflowData,
    ReverseDependenciesAutopkgtestWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.server.workflows.regression_tracking import (
    RegressionTrackingWorkflow,
)
from debusine.tasks.models import (
    BackendType,
    BaseDynamicTaskData,
    LintianFailOnSeverity,
    LookupMultiple,
    LookupSingle,
    OutputData,
    RegressionAnalysisStatus,
)
from debusine.tasks.server import TaskDatabaseInterface


class QAWorkflow(
    RegressionTrackingWorkflow[QAWorkflowData, BaseDynamicTaskData]
):
    """QA workflow."""

    TASK_NAME = "qa"

    def build_dynamic_data(
        self, task_database: TaskDatabaseInterface  # noqa: U100
    ) -> BaseDynamicTaskData:
        """
        Compute dynamic data for this workflow.

        :subject: package name of ``source_artifact``
        """
        source_data = workflow_utils.source_package_data(self)
        return BaseDynamicTaskData(
            subject=source_data.name,
            parameter_summary=f"{source_data.name}_{source_data.version}",
        )

    def populate(self) -> None:
        """Create work requests."""
        if (data_archs := self.data.architectures) is not None:
            architectures = set(data_archs)
        else:
            architectures = workflow_utils.get_available_architectures(
                self, vendor=self.data.vendor, codename=self.data.codename
            )

        if (
            data_archs_allowlist := self.data.architectures_allowlist
        ) is not None:
            architectures.intersection_update(data_archs_allowlist)

        if (
            data_archs_denylist := self.data.architectures_denylist
        ) is not None:
            architectures.difference_update(data_archs_denylist)

        effective_architectures = sorted(architectures)

        filtered_binary_artifacts = (
            workflow_utils.filter_artifact_lookup_by_arch(
                self, self.data.binary_artifacts, architectures
            )
        )

        allow_failure = self.data.fail_on in {
            QAFailOn.NEVER,
            QAFailOn.REGRESSION,
        }

        if self.data.enable_autopkgtest:
            self._populate_autopkgtest(
                prefix=self.data.prefix,
                reference_prefix=self.data.reference_prefix,
                source_artifact=self.data.source_artifact,
                binary_artifacts=filtered_binary_artifacts,
                qa_suite=self.data.qa_suite,
                reference_qa_results=self.data.reference_qa_results,
                enable_regression_tracking=self.data.enable_regression_tracking,
                update_qa_results=self.data.update_qa_results,
                vendor=self.data.vendor,
                codename=self.data.codename,
                backend=self.data.autopkgtest_backend,
                architectures=effective_architectures,
                arch_all_host_architecture=self.data.arch_all_host_architecture,
                allow_failure=allow_failure,
            )

        if self.data.enable_reverse_dependencies_autopkgtest:
            # Checked by
            # QAWorkflowData.check_reverse_dependencies_autopkgtest_consistency.
            assert self.data.qa_suite is not None
            self._populate_reverse_dependencies_autopkgtest(
                prefix=self.data.prefix,
                reference_prefix=self.data.reference_prefix,
                source_artifact=self.data.source_artifact,
                binary_artifacts=filtered_binary_artifacts,
                qa_suite=self.data.qa_suite,
                reference_qa_results=self.data.reference_qa_results,
                enable_regression_tracking=self.data.enable_regression_tracking,
                update_qa_results=self.data.update_qa_results,
                vendor=self.data.vendor,
                codename=self.data.codename,
                backend=self.data.autopkgtest_backend,
                architectures=effective_architectures,
                arch_all_host_architecture=self.data.arch_all_host_architecture,
                allow_failure=allow_failure,
            )

        if self.data.enable_lintian:
            self._populate_lintian(
                prefix=self.data.prefix,
                reference_prefix=self.data.reference_prefix,
                source_artifact=self.data.source_artifact,
                binary_artifacts=filtered_binary_artifacts,
                qa_suite=self.data.qa_suite,
                reference_qa_results=self.data.reference_qa_results,
                enable_regression_tracking=self.data.enable_regression_tracking,
                update_qa_results=self.data.update_qa_results,
                vendor=self.data.vendor,
                codename=self.data.codename,
                backend=self.data.lintian_backend,
                architectures=effective_architectures,
                arch_all_host_architecture=self.data.arch_all_host_architecture,
                fail_on_severity=self.data.lintian_fail_on_severity,
                allow_failure=allow_failure,
            )

        if self.data.enable_piuparts:
            self._populate_piuparts(
                prefix=self.data.prefix,
                reference_prefix=self.data.reference_prefix,
                source_artifact=self.data.source_artifact,
                binary_artifacts=filtered_binary_artifacts,
                qa_suite=self.data.qa_suite,
                reference_qa_results=self.data.reference_qa_results,
                enable_regression_tracking=self.data.enable_regression_tracking,
                update_qa_results=self.data.update_qa_results,
                vendor=self.data.vendor,
                codename=self.data.codename,
                architectures=effective_architectures,
                backend=self.data.piuparts_backend,
                environment=self.data.piuparts_environment,
                arch_all_host_architecture=self.data.arch_all_host_architecture,
                allow_failure=allow_failure,
            )

        if self.data.enable_debdiff:
            # Checked by
            # QAWorkflowData.enable_debdiff_consistency.
            assert self.data.qa_suite is not None
            self._populate_debdiff(
                source_artifact=self.data.source_artifact,
                binary_artifacts=self.data.binary_artifacts,
                vendor=self.data.vendor,
                codename=self.data.codename,
                original=self.data.qa_suite,
                allow_failure=allow_failure,
            )

        if self.data.enable_blhc:
            # Checked by
            # QAWorkflowData.enable_blhc_consistency.
            assert self.data.package_build_logs
            self._populate_blhc(
                prefix=self.data.prefix,
                reference_prefix=self.data.reference_prefix,
                source_artifact=self.data.source_artifact,
                package_build_logs=self.data.package_build_logs,
                qa_suite=self.data.qa_suite,
                reference_qa_results=self.data.reference_qa_results,
                enable_regression_tracking=self.data.enable_regression_tracking,
                update_qa_results=self.data.update_qa_results,
                vendor=self.data.vendor,
                codename=self.data.codename,
                arch_all_host_architecture=self.data.arch_all_host_architecture,
                allow_failure=allow_failure,
            )

        if self.data.fail_on == QAFailOn.REGRESSION:
            self._populate_regression_analysis_callback()

    def _populate_autopkgtest(
        self,
        *,
        prefix: str,
        reference_prefix: str,
        source_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        qa_suite: LookupSingle | None,
        reference_qa_results: LookupSingle | None,
        enable_regression_tracking: bool,
        update_qa_results: bool,
        vendor: str,
        codename: str,
        backend: BackendType,
        architectures: list[str],
        arch_all_host_architecture: str,
        allow_failure: bool,
    ) -> None:
        """Create work request for autopkgtest workflow."""
        wr = self.work_request_ensure_child_workflow(
            task_name="autopkgtest",
            task_data=AutopkgtestWorkflowData(
                prefix=prefix,
                reference_prefix=reference_prefix,
                source_artifact=source_artifact,
                binary_artifacts=binary_artifacts,
                qa_suite=qa_suite,
                reference_qa_results=reference_qa_results,
                enable_regression_tracking=enable_regression_tracking,
                update_qa_results=update_qa_results,
                vendor=vendor,
                codename=codename,
                backend=backend,
                architectures=architectures,
                arch_all_host_architecture=arch_all_host_architecture,
                extra_repositories=self.data.extra_repositories,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name="autopkgtest",
                step="autopkgtest",
                allow_failure=allow_failure,
            ),
        )
        # The autopkgtest workflow's children will have dependencies on the
        # work requests creating source_artifact and binary_artifacts, but the
        # autopkgtest workflow itself doesn't need that in order to populate
        # itself.
        self.orchestrate_child(wr)

    def _populate_reverse_dependencies_autopkgtest(
        self,
        *,
        prefix: str,
        reference_prefix: str,
        source_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        qa_suite: LookupSingle,
        reference_qa_results: LookupSingle | None,
        enable_regression_tracking: bool,
        update_qa_results: bool,
        vendor: str,
        codename: str,
        backend: BackendType,
        architectures: list[str],
        arch_all_host_architecture: str,
        allow_failure: bool,
    ) -> None:
        """Create work request for reverse_dependencies_autopkgtest workflow."""
        wr = self.work_request_ensure_child_workflow(
            task_name="reverse_dependencies_autopkgtest",
            task_data=ReverseDependenciesAutopkgtestWorkflowData(
                prefix=prefix,
                reference_prefix=reference_prefix,
                source_artifact=source_artifact,
                binary_artifacts=binary_artifacts,
                qa_suite=qa_suite,
                reference_qa_results=reference_qa_results,
                enable_regression_tracking=enable_regression_tracking,
                update_qa_results=update_qa_results,
                vendor=vendor,
                codename=codename,
                backend=backend,
                architectures=architectures,
                arch_all_host_architecture=arch_all_host_architecture,
                extra_repositories=self.data.extra_repositories,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name="autopkgtests of reverse-dependencies",
                step="reverse-dependencies-autopkgtest",
                allow_failure=allow_failure,
            ),
        )
        # The reverse_dependencies_autopkgtest workflow's descendants will have
        # dependencies on the work requests creating source_artifact and
        # binary_artifacts, but the reverse_dependencies_autopkgtest workflow
        # itself doesn't need that in order to populate itself.
        self.orchestrate_child(wr)

    def _populate_lintian(
        self,
        *,
        prefix: str,
        reference_prefix: str,
        source_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        qa_suite: LookupSingle | None,
        reference_qa_results: LookupSingle | None,
        enable_regression_tracking: bool,
        update_qa_results: bool,
        vendor: str,
        codename: str,
        backend: BackendType,
        architectures: list[str],
        arch_all_host_architecture: str,
        fail_on_severity: LintianFailOnSeverity,
        allow_failure: bool,
    ) -> None:
        """Create work request for lintian workflow."""
        wr = self.work_request_ensure_child_workflow(
            task_name="lintian",
            task_data=LintianWorkflowData(
                prefix=prefix,
                reference_prefix=reference_prefix,
                source_artifact=source_artifact,
                binary_artifacts=binary_artifacts,
                qa_suite=qa_suite,
                reference_qa_results=reference_qa_results,
                enable_regression_tracking=enable_regression_tracking,
                update_qa_results=update_qa_results,
                vendor=vendor,
                codename=codename,
                backend=backend,
                architectures=architectures,
                arch_all_host_architecture=arch_all_host_architecture,
                fail_on_severity=fail_on_severity,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name="lintian",
                step="lintian",
                allow_failure=allow_failure,
            ),
        )
        # The lintian workflow's descendants will have dependencies on the work
        # requests creating source_artifact and binary_artifacts, but the
        # lintian workflow itself doesn't need that in order to populate
        # itself.
        self.orchestrate_child(wr)

    def _populate_piuparts(
        self,
        *,
        prefix: str,
        reference_prefix: str,
        source_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        qa_suite: LookupSingle | None,
        reference_qa_results: LookupSingle | None,
        enable_regression_tracking: bool,
        update_qa_results: bool,
        vendor: str,
        codename: str,
        architectures: list[str],
        backend: BackendType,
        environment: LookupSingle | None,
        arch_all_host_architecture: str,
        allow_failure: bool,
    ) -> None:
        data = PiupartsWorkflowData(
            prefix=prefix,
            reference_prefix=reference_prefix,
            source_artifact=source_artifact,
            binary_artifacts=binary_artifacts,
            qa_suite=qa_suite,
            reference_qa_results=reference_qa_results,
            enable_regression_tracking=enable_regression_tracking,
            update_qa_results=update_qa_results,
            vendor=vendor,
            codename=codename,
            architectures=architectures,
            backend=backend,
            arch_all_host_architecture=arch_all_host_architecture,
            extra_repositories=self.data.extra_repositories,
        )
        if environment is not None:
            data.environment = environment
        wr = self.work_request_ensure_child_workflow(
            task_name="piuparts",
            task_data=data,
            workflow_data=WorkRequestWorkflowData(
                display_name="piuparts",
                step="piuparts",
                allow_failure=allow_failure,
            ),
        )
        # The piuparts workflow's descendants will have dependencies on the
        # work requests creating binary_artifacts, but the piuparts workflow
        # itself doesn't need that in order to populate itself.
        self.orchestrate_child(wr)

    def _populate_debdiff(
        self,
        *,
        vendor: str,
        codename: str,
        source_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        original: LookupSingle,
        allow_failure: bool,
    ) -> None:
        data = DebDiffWorkflowData(
            original=original,
            source_artifact=source_artifact,
            binary_artifacts=binary_artifacts,
            vendor=vendor,
            codename=codename,
        )

        wr = self.work_request_ensure_child_workflow(
            task_name="debdiff",
            task_data=data,
            workflow_data=WorkRequestWorkflowData(
                display_name="DebDiff",
                step="debdiff",
                allow_failure=allow_failure,
            ),
        )

        # Do not mark as running: the status is BLOCKED because the
        # binary_artifacts do not exist yet and DebDiffWorkflow needs
        # the binary artifacts in order to create the tasks
        self.requires_artifact(wr, binary_artifacts)

        self.orchestrate_child(wr)

    def _populate_blhc(
        self,
        *,
        prefix: str,
        reference_prefix: str,
        source_artifact: LookupSingle,
        package_build_logs: LookupMultiple,
        qa_suite: LookupSingle | None,
        reference_qa_results: LookupSingle | None,
        enable_regression_tracking: bool,
        update_qa_results: bool,
        vendor: str,
        codename: str,
        arch_all_host_architecture: str,
        allow_failure: bool,
    ) -> None:
        wr = self.work_request_ensure_child_workflow(
            task_name="blhc",
            task_data=BlhcWorkflowData(
                prefix=prefix,
                reference_prefix=reference_prefix,
                source_artifact=source_artifact,
                package_build_logs=package_build_logs,
                qa_suite=qa_suite,
                reference_qa_results=reference_qa_results,
                enable_regression_tracking=enable_regression_tracking,
                update_qa_results=update_qa_results,
                vendor=vendor,
                codename=codename,
                arch_all_host_architecture=arch_all_host_architecture,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name="build log hardening check",
                step="blhc",
                allow_failure=allow_failure,
            ),
        )
        self.orchestrate_child(wr)

    def _find_regression_analysis_sources(self) -> QuerySet[WorkRequest]:
        """Find descendants that contribute regression analysis results."""
        return WorkRequest.objects.filter(
            Q(task_type=TaskTypes.WORKFLOW)
            & (
                # Most of the relevant tasks are children of this work
                # request.
                Q(parent=self.work_request)
                # Reverse-dependency autopkgtests are grandchildren of this
                # work request.
                | Q(parent__parent=self.work_request)
            )
        )

    def _populate_regression_analysis_callback(self) -> None:
        """Create a final callback to collate regression analysis results."""
        wr = self.work_request_ensure_child_internal(
            task_name="workflow",
            workflow_data=WorkRequestWorkflowData(
                allow_failure=False,
                display_name="Regression analysis",
                step="regression-analysis",
                visible=True,
            ),
        )
        for callback in WorkRequest.objects.filter(
            task_type=TaskTypes.INTERNAL,
            task_name="workflow",
            workflow_data_json__step="regression-analysis",
            parent__in=self._find_regression_analysis_sources(),
        ):
            # Each individual regression analysis callback is a descendant
            # of this work request, and therefore must be in the same
            # workflow as the final callback.
            wr.add_dependency(callback, skip_same_workflow_check=True)

    def callback_regression_analysis(self) -> bool:
        """Collate regression analysis results."""
        output_data = self.work_request.output_data or OutputData()
        output_data.regression_analysis = {}
        overall_result = True
        for (
            task_name,
            subject,
            analysis_by_architecture,
        ) in self._find_regression_analysis_sources().values_list(
            "task_name",
            "dynamic_task_data__subject",
            "output_data_json__regression_analysis",
        ):
            for key, analysis in (analysis_by_architecture or {}).items():
                # If we have output data, then dynamic task data must have
                # been populated, and all our sub-workflows have a subject.
                assert subject is not None
                output_data.regression_analysis[
                    f"{task_name}:{subject}:{key}"
                ] = analysis
                if analysis["status"] in {
                    RegressionAnalysisStatus.ERROR,
                    RegressionAnalysisStatus.REGRESSION,
                }:
                    overall_result = False
        self.work_request.output_data = output_data
        self.work_request.save()
        return overall_result
