"""Command-line interface for Debian Repository Manager.

This module provides the main CLI entry point using Click.
"""

import logging
import os
import sys
from pathlib import Path
from typing import List, Optional

import click

from debrepomanager.aptly import AptlyError, AptlyManager
from debrepomanager.config import Config, ConfigError
from debrepomanager.release import ReleaseError, ReleaseManager
from debrepomanager.utils import find_deb_files, setup_logging

logger = logging.getLogger(__name__)


def _collect_package_files(
    packages: tuple, package_dir: Optional[str], verbose: bool
) -> List[str]:
    """Collect package files from arguments.

    Args:
        packages: Tuple of package file paths
        package_dir: Optional directory containing packages
        verbose: Whether to print verbose messages

    Returns:
        List of package file paths

    Raises:
        click.ClickException: If package collection fails
    """
    pkg_files: List[str] = list(packages)

    if package_dir:
        try:
            found_files = find_deb_files(package_dir, recursive=True)
            pkg_files.extend(found_files)
            if verbose:
                click.echo(f"Found {len(found_files)} package(s) in {package_dir}")
        except (FileNotFoundError, ValueError) as e:
            raise click.ClickException(str(e))

    if not pkg_files:
        raise click.ClickException(
            "No packages specified. Use --packages or --package-dir"
        )

    return pkg_files


def _ensure_repo_exists(
    manager: AptlyManager, codename: str, component: str, force: bool, auto_create: bool
) -> None:
    """Ensure repository exists, creating if needed.

    Args:
        manager: AptlyManager instance
        codename: Distribution codename
        component: Repository component
        force: Whether to force creation
        auto_create: Whether auto-create is enabled

    Raises:
        click.ClickException: If repo doesn't exist and can't be created
    """
    if not manager.repo_exists(codename, component):
        if force or auto_create:
            click.echo(f"Repository {component} doesn't exist, creating...")
            manager.create_repo(codename, component)
            click.echo("✓ Repository created")
        else:
            raise click.ClickException(
                f"Repository {codename}/{component} doesn't exist. "
                "Use --force to create or enable auto_create in config."
            )


@click.group()
@click.option(
    "--config",
    type=click.Path(exists=True),
    help="Path to configuration file",
)
@click.option(
    "--verbose",
    "-v",
    is_flag=True,
    help="Enable verbose logging (DEBUG level)",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Simulate operations without making changes",
)
@click.option(
    "--publish-prefix",
    type=str,
    default="",
    help=(
        "Publish prefix for environment isolation (e.g., 'beta', 'test'). "
        "Empty for stable."
    ),
)
@click.pass_context
def cli(
    ctx: click.Context,
    config: Optional[str],
    verbose: bool,
    dry_run: bool,
    publish_prefix: str,
) -> None:
    """Debian Repository Manager - manage Debian-like repositories with aptly.

    Examples:

        \b
        # Add packages to repository
        repomanager add --codename bookworm --component jethome-tools --packages *.deb

        \b
        # Create new repository
        repomanager create-repo --codename noble --component jethome-armbian

        \b
        # List repositories
        repomanager list --codename bookworm
    """
    # Ensure context object exists
    ctx.ensure_object(dict)

    # Setup logging
    log_level = "DEBUG" if verbose else "INFO"
    ctx.obj["logger"] = setup_logging(level=log_level)
    ctx.obj["verbose"] = verbose
    ctx.obj["dry_run"] = dry_run
    ctx.obj["publish_prefix"] = publish_prefix

    # Set publish_prefix as ENV variable for Config to pick up
    if publish_prefix:
        os.environ["REPOMANAGER_PUBLISH_PREFIX"] = publish_prefix

    # Load configuration
    try:
        ctx.obj["config"] = Config(config)
        if verbose:
            click.echo(f"Loaded configuration from: {config or 'default locations'}")
            if publish_prefix:
                click.echo(f"Using publish prefix: {publish_prefix}")
    except ConfigError as e:
        click.echo(f"Error: Configuration error: {e}", err=True)
        sys.exit(1)


