"""``ilum airgap`` command group -- offline/air-gapped tooling."""

from __future__ import annotations

import json
import subprocess
from datetime import UTC, datetime
from pathlib import Path

import typer

import ilum.cli.output as output_mod
from ilum.constants import DEFAULT_CHART_REF
from ilum.core.helm import HelmClient
from ilum.core.images import parse_images_from_yaml
from ilum.core.modules import ModuleResolver
from ilum.errors import IlumError
from ilum.wizard.deps import ensure_tools

airgap_app = typer.Typer(help="Air-gapped deployment tooling.")


def _discover_images(
    modules: list[str],
    resolver: ModuleResolver,
    version: str,
    chart: str = "",
) -> list:
    """Run ``helm template`` and parse image references."""
    enable_flags: list[str] = []
    for mod_name in modules:
        enable_flags.extend(resolver.resolve_enables(mod_name))
    # Disable everything not in the selection
    all_names = {m.name for m in resolver.all_modules()}
    disabled = all_names - set(modules)
    disable_flags: list[str] = []
    for name in disabled:
        mod = resolver.get(name)
        if mod:
            disable_flags.extend(mod.disable_flags)

    set_flags = list(enable_flags) + list(disable_flags)
    chart_ref = chart or DEFAULT_CHART_REF

    helm = HelmClient()
    result = helm.template(
        chart=chart_ref,
        set_flags=set_flags or None,
    )
    return parse_images_from_yaml(result.stdout)


def _resolve_modules(
    preset: str,
    module: list[str] | None,
    resolver: ModuleResolver,
) -> list[str]:
    """Determine the module list from --preset / --module flags."""
    from ilum.core.presets import get_preset

    if preset:
        preset_obj = get_preset(preset)
        if preset_obj is None:
            raise IlumError(f"Unknown preset: '{preset}'")
        modules = list(preset_obj.modules)
    elif module:
        modules = list(module)
    else:
        modules = resolver.default_enabled()

    # Add extra modules on top of preset
    if preset and module:
        extra = [m for m in module if m not in modules]
        modules.extend(extra)

    return modules


@airgap_app.command("images")
def airgap_images(
    preset: str = typer.Option("", "--preset", help="Deployment preset."),
    module: list[str] | None = typer.Option(  # noqa: B008
        None, "--module", "-m", help="Additional module (repeatable)."
    ),
    chart: str = typer.Option(
        "", "--chart", help="Chart reference or local path (default: ilum/ilum)."
    ),
    version: str = typer.Option("", "--version", help="Chart version."),
    fmt: str = typer.Option("table", "--format", help="Output format: table or plain."),
) -> None:
    """List container images required by the selected modules."""
    console = output_mod.console
    resolver = ModuleResolver()

    try:
        ensure_tools(["helm"], console)
        modules = _resolve_modules(preset, module, resolver)
        images = _discover_images(modules, resolver, version, chart=chart)

        if not images:
            console.info("No images found.")
            return

        if fmt == "plain":
            for img in images:
                typer.echo(img.reference)
        else:
            rows = [[img.reference] for img in images]
            console.table(
                title=f"Container Images ({len(images)})",
                columns=["Image Reference"],
                rows=rows,
            )

    except IlumError as exc:
        console.handle_error(exc)
        raise typer.Exit(code=1) from exc


