"""Mobster Command Line Interface (CLI) Module"""

import argparse
import re
from pathlib import Path
from typing import Any

from mobster.cmd import augment
from mobster.cmd.delete import delete_tpa
from mobster.cmd.download import download_tpa
from mobster.cmd.generate import modelcar, oci_artifact, oci_image, oci_index, product
from mobster.cmd.upload import upload
from mobster.image import ARTIFACT_PATTERN, PULLSPEC_PATTERN
from mobster.release import ReleaseId


def setup_arg_parser() -> argparse.ArgumentParser:
    """
    Setup the argument parser for the CLI.
    This function creates a command-line interface (CLI) for the Mobster application.

    Returns:
        argparse.ArgumentParser: Argument parser object configured with subparsers
        for different commands.
    """
    parser = argparse.ArgumentParser(description="Mobster CLI")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # Generate command
    generate_command_parser(subparsers)
    # Augment command
    augment_command_parser(subparsers)
    # Upload command
    upload_command_parser(subparsers)
    # Download command
    download_command_parser(subparsers)
    # Delete command
    delete_command_parser(subparsers)

    parser.add_argument("--verbose", action="store_true", help="Enable verbose output.")

    return parser


def generate_command_parser(subparsers: Any) -> None:
    """
    Create the command parser for generating SBOM documents.
    """
    generate_parser = subparsers.add_parser(
        "generate", help="Generate an SBOM document for given content type"
    )
    generate_subparsers = generate_parser.add_subparsers(dest="type", required=True)

    generate_parser.add_argument(
        "--output",
        type=Path,
        help="Path to the output file. If not provided, the output will be printed"
        "to stdout.",
    )

    generate_oci_image_parser(generate_subparsers)
    generate_oci_index_parser(generate_subparsers)
    generate_product_parser(generate_subparsers)
    generate_modelcar_parser(generate_subparsers)
    generate_oci_artifact_parser(generate_subparsers)


def generate_oci_image_parser(subparsers: Any) -> None:
    """
    Generate the command parser for the oci image.
    """

    def validated_digest(value: str) -> str:
        assert re.match(r"^sha256:[0-9a-f]{64}$", value, re.IGNORECASE), (
            "The digest must start with 'sha256:' and contain 64 hexa symbols!"
        )
        return value.lower()

    def validated_pullspec(value: str) -> str:
        assert re.match(PULLSPEC_PATTERN, value), (
            "The pullspec must contain a tag! "
            "Use the '<registry>/<repository>:<tag>' format."
        )
        return value

    def validated_additional_reference(value: str) -> str:
        assert re.match(ARTIFACT_PATTERN, value), (
            "Additional references must be in the format "
            "<registry>/<repository>:<tag>@sha256:<digest>"
        )
        return value

    oci_image_parser = subparsers.add_parser(
        "oci-image", help="Generate an SBOM document for OCI image"
    )
    oci_image_parser.add_argument(
        "--from-syft",
        type=Path,
        help="Path to the SBOM file generated by Syft",
        action="append",
    )
    oci_image_parser.add_argument(
        "--from-hermeto",
        type=Path,
        help="Path to the SBOM file generated by Hermeto (Cachi2)",
    )
    oci_image_parser.add_argument(
        "--image-pullspec",
        type=validated_pullspec,
        help="Image pullspec for the OCI image in the format "
        "<registry>/<repository>:<tag>",
    )
    oci_image_parser.add_argument(
        "--image-digest",
        type=validated_digest,
        help="Image digest for the OCI image in the format sha256:<digest>",
    )
    oci_image_parser.add_argument(
        "--parsed-dockerfile-path",
        type=Path,
        help="Path to the parsed Dockerfile file",
    )
    oci_image_parser.add_argument(
        "--base-image-digest-file",
        type=Path,
        help="Path to the file containing references "
        "to images in the Dockerfile and their digests. "
        "Expected format: "
        "`<registry>/<repository>:<tag> <registry>/<repository>:<tag>@sha256:<digest>`",
    )
    oci_image_parser.add_argument(
        "--dockerfile-target",
        type=str,
        help="The name of the build target from the Dockerfile",
        default=None,
    )
    oci_image_parser.add_argument(
        "--additional-base-image",
        type=validated_additional_reference,
        action="append",
        default=[],
        help="Base (builder) image to add, can be specified multiple times. "
        "Expects the format <registry>/<repository>:<tag>@sha256:<digest value>",
    )
    oci_image_parser.add_argument(
        "--contextualize",
        action="store_true",
        help="Contextualize the SBOM",
        default=False,
    )
    oci_image_parser.set_defaults(func=oci_image.GenerateOciImageCommand)