@cli.command()
@click.option(
    "--environment",
    type=click.Choice(["stable", "beta", "test"]),
    help="Repository environment (stable/beta/test) for isolation",
)
@click.option(
    "--codename",
    required=True,
    help="Distribution codename (e.g., bookworm, noble, trixie)",
)
@click.option(
    "--component",
    help=(
        "Repository component (e.g., jethome-tools, jethome-armbian). "
        "Auto-generated from package-dir basename if not specified when "
        "using --environment."
    ),
)
@click.option(
    "--packages",
    multiple=True,
    type=click.Path(exists=True),
    help="Package files to add (can be specified multiple times)",
)
@click.option(
    "--package-dir",
    type=click.Path(exists=True, file_okay=False, dir_okay=True),
    help="Directory containing .deb packages (searched recursively)",
)
@click.option(
    "--force",
    is_flag=True,
    help="Create repository if it doesn't exist (even if auto_create is disabled)",
)
@click.pass_context
def add(  # noqa: C901
    ctx: click.Context,
    environment: Optional[str],
    codename: str,
    component: Optional[str],
    packages: tuple,
    package_dir: Optional[str],
    force: bool,
) -> None:
    """Add packages to repository with atomic snapshot publication.

    Supports environment isolation (stable/beta/test) and auto-generation
    of component names from directory basename.

    Examples:

        \b
        # Add to stable environment (auto-generate component from dir name)
        debrepomanager add --environment stable --codename bookworm \\
            --package-dir /path/to/armbian-bookworm/

        \b
        # Add to beta environment with explicit component
        debrepomanager add --environment beta --codename bookworm \\
            --component jethome-tools --package-dir /path/to/packages/

        \b
        # Traditional usage (no environment)
        debrepomanager add --codename bookworm --component jethome-tools \\
            --packages pkg1.deb --packages pkg2.deb

        \b
        # Add all packages from directory
        debrepomanager add --codename bookworm --component jethome-tools \\
            --package-dir /path/to/packages/

        \b
        # Force create repository if doesn't exist
        debrepomanager add --codename bookworm --component jethome-tools \\
            --package-dir /path/to/packages/ --force
    """
    config: Config = ctx.obj["config"]
    dry_run: bool = ctx.obj["dry_run"]
    verbose: bool = ctx.obj["verbose"]

    # Handle environment-based configuration
    if environment:
        # Set publish_prefix based on environment
        if environment == "stable":
            publish_prefix = ""
        elif environment == "beta":
            publish_prefix = "beta"
        elif environment == "test":
            publish_prefix = "test"
        else:
            raise click.ClickException(f"Invalid environment: {environment}")

        # Update config/context with publish_prefix
        os.environ["REPOMANAGER_PUBLISH_PREFIX"] = publish_prefix
        if verbose and publish_prefix:
            click.echo(f"Using publish prefix: {publish_prefix}")

        # Auto-generate component from package_dir if not specified
        if not component:
            if not package_dir:
                raise click.ClickException(
                    "--component is required when --environment is used "
                    "without --package-dir"
                )

            # Extract basename from package_dir
            component_base = os.path.basename(os.path.abspath(package_dir))

            # Add jethome- prefix if not already present
            if component_base.startswith("jethome-"):
                component = component_base
            else:
                component = f"jethome-{component_base}"

            if verbose:
                click.echo(f"Auto-generated component: {component}")

    # Validate component is set
    if not component:
        raise click.ClickException(
            "--component is required (or use --environment with --package-dir "
            "for auto-generation)"
        )

    # Collect package files
    try:
        pkg_files = _collect_package_files(packages, package_dir, verbose)
    except click.ClickException:
        raise

    if verbose:
        click.echo(f"Adding {len(pkg_files)} package(s) to {codename}/{component}")

    # Dry run mode
    if dry_run:
        click.echo("Dry-run mode: No changes will be made")
        click.echo(f"Would add {len(pkg_files)} package(s):")
        for pkg in pkg_files:
            click.echo(f"  - {Path(pkg).name}")
        return

    # Add packages
    try:
        manager = AptlyManager(config)
        _ensure_repo_exists(
            manager, codename, component, force, config.auto_create_repos
        )

        click.echo(f"Adding {len(pkg_files)} package(s)...")
        manager.add_packages(codename, component, pkg_files)

        click.echo("✓ Packages added successfully")
        click.echo(f"Repository: {codename}/{component}")

    except click.ClickException:
        raise
    except (AptlyError, ConfigError, FileNotFoundError, ValueError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Unexpected error: {e}", err=True)
        logger.exception("Unexpected error in add command")
        sys.exit(99)


@cli.command("create-repo")
@click.option(
    "--codename",
    required=True,
    help="Distribution codename (e.g., bookworm, noble)",
)
@click.option(
    "--component",
    required=True,
    help="Repository component (e.g., jethome-tools)",
)
@click.option(
    "--architectures",
    multiple=True,
    help="Architectures to support (default: from config)",
)
@click.option(
    "--force",
    is_flag=True,
    help="Recreate repository if it already exists",
)
@click.pass_context
def create_repo(
    ctx: click.Context,
    codename: str,
    component: str,
    architectures: tuple,
    force: bool,
) -> None:
    """Create new repository.

    Examples:

        \b
        # Create repository with default architectures
        repomanager create-repo --codename bookworm --component jethome-tools

        \b
        # Create with specific architectures
        repomanager create-repo --codename bookworm --component test \\
            --architectures amd64 --architectures arm64

        \b
        # Force recreate if exists
        repomanager create-repo --codename bookworm --component test --force
    """
    config: Config = ctx.obj["config"]
    dry_run: bool = ctx.obj["dry_run"]

    if dry_run:
        click.echo("Dry-run mode: No changes will be made")
        click.echo(f"Would create repository: {codename}/{component}")
        if architectures:
            click.echo(f"Architectures: {', '.join(architectures)}")
        return

    try:
        manager = AptlyManager(config)

        archs = list(architectures) if architectures else None

        if ctx.obj["verbose"]:
            click.echo(f"Creating repository {codename}/{component}...")

        manager.create_repo(codename, component, architectures=archs, force=force)

        click.echo(f"✓ Repository created: {codename}/{component}")

    except ValueError as e:
        # Repository already exists
        click.echo(f"Error: {e}", err=True)
        click.echo("Hint: Use --force to recreate", err=True)
        sys.exit(1)
    except (AptlyError, ConfigError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@cli.command("delete-repo")
@click.option(
    "--codename",
    required=True,
    help="Distribution codename",
)
@click.option(
    "--component",
    required=True,
    help="Repository component",
)
@click.option(
    "--confirm",
    "--yes",
    is_flag=True,
    help="Confirm deletion without prompt (required for safety)",
)
@click.pass_context
def delete_repo(
    ctx: click.Context,
    codename: str,
    component: str,
    confirm: bool,
) -> None:
    """Delete repository and all its snapshots.

    Examples:

        \b
        # Delete repository (requires --confirm or --yes)
        debrepomanager delete-repo --codename bookworm --component old-repo --confirm
        debrepomanager delete-repo --codename bullseye --component tools --yes
    """
    config: Config = ctx.obj["config"]
    dry_run: bool = ctx.obj["dry_run"]

    if dry_run:
        click.echo("Dry-run mode: No changes will be made")
        click.echo(f"Would delete repository: {codename}/{component}")
        return

    if not confirm:
        click.echo(
            "Error: Repository deletion requires --confirm (or --yes) flag", err=True
        )
        click.echo(f"To delete {codename}/{component}, run:", err=True)
        click.echo(
            f"  debrepomanager delete-repo --codename {codename} "
            f"--component {component} --yes",
            err=True,
        )
        sys.exit(1)

    try:
        manager = AptlyManager(config)

        # Check if repository exists BEFORE confirmation prompt
        if not manager.repo_exists(codename, component):
            click.echo(
                f"Error: Repository {codename}/{component} doesn't exist", err=True
            )
            sys.exit(1)

        # Additional confirmation prompt
        click.echo(f"⚠️  WARNING: This will delete repository {codename}/{component}")
        click.echo("⚠️  This action cannot be undone!")

        if not click.confirm("Are you sure you want to continue?"):
            click.echo("Cancelled.")
            sys.exit(0)

        click.echo(f"Deleting repository {codename}/{component}...")
        manager.delete_repo(codename, component)

        click.echo(f"✓ Repository deleted: {codename}/{component}")

    except (AptlyError, ConfigError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


def _list_packages_in_component(
    manager: AptlyManager, codename: str, component: str, stats: bool
) -> None:
    """Display packages in specific component."""
    if not manager.repo_exists(codename, component):
        click.echo(f"Repository {codename}/{component} doesn't exist", err=True)
        sys.exit(1)

    packages = manager.list_packages(codename, component)

    click.echo(f"Repository: {codename}/{component}")
    click.echo(f"Packages: {len(packages)}")

    if stats:
        repo_stats = manager.get_repo_stats(codename, component)
        size_mb = repo_stats["size_bytes"] / (1024 * 1024)
        click.echo(f"Size: {size_mb:.2f} MB")

    click.echo()

    if packages:
        for pkg in packages:
            click.echo(f"  {pkg}")
    else:
        click.echo("  (empty)")


def _display_repositories_grouped(
    manager: AptlyManager, codename: Optional[str], stats: bool
) -> None:
    """Display all repositories grouped by codename."""
    repo_list = manager.metadata.list_repositories(codename)

    if not repo_list:
        click.echo("No repositories found.")
        sys.exit(0)

    # Group repositories by codename
    repos_by_codename: dict = {}
    for repo in repo_list:
        cn = repo["codename"]
        comp = repo["component"]

        if cn not in repos_by_codename:
            repos_by_codename[cn] = []

        repos_by_codename[cn].append(comp)

    # Display results
    total_repos = len(repo_list)
    total_codenames = len(repos_by_codename)

    click.echo(
        f"Total: {total_repos} repository(ies) " f"across {total_codenames} codename(s)"
    )
    click.echo()

    # Show each codename and its components
    for cn in sorted(repos_by_codename.keys()):
        components = sorted(repos_by_codename[cn])
        click.echo(f"📦 {cn}:")

        for comp in components:
            if stats:
                try:
                    repo_stats = manager.get_repo_stats(cn, comp)
                    pkg_count = repo_stats["package_count"]
                    size_mb = repo_stats["size_bytes"] / (1024 * 1024)
                    click.echo(
                        f"   └─ {comp:30s} " f"({pkg_count} packages, {size_mb:.2f} MB)"
                    )
                except Exception as e:
                    logger.warning(f"Failed to get stats for {cn}/{comp}: {e}")
                    click.echo(f"   └─ {comp}")
            else:
                click.echo(f"   └─ {comp}")

        click.echo()


@cli.command("list")
@click.option(
    "--codename",
    help="Filter by distribution codename",
)
@click.option(
    "--component",
    help="Filter by component",
)
@click.option(
    "--stats",
    is_flag=True,
    help="Show package count and repository size",
)
@click.pass_context
def list_repos(
    ctx: click.Context,
    codename: Optional[str],
    component: Optional[str],
    stats: bool,
) -> None:
    """List repositories grouped by codename and component.

    Examples:

        \b
        # List all repositories grouped by codename
        debrepomanager list

        \b
        # List all repositories with package counts and sizes
        debrepomanager list --stats

        \b
        # List repos for specific codename
        debrepomanager list --codename bookworm

        \b
        # List packages in specific component
        debrepomanager list --codename bookworm --component jethome-tools
    """
    config: Config = ctx.obj["config"]

    try:
        manager = AptlyManager(config)

        if codename and component:
            _list_packages_in_component(manager, codename, component, stats)
        else:
            _display_repositories_grouped(manager, codename, stats)

    except (AptlyError, ConfigError, ValueError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@cli.command()
@click.pass_context
def sync(ctx: click.Context) -> None:
    """Sync metadata with actual repository state.

    Scans all aptly roots to find existing repositories and rebuilds
    metadata from scratch. Useful after manual aptly operations or
    to recover from metadata corruption.

    Example:
        debrepomanager sync
    """
    verbose: bool = ctx.obj["verbose"]
    config: Config = ctx.obj["config"]

    try:
        manager = AptlyManager(config)

        if verbose:
            click.echo("Syncing metadata with actual repository state...")

        count = manager.sync_metadata()

        click.echo(f"✓ Synced {count} repositories")

        if verbose:
            # Show synced repositories
            repos = manager.metadata.list_repositories()
            if repos:
                click.echo("\nRepositories:")
                for repo in repos:
                    click.echo(f"  {repo['codename']}/{repo['component']}")

    except (AptlyError, ConfigError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@cli.command()
@click.option(
    "--codename",
    required=True,
    help="Distribution codename (e.g., bookworm, noble)",
)
@click.option(
    "--component",
    required=True,
    help="Repository component (e.g., jethome-tools)",
)
@click.option(
    "--apply",
    is_flag=True,
    default=False,
    help="Actually remove packages (default is dry-run)",
)
@click.option("--verbose", "-v", is_flag=True, help="Verbose output")
@click.pass_context
def cleanup(  # noqa: C901
    ctx: click.Context,
    codename: str,
    component: str,
    apply: bool,
    verbose: bool,
) -> None:
    """Clean up old package versions based on retention policy.

    By default runs in DRY-RUN mode to show what would be removed.
    Use --apply to actually remove packages.

    The retention policy is configured in config.yaml:
    - min_versions: Minimum number of versions to keep (always preserved)
    - max_age_days: Maximum age in days (older packages removed if > min_versions)

    Examples:

        # Dry-run (show what would be removed):
        debrepomanager cleanup --codename bookworm --component jethome-tools

        # Actually remove packages:
        debrepomanager cleanup --codename bookworm --component jethome-tools --apply

        # With verbose output:
        debrepomanager cleanup --codename bookworm --component jethome-tools -v
    """
    from debrepomanager.retention import RetentionPolicy

    config: Config = ctx.obj["config"]
    # Setup logging with config level
    if verbose:
        setup_logging(level="DEBUG")
    else:
        setup_logging(level=config.logging_level)

    try:
        # Initialize managers
        aptly = AptlyManager(config)
        retention = RetentionPolicy(config, aptly)

        # Determine dry-run mode
        dry_run = not apply

        # Show mode
        if dry_run:
            click.echo("🔍 DRY RUN MODE - No packages will be removed\n")
        else:
            click.secho(
                "⚠️  APPLY MODE - Packages will be permanently removed!",
                fg="yellow",
                bold=True,
            )
            click.echo()

        # Get retention policy for component
        policy = retention.get_policy(component)
        click.echo(f"Retention Policy for {component}:")
        click.echo(f"  - Min versions to keep: {policy.get('min_versions', 'N/A')}")
        click.echo(f"  - Max age (days): {policy.get('max_age_days', 'N/A')}")
        click.echo()

        # Perform cleanup
        click.echo(f"Analyzing repository {codename}/{component}...")
        result = retention.cleanup(codename, component, dry_run=dry_run)

        # Display results
        click.echo()
        click.secho("📊 Cleanup Report:", bold=True)
        click.echo(f"  Packages analyzed: {result['analyzed']}")
        click.echo(f"  Packages to remove: {result['to_remove']}")

        if result["removed"] > 0:
            click.secho(
                f"  Packages removed: {result['removed']}", fg="green", bold=True
            )
        elif not dry_run and result["to_remove"] > 0:
            click.secho("  Packages removed: 0 (failed)", fg="red")

        click.echo(f"  Estimated space to free: ~{result['space_mb']} MB")

        # Show package list if verbose or dry-run
        if (verbose or dry_run) and result["packages"]:
            click.echo()
            click.echo("Packages to remove:")
            for pkg in result["packages"][:20]:  # Show first 20
                click.echo(f"  - {pkg}")
            if len(result["packages"]) > 20:
                click.echo(f"  ... and {len(result['packages']) - 20} more")

        # Final message
        click.echo()
        if dry_run and result["to_remove"] > 0:
            click.secho(
                "✨ Run with --apply to actually remove these packages",
                fg="cyan",
                bold=True,
            )
        elif not dry_run and result["removed"] > 0:
            click.secho("✅ Cleanup completed successfully!", fg="green", bold=True)
        elif result["to_remove"] == 0:
            click.secho("✨ No packages to remove - repository is clean!", fg="green")

    except (AptlyError, ConfigError, ValueError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@cli.group()
def release() -> None:
    """Manage distribution releases (add/remove)."""
    pass


@release.command("add")
@click.argument("codename")
@click.option(
    "--components",
    multiple=True,
    help=(
        "Components to create "
        "(default: jethome-tools jethome-{codename} jethome-armbian)"
    ),
)
@click.option(
    "--skip-symlinks",
    is_flag=True,
    help="Skip dual format symlinks creation",
)
@click.pass_context
def release_add(  # noqa: C901
    ctx: click.Context,
    codename: str,
    components: tuple,
    skip_symlinks: bool,
) -> None:
    """Add new distribution release.

    Creates repositories for all specified components and optionally
    creates dual format symlinks for backward compatibility.

    Examples:

        \b
        # Add release with default components
        debrepomanager release add trixie

        \b
        # Add with specific components
        debrepomanager release add trixie \\
            --components jethome-tools --components jethome-trixie

        \b
        # Skip symlink creation
        debrepomanager release add oracular --skip-symlinks
    """
    config: Config = ctx.obj["config"]
    dry_run: bool = ctx.obj["dry_run"]
    verbose: bool = ctx.obj["verbose"]

    # Convert components tuple to list, use None if empty
    components_list = list(components) if components else None

    if dry_run:
        click.echo("Dry-run mode: No changes will be made")
        click.echo(f"Would create release: {codename}")
        if components_list:
            click.echo(f"Components: {', '.join(components_list)}")
        else:
            click.echo(
                f"Components: jethome-tools, jethome-{codename}, "
                "jethome-armbian (default)"
            )
        return

    try:
        manager = AptlyManager(config)
        release_mgr = ReleaseManager(config, manager)

        if verbose:
            click.echo(f"Creating release: {codename}")
            if components_list:
                click.echo(f"Components: {', '.join(components_list)}")

        result = release_mgr.add_release(
            codename=codename,
            components=components_list,
            skip_symlinks=skip_symlinks,
        )

        # Display results
        click.echo()
        click.echo(f"✓ Release {codename} created")
        click.echo(f"  Repositories created: {result['created']}")

        if result["failed"] > 0:
            click.secho(f"  Failed: {result['failed']}", fg="red")
            for failure in result["failures"]:
                click.echo(f"    - {failure['component']}: {failure['error']}")

        if verbose and result["components"]:
            click.echo()
            click.echo("Created components:")
            for comp in result["components"]:
                click.echo(f"  - {comp}")

        # Next steps
        click.echo()
        click.secho("📋 Next steps:", fg="blue", bold=True)
        click.echo()
        click.echo("1. Add packages:")
        click.echo(
            f"   debrepomanager add --codename {codename} "
            "--component <component> --package-dir <dir>"
        )
        click.echo()
        click.echo("2. Update nginx codenames (if configured):")
        click.echo("   ./scripts/update-nginx-codenames.sh")
        click.echo("   sudo systemctl reload nginx")
        click.echo()
        click.echo("3. Verify:")
        click.echo(f"   debrepomanager list --codename {codename}")

    except (AptlyError, ConfigError, ReleaseError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Unexpected error: {e}", err=True)
        logger.exception("Unexpected error in release add command")
        sys.exit(99)


@release.command("remove")
@click.argument("codename")
@click.option(
    "--backup/--no-backup",
    default=True,
    help="Create backup before removal (default: yes)",
)
@click.option(
    "--force",
    is_flag=True,
    help="Skip activity check",
)
@click.option(
    "--confirm",
    "--yes",
    is_flag=True,
    help="Confirm deletion without prompt (required for safety)",
)
@click.option(
    "--backup-dir",
    default="/backup",
    help="Directory for backups (default: /backup)",
)
@click.pass_context
def release_remove(  # noqa: C901
    ctx: click.Context,
    codename: str,
    backup: bool,
    force: bool,
    confirm: bool,
    backup_dir: str,
) -> None:
    """Remove old distribution release.

    Removes all repositories for specified codename, optionally creating
    backup before removal. Can check activity in nginx logs before removal.

    WARNING: This is destructive and cannot be undone (unless backed up)!

    Examples:

        \b
        # Remove with backup (safe)
        debrepomanager release remove bullseye --confirm

        \b
        # Remove without backup
        debrepomanager release remove jammy --no-backup --confirm

        \b
        # Force removal (skip activity check)
        debrepomanager release remove bullseye --force --confirm
    """
    config: Config = ctx.obj["config"]
    dry_run: bool = ctx.obj["dry_run"]

    if dry_run:
        click.echo("Dry-run mode: No changes will be made")
        click.echo(f"Would remove release: {codename}")
        click.echo(f"Backup: {'yes' if backup else 'no'}")
        return

    if not confirm:
        click.echo(
            "Error: Release removal requires --confirm (or --yes) flag", err=True
        )
        click.echo(f"To remove {codename}, run:", err=True)
        click.echo(
            f"  debrepomanager release remove {codename} --confirm",
            err=True,
        )
        sys.exit(1)

    try:
        manager = AptlyManager(config)
        release_mgr = ReleaseManager(config, manager)

        # Check if release exists
        repos = manager.list_repos(codename)
        if not repos:
            click.echo(f"Error: No repositories found for {codename}", err=True)
            sys.exit(1)

        # Show warning
        click.echo(f"⚠️  WARNING: This will delete release {codename}")
        click.echo(f"⚠️  Repositories to delete: {len(repos)}")
        for repo in repos[:5]:  # Show first 5
            click.echo(f"    - {repo}")
        if len(repos) > 5:
            click.echo(f"    ... and {len(repos) - 5} more")
        click.echo()

        # Additional confirmation prompt (only in interactive mode)
        if sys.stdin.isatty():
            typed = click.prompt(f"Type '{codename}' to confirm")
            if typed != codename:
                click.echo("Cancelled.")
                sys.exit(0)
        else:
            # Non-interactive mode: --confirm flag is sufficient
            pass

        click.echo()
        click.echo(f"Removing release {codename}...")

        result = release_mgr.remove_release(
            codename=codename,
            backup=backup,
            force=force,
            backup_dir=backup_dir,
        )

        # Display results
        click.echo()
        click.secho("📊 Removal Results:", bold=True)
        click.echo(f"  Repositories deleted: {result['deleted']}")

        if result["failed"] > 0:
            click.secho(f"  Failed: {result['failed']}", fg="red")
            for failure in result.get("failures", []):
                click.echo(f"    - {failure['component']}: {failure['error']}")

        if result.get("backup_file"):
            click.echo(f"  Backup saved: {result['backup_file']}")

        if result.get("activity", 0) > 0:
            click.echo(f"  Activity in logs: {result['activity']} requests")

        # Success
        click.echo()
        if result["failed"] == 0:
            click.secho(
                f"✅ Release {codename} removed successfully!", fg="green", bold=True
            )
        else:
            click.secho(
                f"⚠️  Release {codename} removed with errors", fg="yellow", bold=True
            )

        # Next steps
        click.echo()
        click.secho("📋 Next steps:", fg="blue", bold=True)
        click.echo()
        click.echo("1. Update nginx codenames (if configured):")
        click.echo("   ./scripts/update-nginx-codenames.sh")
        click.echo("   sudo systemctl reload nginx")
        click.echo()
        click.echo("2. Update client documentation")
        click.echo()

        if result.get("backup_file"):
            click.echo("3. Backup saved to:")
            click.echo(f"   {result['backup_file']}")
            click.echo("   Restore with: sudo tar xzf <backup> -C /")

    except (AptlyError, ConfigError, ReleaseError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Unexpected error: {e}", err=True)
        logger.exception("Unexpected error in release remove command")
        sys.exit(99)


def main() -> None:
    """Main entry point for CLI."""
    cli(obj={})


if __name__ == "__main__":
    main()


@cli.command("rotate-gpg-key")
@click.option(
    "--new-key-id",
    required=True,
    help="New GPG key ID to rotate to",
)
@click.option(
    "--verify-only",
    is_flag=True,
    default=False,
    help="Only verify new key without rotating",
)
@click.option(
    "--grace-period",
    is_flag=True,
    default=False,
    help="Enable grace period (both old and new keys valid)",
)
@click.option(
    "--rollback",
    is_flag=True,
    default=False,
    help="Rollback to old key (use with --old-key-id)",
)
@click.option(
    "--old-key-id",
    help="Old key ID for rollback",
)
@click.pass_context
def rotate_gpg_key(  # noqa: C901
    ctx: click.Context,
    new_key_id: str,
    verify_only: bool,
    grace_period: bool,
    rollback: bool,
    old_key_id: Optional[str],
) -> None:
    """Rotate GPG key for all repositories with zero downtime.

    This command performs GPG key rotation for all published repositories.
    By default, all repositories are re-signed with the new key.

    Examples:
        # Verify new key is available
        debrepomanager rotate-gpg-key --new-key-id NEWKEY123 --verify-only

        # Rotate with grace period (both keys valid)
        debrepomanager rotate-gpg-key --new-key-id NEWKEY123 --grace-period

        # Full rotation (new key only)
        debrepomanager rotate-gpg-key --new-key-id NEWKEY123

        # Rollback to old key
        debrepomanager rotate-gpg-key --rollback --old-key-id OLDKEY456
    """
    from debrepomanager.gpg_rotation import GPGRotationError, GPGRotationManager

    config: Config = ctx.obj["config"]

    try:
        # Initialize managers
        aptly = AptlyManager(config)
        rotation = GPGRotationManager(config, aptly)

        # Handle rollback
        if rollback:
            if not old_key_id:
                click.echo("Error: --old-key-id required for rollback", err=True)
                sys.exit(1)

            click.secho(
                f"⚠️  ROLLBACK MODE - Reverting to key {old_key_id}",
                fg="yellow",
                bold=True,
            )
            click.echo()

            if not click.confirm("Continue with rollback?"):
                click.echo("Rollback cancelled")
                return

            result = rotation.rollback_rotation(old_key_id)

            click.echo()
            click.echo("Rollback Results:")
            click.echo(f"  Total repositories: {result['total']}")
            click.secho(f"  Success: {result['success']}", fg="green")
            click.secho(
                f"  Failed: {result['failed']}",
                fg="red" if result["failed"] > 0 else "green",
            )
            click.echo(f"  Skipped: {result['skipped']}")

            if result["failed"] > 0:
                click.echo("\nFailed repositories:")
            for failure in result.get("failures", []):
                click.echo(
                    f"  - {failure['codename']}/{failure['component']}: "
                    f"{failure['error']}"
                )
                sys.exit(1)

            click.secho("\n✅ Rollback completed successfully!", fg="green", bold=True)
            return

        # Verify new key
        click.echo(f"Validating GPG key: {new_key_id}...")

        try:
            rotation.validate_new_key(new_key_id)
            click.secho(f"✓ Key {new_key_id} is valid and available", fg="green")
        except GPGRotationError as e:
            click.echo(f"Error: {e}", err=True)
            sys.exit(1)

        if verify_only:
            click.echo("\n✓ Verification complete. Key is ready for rotation.")
            return

        # Show rotation plan
        repos = rotation.get_all_published_repos()
        click.echo("\n📋 Rotation Plan:")
        click.echo(f"  Repositories to rotate: {len(repos)}")
        click.echo(f"  New GPG key: {new_key_id}")
        click.echo(f"  Grace period: {'Yes' if grace_period else 'No'}")
        click.echo()

        # Confirmation
        click.secho(
            "⚠️  This will re-sign all repositories with the new key!",
            fg="yellow",
            bold=True,
        )
        if not click.confirm("Continue with key rotation?"):
            click.echo("Rotation cancelled")
            return

        # Perform rotation
        click.echo("\n🔄 Starting GPG key rotation...")
        click.echo()

        result = rotation.rotate_all_repos(new_key_id, grace_period=grace_period)

        # Display results
        click.echo()
        click.secho("📊 Rotation Results:", bold=True)
        click.echo(f"  Total repositories: {result['total']}")
        click.secho(f"  Success: {result['success']}", fg="green")

        if result["failed"] > 0:
            click.secho(f"  Failed: {result['failed']}", fg="red")
        else:
            click.secho(f"  Failed: {result['failed']}", fg="green")

        click.echo(f"  Skipped: {result['skipped']}")

        if result["failures"]:
            click.echo("\n❌ Failed repositories:")
            for failure in result["failures"]:
                click.echo(
                    f"  - {failure['codename']}/{failure['component']}: "
                    f"{failure['error']}"
                )

        # Grace period notice
        if grace_period and result["success"] > 0:
            click.echo()
            click.secho(
                "⚠️  GRACE PERIOD ENABLED",
                fg="yellow",
                bold=True,
            )
            click.echo("Both old and new keys are valid.")
            click.echo("Remember to:")
            click.echo("  1. Communicate key change to users")
            click.echo("  2. Provide migration instructions")
            click.echo("  3. Remove old key after grace period")

        # Success
        click.echo()
        if result["failed"] == 0:
            click.secho(
                "✅ GPG key rotation completed successfully!", fg="green", bold=True
            )

            # Verification
            click.echo("\nVerifying rotation...")
            verify_result = rotation.verify_rotation(new_key_id)
            click.echo(
                f"  Verified: {verify_result['correct']}/"
                f"{verify_result['total']} repositories"
            )

        else:
            click.secho(
                f"⚠️  Rotation completed with {result['failed']} errors",
                fg="yellow",
                bold=True,
            )
            click.echo("\nConsider:")
            click.echo("  1. Check failed repositories manually")
            click.echo(
                f"  2. Rollback if needed: --rollback --old-key-id {config.gpg_key_id}"
            )
            sys.exit(1)

    except (GPGRotationError, AptlyError, ConfigError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@cli.command("fix-symlinks")
@click.option(
    "--codename",
    help="Specific codename to fix (all if not specified)",
)
@click.option(
    "--component",
    help="Specific component to fix (all if not specified)",
)
@click.option(
    "--check-only",
    is_flag=True,
    help="Check symlinks without creating/updating them",
)
@click.pass_context
def fix_symlinks_cmd(  # noqa: C901
    ctx: click.Context,
    codename: Optional[str],
    component: Optional[str],
    check_only: bool,
) -> None:
    """Check and fix dual format symlinks.

    Scans published repositories and creates/updates symlinks for old format access.

    Examples:
        # Check all symlinks
        debrepomanager fix-symlinks --check-only

        # Fix all symlinks
        debrepomanager fix-symlinks

        # Fix specific codename
        debrepomanager fix-symlinks --codename trixie

        # Fix specific repository
        debrepomanager fix-symlinks --codename trixie --component jethome-tools
    """
    try:
        config: Config = ctx.obj["config"]

        if not config.dual_format_enabled:
            click.secho(
                "⚠️  Dual format support is disabled in config", fg="yellow", bold=True
            )
            click.echo("\nTo enable, add to config.yaml:")
            click.echo("repositories:")
            click.echo("  dual_format:")
            click.echo("    enabled: true")
            click.echo("    auto_symlink: true")
            sys.exit(1)

        publish_base = Path(config.publish_base)

        if not publish_base.exists():
            click.secho(
                f"❌ Publish base doesn't exist: {publish_base}", fg="red", bold=True
            )
            sys.exit(1)

        click.echo(f"📁 Publish base: {publish_base}")
        click.echo(f"🔧 Mode: {'CHECK ONLY' if check_only else 'FIX'}")
        click.echo()

        # Scan for published repositories
        repos_found = []

        # If specific codename/component provided
        if codename and component:
            repos_to_check = [(codename, component)]
        elif codename:
            # Find all components for this codename
            # Look in {codename}/dists/{component}/ directories
            codename_dir = publish_base / codename
            if codename_dir.exists() and (codename_dir / "dists").exists():
                dists_dir = codename_dir / "dists"
                repos_to_check = [
                    (codename, comp_dir.name)
                    for comp_dir in dists_dir.iterdir()
                    if comp_dir.is_dir() and (comp_dir / "Release").exists()
                ]
            else:
                repos_to_check = []
        else:
            # Scan all codenames and components
            # Look for pattern: {codename}/dists/{component}/Release
            repos_to_check = []
            for codename_dir in publish_base.iterdir():
                if not codename_dir.is_dir():
                    continue
                if codename_dir.name == "dists":
                    continue  # Skip symlink directory

                # Check if this is a codename with dists subdirectory
                dists_dir = codename_dir / "dists"
                if dists_dir.exists() and dists_dir.is_dir():
                    for comp_dir in dists_dir.iterdir():
                        if comp_dir.is_dir() and (comp_dir / "Release").exists():
                            repos_to_check.append((codename_dir.name, comp_dir.name))

        if not repos_to_check:
            click.secho("⚠️  No published repositories found", fg="yellow")
            sys.exit(0)

        # Check/fix each repository
        ok_count = 0
        created_count = 0
        updated_count = 0
        error_count = 0

        for cn, comp in repos_to_check:
            # Expected paths (when publish_prefix is empty)
            # aptly publishes to: /opt/repo/public/{codename}/dists/{component}/
            new_path = publish_base / cn / "dists" / comp
            old_path = publish_base / "dists" / cn / comp

            status = "  "
            msg = ""

            # Check if new format path exists
            if not new_path.exists():
                status = "⚠️ "
                msg = "new format path missing"
                error_count += 1
            elif old_path.exists() and old_path.is_symlink():
                # Symlink exists, check if it's correct
                try:
                    target = os.readlink(old_path)
                    expected_rel = os.path.relpath(new_path, old_path.parent)
                    if target == expected_rel:
                        status = "✅"
                        msg = "symlink OK"
                        ok_count += 1
                    else:
                        status = "⚠️ "
                        msg = f"wrong target: {target}"
                        if not check_only:
                            # Fix it
                            old_path.unlink()
                            os.symlink(expected_rel, old_path)
                            status = "🔧"
                            msg = "symlink updated"
                            updated_count += 1
                        else:
                            error_count += 1
                except OSError as e:
                    status = "❌"
                    msg = f"error: {e}"
                    error_count += 1
            elif old_path.exists():
                # Path exists but is not a symlink
                status = "⚠️ "
                msg = "path exists (not symlink)"
                error_count += 1
            else:
                # Symlink missing
                if not check_only:
                    try:
                        old_path.parent.mkdir(parents=True, exist_ok=True)
                        rel_path = os.path.relpath(new_path, old_path.parent)
                        os.symlink(rel_path, old_path)
                        status = "✨"
                        msg = "symlink created"
                        created_count += 1

                    except OSError as e:
                        status = "❌"
                        msg = f"error: {e}"
                        error_count += 1
                else:
                    status = "⚠️ "
                    msg = "symlink missing"
                    error_count += 1

            click.echo(f"{status} {cn}/{comp:<30} {msg}")
            repos_found.append((cn, comp, status, msg))

        # Summary
        click.echo()
        click.echo("=" * 60)
        click.echo(f"Total repositories: {len(repos_found)}")

        if check_only:
            click.echo(f"  ✅ OK: {ok_count}")
            if error_count > 0:
                click.secho(f"  ⚠️  Need fixing: {error_count}", fg="yellow")
        else:
            click.echo(f"  ✅ OK: {ok_count}")
            if created_count > 0:
                click.secho(f"  ✨ Created: {created_count}", fg="green")
            if updated_count > 0:
                click.secho(f"  🔧 Updated: {updated_count}", fg="blue")
            if error_count > 0:
                click.secho(f"  ❌ Errors: {error_count}", fg="red")

        if check_only and (created_count + updated_count + error_count) > 0:
            click.echo()
            click.echo("Run without --check-only to fix the issues")

        if error_count > 0:
            sys.exit(1)

    except (ConfigError, AptlyError) as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)
