# Copyright (c) 2022  Peter Pentchev <roam@ringlet.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
"""Build a Docker container for the specified Linux distribution."""

from __future__ import annotations

import argparse
import dataclasses
import pathlib
import shlex
import subprocess
import sys
import tempfile

import cfg_diag
import utf8_locale


@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(cfg_diag.Config):
    """Runtime configuration for the build_container tool."""

    distro: Distro
    noop: bool
    pull: bool
    run_test: bool
    utf8_env: dict[str, str]


DISTROS = [
    Distro(name="unstable", distro="debian", version="unstable", pkg_system="apt"),
    Distro(name="bullseye", distro="debian", version="bullseye", pkg_system="apt"),
    Distro(name="buster", distro="debian", version="buster", pkg_system="apt"),
    Distro(name="jammy", distro="ubuntu", version="jammy", pkg_system="apt"),
    Distro(name="focal", distro="ubuntu", version="focal", pkg_system="apt"),
    Distro(
        name="centos9",
        distro="quay.io/centos/centos",
        version="stream9",
        pkg_system="yum",
    ),
    Distro(
        name="centos8",
        distro="quay.io/centos/centos",
        version="stream8",
        pkg_system="yum",
    ),
    Distro(name="alma8", distro="almalinux/almalinux", version="8", pkg_system="yum"),
    Distro(
        name="rocky8", distro="rockylinux/rockylinux", version="8", pkg_system="yum"
    ),
]


def parse_args() -> Config:
    """Parse the command-line arguments."""
    parser = 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 = 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(
        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=utf8_locale.UTF8Detect().detect().env,
        verbose=args.verbose,
    )


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 = " ".join(shlex.quote(word) for word in cmd)
    if cfg.noop:
        print(cmd_str)
        return

    cfg.diag(lambda: f"Running `{cmd_str}`")
    subprocess.check_call(cmd, env=cfg.utf8_env)


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


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

    with tempfile.TemporaryDirectory(prefix="remrun-docker-build.") as tempd_obj:
        tempd = 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()
