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

"""Ontology definition of SystemBootstrap."""
import abc
import re
import tempfile
from pathlib import Path
from typing import Generic, TypeVar, cast
from urllib.parse import urljoin

from debian.deb822 import Deb822, Release

import requests

from debusine.client.exceptions import ContentValidationError
from debusine.client.utils import get_url_contents_sha256sum
from debusine.tasks import RunCommandTask
from debusine.tasks.models import (
    BaseTaskData,
    SystemBootstrapRepository,
    SystemBootstrapRepositoryCheckSignatureWith,
    SystemBootstrapRepositoryKeyring,
)


TD = TypeVar("TD", bound=BaseTaskData)


# TODO: this should be generic over SystemBootstrapData instead of
# BaseTaskData, but we can't currently do that because
# MmDebstrapVariant can't inherit from
# SystemBootstrapOptionsVariant
class SystemBootstrap(abc.ABC, RunCommandTask[TD], Generic[TD]):
    """Implement ontology SystemBootstrap."""

    TASK_VERSION = 1

    @staticmethod
    def _download_key(
        repository: SystemBootstrapRepository,
        *,
        keyring_directory: Path,
    ) -> str:
        # Using cast because repository's validator ensures keyring is set
        # when using external repositories
        repo_keyring = cast(
            SystemBootstrapRepositoryKeyring, repository.keyring
        )
        keyring, actual_sha256sum = get_url_contents_sha256sum(
            repo_keyring.url, 100 * 1024 * 1024
        )

        if expected := repo_keyring.sha256sum:
            if actual_sha256sum != expected:
                raise ContentValidationError(
                    f"sha256 mismatch for keyring repository "
                    f"{repository.mirror}. "
                    f"Actual: {actual_sha256sum} expected: {expected}"
                )

        # Disable auto-deletion: the file will be deleted together
        # with keyring_directory when the RunCommandTask finishes
        file = tempfile.NamedTemporaryFile(
            dir=keyring_directory,
            prefix="keyring-repo-",
            suffix=".asc",
            delete=False,
        )
        Path(file.name).write_bytes(keyring)
        return file.name

    @classmethod
    def _deb822_source(
        cls,
        repository: SystemBootstrapRepository,
        *,
        keyring_directory: Path,
        use_signed_by: bool,
    ) -> Deb822:
        """
        Create a deb822 from repository and return it.

        :raise: ContentValidationError if the repository["keyring"]["sha256sum"]
          does not match the one from the downloaded keyring
        :param keyring_directory: directory to save the gpg keys
        :param use_signed_by: add Signed-By in the repository
        :return: repository
        """
        deb822 = Deb822()

        deb822["Types"] = " ".join(repository.types)
        deb822["URIs"] = repository.mirror
        deb822["Suites"] = repository.suite

        if (components := repository.components) is None:
            components = cls._list_components_for_suite(
                repository.mirror, repository.suite
            )

        deb822["Components"] = " ".join(components)

        if repository.check_signature_with == "no-check":
            deb822["Trusted"] = "yes"

        if (
            repository.check_signature_with
            == SystemBootstrapRepositoryCheckSignatureWith.EXTERNAL
        ):
            key_file = SystemBootstrap._download_key(
                repository, keyring_directory=keyring_directory
            )
            if use_signed_by:
                deb822["Signed-By"] = key_file

        return deb822

    @classmethod
    def _write_deb822s(cls, deb822s: list[Deb822], destination: Path):
        with destination.open("wb") as f:
            for deb822 in deb822s:
                deb822.dump(f)
                f.write(b"\n")

    def _generate_deb822_sources(
        self,
        repositories: list[SystemBootstrapRepository],
        *,
        keyrings_dir: Path,
        use_signed_by: bool,
    ) -> list[Deb822]:
        """
        Return list of Deb822 repositories.

        :param keyrings_dir: write gpg keys into it.
        :use_signed_by: if True, add Signed-By to the repository with the path
          to the file.
        """
        deb822s = []
        for repository in repositories:
            deb822s.append(
                self._deb822_source(
                    repository,
                    keyring_directory=keyrings_dir,
                    use_signed_by=use_signed_by,
                )
            )

        return deb822s

    @classmethod
    def _list_components_for_suite(
        cls, mirror_url: str, suite: str
    ) -> list[str]:
        """
        Return components listed in the Release file.

        :raises ValueError: if components cannot be found or invalid names.
        """
        release_url = urljoin(mirror_url + "/", f"dists/{suite}/Release")
        try:
            components = Release(requests.get(release_url).iter_lines())[
                "components"
            ].split()
        except KeyError:
            raise ValueError(f"Cannot find components in {release_url}")

        for component in components:
            if re.search("^[A-Za-z][-/A-Za-z]*$", component) is None:
                raise ValueError(
                    f'Invalid component name from {release_url}: '
                    f'"{component}" must start with [A-Za-z] and have only '
                    f'[-/A-Za-z] characters'
                )

        return components
