"""CLI main entry point."""

import atexit
import json
import os
import shutil
import sys
import tempfile
from datetime import UTC
from pathlib import Path
from typing import Any

import click

from ... import __version__
from ...adapters import (
    NoopMetricsAdapter,
    S3StorageAdapter,
    Sha256Adapter,
    StdLoggerAdapter,
    UtcClockAdapter,
    XdeltaAdapter,
)
from ...core import DeltaService, ObjectKey
from ...core.config import DeltaGliderConfig
from ...ports import MetricsPort
from ...ports.cache import CachePort
from .aws_compat import (
    copy_s3_to_s3,
    determine_operation,
    download_file,
    handle_recursive,
    is_s3_path,
    parse_s3_url,
    upload_file,
)
from .sync import sync_from_s3, sync_to_s3


def create_service(
    log_level: str = "INFO",
    endpoint_url: str | None = None,
    region: str | None = None,
    profile: str | None = None,
    *,
    config: DeltaGliderConfig | None = None,
) -> DeltaService:
    """Create service with wired adapters.

    Args:
        log_level: Logging level (overridden by config.log_level if config provided).
        endpoint_url: S3 endpoint URL (overridden by config if provided).
        region: AWS region (overridden by config if provided).
        profile: AWS profile (overridden by config if provided).
        config: Optional pre-built config. If None, built from env vars + explicit params.
    """
    if config is None:
        config = DeltaGliderConfig.from_env(
            log_level=log_level,
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    # SECURITY: Always use ephemeral process-isolated cache
    cache_dir = Path(tempfile.mkdtemp(prefix="deltaglider-", dir="/tmp"))
    # Register cleanup handler to remove cache on exit
    atexit.register(lambda: shutil.rmtree(cache_dir, ignore_errors=True))

    # Set AWS environment variables if provided (for compatibility with other AWS tools)
    if config.endpoint_url:
        os.environ["AWS_ENDPOINT_URL"] = config.endpoint_url
    if config.region:
        os.environ["AWS_DEFAULT_REGION"] = config.region
    if config.profile:
        os.environ["AWS_PROFILE"] = config.profile

    # Build boto3_kwargs for explicit parameter passing (preferred over env vars)
    boto3_kwargs: dict[str, Any] = {}
    if config.region:
        boto3_kwargs["region_name"] = config.region

    # Create adapters
    hasher = Sha256Adapter()
    storage = S3StorageAdapter(endpoint_url=config.endpoint_url, boto3_kwargs=boto3_kwargs)
    diff = XdeltaAdapter()

    # SECURITY: Configurable cache with encryption and backend selection
    from deltaglider.adapters import ContentAddressedCache, EncryptedCache, MemoryCache

    base_cache: CachePort
    if config.cache_backend == "memory":
        base_cache = MemoryCache(
            hasher, max_size_mb=config.cache_memory_size_mb, temp_dir=cache_dir
        )
    else:
        base_cache = ContentAddressedCache(cache_dir, hasher)

    # Always apply encryption with ephemeral keys (security hardening)
    cache: CachePort = EncryptedCache.from_env(base_cache)

    clock = UtcClockAdapter()
    logger = StdLoggerAdapter(level=config.log_level)

    # Create metrics adapter based on configuration
    metrics: MetricsPort
    if config.metrics_type == "cloudwatch":
        from ...adapters.metrics_cloudwatch import CloudWatchMetricsAdapter

        metrics = CloudWatchMetricsAdapter(
            namespace=config.metrics_namespace,
            region=config.region,
            endpoint_url=(
                config.endpoint_url
                if config.endpoint_url and "localhost" in config.endpoint_url
                else None
            ),
        )
    elif config.metrics_type == "logging":
        from ...adapters.metrics_cloudwatch import LoggingMetricsAdapter

        metrics = LoggingMetricsAdapter(log_level=config.log_level)
    else:
        metrics = NoopMetricsAdapter()

    return DeltaService(
        storage=storage,
        diff=diff,
        hasher=hasher,
        cache=cache,
        clock=clock,
        logger=logger,
        metrics=metrics,
        max_ratio=config.max_ratio,
    )


def _version_callback(ctx: click.Context, param: click.Parameter, value: bool) -> None:
    """Callback for --version option."""
    if value:
        click.echo(f"deltaglider {__version__}")
        ctx.exit(0)


# Deprecation banner shown at the top of every CLI invocation (stderr).
# Suppressed by setting DG_SUPPRESS_DEPRECATION=1 in the environment — for
# CI/automation that hasn't migrated yet and doesn't want noise on every
# run. Print only once per process; some commands (e.g. `migrate`) invoke
# `cli` callbacks transitively.
_DEPRECATION_PRINTED = False


def _print_deprecation_notice() -> None:
    global _DEPRECATION_PRINTED
    if _DEPRECATION_PRINTED:
        return
    _DEPRECATION_PRINTED = True
    if os.environ.get("DG_SUPPRESS_DEPRECATION") == "1":
        return
    click.echo(
        "═════════════════════════════════════════════════════════════════\n"
        " deltaglider (Python) is DEPRECATED as of v6.2.0.\n"
        "\n"
        " The canonical implementation is now `deltaglider_proxy`, a single\n"
        " Rust binary that ships the S3-compatible proxy, the CLI, and the\n"
        " web UI. Wire format is byte-identical with this tool.\n"
        "\n"
        "   brew install beshu-tech/tap/deltaglider_proxy   # or download:\n"
        "   https://github.com/beshu-tech/deltaglider_proxy/releases\n"
        "\n"
        " Then:\n"
        "   alias dg='deltaglider_proxy s3'\n"
        "   dg cp s3://bucket/key .\n"
        "\n"
        " Suppress this notice: DG_SUPPRESS_DEPRECATION=1\n"
        "═════════════════════════════════════════════════════════════════",
        err=True,
    )


@click.group()
@click.option("--debug", is_flag=True, help="Enable debug logging")
@click.option(
    "--version",
    is_flag=True,
    is_eager=True,
    expose_value=False,
    callback=_version_callback,
    help="Show version and exit",
)
@click.pass_context
def cli(ctx: click.Context, debug: bool) -> None:
    """DeltaGlider - Delta-aware S3 file storage wrapper.

    DEPRECATED as of v6.2.0. Migrate to `deltaglider_proxy` — see
    https://github.com/beshu-tech/deltaglider_proxy
    """
    import logging

    _print_deprecation_notice()
    log_level = "DEBUG" if debug else os.environ.get("DG_LOG_LEVEL", "INFO")
    ctx.obj = create_service(log_level)
    logging.getLogger("deltaglider").info("deltaglider %s", __version__)


@cli.command()
@click.argument("source")
@click.argument("dest")
@click.option("--recursive", "-r", is_flag=True, help="Copy files recursively")
@click.option("--exclude", help="Exclude files matching pattern")
@click.option("--include", help="Include only files matching pattern")
@click.option("--quiet", "-q", is_flag=True, help="Suppress output")
@click.option("--no-delta", is_flag=True, help="Disable delta compression")
@click.option("--max-ratio", type=float, help="Max delta/file ratio (default: 0.5)")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def cp(
    service: DeltaService,
    source: str,
    dest: str,
    recursive: bool,
    exclude: str | None,
    include: str | None,
    quiet: bool,
    no_delta: bool,
    max_ratio: float | None,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Copy files to/from S3 (AWS S3 compatible).

    Examples:
        deltaglider cp myfile.zip s3://bucket/path/
        deltaglider cp s3://bucket/file.zip ./
        deltaglider cp -r local_dir/ s3://bucket/path/
        deltaglider cp s3://bucket1/file s3://bucket2/file
    """
    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        # Determine operation type
        operation = determine_operation(source, dest)

        # Handle recursive operations for directories
        if recursive:
            handle_recursive(
                service, source, dest, recursive, exclude, include, quiet, no_delta, max_ratio
            )
            return

        # Handle single file operations
        if operation == "upload":
            local_path = Path(source)
            if not local_path.exists():
                click.echo(f"Error: File not found: {source}", err=True)
                sys.exit(1)
            upload_file(service, local_path, dest, max_ratio, no_delta, quiet)

        elif operation == "download":
            # Determine local path
            local_path = None
            if dest != ".":
                local_path = Path(dest)
            download_file(service, source, local_path, quiet)

        elif operation == "copy":
            copy_s3_to_s3(service, source, dest, quiet, max_ratio, no_delta)

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


@cli.command()
@click.argument("s3_url", required=False)
@click.option("--recursive", "-r", is_flag=True, help="List recursively")
@click.option("--human-readable", "-h", is_flag=True, help="Human readable sizes")
@click.option("--summarize", is_flag=True, help="Display summary information")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def ls(
    service: DeltaService,
    s3_url: str | None,
    recursive: bool,
    human_readable: bool,
    summarize: bool,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """List S3 buckets or objects (AWS S3 compatible).

    Examples:
        deltaglider ls                           # List all buckets
        deltaglider ls s3://bucket/               # List objects in bucket
        deltaglider ls s3://bucket/prefix/        # List objects with prefix
        deltaglider ls -r s3://bucket/            # List recursively
        deltaglider ls -h s3://bucket/            # Human readable sizes
    """
    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        if not s3_url:
            # List all buckets
            import boto3

            s3_client = boto3.client(
                "s3",
                endpoint_url=endpoint_url or os.environ.get("AWS_ENDPOINT_URL"),
            )
            response = s3_client.list_buckets()
            for bucket in response.get("Buckets", []):
                click.echo(
                    f"{bucket['CreationDate'].strftime('%Y-%m-%d %H:%M:%S')}  s3://{bucket['Name']}"
                )

        else:
            # List objects in bucket/prefix
            bucket_name: str
            prefix_str: str
            bucket_name, prefix_str = parse_s3_url(s3_url)

            # Ensure prefix ends with / if it's meant to be a directory
            # This helps with proper path handling
            if prefix_str and not prefix_str.endswith("/"):
                # Check if this is a file or directory by listing
                # For now, assume it's a directory prefix
                prefix_str = prefix_str + "/"

            # Format bytes to human readable
            def format_bytes(size: int) -> str:
                if not human_readable:
                    return str(size)
                size_float = float(size)
                for unit in ["B", "K", "M", "G", "T"]:
                    if size_float < 1024.0:
                        return f"{size_float:6.1f}{unit}"
                    size_float /= 1024.0
                return f"{size_float:.1f}P"

            # List objects using SDK (automatically filters .delta and reference.bin)
            from deltaglider.client import DeltaGliderClient

            client = DeltaGliderClient(service)
            dg_response = client.list_objects(
                Bucket=bucket_name,
                Prefix=prefix_str,
                MaxKeys=10000,
                Delimiter="/" if not recursive else "",
            )
            objects = dg_response["Contents"]

            # Filter by recursive flag
            if not recursive:
                # Show common prefixes (subdirectories) from S3 response
                for common_prefix in dg_response.get("CommonPrefixes", []):
                    prefix_path = common_prefix.get("Prefix", "")
                    # Show only the directory name, not the full path
                    if prefix_str:
                        # Strip the current prefix to show only the subdirectory
                        display_name = prefix_path[len(prefix_str) :]
                    else:
                        display_name = prefix_path
                    click.echo(f"                           PRE {display_name}")

                # Only show files at current level (not in subdirectories)
                filtered_objects = []
                for obj in objects:
                    obj_key = obj["Key"]
                    rel_path = obj_key[len(prefix_str) :] if prefix_str else obj_key
                    # Only include if it's a direct child (no / in relative path)
                    if "/" not in rel_path and rel_path:
                        filtered_objects.append(obj)
                objects = filtered_objects

            # Display objects (SDK already filters reference.bin and strips .delta)
            total_size = 0
            total_count = 0

            for obj in objects:
                total_size += obj["Size"]
                total_count += 1

                # Format the display
                size_str = format_bytes(obj["Size"])
                # last_modified is a string from SDK, parse it if needed
                last_modified = obj.get("LastModified", "")
                if isinstance(last_modified, str):
                    # Already a string, extract date portion
                    date_str = last_modified[:19].replace("T", " ")
                else:
                    date_str = last_modified.strftime("%Y-%m-%d %H:%M:%S")

                # Show only the filename relative to current prefix (like AWS CLI)
                if prefix_str:
                    display_key = obj["Key"][len(prefix_str) :]
                else:
                    display_key = obj["Key"]

                click.echo(f"{date_str} {size_str:>10} {display_key}")

            # Show summary if requested
            if summarize:
                click.echo("")
                click.echo(f"Total Objects: {total_count}")
                click.echo(f"   Total Size: {format_bytes(total_size)}")

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


@cli.command()
@click.argument("s3_url")
@click.option("--recursive", "-r", is_flag=True, help="Remove recursively")
@click.option("--dryrun", is_flag=True, help="Show what would be deleted without deleting")
@click.option("--quiet", "-q", is_flag=True, help="Suppress output")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def rm(
    service: DeltaService,
    s3_url: str,
    recursive: bool,
    dryrun: bool,
    quiet: bool,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Remove S3 objects (AWS S3 compatible).

    Examples:
        deltaglider rm s3://bucket/file.zip           # Remove single file
        deltaglider rm -r s3://bucket/prefix/         # Remove recursively
        deltaglider rm --dryrun s3://bucket/file     # Preview what would be deleted
    """
    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        bucket, prefix = parse_s3_url(s3_url)

        # Check if this is a single object or prefix
        if not recursive and not prefix.endswith("/"):
            # Single object deletion
            objects_to_delete = []

            # Check for the object itself
            obj_key = prefix
            obj = service.storage.head(f"{bucket}/{obj_key}")
            if obj:
                objects_to_delete.append(obj_key)

            # Check for .delta version
            if not obj_key.endswith(".delta"):
                delta_key = f"{obj_key}.delta"
                delta_obj = service.storage.head(f"{bucket}/{delta_key}")
                if delta_obj:
                    objects_to_delete.append(delta_key)

            # Check for reference.bin in the same deltaspace
            if "/" in obj_key:
                deltaspace_prefix = "/".join(obj_key.split("/")[:-1])
                ref_key = f"{deltaspace_prefix}/reference.bin"
            else:
                ref_key = "reference.bin"

            # Only delete reference.bin if it's the last file in the deltaspace
            ref_obj = service.storage.head(f"{bucket}/{ref_key}")
            if ref_obj:
                # Check if there are other files in this deltaspace
                list_prefix = f"{bucket}/{deltaspace_prefix}" if "/" in obj_key else bucket
                other_files = list(service.storage.list(list_prefix))
                # Count files excluding reference.bin
                non_ref_files = [o for o in other_files if not o.key.endswith("/reference.bin")]
                if len(non_ref_files) <= len(objects_to_delete):
                    # This would be the last file(s), safe to delete reference.bin
                    objects_to_delete.append(ref_key)

            if not objects_to_delete:
                if not quiet:
                    click.echo(f"delete: Object not found: s3://{bucket}/{obj_key}")
                return

            # Delete objects
            for key in objects_to_delete:
                if dryrun:
                    click.echo(f"(dryrun) delete: s3://{bucket}/{key}")
                else:
                    service.storage.delete(f"{bucket}/{key}")
                    if not quiet:
                        click.echo(f"delete: s3://{bucket}/{key}")

        else:
            # Recursive deletion or prefix deletion
            if not recursive:
                click.echo("Error: Cannot remove directories. Use --recursive", err=True)
                sys.exit(1)

            # Use the service's delete_recursive method for proper delta-aware deletion
            if dryrun:
                # For dryrun, we need to simulate what would be deleted
                objects = list(service.storage.list(f"{bucket}/{prefix}" if prefix else bucket))
                if not objects:
                    if not quiet:
                        click.echo(f"delete: No objects found with prefix: s3://{bucket}/{prefix}")
                    return

                for obj in objects:
                    click.echo(f"(dryrun) delete: s3://{bucket}/{obj.key}")

                if not quiet:
                    click.echo(f"Would delete {len(objects)} object(s)")
            else:
                # Use the core service method for actual deletion
                result = service.delete_recursive(bucket, prefix)

                # Report the results
                if not quiet:
                    if result.deleted_count == 0:
                        click.echo(f"delete: No objects found with prefix: s3://{bucket}/{prefix}")
                    else:
                        click.echo(f"Deleted {result.deleted_count} object(s)")

                        # Show warnings if any references were kept
                        for warning in result.warnings:
                            if "Kept reference" in warning:
                                click.echo(
                                    f"Keeping reference file (still in use): s3://{bucket}/{warning.split()[2]}"
                                )

                # Report any errors
                if result.failed_count > 0:
                    for error in result.errors:
                        click.echo(f"Error: {error}", err=True)

                    if result.failed_count > 0:
                        sys.exit(1)

    except Exception as e:
        click.echo(f"delete failed: {e}", err=True)
        sys.exit(1)


@cli.command()
@click.argument("source")
@click.argument("dest")
@click.option("--delete", is_flag=True, help="Delete dest files not in source")
@click.option("--exclude", help="Exclude files matching pattern")
@click.option("--include", help="Include only files matching pattern")
@click.option("--dryrun", is_flag=True, help="Show what would be synced without syncing")
@click.option("--quiet", "-q", is_flag=True, help="Suppress output")
@click.option("--size-only", is_flag=True, help="Compare only file sizes, not timestamps")
@click.option("--no-delta", is_flag=True, help="Disable delta compression")
@click.option("--max-ratio", type=float, help="Max delta/file ratio (default: 0.5)")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def sync(
    service: DeltaService,
    source: str,
    dest: str,
    delete: bool,
    exclude: str | None,
    include: str | None,
    dryrun: bool,
    quiet: bool,
    size_only: bool,
    no_delta: bool,
    max_ratio: float | None,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Synchronize directories with S3 (AWS S3 compatible).

    Examples:
        deltaglider sync ./local-dir/ s3://bucket/path/     # Local to S3
        deltaglider sync s3://bucket/path/ ./local-dir/     # S3 to local
        deltaglider sync --delete ./dir/ s3://bucket/       # Mirror exactly
        deltaglider sync --exclude "*.log" ./dir/ s3://bucket/
    """
    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        # Determine sync direction
        source_is_s3 = is_s3_path(source)
        dest_is_s3 = is_s3_path(dest)

        if source_is_s3 and dest_is_s3:
            click.echo("Error: S3 to S3 sync not yet implemented", err=True)
            sys.exit(1)
        elif not source_is_s3 and not dest_is_s3:
            click.echo("Error: At least one path must be an S3 URL", err=True)
            sys.exit(1)

        if dest_is_s3:
            # Sync local to S3
            local_dir = Path(source)
            if not local_dir.is_dir():
                click.echo(f"Error: Source must be a directory: {source}", err=True)
                sys.exit(1)

            bucket, prefix = parse_s3_url(dest)
            sync_to_s3(
                service,
                local_dir,
                bucket,
                prefix,
                delete,
                dryrun,
                quiet,
                exclude,
                include,
                size_only,
                no_delta,
                max_ratio,
            )
        else:
            # Sync S3 to local
            bucket, prefix = parse_s3_url(source)
            local_dir = Path(dest)

            sync_from_s3(
                service,
                bucket,
                prefix,
                local_dir,
                delete,
                dryrun,
                quiet,
                exclude,
                include,
                size_only,
            )

    except Exception as e:
        click.echo(f"sync failed: {e}", err=True)
        sys.exit(1)


@cli.command()
@click.argument("s3_url")
@click.pass_obj
def verify(service: DeltaService, s3_url: str) -> None:
    """Verify integrity of delta file."""
    try:
        bucket, key = parse_s3_url(s3_url)
        if not key:
            raise ValueError("Missing key")
    except ValueError:
        click.echo(f"Error: Invalid S3 URL: {s3_url}", err=True)
        sys.exit(1)

    obj_key = ObjectKey(bucket=bucket, key=key)

    try:
        result = service.verify(obj_key)

        output = {
            "valid": result.valid,
            "expected_sha256": result.expected_sha256,
            "actual_sha256": result.actual_sha256,
            "message": result.message,
        }

        click.echo(json.dumps(output, indent=2))

        if not result.valid:
            sys.exit(1)

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


@cli.command()
@click.argument("source")
@click.argument("dest")
@click.option("--exclude", help="Exclude files matching pattern")
@click.option("--include", help="Include only files matching pattern")
@click.option("--quiet", "-q", is_flag=True, help="Suppress output")
@click.option("--no-delta", is_flag=True, help="Disable delta compression")
@click.option("--max-ratio", type=float, help="Max delta/file ratio (default: 0.5)")
@click.option("--dry-run", is_flag=True, help="Show what would be migrated without migrating")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
@click.option(
    "--no-preserve-prefix", is_flag=True, help="Don't preserve source prefix in destination"
)
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def migrate(
    service: DeltaService,
    source: str,
    dest: str,
    exclude: str | None,
    include: str | None,
    quiet: bool,
    no_delta: bool,
    max_ratio: float | None,
    dry_run: bool,
    yes: bool,
    no_preserve_prefix: bool,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Migrate S3 bucket/prefix to DeltaGlider-compressed storage.

    This command facilitates the migration of existing S3 objects to another bucket
    with DeltaGlider compression. It supports:
    - Resume capability: Only copies files that don't exist in destination
    - Progress tracking: Shows migration progress
    - Confirmation prompt: Shows file count before starting (use --yes to skip)
    - Prefix preservation: By default, source prefix is preserved in destination

    When migrating a prefix, the source prefix name is preserved by default:
        s3://src/prefix1/ → s3://dest/      creates s3://dest/prefix1/
        s3://src/a/b/c/  → s3://dest/x/    creates s3://dest/x/c/

    Use --no-preserve-prefix to disable this behavior:
        s3://src/prefix1/ → s3://dest/      creates s3://dest/ (files at root)

    Examples:
        deltaglider migrate s3://old-bucket/ s3://new-bucket/
        deltaglider migrate s3://old-bucket/data/ s3://new-bucket/
        deltaglider migrate --no-preserve-prefix s3://src/v1/ s3://dest/
        deltaglider migrate --dry-run s3://old-bucket/ s3://new-bucket/
        deltaglider migrate --yes --quiet s3://old-bucket/ s3://new-bucket/
    """
    from .aws_compat import is_s3_path, migrate_s3_to_s3

    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        # Validate both paths are S3
        if not is_s3_path(source) or not is_s3_path(dest):
            click.echo("Error: Both source and destination must be S3 paths", err=True)
            sys.exit(1)

        # Perform migration
        migrate_s3_to_s3(
            service,
            source,
            dest,
            exclude=exclude,
            include=include,
            quiet=quiet,
            no_delta=no_delta,
            max_ratio=max_ratio,
            dry_run=dry_run,
            skip_confirm=yes,
            preserve_prefix=not no_preserve_prefix,
            region_override=region is not None,  # True if user explicitly specified --region
        )

    except Exception as e:
        click.echo(f"Migration failed: {e}", err=True)
        sys.exit(1)


@cli.command(short_help="Get bucket statistics and compression metrics")
@click.argument("bucket")
@click.option("--sampled", is_flag=True, help="Balanced mode: one sample per deltaspace (~5-15s)")
@click.option(
    "--detailed", is_flag=True, help="Most accurate: HEAD for all deltas (slowest, ~1min+)"
)
@click.option("--refresh", is_flag=True, help="Force cache refresh even if valid")
@click.option("--no-cache", is_flag=True, help="Skip caching entirely (both read and write)")
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
@click.pass_obj
def stats(
    service: DeltaService,
    bucket: str,
    sampled: bool,
    detailed: bool,
    refresh: bool,
    no_cache: bool,
    output_json: bool,
) -> None:
    """Get bucket statistics and compression metrics with intelligent S3-based caching.

    BUCKET can be specified as:
      - s3://bucket-name/
      - s3://bucket-name
      - bucket-name

    Modes (mutually exclusive):
      - quick (default): Fast listing-only stats (~0.5s), approximate compression metrics
      - --sampled: Balanced mode - one HEAD per deltaspace (~5-15s for typical buckets)
      - --detailed: Most accurate - HEAD for every delta file (slowest, ~1min+ for large buckets)

    Caching (NEW - massive performance improvement!):
      Stats are cached in S3 at .deltaglider/stats_{mode}.json (one per mode).
      Cache is automatically validated on every call using object count + size.
      If bucket changed, stats are recomputed automatically.

      Performance with cache:
        - Cache hit: ~0.1s (200x faster than recomputation!)
        - Cache miss: Full computation time (creates cache for next time)
        - Cache invalid: Auto-recomputes when bucket changes

    Options:
      --refresh: Force cache refresh even if valid (use when you need fresh data now)
      --no-cache: Skip caching entirely - always recompute (useful for testing/debugging)
      --json: Output in JSON format for automation/scripting

    Examples:
      deltaglider stats mybucket                    # Fast (~0.1s with cache, ~0.5s without)
      deltaglider stats mybucket --sampled          # Balanced accuracy/speed (~5-15s first run)
      deltaglider stats mybucket --detailed         # Most accurate (~1-10min first run, ~0.1s cached)
      deltaglider stats mybucket --refresh          # Force recomputation even if cached
      deltaglider stats mybucket --no-cache         # Always compute fresh (skip cache)
      deltaglider stats mybucket --json             # JSON output for scripts
      deltaglider stats s3://mybucket/              # Also accepts s3:// URLs

    Timing Logs:
      Set DG_LOG_LEVEL=INFO to see detailed phase timing with timestamps:
        [HH:MM:SS.mmm] Phase 1: LIST completed in 0.52s - Found 1523 objects
        [HH:MM:SS.mmm] Phase 2: Cache HIT in 0.06s - Using cached stats
        [HH:MM:SS.mmm] COMPLETE: Total time 0.58s

    See docs/STATS_CACHING.md for complete documentation.
    """
    from ...client import DeltaGliderClient
    from ...client_operations.stats import StatsMode

    try:
        # Parse bucket from S3 URL if needed
        if is_s3_path(bucket):
            bucket, _prefix = parse_s3_url(bucket)

        if not bucket:
            click.echo("Error: Invalid bucket name", err=True)
            sys.exit(1)

        if sampled and detailed:
            click.echo("Error: --sampled and --detailed cannot be used together", err=True)
            sys.exit(1)

        if refresh and no_cache:
            click.echo("Error: --refresh and --no-cache cannot be used together", err=True)
            sys.exit(1)

        mode: StatsMode = "quick"
        if sampled:
            mode = "sampled"
        if detailed:
            mode = "detailed"

        # Create client from service
        client = DeltaGliderClient(service=service)

        # Get bucket stats with caching control
        use_cache = not no_cache
        bucket_stats = client.get_bucket_stats(
            bucket, mode=mode, use_cache=use_cache, refresh_cache=refresh
        )

        if output_json:
            # JSON output
            output = {
                "bucket": bucket_stats.bucket,
                "object_count": bucket_stats.object_count,
                "total_size": bucket_stats.total_size,
                "compressed_size": bucket_stats.compressed_size,
                "space_saved": bucket_stats.space_saved,
                "average_compression_ratio": bucket_stats.average_compression_ratio,
                "delta_objects": bucket_stats.delta_objects,
                "direct_objects": bucket_stats.direct_objects,
            }
            click.echo(json.dumps(output, indent=2))
        else:
            # Human-readable output
            def format_bytes(size: float) -> str:
                """Format bytes to human-readable size."""
                for unit in ["B", "KB", "MB", "GB", "TB"]:
                    if size < 1024.0:
                        return f"{size:.2f} {unit}"
                    size /= 1024.0
                return f"{size:.2f} PB"

            click.echo(f"Bucket Statistics: {bucket_stats.bucket}")
            click.echo(f"{'=' * 60}")
            click.echo(f"Total Objects:        {bucket_stats.object_count:,}")
            click.echo(f"  Delta Objects:      {bucket_stats.delta_objects:,}")
            click.echo(f"  Direct Objects:     {bucket_stats.direct_objects:,}")
            click.echo("")
            click.echo(
                f"Original Size:        {format_bytes(bucket_stats.total_size)} ({bucket_stats.total_size:,} bytes)"
            )
            click.echo(
                f"Compressed Size:      {format_bytes(bucket_stats.compressed_size)} ({bucket_stats.compressed_size:,} bytes)"
            )
            click.echo(
                f"Space Saved:          {format_bytes(bucket_stats.space_saved)} ({bucket_stats.space_saved:,} bytes)"
            )
            click.echo(f"Compression Ratio:    {bucket_stats.average_compression_ratio:.1%}")

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


@cli.command()
@click.argument("bucket")
@click.option("--dry-run", is_flag=True, help="Show what would be deleted without deleting")
@click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def purge(
    service: DeltaService,
    bucket: str,
    dry_run: bool,
    output_json: bool,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Purge expired temporary files from .deltaglider/tmp/.

    This command scans the .deltaglider/tmp/ prefix in the specified bucket
    and deletes any files whose dg-expires-at metadata indicates they have expired.

    These temporary files are created by the rehydration process when deltaglider-compressed
    files need to be made available for direct download (e.g., via presigned URLs).

    BUCKET can be specified as:
      - s3://bucket-name/
      - s3://bucket-name
      - bucket-name

    Examples:
      deltaglider purge mybucket                    # Purge expired files
      deltaglider purge mybucket --dry-run          # Preview what would be deleted
      deltaglider purge mybucket --json             # JSON output for automation
      deltaglider purge s3://mybucket/              # Also accepts s3:// URLs
    """
    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        # Parse bucket from S3 URL if needed
        if is_s3_path(bucket):
            bucket, _prefix = parse_s3_url(bucket)

        if not bucket:
            click.echo("Error: Invalid bucket name", err=True)
            sys.exit(1)

        # Perform the purge (or dry run simulation)
        if dry_run:
            # For dry run, we need to simulate what would be deleted
            prefix = ".deltaglider/tmp/"
            expired_files = []
            total_size = 0

            # List all objects in temp directory
            from datetime import datetime

            import boto3

            s3_client = boto3.client(
                "s3",
                endpoint_url=endpoint_url or os.environ.get("AWS_ENDPOINT_URL"),
                region_name=region,
            )

            paginator = s3_client.get_paginator("list_objects_v2")
            page_iterator = paginator.paginate(Bucket=bucket, Prefix=prefix)

            for page in page_iterator:
                for obj in page.get("Contents", []):
                    # Get object metadata
                    head_response = s3_client.head_object(Bucket=bucket, Key=obj["Key"])
                    metadata = head_response.get("Metadata", {})

                    expires_at_str = metadata.get("dg-expires-at")
                    if expires_at_str:
                        try:
                            expires_at = datetime.fromisoformat(
                                expires_at_str.replace("Z", "+00:00")
                            )
                            if expires_at.tzinfo is None:
                                expires_at = expires_at.replace(tzinfo=UTC)

                            if datetime.now(UTC) >= expires_at:
                                expired_files.append(
                                    {
                                        "key": obj["Key"],
                                        "size": obj["Size"],
                                        "expires_at": expires_at_str,
                                    }
                                )
                                total_size += obj["Size"]
                        except ValueError:
                            pass

            if output_json:
                output = {
                    "bucket": bucket,
                    "prefix": prefix,
                    "dry_run": True,
                    "would_delete_count": len(expired_files),
                    "total_size_to_free": total_size,
                    "expired_files": expired_files[:10],  # Show first 10
                }
                click.echo(json.dumps(output, indent=2))
            else:
                click.echo(f"Dry run: Would delete {len(expired_files)} expired file(s)")
                click.echo(f"Total space to free: {total_size:,} bytes")
                if expired_files:
                    click.echo("\nFiles that would be deleted (first 10):")
                    for file_info in expired_files[:10]:
                        click.echo(f"  {file_info['key']} (expires: {file_info['expires_at']})")
                    if len(expired_files) > 10:
                        click.echo(f"  ... and {len(expired_files) - 10} more")
        else:
            # Perform actual purge using the service method
            result = service.purge_temp_files(bucket)

            if output_json:
                # JSON output
                click.echo(json.dumps(result, indent=2))
            else:
                # Human-readable output
                click.echo(f"Purge Statistics for bucket: {bucket}")
                click.echo(f"{'=' * 60}")
                click.echo(f"Expired files found:  {result['expired_count']}")
                click.echo(f"Files deleted:        {result['deleted_count']}")
                click.echo(f"Errors:               {result['error_count']}")
                click.echo(f"Space freed:          {result['total_size_freed']:,} bytes")
                click.echo(f"Duration:             {result['duration_seconds']:.2f} seconds")

                if result["errors"]:
                    click.echo("\nErrors encountered:")
                    for error in result["errors"][:5]:
                        click.echo(f"  - {error}")
                    if len(result["errors"]) > 5:
                        click.echo(f"  ... and {len(result['errors']) - 5} more errors")

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


@cli.command("put-bucket-acl")
@click.argument("bucket")
@click.option(
    "--acl",
    type=click.Choice(["private", "public-read", "public-read-write", "authenticated-read"]),
    help="Canned ACL to apply",
)
@click.option("--grant-full-control", help="Grants full control (e.g., id=account-id)")
@click.option("--grant-read", help="Allows grantee to list objects (e.g., id=account-id)")
@click.option("--grant-read-acp", help="Allows grantee to read the bucket ACL")
@click.option("--grant-write", help="Allows grantee to create objects in the bucket")
@click.option("--grant-write-acp", help="Allows grantee to write the ACL for the bucket")
@click.option("--access-control-policy", help="Full ACL policy as JSON string")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def put_bucket_acl(
    service: DeltaService,
    bucket: str,
    acl: str | None,
    grant_full_control: str | None,
    grant_read: str | None,
    grant_read_acp: str | None,
    grant_write: str | None,
    grant_write_acp: str | None,
    access_control_policy: str | None,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Set the access control list (ACL) for an S3 bucket.

    BUCKET can be specified as:
      - s3://bucket-name
      - bucket-name

    Examples:
        deltaglider put-bucket-acl my-bucket --acl private
        deltaglider put-bucket-acl my-bucket --acl public-read
        deltaglider put-bucket-acl my-bucket --grant-read id=12345
    """
    from ...client import DeltaGliderClient

    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        # Parse bucket from S3 URL if needed
        if is_s3_path(bucket):
            bucket, _prefix = parse_s3_url(bucket)

        if not bucket:
            click.echo("Error: Invalid bucket name", err=True)
            sys.exit(1)

        client = DeltaGliderClient(service=service)

        kwargs: dict[str, Any] = {}
        if acl is not None:
            kwargs["ACL"] = acl
        if grant_full_control is not None:
            kwargs["GrantFullControl"] = grant_full_control
        if grant_read is not None:
            kwargs["GrantRead"] = grant_read
        if grant_read_acp is not None:
            kwargs["GrantReadACP"] = grant_read_acp
        if grant_write is not None:
            kwargs["GrantWrite"] = grant_write
        if grant_write_acp is not None:
            kwargs["GrantWriteACP"] = grant_write_acp
        if access_control_policy is not None:
            kwargs["AccessControlPolicy"] = json.loads(access_control_policy)

        client.put_bucket_acl(Bucket=bucket, **kwargs)
        click.echo(f"ACL updated for bucket: {bucket}")

    except json.JSONDecodeError as e:
        click.echo(f"Error: Invalid JSON for --access-control-policy: {e}", err=True)
        sys.exit(1)
    except Exception as e:
        click.echo(f"Error: {e}", err=True)
        sys.exit(1)


@cli.command("get-bucket-acl")
@click.argument("bucket")
@click.option("--endpoint-url", help="Override S3 endpoint URL")
@click.option("--region", help="AWS region")
@click.option("--profile", help="AWS profile to use")
@click.pass_obj
def get_bucket_acl(
    service: DeltaService,
    bucket: str,
    endpoint_url: str | None,
    region: str | None,
    profile: str | None,
) -> None:
    """Get the access control list (ACL) for an S3 bucket.

    BUCKET can be specified as:
      - s3://bucket-name
      - bucket-name

    Examples:
        deltaglider get-bucket-acl my-bucket
        deltaglider get-bucket-acl s3://my-bucket
    """
    from ...client import DeltaGliderClient

    # Recreate service with AWS parameters if provided
    if endpoint_url or region or profile:
        service = create_service(
            log_level=os.environ.get("DG_LOG_LEVEL", "INFO"),
            endpoint_url=endpoint_url,
            region=region,
            profile=profile,
        )

    try:
        # Parse bucket from S3 URL if needed
        if is_s3_path(bucket):
            bucket, _prefix = parse_s3_url(bucket)

        if not bucket:
            click.echo("Error: Invalid bucket name", err=True)
            sys.exit(1)

        client = DeltaGliderClient(service=service)
        response = client.get_bucket_acl(Bucket=bucket)

        # Output as JSON like aws s3api get-bucket-acl
        click.echo(json.dumps(response, indent=2, default=str))

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


def main() -> None:
    """Main entry point."""
    cli()