def generate_oci_index_parser(subparsers: Any) -> None:
    """
    Generate the command parser for the oci index.
    """

    oci_index_parser = subparsers.add_parser(
        "oci-index", help="Generate an SBOM document for OCI index"
    )
    oci_index_parser.add_argument(
        "--index-image-pullspec",
        type=str,
        required=True,
        help="Image pullspec for the OCI index in the format "
        "<registry>/<repository>:<tag>",
    )
    oci_index_parser.add_argument(
        "--index-image-digest",
        type=str,
        required=True,
        help="Image digest for the OCI index in the format sha256:<digest>",
    )
    oci_index_parser.add_argument(
        "--index-manifest-path",
        type=Path,
        required=True,
        help="Path to the OCI index manifest file",
    )
    oci_index_parser.set_defaults(func=oci_index.GenerateOciIndexCommand)


def generate_product_parser(subparsers: Any) -> None:
    """
    Generate the command parser for the product level SBOM.
    """

    product_parser = subparsers.add_parser(
        "product", help="generate an SBOM document for product"
    )
    product_parser.add_argument(
        "--release-data",
        required=True,
        type=Path,
        help="path to the merged data file in JSON format.",
    )
    product_parser.add_argument(
        "--snapshot",
        required=True,
        type=Path,
        help="path to the mapped snapshot spec data in JSON format.",
    )
    product_parser.add_argument(
        "--release-id",
        type=ReleaseId,
        help="UUID4 release_id to associate with the SBOM file",
    )
    product_parser.add_argument(
        "--concurrency",
        type=parse_concurrency,
        default=8,
        help="concurrency limit for snapshot parsing (non-zero integer)",
    )

    product_parser.set_defaults(func=product.GenerateProductCommand)


def generate_modelcar_parser(subparsers: Any) -> None:
    """
    Generate the command parser for the modelcar content type.
    """
    modelcar_parser = subparsers.add_parser(
        "modelcar", help="Generate an SBOM document for modelcar"
    )
    modelcar_parser.add_argument(
        "--modelcar-image",
        type=str,
        help="Modelcar OCI artifact reference in the format "
        "<registry>/<repository>:<sha265:digest>",
        required=True,
    )
    modelcar_parser.add_argument(
        "--base-image",
        type=str,
        help="Base image OCI artifact reference in the format "
        "<registry>/<repository>:<sha265:digest>",
        required=True,
    )
    modelcar_parser.add_argument(
        "--model-image",
        type=str,
        help="Model OCI artifact reference in the format "
        "<registry>/<repository>:<sha265:digest>",
        required=True,
    )
    modelcar_parser.add_argument(
        "--sbom-type",
        choices=["cyclonedx", "spdx"],
        default="cyclonedx",
        help="Type of SBOM to generate (default: cyclonedx)",
    )
    modelcar_parser.set_defaults(func=modelcar.GenerateModelcarCommand)


