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

"""Generate cloud-init scripts for bootstrapping Debusine Workers."""

from base64 import b64encode
from textwrap import dedent

import yaml
from django.conf import settings

from debusine.assets.models import APTAuthenticationData
from debusine.db.models.auth import Token
from debusine.server.worker_pools.models import (
    BaseWorkerPoolDataModel,
    DebianRelease,
    DebusineInstallSource,
)
from debusine.tasks.models import ExtraExternalRepository


def apt_source(
    source: DebusineInstallSource,
    custom_source: ExtraExternalRepository | None = None,
    release: DebianRelease | None = None,
) -> ExtraExternalRepository:
    """Generate the APT source for source."""
    if release is None:
        release = DebianRelease.TRIXIE
    match source:
        case DebusineInstallSource.CUSTOM:
            if custom_source is None:
                raise ValueError(
                    "debusine_install_source_custom is not specified"
                )
            return custom_source
        case DebusineInstallSource.DAILY_BUILDS:
            signing_key = dedent(
                """\
                -----BEGIN PGP PUBLIC KEY BLOCK-----
                .
                mDMEaaX1bBYJKwYBBAHaRw8BAQdAfpkAx9/TxpbnGHezlUiruo7YT0EcFHEQ9Swt
                65r8wEa0KUFyY2hpdmUgc2lnbmluZyBrZXkgZm9yIGZyZWV4aWFuL2RlYnVzaW5l
                iJAEExYKADgWIQT8mEBDdxrXXLXVVA1rNLmlWenTHAUCaaX1bAIbAwULCQgHAgYV
                CgkICwIEFgIDAQIeAQIXgAAKCRBrNLmlWenTHDBIAP0S1OwE1zTfV+0jFXYZW6J8
                dcS+fKF1S1YCi6cXLuWzZwEApcVXixlRDzOmQQBymyG35nf2ZRYFuhLOlkD/Ad/W
                Xg8=
                =VWZC
                -----END PGP PUBLIC KEY BLOCK-----
                """
            )
            return ExtraExternalRepository(
                components=["main"],
                url="https://deb.debusine.freexian.com/freexian/debusine",
                suite=f"{release}-devel",
                signing_key=signing_key,
            )
        case DebusineInstallSource.ENTERPRISE:
            suite = f"{release}-enterprise"
        case DebusineInstallSource.ENTERPRISE_STAGING:
            suite = f"{release}-staging"
        case _:  # pragma: no cover
            raise NotImplementedError(f"Unknown source: {source}")

    signing_key = dedent(
        """\
        -----BEGIN PGP PUBLIC KEY BLOCK-----
        .
        mDMEablL9RYJKwYBBAHaRw8BAQdAtjhG7o+/dGAdH1FzsOLmz12j7eUYErbaAodm
        Uhi2tGC0NEFyY2hpdmUgc2lnbmluZyBrZXkgZm9yIGZyZWV4aWFuL2RlYnVzaW5l
        LWVudGVycHJpc2WIkAQTFgoAOBYhBHoYaXCPZVaNbZ2nHZhqj900jkTdBQJpuUv1
        AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEJhqj900jkTdZ3gA/35jIyT/
        SpM7IJvSta8mX/ntoiGX1Kz0RY+vtAg8ZqfnAQDpEbh76mrYhOH8260HKquubrkJ
        B9EY63RFDSkG3gDbBA==
        =i1MD
        -----END PGP PUBLIC KEY BLOCK-----
        """
    )
    return ExtraExternalRepository(
        components=["main"],
        url="https://deb.debusine.freexian.com/freexian/debusine-enterprise",
        suite=suite,
        signing_key=signing_key,
    )


FIREWALL = dedent(
    """\
    #!/usr/sbin/nft -f

    add table inet filter;
    flush table inet filter;

    table inet filter {
        chain input {
            type filter hook input priority filter;
            iif "lo" accept;  # codespell:ignore iif
            tcp dport ssh counter accept;
            udp dport dhcpv6-client counter accept;
            meta l4proto {icmp, icmpv6} counter accept;
            iifname "debusinebr0" udp dport {domain, bootps} accept;
            ct state invalid,new,untracked counter drop;
        }
        chain forward {
            type filter hook forward priority filter;
            # Cloud Metadata endpoints
            ip daddr 169.254.169.254 counter drop;
            ip6 daddr fd00:ec2::254 counter drop;
        }
        chain output {
            type filter hook output priority filter;
            # Cloud Metadata endpoints
            ip daddr 169.254.169.254 meta skuid != 0 counter drop;
            ip6 daddr fd00:ec2::254 meta skuid != 0 counter drop;
        }
    }
    """
)


class AptConfig(BaseWorkerPoolDataModel):
    """Model for cloud-init APT Configure object."""

    preserve_sources_list: bool
    sources_list: str


class WriteFile(BaseWorkerPoolDataModel):
    """Model for cloud-init Write Files write_files objects."""

    path: str
    content: str
    permissions: str | None = None
    owner: str | None = None


class FilesystemSetup(BaseWorkerPoolDataModel):
    """Model for cloud-init Disk Setup fs_setup objects."""

    device: str
    filesystem: str