@airgap_app.command("export")
def airgap_export(
    output_dir: str = typer.Argument(..., help="Output directory for the bundle."),
    preset: str = typer.Option("", "--preset", help="Deployment preset."),
    module: list[str] | None = typer.Option(  # noqa: B008
        None, "--module", "-m", help="Additional module (repeatable)."
    ),
    chart: str = typer.Option(
        "", "--chart", help="Chart reference or local path (default: ilum/ilum)."
    ),
    version: str = typer.Option("", "--version", help="Chart version."),
) -> None:
    """Export images and chart into a portable air-gapped bundle."""
    console = output_mod.console
    resolver = ModuleResolver()

    try:
        ensure_tools(["helm", "docker"], console)
        modules = _resolve_modules(preset, module, resolver)
        images = _discover_images(modules, resolver, version, chart=chart)

        if not images:
            console.info("No images found — nothing to export.")
            return

        out = Path(output_dir)
        images_dir = out / "images"
        images_dir.mkdir(parents=True, exist_ok=True)

        # Pull and save each image
        manifest_images = []
        for img in images:
            console.info(f"Pulling {img.reference}...")
            proc = subprocess.run(
                ["docker", "pull", img.reference],
                capture_output=True,
                text=True,
            )
            if proc.returncode != 0:
                console.error(f"Failed to pull {img.reference}: {proc.stderr.strip()}")
                raise typer.Exit(code=1)

            tar_name = img.sanitized_filename()
            tar_path = images_dir / tar_name
            console.info(f"Saving {img.reference} → {tar_name}")
            proc = subprocess.run(
                ["docker", "save", img.reference, "-o", str(tar_path)],
                capture_output=True,
                text=True,
            )
            if proc.returncode != 0:
                console.error(f"Failed to save {img.reference}: {proc.stderr.strip()}")
                raise typer.Exit(code=1)

            manifest_images.append(
                {
                    "reference": img.reference,
                    "file": f"images/{tar_name}",
                }
            )

        # Write manifest
        manifest = {
            "version": version or "latest",
            "created_at": datetime.now(UTC).isoformat(),
            "images": manifest_images,
            "modules": modules,
        }
        manifest_path = out / "manifest.json"
        manifest_path.write_text(json.dumps(manifest, indent=2))

        console.success(f"Bundle exported to {out} ({len(images)} images, {len(modules)} modules)")

    except IlumError as exc:
        console.handle_error(exc)
        raise typer.Exit(code=1) from exc


@airgap_app.command("import")
def airgap_import(
    bundle_dir: str = typer.Argument(..., help="Path to the exported bundle directory."),
    registry: str = typer.Option(..., "--registry", help="Target private registry URL (required)."),
) -> None:
    """Import an air-gapped bundle into a private container registry."""
    console = output_mod.console
    bundle = Path(bundle_dir)
    manifest_path = bundle / "manifest.json"

    if not manifest_path.exists():
        console.error(f"manifest.json not found in {bundle}")
        raise typer.Exit(code=1)

    try:
        manifest = json.loads(manifest_path.read_text())
    except (json.JSONDecodeError, OSError) as exc:
        console.error(f"Failed to read manifest: {exc}")
        raise typer.Exit(code=1) from exc

    images = manifest.get("images", [])
    if not images:
        console.info("No images in manifest — nothing to import.")
        return

    ensure_tools(["docker"], console)

    for entry in images:
        ref = entry["reference"]
        tar_file = bundle / entry["file"]

        # Load
        console.info(f"Loading {tar_file.name}...")
        proc = subprocess.run(
            ["docker", "load", "-i", str(tar_file)],
            capture_output=True,
            text=True,
        )
        if proc.returncode != 0:
            console.error(f"Failed to load {tar_file.name}: {proc.stderr.strip()}")
            raise typer.Exit(code=1)

        # Determine new tag
        parts = ref.split("/", 1)
        image_path = parts[1] if len(parts) > 1 else parts[0]
        new_ref = f"{registry}/{image_path}"

        # Tag
        proc = subprocess.run(
            ["docker", "tag", ref, new_ref],
            capture_output=True,
            text=True,
        )
        if proc.returncode != 0:
            console.error(f"Failed to tag {ref} → {new_ref}: {proc.stderr.strip()}")
            raise typer.Exit(code=1)

        # Push
        console.info(f"Pushing {new_ref}...")
        proc = subprocess.run(
            ["docker", "push", new_ref],
            capture_output=True,
            text=True,
        )
        if proc.returncode != 0:
            console.error(f"Failed to push {new_ref}: {proc.stderr.strip()}")
            raise typer.Exit(code=1)

        console.success(f"{ref} → {new_ref}")

    console.success(f"All {len(images)} images imported to {registry}")
    console.info("")
    console.info("To install with the private registry, use:")
    console.info(f"  ilum install --set global.imageRegistry={registry}")