def generate_oci_artifact_parser(subparsers: Any) -> None:
    """
    Generate the command parser for oci artifact content type.
    """
    oci_artifact_parser = subparsers.add_parser(
        "oci-artifact", help="Generate an SBOM document for OCI artifact"
    )
    oci_artifact_parser.add_argument(
        "--image-pullspec",
        type=str,
        required=True,
        help="Image pullspec for the OCI image in the format "
        "<registry>/<repository>:<tag>",
    )
    oci_artifact_parser.add_argument(
        "--image-digest",
        type=str,
        required=True,
        help="Image digest for the OCI image in the format sha256:<digest>",
    )
    oci_artifact_parser.add_argument(
        "--oci-copy-yaml",
        type=Path,
        help="Path to the OCI copy YAML file",
        required=True,
    )
    oci_artifact_parser.add_argument(
        "--sbom-type",
        choices=["cyclonedx", "spdx"],
        default="cyclonedx",
        help="Type of SBOM to generate (default: cyclonedx)",
    )

    oci_artifact_parser.set_defaults(func=oci_artifact.GenerateOciArtifactCommand)


def augment_command_parser(subparsers: Any) -> None:
    """
    A parser for augmenting SBOMs documents.
    """
    augment_parser = subparsers.add_parser(
        "augment", help="augment SBOMs with additional information"
    )
    augment_parser.add_argument(
        "--output",
        type=Path,
        default=Path.cwd(),
        help="path to the output file. If not provided, the SBOMs will be saved "
        "to the working directory",
    )

    augment_subparsers = augment_parser.add_subparsers(dest="type", required=True)
    generate_augment_oci_image_parser(augment_subparsers)


def generate_augment_oci_image_parser(subparsers: Any) -> None:
    """
    A parser for augmenting SBOMs for OCI images.
    """
    augment_oci_image_parser = subparsers.add_parser(
        "oci-image",
        help="augment SBOM documents with additional information from a mapped"
        "snapshot spec and save them to a directory",
    )
    augment_oci_image_parser.add_argument(
        "--snapshot",
        type=Path,
        required=True,
        help="path to the mapped snapshot spec file in JSON format",
    )
    augment_oci_image_parser.add_argument(
        "--reference",
        type=str,
        help="OCI image reference in the format <repository>@<digest>. If "
        "unspecified, SBOMs for all images in the snapshot will be augmented",
    )
    augment_oci_image_parser.add_argument(
        "--verification-key",
        type=Path,
        help="path to public key used to verify the image provenance",
    )
    augment_oci_image_parser.add_argument(
        "--concurrency",
        type=parse_concurrency,
        default=8,
        help="concurrency limit for SBOM updates (non-zero integer)",
    )
    augment_oci_image_parser.add_argument(
        "--release-id",
        type=ReleaseId,
        help="UUID4 release_id to associate with the SBOM file",
    )

    augment_oci_image_parser.set_defaults(func=augment.AugmentImageCommand)


def upload_command_parser(subparsers: Any) -> None:
    """
    A parser for uploading SBOMs documents.

    """
    upload_parser = subparsers.add_parser(
        "upload", help="Upload an SBOM document to a given destination"
    )
    upload_subparser = upload_parser.add_subparsers(dest="destination", required=True)
    upload_tpa_parser(upload_subparser)


def upload_tpa_parser(subparsers: Any) -> None:
    """
    A parser for uploading SBOMs to TPA.
    """
    tpa_parser = subparsers.add_parser(
        "tpa", help="Upload an SBOM document to TPA v2 server"
    )

    tpa_parser.add_argument(
        "--tpa-base-url",
        type=str,
        required=True,
        help="URL of the TPA server",
    )
    tpa_parser.add_argument(
        "--workers",
        type=int,
        default=1,
        help="Number of workers to execute uploads in parallel",
    )
    tpa_parser.add_argument(
        "--report",
        action="store_true",
        help="Print upload report to stdout",
    )
    tpa_parser.add_argument(
        "--labels",
        type=parse_tpa_labels,
        help='Comma-separated "key=value" pairs denoting '
        "labels to attach to the uploaded SBOMs",
        default={},
    )
    tpa_parser.add_argument(
        "--retries",
        type=int,
        help="How many retries will be attempted to upload to TPA before failing.",
        default=3,
    )

    # Create a mutually exclusive group and require one of the arguments
    source_group = tpa_parser.add_mutually_exclusive_group(required=True)
    source_group.add_argument("--from-dir", type=Path, help="Directory to upload from")
    source_group.add_argument("--file", type=Path, help="File to upload")

    tpa_parser.set_defaults(func=upload.TPAUploadCommand)