class CloudInitConfig(BaseWorkerPoolDataModel):
    """Model for cloud-init configuration."""

    apt: AptConfig | None = None
    device_aliases: dict[str, str] = {}
    fs_setup: list[FilesystemSetup] = []
    hostname: str | None = None
    mounts: list[list[str]] = []
    package_reboot_if_required: bool = False
    package_update: bool = False
    package_upgrade: bool = False
    packages: list[str] = []
    bootcmd: list[str] = []
    runcmd: list[str] = []
    write_files: list[WriteFile] = []

    def as_str(self) -> str:
        """Render configuration file in YAML."""
        return "#cloud-config\n" + yaml.safe_dump(
            self.model_dump(exclude_defaults=True)
        )

    def as_base64_str(self) -> str:
        """Render configuration file in YAML and base64-encode the result."""
        return b64encode(self.as_str().encode("utf-8")).decode("ascii")


def worker_bootstrap_cloud_init(
    worker_name: str,
    activation_token: Token,
    *,
    debusine_install_source: DebusineInstallSource,
    debusine_install_source_custom: ExtraExternalRepository | None = None,
    debian_release: DebianRelease | None = None,
    apt_authentication: APTAuthenticationData | None = None,
) -> CloudInitConfig:
    """Generate cloud-init configuration for a new instance."""
    if getattr(activation_token, "key", None) is None:
        raise ValueError(
            f"Requires a fresh token with a key. Token<{activation_token.pk}> "
            f"does not have one."
        )

    if debusine_install_source == DebusineInstallSource.PRE_INSTALLED:
        return CloudInitConfig(
            package_update=True,
            package_upgrade=True,
            package_reboot_if_required=True,
            hostname=worker_name,
            write_files=[
                WriteFile(
                    path="/etc/debusine/worker/activation-token",
                    permissions="0o600",
                    content=activation_token.key,
                    owner="debusine-worker:debusine-worker",
                ),
            ],
            runcmd=["systemctl restart debusine-worker.service"],
        )

    config = CloudInitConfig(
        package_update=True,
        package_upgrade=True,
        package_reboot_if_required=True,
        apt=None,
        bootcmd=[
            # Executes early in boot, before apt is configured
            (
                "[ -e /usr/bin/eatmydata ] "
                "|| (apt-get update && apt-get -y install eatmydata)"
            ),
            # Don't start the worker until we're ready
            "systemctl mask debusine-worker.service",
        ],
        hostname=worker_name,
        write_files=[
            # We set the owner in a runcmd after the user has been created
            WriteFile(
                path="/etc/debusine/worker/activation-token",
                permissions="0o600",
                content=activation_token.key,
            ),
            WriteFile(
                path="/etc/nftables.conf",
                permissions="0o755",
                content=FIREWALL,
            ),
            WriteFile(
                path="/etc/debusine/worker/config.ini",
                content=dedent(
                    f"""\
                    [General]
                    api-url = https://{settings.DEBUSINE_FQDN}/api
                    log-file = /var/log/debusine/worker/worker.log
                    log-level = DEBUG
                    """
                ),
            ),
        ],
        packages=[
            "autopkgtest",
            "ca-certificates",
            "debusine-worker",
            "gpg",
            "incus",
            "mmdebstrap",
            "nftables",
            "sbuild",
            "uidmap",
        ],
        runcmd=[
            (
                "chown debusine-worker:debusine-worker "
                "/etc/debusine/worker/activation-token"
            ),
            "/usr/share/doc/debusine-worker/examples/configure-worker-incus.sh",
            "systemctl enable nftables.service",
            "systemctl start nftables.service",
            "systemctl unmask debusine-worker.service",
            "systemctl restart debusine-worker.service",
        ],
    )
    backports_packages: list[str] = []
    if debusine_install_source == DebusineInstallSource.BACKPORTS:
        backports_packages.append("src:debusine")

    if backports_packages:
        if debian_release:
            pin_release = f"n={debian_release}"
        else:
            pin_release = "a=stable"
        config.write_files += [
            WriteFile(
                path="/etc/apt/preferences.d/90-debusine-backports",
                content=dedent(
                    f"""\
                    Package: {" ".join(backports_packages)}
                    Pin: release {pin_release}-backports
                    Pin-Priority: 900

                    Package: {" ".join(backports_packages)}
                    Pin: release {pin_release}
                    Pin-Priority: -1
                    """
                ),
            ),
        ]
    if debusine_install_source in (
        DebusineInstallSource.DAILY_BUILDS,
        DebusineInstallSource.ENTERPRISE,
        DebusineInstallSource.ENTERPRISE_STAGING,
        DebusineInstallSource.CUSTOM,
    ):
        source = apt_source(
            debusine_install_source,
            debusine_install_source_custom,
            debian_release,
        )
        config.write_files += [
            WriteFile(
                path="/etc/apt/sources.list.d/debusine.sources",
                content=source.as_deb822_source(),
            ),
            WriteFile(
                path="/etc/apt/preferences.d/90-debusine-ignore-debian",
                content=dedent(
                    """\
                    Package: src:debusine
                    Pin: release o=Debian Backports
                    Pin-Priority: -1

                    Package: src:debusine
                    Pin: release o=Debian
                    Pin-Priority: -1
                    """
                ),
            ),
        ]
        if apt_authentication:
            config.write_files += [
                WriteFile(
                    path=f"/etc/apt/auth.conf.d/{apt_authentication.name}.conf",
                    content=dedent(
                        f"""\
                        machine {source.url}
                        login {apt_authentication.credentials.username}
                        password {apt_authentication.credentials.password}
                        """
                    ),
                ),
            ]
        elif debusine_install_source in (
            DebusineInstallSource.ENTERPRISE,
            DebusineInstallSource.ENTERPRISE_STAGING,
        ):
            raise ValueError(
                "apt_authentication is required for ENTERPRISE sources."
            )

    return config
