# Copyright 2021 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 build Debian packages with sbuild.

This task module implements the PackageBuild ontology for its task_data:
https://freexian-team.pages.debian.net/debusine/design/ontology.html#task-packagebuild
"""
import subprocess

from debusine.tasks import Task, TaskConfigError
from debusine.tasks._task_mixins import TaskRunCommandMixin


class Sbuild(Task, TaskRunCommandMixin):
    """Task implementing a Debian package build with sbuild."""

    TASK_VERSION = 1
    TASK_DATA_SCHEMA = {
        "type": "object",
        "properties": {
            "input": {
                "type": "object",
                "properties": {
                    "source_package_url": {
                        "type": "string",
                    },
                },
                "required": ["source_package_url"],
            },
            "distribution": {
                "type": "string",
            },
            "host_architecture": {
                "type": "string",
            },
            "build_components": {
                "type": "array",
                "items": {
                    "enum": ["any", "all", "source"],
                },
                "uniqueItems": True,
            },
        },
        "required": ["input", "distribution", "host_architecture"],
    }

    def __init__(self):
        """Initialize the sbuild task."""
        super().__init__()
        self.chroots = None
        self.builder = "sbuild"

    @property
    def chroot_name(self) -> str:
        """Build name of required chroot."""
        return "%s-%s" % (
            self.data["distribution"],
            self.data["host_architecture"],
        )

    @staticmethod
    def _call_dpkg_architecture():  # pragma: no cover
        return (
            subprocess.check_output(["dpkg", "--print-architecture"])
            .decode("utf-8")
            .strip()
        )

    def analyze_worker(self):
        """Report metadata for this task on this worker."""
        metadata = super().analyze_worker()

        self._update_chroots_list()
        chroots_key = self.prefix_with_task_name("chroots")
        metadata[chroots_key] = self.chroots.copy()

        host_arch_key = self.prefix_with_task_name("host_architecture")
        metadata[host_arch_key] = self._call_dpkg_architecture()

        return metadata

    def can_run_on(self, worker_metadata: dict) -> bool:
        """Check the specified worker can run the requested task."""
        if not super().can_run_on(worker_metadata):
            return False

        chroot_key = self.prefix_with_task_name("chroots")
        if self.chroot_name not in worker_metadata.get(chroot_key, []):
            return False

        return True

    @staticmethod
    def _call_schroot_list():  # pragma: no cover
        return (
            subprocess.check_output(["schroot", "--list"])
            .decode("utf-8")
            .strip()
        )

    def _update_chroots_list(self):
        """
        Provide support for finding available chroots.

        Ensure that aliases are supported as the DSC may explicitly refer to
        <codename>-security (or -backports) etc.

        Populates the self.chroots list, if the list is empty.
        No return value, this is a find, not a get.
        """
        if self.chroots is not None:
            return
        self.chroots = []
        output = self._call_schroot_list()
        for line in output.split("\n"):
            if line.startswith("chroot:") and line.endswith("-sbuild"):
                self.chroots.append(line[7:-7])

    def _verify_distribution(self):
        """Verify a suitable schroot exists."""
        self._update_chroots_list()

        if not self.chroots:
            self.logger.error("No sbuild chroots found")
            return False

        if self.chroot_name in self.chroots:
            return True

        self.logger.error("No suitable chroot found for %s", self.chroot_name)
        return False

    def configure(self, task_data):
        """Handle sbuild-specific configuration."""
        super().configure(task_data)

        # Handle default values
        self.data.setdefault("build_components", ["any"])
        self.data.setdefault("sbuild_options", [])

    def _cmdline(self):
        """Build the sbuild command line (idempotent)."""
        cmd = [
            self.builder,
            "--no-clean",
        ]
        if "any" in self.data["build_components"]:
            cmd.append("--arch-any")
        else:
            cmd.append("--no-arch-any")
        if "all" in self.data["build_components"]:
            cmd.append("--arch-all")
        else:
            cmd.append("--no-arch-all")
        if "source" in self.data["build_components"]:
            cmd.append("--source")
        else:
            cmd.append("--no-source")
        cmd.append("--dist=" + self.data["distribution"])
        cmd.append("--arch=" + self.data["host_architecture"])
        cmd.extend(self.data["sbuild_options"])
        cmd.append(self.data["input"]["source_package_url"])

        return cmd

    def execute(self):
        """
        Call sbuild with the right options.

        :raises: TaskConfigError.
        """
        if not self._verify_distribution():
            raise TaskConfigError(
                "No suitable schroot for %s-%s"
                % (self.data["distribution"], self.data["host_architecture"])
            )

        cmd = self._cmdline()
        self.logger.info("Executing %s", " ".join(cmd))
        return self.run_cmd(cmd)