def parse_concurrency(val: str) -> int:
    """Parse and validate concurrency limit from command line argument.

    Args:
        val: String value from command line argument.

    Returns:
        Validated integer concurrency limit.

    Raises:
        argparse.ArgumentTypeError: If value is not a positive integer.
    """
    num = int(val)
    if num < 1:
        raise argparse.ArgumentTypeError(
            "Concurrency limit must be a non-zero integer."
        )
    return num


def parse_tpa_labels(val: str) -> dict[str, str]:
    """
    Parse and validate labels from command line argument.

    Args:
        val: String value from command line argument.

    Returns:
        Dictionary mapping label keys to label values.

    Raises:
        argparse.ArgumentTypeError: On incorrect format.
    """
    labels = {}
    pairs = val.split(",")
    for pair in pairs:
        if pair.count("=") != 1:
            raise argparse.ArgumentTypeError(
                "Each key=value pair must contain exactly one '=' character."
            )

        key, value = pair.split("=")
        if not key:
            raise argparse.ArgumentTypeError("Label key cannot be empty.")

        if not value:
            raise argparse.ArgumentTypeError("Label value cannot be empty.")

        labels[key] = value

    return labels


def download_command_parser(subparsers: Any) -> None:
    """
    A parser for downloading SBOMs documents.

    """
    download_parser = subparsers.add_parser(
        "download", help="Download an SBOM document from TPA server"
    )
    download_subparser = download_parser.add_subparsers(
        dest="destination", required=True
    )
    download_tpa_parser(download_subparser)


def download_tpa_parser(subparsers: Any) -> None:
    """
    A parser for downloading SBOMs from TPA.
    """
    tpa_parser = subparsers.add_parser(
        "tpa", help="Download an SBOM document from TPA v2 server"
    )

    tpa_parser.add_argument(
        "--tpa-base-url",
        type=str,
        required=True,
        help="URL of the TPA server",
    )
    tpa_parser.add_argument(
        "--output",
        type=Path,
        required=True,
        help="A directory to save the downloaded SBOM file(s)",
    )

    # Create a mutually exclusive group and require one of the arguments
    source_group = tpa_parser.add_mutually_exclusive_group(required=True)
    source_group.add_argument("--query", type=Path, help="A query to identify SBOMs")
    source_group.add_argument("--uuid", type=Path, help="SBOM UUID to download")

    tpa_parser.set_defaults(func=download_tpa.TPADownloadCommand)


def delete_command_parser(subparsers: Any) -> None:
    """
    A parser for downloading SBOMs documents.

    """
    delete_parser = subparsers.add_parser(
        "delete", help="Delete an SBOM document from a remote location"
    )
    delete_subparser = delete_parser.add_subparsers(dest="destination", required=True)
    delete_tpa_parser(delete_subparser)


def delete_tpa_parser(subparsers: Any) -> None:
    """
    A parser for deleteing SBOMs from TPA.
    """
    tpa_parser = subparsers.add_parser(
        "tpa", help="Delete an SBOM document from TPA v2 server"
    )

    tpa_parser.add_argument(
        "--tpa-base-url",
        type=str,
        required=True,
        help="URL of the TPA server",
    )

    tpa_parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Perform a dry run without actually deleting SBOMs",
    )

    # Create a mutually exclusive group and require one of the arguments
    source_group = tpa_parser.add_mutually_exclusive_group(required=True)
    source_group.add_argument("--query", type=Path, help="A query to identify SBOMs")
    source_group.add_argument("--uuid", type=Path, help="SBOM UUID to download")

    tpa_parser.set_defaults(func=delete_tpa.TPADeleteCommand)
