# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: BSD-2-Clause
"""Build a Docker container for the specified Linux distribution."""

from __future__ import annotations

import argparse
import dataclasses
import pathlib
import shlex
import subprocess  # noqa: S404
import sys
import tempfile
import typing

from run_sshd_test import util


if typing.TYPE_CHECKING:
    import logging
    from typing import Final


@dataclasses.dataclass(frozen=True)
class Distro:
    """A description of a single Linux distribution to build a container for."""

    name: str
    distro: str
    version: str
    pkg_system: str


@dataclasses.dataclass(frozen=True)
class Config:
    """Runtime configuration for the build_container tool."""

    log: logging.Logger
    distro: Distro
    noop: bool
    pull: bool
    run_test: bool
    utf8_env: dict[str, str]


DISTROS: Final = [
    Distro(name="unstable", distro="debian", version="unstable", pkg_system="apt"),
    Distro(name="bookworm", distro="debian", version="bookworm", pkg_system="apt"),
    Distro(name="noble", distro="ubuntu", version="noble", pkg_system="apt"),
    Distro(name="jammy", distro="ubuntu", version="jammy", pkg_system="apt"),
    Distro(
        name="centos9",
        distro="quay.io/centos/centos",
        version="stream9",
        pkg_system="yum",
    ),
    Distro(name="rocky9", distro="rockylinux/rockylinux", version="9", pkg_system="yum"),
]


def parse_args() -> Config:
    """Parse the command-line arguments."""
    parser: Final = argparse.ArgumentParser(prog="build_container")
    parser.add_argument(
        "-N",
        "--noop",
        action="store_true",
        help="no-operation mode; display what would be done",
    )
    parser.add_argument(
        "-P",
        "--pull",
        action="store_true",
        help="pull a newer base image if available",
    )
    parser.add_argument(
        "-T",
        "--run-test",
        action="store_true",
        help="run the test suite within the container",
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="verbose operation; display diagnostic output",
    )
    parser.add_argument(
        "distro",
        type=str,
        choices=[item.name for item in DISTROS] + ["list"],
        help="the Linux distribution to build a container for, or 'list'",
    )

    args: Final = parser.parse_args()

    if args.distro == "list":
        for item in DISTROS:
            print(f"{item.name}\t{item.distro}:{item.version}")
        sys.exit(0)

    return Config(
        log=util.build_logger(name="build_container", verbose=args.verbose, quiet=False),
        distro=next(item for item in DISTROS if item.name == args.distro),
        noop=args.noop,
        pull=args.pull,
        run_test=args.run_test,
        utf8_env=util.get_utf8_env(),
    )


def get_build_command(cfg: Config, tempd: pathlib.Path) -> list[str]:
    """Construct the `docker build` command to run."""
    return (
        [
            "docker",
            "build",
        ]
        + (["--pull"] if cfg.pull else [])
        + [
            "--build-arg",
            f"DISTRO={cfg.distro.distro}",
            "--build-arg",
            f"DISTRO_VERSION={cfg.distro.version}",
            "--build-arg",
            f"PKG_SYSTEM={cfg.distro.pkg_system}",
            "-t",
            f"remrun/sshd:{cfg.distro.name}",
            "--",
            str(tempd),
        ]
    )


def get_test_command(cfg: Config) -> list[str]:
    """Construct the run-docker-test command to run."""
    return ["docker/run-docker-test.sh", cfg.distro.name]


def run_command(cfg: Config, cmd: list[str]) -> None:
    """Run a command or, in no-op mode, display it."""
    cmd_str: Final = shlex.join(cmd)
    if cfg.noop:
        print(cmd_str)
        return

    cfg.log.debug("Running `%(cmd_str)s`", {"cmd_str": cmd_str})
    subprocess.check_call(cmd, env=cfg.utf8_env)  # noqa: S603


def copy_files(cfg: Config, tempd: pathlib.Path) -> None:
    """Copy the necessary files for building the container."""
    flist: Final = (
        subprocess.check_output(  # noqa: S603
            ["git", "ls-files", "-z", "docker"],  # noqa: S607
            encoding="UTF-8",
            env=cfg.utf8_env,
        )
        .strip("\0")
        .split("\0")
    )
    cmd: Final = ["cp", "-p", "-v", "--", *flist, "requirements/install.txt", str(tempd)]
    run_command(cfg, cmd)


def main() -> None:
    """Parse command-line options, run 'docker build'."""
    cfg: Final = parse_args()

    with tempfile.TemporaryDirectory(prefix="remrun-docker-build.") as tempd_obj:
        tempd: Final = pathlib.Path(tempd_obj).absolute()
        copy_files(cfg, tempd)
        run_command(cfg, get_build_command(cfg, tempd))

    if cfg.run_test:
        run_command(cfg, get_test_command(cfg))


if __name__ == "__main__":
    main()
