#!/usr/bin/python3

# Building an ISO requires knowing:
#
# * The architecture and series we are building for
# * The address of the mirror to pull packages from the pool from and the
#   components of that mirror to use
# * The list of packages to include in the pool
# * Where the squashfs files that contain the rootfs and other metadata layers
#   are
# * Where to put the final ISO
# * All the bits of information that end up in .disk/info on the ISO and in the
#   "volume ID" for the ISO
#
# It's not completely trivial to come up with a nice feeling interface between
# livecd-rootfs and this tool. There are about 13 parameters that are needed to
# build the ISO and having a tool take 13 arguments seems a bit overwhelming. In
# addition some steps need to run before the layers are made into squashfs files
# and some after. It felt nicer to have a tool with a few subcommands (7, in the
# end) and taking arguments relevant to each step:
#
# $ isobuild --work-dir "" init --disk-id "" --series "" --arch ""
#
# Set up the work-dir for later steps. Create the skeleton file layout of the
# ISO, populate .disk/info etc, create the gpg key referred to above. Store
# series and arch somewhere that later steps can refer to.
#
# $ isobuild --work-dir "" setup-apt --chroot ""
#
# Set up aptfor use by later steps, using the configuration from the passed
# chroot.
#
# $ isobuild --work-dir "" generate-pool --package-list-file ""
#
# Create the pool from the passed germinate output file.
#
# $ isobuild --work-dir "" generate-sources --mountpoint ""
#
# Generate an apt deb822 source for the pool, assuming it is mounted at the
# passed mountpoint, and output it on stdout.
#
# $ isobuild --work-dir "" add-live-filesystem --artifact-prefix ""
#
# Copy the relevant artifacts to the casper directory (and extract the uuids
# from the initrds)
#
# $ isobuild --work-dir "" make-bootable --project "" --capitalized-project ""
#            --subarch ""
#
# Set up the bootloader etc so that the ISO can boot (for this clones debian-cd
# and run the tools/boot/$series-$arch script but those should be folded into
# isobuild fairly promptly IMO).
#
# $ isobuild --work-dir "" make-iso --vol-id "" --dest ""
#
# Generate the checksum file and run xorriso to build the final ISO.


import pathlib
import shlex

import click

from isobuilder.builder import ISOBuilder


@click.group()
@click.option(
    "--workdir",
    type=click.Path(file_okay=False, resolve_path=True, path_type=pathlib.Path),
    required=True,
    help="working directory",
)
@click.pass_context
def main(ctxt, workdir):
    ctxt.obj = ISOBuilder(workdir)
    cwd = pathlib.Path().cwd()
    if workdir.is_relative_to(cwd):
        workdir = workdir.relative_to(cwd)
    ctxt.obj.logger.log(f"isobuild starting, workdir: {workdir}")


def subcommand(f):
    """Decorator that converts a function into a Click subcommand with logging.

    This decorator:
    1. Converts function name from snake_case to kebab-case for the CLI
    2. Wraps the function to log the subcommand name and all parameters
    3. Registers it as a Click command under the main command group
    4. Extracts the ISOBuilder instance from the context and passes it as first arg
    """
    name = f.__name__.replace("_", "-")

    def wrapped(ctxt, **kw):
        # Build a log message showing the subcommand and all its parameters.
        # We use ctxt.params (Click's resolved parameters) rather than **kw
        # because ctxt.params includes path resolution and type conversion.
        # Paths are converted to relative form to keep logs readable and avoid
        # exposing full filesystem paths in build artifacts.
        msg = f"subcommand {name}"
        cwd = pathlib.Path().cwd()
        for k, v in sorted(ctxt.params.items()):
            if isinstance(v, pathlib.Path):
                if v.is_relative_to(cwd):
                    v = v.relative_to(cwd)
            v = shlex.quote(str(v))
            msg += f" {k}={v}"
        with ctxt.obj.logger.logged(msg):
            f(ctxt.obj, **kw)

    return main.command(name=name)(click.pass_context(wrapped))


@click.option(
    "--disk-info",
    type=str,
    required=True,
    help="contents of .disk/info",
)
@click.option(
    "--series",
    type=str,
    required=True,
    help="series being built",
)
@click.option(
    "--arch",
    type=str,
    required=True,
    help="architecture being built",
)
@subcommand
def init(builder, disk_info, series, arch):
    builder.init(disk_info, series, arch)


@click.option(
    "--chroot",
    type=click.Path(
        file_okay=False, resolve_path=True, path_type=pathlib.Path, exists=True
    ),
    required=True,
)
@subcommand
def setup_apt(builder, chroot: pathlib.Path):
    builder.setup_apt(chroot)


@click.pass_obj
@click.option(
    "--package-list-file",
    type=click.Path(
        dir_okay=False, exists=True, resolve_path=True, path_type=pathlib.Path
    ),
    required=True,
)
@subcommand
def generate_pool(builder, package_list_file: pathlib.Path):
    builder.generate_pool(package_list_file)


@click.option(
    "--mountpoint",
    type=str,
    required=True,
)
@subcommand
def generate_sources(builder, mountpoint: str):
    builder.generate_sources(mountpoint)


@click.option(
    "--artifact-prefix",
    type=click.Path(dir_okay=False, resolve_path=True, path_type=pathlib.Path),
    required=True,
)
@subcommand
def add_live_filesystem(builder, artifact_prefix: pathlib.Path):
    builder.add_live_filesystem(artifact_prefix)


@click.option(
    "--project",
    type=str,
    required=True,
)
@click.option("--capproject", type=str, required=True)
@click.option(
    "--subarch",
    type=str,
    default="",
)
@subcommand
def make_bootable(builder, project: str, capproject: str | None, subarch: str):
    # capproject is the "capitalized project name" used in GRUB menu entries,
    # e.g. "Ubuntu" or "Kubuntu". It should come from gen-iso-ids (which uses
    # project_to_capproject_map for proper formatting like "Ubuntu-MATE"), but
    # we provide a simple .capitalize() fallback for cases where the caller
    # doesn't have the pre-computed value.
    if capproject is None:
        capproject = project.capitalize()
    builder.make_bootable(project, capproject, subarch)


@click.option(
    "--dest",
    type=click.Path(dir_okay=False, resolve_path=True, path_type=pathlib.Path),
    required=True,
)
@click.option(
    "--volid",
    type=str,
    default=None,
)
@subcommand
def make_iso(builder, dest: pathlib.Path, volid: str | None):
    builder.make_iso(dest, volid)


if __name__ == "__main__":
    main()
