"""Command-line interface for zae-limiter infrastructure management."""

import asyncio
import sys
from pathlib import Path

import click

from .infra.lambda_builder import get_package_info, write_lambda_package
from .infra.stack_manager import StackManager
from .models import StackOptions


@click.group()
@click.version_option()
def cli() -> None:
    """zae-limiter infrastructure management CLI."""
    pass


@cli.command()
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
@click.option(
    "--snapshot-windows",
    default="hourly,daily",
    help="Comma-separated list of snapshot windows",
)
@click.option(
    "--retention-days",
    default=90,
    type=int,
    help="Number of days to retain usage snapshots",
)
@click.option(
    "--enable-aggregator/--no-aggregator",
    default=True,
    help="Deploy Lambda aggregator for usage snapshots",
)
@click.option(
    "--pitr-recovery-days",
    type=int,
    help="Point-in-Time Recovery period in days (1-35, default: AWS default of 35)",
)
@click.option(
    "--log-retention-days",
    default="30",
    type=click.Choice(
        [
            "1",
            "3",
            "5",
            "7",
            "14",
            "30",
            "60",
            "90",
            "120",
            "150",
            "180",
            "365",
            "400",
            "545",
            "731",
            "1096",
            "1827",
            "2192",
            "2557",
            "2922",
            "3288",
            "3653",
        ]
    ),
    help="Number of days to retain Lambda logs (CloudWatch standard retention periods)",
)
@click.option(
    "--lambda-timeout",
    type=click.IntRange(1, 900),
    default=60,
    help="Lambda timeout in seconds (1-900, default: 60)",
)
@click.option(
    "--lambda-memory",
    type=click.IntRange(128, 3008),
    default=256,
    help="Lambda memory size in MB (128-3008, default: 256)",
)
@click.option(
    "--enable-alarms/--no-alarms",
    default=True,
    help="Deploy CloudWatch alarms for monitoring (default: enabled)",
)
@click.option(
    "--alarm-sns-topic",
    type=str,
    default=None,
    help="SNS topic ARN for alarm notifications (optional)",
)
@click.option(
    "--lambda-duration-threshold-pct",
    type=click.IntRange(1, 100),
    default=80,
    help="Lambda duration alarm threshold as percentage of timeout (1-100, default: 80)",
)
@click.option(
    "--wait/--no-wait",
    default=True,
    help="Wait for stack creation to complete",
)
@click.option(
    "--permission-boundary",
    type=str,
    default=None,
    help="IAM permission boundary for Lambda role (policy name or full ARN)",
)
@click.option(
    "--role-name-format",
    type=str,
    default=None,
    help=(
        "Format template for Lambda role name. "
        "Use {} as placeholder for default name. "
        "Example: 'app-{}' produces 'app-mytable-aggregator-role'."
    ),
)
@click.option(
    "--enable-audit-archival/--no-audit-archival",
    default=True,
    help="Archive expired audit events to S3 (default: enabled)",
)
@click.option(
    "--audit-archive-glacier-days",
    type=click.IntRange(1, 3650),
    default=90,
    help="Days before transitioning audit archives to Glacier Instant Retrieval (default: 90)",
)
@click.option(
    "--enable-tracing/--no-tracing",
    default=False,
    help="Enable AWS X-Ray tracing for Lambda aggregator (default: disabled)",
)
def deploy(
    name: str,
    region: str | None,
    endpoint_url: str | None,
    snapshot_windows: str,
    retention_days: int,
    enable_aggregator: bool,
    pitr_recovery_days: int | None,
    log_retention_days: str,
    lambda_timeout: int,
    lambda_memory: int,
    enable_alarms: bool,
    alarm_sns_topic: str | None,
    lambda_duration_threshold_pct: int,
    wait: bool,
    permission_boundary: str | None,
    role_name_format: str | None,
    enable_audit_archival: bool,
    audit_archive_glacier_days: int,
    enable_tracing: bool,
) -> None:
    """Deploy CloudFormation stack with DynamoDB table and Lambda aggregator."""
    from .exceptions import ValidationError

    try:
        manager = StackManager(name, region, endpoint_url)
    except ValidationError as e:
        click.echo(f"Error: {e.reason}", err=True)
        sys.exit(1)

    async def _deploy() -> None:
        async with manager:
            # Build StackOptions from CLI arguments
            stack_options = StackOptions(
                snapshot_windows=snapshot_windows,
                retention_days=retention_days,
                enable_aggregator=enable_aggregator,
                pitr_recovery_days=pitr_recovery_days,
                log_retention_days=int(log_retention_days),
                lambda_timeout=lambda_timeout,
                lambda_memory=lambda_memory,
                enable_alarms=enable_alarms,
                alarm_sns_topic=alarm_sns_topic,
                lambda_duration_threshold_pct=lambda_duration_threshold_pct,
                permission_boundary=permission_boundary,
                role_name_format=role_name_format,
                enable_audit_archival=enable_audit_archival,
                audit_archive_glacier_days=audit_archive_glacier_days,
                enable_tracing=enable_tracing,
            )

            click.echo(f"Deploying stack: {manager.stack_name}")
            click.echo(f"  Table name: {manager.table_name}")
            click.echo(f"  Region: {region or 'default'}")
            click.echo(f"  Snapshot windows: {stack_options.snapshot_windows}")
            click.echo(f"  Retention days: {stack_options.retention_days}")
            click.echo(
                f"  Aggregator: {'enabled' if stack_options.enable_aggregator else 'disabled'}"
            )
            if stack_options.enable_aggregator:
                click.echo(f"  Lambda timeout: {stack_options.lambda_timeout}s")
                click.echo(f"  Lambda memory: {stack_options.lambda_memory}MB")
                click.echo(
                    f"  X-Ray tracing: {'enabled' if stack_options.enable_tracing else 'disabled'}"
                )
            click.echo(f"  Alarms: {'enabled' if stack_options.enable_alarms else 'disabled'}")
            if stack_options.enable_alarms and stack_options.alarm_sns_topic:
                click.echo(f"  Alarm SNS topic: {stack_options.alarm_sns_topic}")
            if stack_options.enable_aggregator:
                archival_status = "enabled" if stack_options.enable_audit_archival else "disabled"
                click.echo(f"  Audit archival: {archival_status}")
                if stack_options.enable_audit_archival:
                    click.echo(
                        f"  Glacier transition: {stack_options.audit_archive_glacier_days} days"
                    )
            click.echo()

            try:
                # Step 1: Create CloudFormation stack
                result = await manager.create_stack(
                    stack_options=stack_options,
                    wait=wait,
                )

                status = result.get("status", "unknown")
                if status == "skipped_local":
                    click.echo("⚠️  CloudFormation deployment skipped (local DynamoDB detected)")
                    sys.exit(0)

                click.echo(f"✓ Stack {status.lower().replace('_', ' ')}")

                if result.get("stack_id"):
                    click.echo(f"  Stack ID: {result['stack_id']}")

                # Step 2: Deploy Lambda code if aggregator is enabled
                if stack_options.enable_aggregator and wait:
                    click.echo()
                    click.echo("Deploying Lambda function code...")

                    try:
                        lambda_result = await manager.deploy_lambda_code(wait=True)

                        if lambda_result.get("status") == "deployed":
                            size_kb = lambda_result.get("size_bytes", 0) / 1024
                            click.echo(f"✓ Lambda code deployed ({size_kb:.1f} KB)")
                            click.echo(f"  Function ARN: {lambda_result['function_arn']}")
                            click.echo(f"  Code SHA256: {lambda_result['code_sha256'][:16]}...")
                        elif lambda_result.get("status") == "skipped_local":
                            click.echo("  Lambda deployment skipped (local environment)")
                    except Exception as e:
                        click.echo(f"⚠️  Lambda deployment failed: {e}", err=True)
                        click.echo(
                            "  Stack was created successfully, but Lambda code "
                            "needs manual deployment.",
                            err=True,
                        )
                        sys.exit(1)

                # Step 3: Initialize version record in DynamoDB
                if wait:
                    from . import __version__
                    from .repository import Repository
                    from .version import get_schema_version

                    click.echo()
                    click.echo("Initializing version record...")

                    repo = Repository(manager.table_name, region, endpoint_url)
                    await repo.set_version_record(
                        schema_version=get_schema_version(),
                        lambda_version=__version__ if not endpoint_url else None,
                        client_min_version="0.0.0",
                        updated_by=f"cli:{__version__}",
                    )
                    click.echo(f"✓ Version record initialized (schema {get_schema_version()})")

                if not wait:
                    click.echo()
                    click.echo("Stack creation initiated. Use 'status' command to check progress.")
                    if stack_options.enable_aggregator:
                        click.echo(
                            "Note: Lambda code will not be deployed until stack is ready. "
                            "Run 'zae-limiter deploy' again with --wait to deploy Lambda."
                        )

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

    asyncio.run(_deploy())


@cli.command()
@click.option(
    "--name",
    "-n",
    required=True,
    help="Resource identifier (will be prefixed with 'ZAEL-' if not already)",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
@click.option(
    "--wait/--no-wait",
    default=True,
    help="Wait for stack deletion to complete",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    help="Skip confirmation prompt",
)
def delete(
    name: str,
    region: str | None,
    endpoint_url: str | None,
    wait: bool,
    yes: bool,
) -> None:
    """Delete CloudFormation stack."""
    from .exceptions import ValidationError
    from .naming import normalize_name

    try:
        normalized_name = normalize_name(name)
    except ValidationError as e:
        click.echo(f"Error: {e.reason}", err=True)
        sys.exit(1)

    if not yes:
        click.confirm(
            f"Are you sure you want to delete stack '{normalized_name}'?",
            abort=True,
        )

    async def _delete() -> None:
        async with StackManager(name, region, endpoint_url) as manager:
            click.echo(f"Deleting stack: {manager.stack_name}")

            try:
                await manager.delete_stack(stack_name=manager.stack_name, wait=wait)

                if wait:
                    click.echo(f"✓ Stack '{manager.stack_name}' deleted successfully")
                else:
                    click.echo("Stack deletion initiated. Use 'status' command to check progress.")

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

    asyncio.run(_delete())


@cli.command()
@click.option(
    "--output",
    "-o",
    type=click.Path(),
    help="Output file (default: stdout)",
)
def cfn_template(output: str | None) -> None:
    """Export CloudFormation template for custom deployment."""
    try:
        template_path = Path(__file__).parent / "infra" / "cfn_template.yaml"

        if not template_path.exists():
            click.echo(f"✗ Template not found: {template_path}", err=True)
            sys.exit(1)

        content = template_path.read_text()

        if output:
            output_path = Path(output)
            output_path.write_text(content)
            click.echo(f"✓ Template exported to: {output}")
        else:
            click.echo(content)

    except Exception as e:
        click.echo(f"✗ Failed to export template: {e}", err=True)
        sys.exit(1)


@cli.command("lambda-export")
@click.option(
    "--output",
    "-o",
    type=click.Path(),
    default="lambda.zip",
    help="Output file path (default: lambda.zip)",
)
@click.option(
    "--info",
    is_flag=True,
    help="Show package information without building",
)
@click.option(
    "--force",
    "-f",
    is_flag=True,
    help="Overwrite existing file without prompting",
)
def lambda_export(output: str, info: bool, force: bool) -> None:
    """Export Lambda deployment package for custom deployment."""
    try:
        if info:
            # Show package info without building
            pkg_info = get_package_info()
            click.echo()
            click.echo("Lambda Package Information")
            click.echo("=" * 26)
            click.echo()
            click.echo(f"Package path:      {pkg_info['package_path']}")
            click.echo(f"Python files:      {pkg_info['python_files']}")
            click.echo(f"Uncompressed size: {int(pkg_info['uncompressed_size']) / 1024:.1f} KB")
            click.echo(f"Handler:           {pkg_info['handler']}")
            click.echo()
            return

        output_path = Path(output)

        # Check if file exists
        if output_path.exists() and not force:
            click.echo(f"File already exists: {output_path}", err=True)
            click.echo("Use --force to overwrite.", err=True)
            sys.exit(1)

        # Build and write the package
        size_bytes = write_lambda_package(output_path)
        size_kb = size_bytes / 1024

        click.echo(f"✓ Exported Lambda package to: {output_path} ({size_kb:.1f} KB)")

    except Exception as e:
        click.echo(f"✗ Failed to export Lambda package: {e}", err=True)
        sys.exit(1)


def _format_size(size_bytes: int | None) -> str:
    """Format size in bytes to human-readable format."""
    if size_bytes is None:
        return "N/A"
    if size_bytes < 1024:
        return f"~{size_bytes} B"
    elif size_bytes < 1024 * 1024:
        return f"~{size_bytes / 1024:.1f} KB"
    elif size_bytes < 1024 * 1024 * 1024:
        return f"~{size_bytes / (1024 * 1024):.1f} MB"
    else:
        return f"~{size_bytes / (1024 * 1024 * 1024):.1f} GB"


def _format_count(count: int | None) -> str:
    """Format item count to human-readable format."""
    if count is None:
        return "N/A"
    if count < 1000:
        return f"~{count:,}"
    elif count < 1000000:
        return f"~{count / 1000:.1f}K"
    else:
        return f"~{count / 1000000:.1f}M"


@cli.command()
@click.option(
    "--name",
    "-n",
    required=True,
    help="Resource identifier (will be prefixed with 'ZAEL-' if not already)",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
def status(name: str, region: str | None, endpoint_url: str | None) -> None:
    """Get comprehensive status of rate limiter infrastructure (read-only)."""
    import time

    from . import __version__
    from .exceptions import ValidationError
    from .naming import normalize_name
    from .repository import Repository

    try:
        stack_name = normalize_name(name)
    except ValidationError as e:
        click.echo(f"Error: {e.reason}", err=True)
        sys.exit(1)

    async def _status() -> None:
        # Initialize status values
        available = False
        latency_ms: float | None = None
        cfn_status: str | None = None
        table_status: str | None = None
        aggregator_enabled = False
        schema_version: str | None = None
        lambda_version: str | None = None
        table_item_count: int | None = None
        table_size_bytes: int | None = None

        # Get CloudFormation stack status (read-only)
        try:
            async with StackManager(stack_name, region, endpoint_url) as manager:
                cfn_status = await manager.get_stack_status(stack_name)
        except Exception:
            pass  # Stack status unavailable

        # Create repository for read-only DynamoDB access
        repository = Repository(stack_name, region, endpoint_url)

        try:
            # Ping DynamoDB and measure latency
            try:
                start_time = time.time()
                client = await repository._get_client()

                # Use DescribeTable to check connectivity and get table info
                response = await client.describe_table(TableName=stack_name)
                latency_ms = (time.time() - start_time) * 1000
                available = True

                # Extract table information
                table = response.get("Table", {})
                table_status = table.get("TableStatus")
                table_item_count = table.get("ItemCount")
                table_size_bytes = table.get("TableSizeInBytes")

                # Check if aggregator is enabled by looking for stream specification
                stream_spec = table.get("StreamSpecification", {})
                aggregator_enabled = stream_spec.get("StreamEnabled", False)

            except Exception:
                pass  # DynamoDB unavailable

            # Get version information from DynamoDB
            if available:
                try:
                    version_record = await repository.get_version_record()
                    if version_record:
                        schema_version = version_record.get("schema_version")
                        lambda_version = version_record.get("lambda_version")
                except Exception:
                    pass  # Version record unavailable

            # Header
            click.echo()
            click.echo(f"Status: {stack_name}")
            click.echo("=" * 50)
            click.echo()

            # Connectivity section
            click.echo("Connectivity")
            available_str = "✓ Yes" if available else "✗ No"
            click.echo(f"  Available:     {available_str}")
            if latency_ms is not None:
                click.echo(f"  Latency:       {latency_ms:.0f}ms")
            else:
                click.echo("  Latency:       N/A")
            click.echo(f"  Region:        {region or 'default'}")
            click.echo()

            # Infrastructure section
            click.echo("Infrastructure")
            click.echo(f"  Stack:         {cfn_status or 'Not found'}")
            click.echo(f"  Table:         {table_status or 'Not found'}")
            aggregator_str = "Enabled" if aggregator_enabled else "Disabled"
            click.echo(f"  Aggregator:    {aggregator_str}")
            click.echo()

            # Versions section
            click.echo("Versions")
            click.echo(f"  Client:        {__version__}")
            click.echo(f"  Schema:        {schema_version or 'N/A'}")
            click.echo(f"  Lambda:        {lambda_version or 'N/A'}")
            click.echo()

            # Table Metrics section
            click.echo("Table Metrics")
            click.echo(f"  Items:         {_format_count(table_item_count)}")
            click.echo(f"  Size:          {_format_size(table_size_bytes)}")
            click.echo()

            # Exit with appropriate status code
            if not available:
                click.echo("✗ Infrastructure is not available", err=True)
                sys.exit(1)
            elif cfn_status and ("FAILED" in cfn_status or "ROLLBACK" in cfn_status):
                click.echo("✗ Stack is in failed state", err=True)
                sys.exit(1)
            elif cfn_status and "IN_PROGRESS" in cfn_status:
                click.echo("⏳ Operation in progress...")
            elif cfn_status == "CREATE_COMPLETE":
                click.echo("✓ Infrastructure is ready")

        except Exception as e:
            click.echo(f"✗ Failed to get status: {e}", err=True)
            sys.exit(1)
        finally:
            await repository.close()

    asyncio.run(_status())


@cli.command("list")
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
def list_limiters(region: str | None, endpoint_url: str | None) -> None:
    """List all deployed rate limiter instances in the region."""
    from datetime import datetime

    from .infra.discovery import InfrastructureDiscovery

    async def _list() -> None:
        try:
            async with InfrastructureDiscovery(
                region=region, endpoint_url=endpoint_url
            ) as discovery:
                limiters = await discovery.list_limiters()

            if not limiters:
                click.echo()
                click.echo("No rate limiter instances found in region.")
                click.echo(f"  Region: {region or 'default'}")
                click.echo()
                click.echo("Deploy a new instance with:")
                click.echo("  zae-limiter deploy --name my-app")
                return

            # Build table data
            click.echo()
            region_display = region or "default"
            click.echo(f"Rate Limiter Instances ({region_display})")
            click.echo()

            headers = ["Name", "Status", "Version", "Created"]
            rows: list[list[str]] = []
            for limiter in limiters:
                # Parse and format creation time (ISO 8601 -> readable)
                try:
                    created = datetime.fromisoformat(limiter.creation_time)
                    created_display = created.strftime("%Y-%m-%d")
                except Exception:
                    created_display = "unknown"

                rows.append(
                    [
                        limiter.user_name,
                        limiter.stack_status,
                        limiter.version or "-",
                        created_display,
                    ]
                )

            from .visualization import TableRenderer

            renderer = TableRenderer()
            click.echo(renderer.render(headers, rows))

            # Summary
            click.echo()
            click.echo(f"Total: {len(limiters)} instance(s)")

            # Show problem summary if any
            failed = [lim for lim in limiters if lim.is_failed]
            in_progress = [lim for lim in limiters if lim.is_in_progress]
            if failed:
                click.echo(f"  {len(failed)} failed")
            if in_progress:
                click.echo(f"  {len(in_progress)} in progress")
            click.echo()

        except Exception as e:
            click.echo(f"✗ Failed to list limiters: {e}", err=True)
            sys.exit(1)

    asyncio.run(_list())


@cli.command("version")
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
def version_cmd(
    name: str,
    region: str | None,
    endpoint_url: str | None,
) -> None:
    """Show infrastructure version information."""
    from . import __version__
    from .exceptions import ValidationError
    from .version import (
        InfrastructureVersion,
        check_compatibility,
        get_schema_version,
    )

    async def _version() -> None:
        # Import here to avoid loading aioboto3 at CLI startup
        from .repository import Repository

        try:
            repo = Repository(name, region, endpoint_url)
        except ValidationError as e:
            click.echo(f"Error: {e.reason}", err=True)
            sys.exit(1)

        try:
            click.echo()
            click.echo("zae-limiter Infrastructure Version")
            click.echo("=" * 36)
            click.echo()
            click.echo(f"Client Version:     {__version__}")
            click.echo(f"Schema Version:     {get_schema_version()}")
            click.echo()

            # Get version from DynamoDB
            version_record = await repo.get_version_record()

            if version_record is None:
                click.echo("Infrastructure:     Not initialized")
                click.echo()
                click.echo("Run 'zae-limiter deploy' to initialize infrastructure.")
                return

            infra_version = InfrastructureVersion.from_record(version_record)

            click.echo(f"Infra Schema:       {infra_version.schema_version}")
            click.echo(f"Lambda Version:     {infra_version.lambda_version or 'unknown'}")
            click.echo(f"Min Client Version: {infra_version.client_min_version}")
            click.echo()

            # Check compatibility
            compat = check_compatibility(__version__, infra_version)

            if compat.is_compatible and not compat.requires_lambda_update:
                click.echo("Status: COMPATIBLE")
            elif compat.requires_lambda_update:
                click.echo("Status: COMPATIBLE (Lambda update available)")
                click.echo()
                click.echo(f"  {compat.message}")
                click.echo()
                click.echo("Run 'zae-limiter upgrade' to update Lambda.")
            elif compat.requires_schema_migration:
                click.echo("Status: INCOMPATIBLE (Schema migration required)", err=True)
                click.echo()
                click.echo(f"  {compat.message}")
                sys.exit(1)
            else:
                click.echo("Status: INCOMPATIBLE", err=True)
                click.echo()
                click.echo(f"  {compat.message}")
                sys.exit(1)

        except Exception as e:
            click.echo(f"✗ Failed to get version info: {e}", err=True)
            sys.exit(1)
        finally:
            await repo.close()

    asyncio.run(_version())


@cli.command()
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
@click.option(
    "--lambda-only",
    is_flag=True,
    help="Only update Lambda code",
)
@click.option(
    "--force",
    is_flag=True,
    help="Force update even if version matches",
)
def upgrade(
    name: str,
    region: str | None,
    endpoint_url: str | None,
    lambda_only: bool,
    force: bool,
) -> None:
    """Upgrade infrastructure to match client version."""
    from . import __version__
    from .version import (
        InfrastructureVersion,
        check_compatibility,
        get_schema_version,
    )

    async def _upgrade() -> None:
        from .exceptions import ValidationError
        from .repository import Repository

        try:
            repo = Repository(name, region, endpoint_url)
        except ValidationError as e:
            click.echo(f"Error: {e.reason}", err=True)
            sys.exit(1)

        try:
            click.echo()
            click.echo("Checking infrastructure version...")

            version_record = await repo.get_version_record()

            if version_record is None:
                click.echo("Infrastructure not initialized.")
                click.echo("Run 'zae-limiter deploy' first.")
                sys.exit(1)

            infra_version = InfrastructureVersion.from_record(version_record)
            compat = check_compatibility(__version__, infra_version)

            if not force and compat.is_compatible and not compat.requires_lambda_update:
                click.echo()
                click.echo("Infrastructure is already up to date.")
                click.echo(f"  Client:   {__version__}")
                click.echo(f"  Lambda:   {infra_version.lambda_version}")
                return

            if compat.requires_schema_migration:
                click.echo()
                click.echo("✗ Schema migration required - cannot auto-upgrade", err=True)
                click.echo(f"  {compat.message}")
                sys.exit(1)

            # Perform upgrade
            click.echo()
            click.echo(f"Current: Lambda {infra_version.lambda_version or 'unknown'}")
            click.echo(f"Target:  Lambda {__version__}")
            click.echo()

            async with StackManager(name, region, endpoint_url) as manager:
                # Step 1: Update Lambda code
                click.echo("[1/2] Deploying Lambda code...")
                try:
                    result = await manager.deploy_lambda_code(wait=True)

                    if result.get("status") == "deployed":
                        size_kb = result.get("size_bytes", 0) / 1024
                        click.echo(f"      Lambda code deployed ({size_kb:.1f} KB)")
                    elif result.get("status") == "skipped_local":
                        click.echo("      Skipped (local environment)")

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

                # Step 2: Update version record
                click.echo("[2/2] Updating version record...")
                await repo.set_version_record(
                    schema_version=get_schema_version(),
                    lambda_version=__version__,
                    client_min_version="0.0.0",
                    updated_by=f"cli:{__version__}",
                )
                click.echo("      Version record updated")

            click.echo()
            click.echo("✓ Upgrade complete!")

        except Exception as e:
            click.echo(f"✗ Upgrade failed: {e}", err=True)
            sys.exit(1)
        finally:
            await repo.close()

    asyncio.run(_upgrade())


@cli.command()
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
def check(
    name: str,
    region: str | None,
    endpoint_url: str | None,
) -> None:
    """Check infrastructure compatibility without modifying."""
    from . import __version__
    from .exceptions import ValidationError
    from .version import (
        InfrastructureVersion,
        check_compatibility,
    )

    async def _check() -> None:
        from .repository import Repository

        try:
            repo = Repository(name, region, endpoint_url)
        except ValidationError as e:
            click.echo(f"Error: {e.reason}", err=True)
            sys.exit(1)

        try:
            click.echo()
            click.echo("Compatibility Check")
            click.echo("=" * 20)
            click.echo()

            version_record = await repo.get_version_record()

            if version_record is None:
                click.echo("Result: NOT INITIALIZED")
                click.echo()
                click.echo("Infrastructure has not been deployed yet.")
                click.echo("Run 'zae-limiter deploy' to initialize.")
                sys.exit(1)

            infra_version = InfrastructureVersion.from_record(version_record)
            compat = check_compatibility(__version__, infra_version)

            click.echo(f"Client:      {__version__}")
            click.echo(f"Schema:      {infra_version.schema_version}")
            click.echo(f"Lambda:      {infra_version.lambda_version or 'unknown'}")
            click.echo()

            if compat.is_compatible and not compat.requires_lambda_update:
                click.echo("Result: COMPATIBLE")
                click.echo()
                click.echo("Client and infrastructure are fully compatible.")
            elif compat.requires_lambda_update:
                click.echo("Result: COMPATIBLE (update available)")
                click.echo()
                click.echo(compat.message)
                click.echo()
                click.echo("Run 'zae-limiter upgrade' to update.")
            elif compat.requires_schema_migration:
                click.echo("Result: INCOMPATIBLE", err=True)
                click.echo()
                click.echo(compat.message)
                sys.exit(1)
            else:
                click.echo("Result: INCOMPATIBLE", err=True)
                click.echo()
                click.echo(compat.message)
                sys.exit(1)

        except Exception as e:
            click.echo(f"✗ Check failed: {e}", err=True)
            sys.exit(1)
        finally:
            await repo.close()

    asyncio.run(_check())


# -------------------------------------------------------------------------
# Audit commands
# -------------------------------------------------------------------------


@cli.group()
def audit() -> None:
    """Audit log commands."""
    pass


@audit.command("list")
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
@click.option(
    "--entity-id",
    "-e",
    required=True,
    help="Entity ID to query audit events for",
)
@click.option(
    "--limit",
    "-l",
    default=100,
    type=int,
    help="Maximum number of events to return (default: 100)",
)
@click.option(
    "--start-event-id",
    help="Event ID to start after (for pagination)",
)
def audit_list(
    name: str,
    region: str | None,
    endpoint_url: str | None,
    entity_id: str,
    limit: int,
    start_event_id: str | None,
) -> None:
    """List audit events for an entity."""
    from .exceptions import ValidationError
    from .repository import Repository

    async def _list() -> None:
        try:
            repo = Repository(name, region, endpoint_url)
        except ValidationError as e:
            click.echo(f"Error: {e.reason}", err=True)
            sys.exit(1)

        try:
            events = await repo.get_audit_events(
                entity_id=entity_id,
                limit=limit,
                start_event_id=start_event_id,
            )

            if not events:
                click.echo(f"No audit events found for entity: {entity_id}")
                return

            # Build table data
            click.echo()
            click.echo(f"Audit Events for: {entity_id}")
            click.echo()

            headers = ["Timestamp", "Action", "Principal", "Resource"]
            rows: list[list[str]] = []
            for event in events:
                rows.append(
                    [
                        event.timestamp,
                        event.action,
                        event.principal or "-",
                        event.resource or "-",
                    ]
                )

            from .visualization import TableRenderer

            renderer = TableRenderer()
            click.echo(renderer.render(headers, rows))
            click.echo()
            click.echo(f"Total: {len(events)} events")

            if len(events) == limit:
                last_id = events[-1].event_id
                click.echo(f"More events may exist. Use --start-event-id {last_id}")

        except Exception as e:
            click.echo(f"Error: Failed to list audit events: {e}", err=True)
            sys.exit(1)
        finally:
            await repo.close()

    asyncio.run(_list())


# -------------------------------------------------------------------------
# Usage commands
# -------------------------------------------------------------------------


@cli.group()
def usage() -> None:
    """Usage snapshot commands."""
    pass


@usage.command("list")
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
@click.option(
    "--entity-id",
    "-e",
    help="Entity ID to query (required unless --resource is provided)",
)
@click.option(
    "--resource",
    "-r",
    help="Resource name filter (required if --entity-id is not provided)",
)
@click.option(
    "--window",
    "-w",
    type=click.Choice(["hourly", "daily"]),
    help="Filter by window type",
)
@click.option(
    "--start",
    help="Start time (ISO format, e.g., 2024-01-01T00:00:00Z)",
)
@click.option(
    "--end",
    help="End time (ISO format, e.g., 2024-01-31T23:59:59Z)",
)
@click.option(
    "--limit",
    "-l",
    default=100,
    type=int,
    help="Maximum number of snapshots to return (default: 100)",
)
@click.option(
    "--plot",
    "-p",
    is_flag=True,
    help="Display as ASCII charts instead of table (requires: pip install 'zae-limiter[plot]')",
)
def usage_list(
    name: str,
    region: str | None,
    endpoint_url: str | None,
    entity_id: str | None,
    resource: str | None,
    window: str | None,
    start: str | None,
    end: str | None,
    limit: int,
    plot: bool,
) -> None:
    """List usage snapshots."""
    from .exceptions import ValidationError
    from .repository import Repository

    if entity_id is None and resource is None:
        click.echo("Error: Either --entity-id or --resource must be provided", err=True)
        sys.exit(1)

    async def _list() -> None:
        try:
            repo = Repository(name, region, endpoint_url)
        except ValidationError as e:
            click.echo(f"Error: {e.reason}", err=True)
            sys.exit(1)

        try:
            snapshots, next_key = await repo.get_usage_snapshots(
                entity_id=entity_id,
                resource=resource,
                window_type=window,
                start_time=start,
                end_time=end,
                limit=limit,
            )

            if not snapshots:
                click.echo("No usage snapshots found")
                return

            use_table = not plot
            if plot:
                # Use PlotFormatter for ASCII charts
                from .visualization import UsageFormatter, format_usage_snapshots

                try:
                    output = format_usage_snapshots(snapshots, formatter=UsageFormatter.PLOT)
                    click.echo(output)
                except ImportError as e:
                    # Graceful fallback for missing asciichartpy
                    click.echo(f"Warning: {e}", err=True)
                    click.echo("Falling back to table format...", err=True)
                    use_table = True

            if use_table:
                # Use TableRenderer for box-drawing table (auto-sized columns)
                from .visualization import TableRenderer

                click.echo()
                click.echo("Usage Snapshots")
                click.echo()

                headers = ["Window Start", "Type", "Resource", "Entity", "Events", "Counters"]
                rows: list[list[str]] = []
                for snap in snapshots:
                    counters_str = ", ".join(f"{k}={v:,}" for k, v in sorted(snap.counters.items()))
                    rows.append(
                        [
                            snap.window_start,
                            snap.window_type,
                            snap.resource,
                            snap.entity_id,
                            str(snap.total_events),
                            counters_str,
                        ]
                    )

                renderer = TableRenderer()
                click.echo(renderer.render(headers, rows))

            click.echo()
            click.echo(f"Total: {len(snapshots)} snapshots")

            if next_key:
                click.echo()
                click.echo("More snapshots exist. Use pagination to see more.")

        except ValueError as e:
            click.echo(f"Error: {e}", err=True)
            sys.exit(1)
        except Exception as e:
            click.echo(f"Error: Failed to list usage snapshots: {e}", err=True)
            sys.exit(1)
        finally:
            await repo.close()

    asyncio.run(_list())


@usage.command("summary")
@click.option(
    "--name",
    "-n",
    default="limiter",
    help="Resource identifier (will be prefixed with 'ZAEL-'). Default: limiter",
)
@click.option(
    "--region",
    help="AWS region (default: use boto3 defaults)",
)
@click.option(
    "--endpoint-url",
    help=(
        "AWS endpoint URL "
        "(e.g., http://localhost:4566 for LocalStack, or other AWS-compatible services)"
    ),
)
@click.option(
    "--entity-id",
    "-e",
    help="Entity ID to query (required unless --resource is provided)",
)
@click.option(
    "--resource",
    "-r",
    help="Resource name filter (required if --entity-id is not provided)",
)
@click.option(
    "--window",
    "-w",
    type=click.Choice(["hourly", "daily"]),
    help="Filter by window type",
)
@click.option(
    "--start",
    help="Start time (ISO format, e.g., 2024-01-01T00:00:00Z)",
)
@click.option(
    "--end",
    help="End time (ISO format, e.g., 2024-01-31T23:59:59Z)",
)
def usage_summary(
    name: str,
    region: str | None,
    endpoint_url: str | None,
    entity_id: str | None,
    resource: str | None,
    window: str | None,
    start: str | None,
    end: str | None,
) -> None:
    """Show aggregated usage summary."""
    from .exceptions import ValidationError
    from .repository import Repository

    if entity_id is None and resource is None:
        click.echo("Error: Either --entity-id or --resource must be provided", err=True)
        sys.exit(1)

    async def _summary() -> None:
        try:
            repo = Repository(name, region, endpoint_url)
        except ValidationError as e:
            click.echo(f"Error: {e.reason}", err=True)
            sys.exit(1)

        try:
            summary = await repo.get_usage_summary(
                entity_id=entity_id,
                resource=resource,
                window_type=window,
                start_time=start,
                end_time=end,
            )

            if summary.snapshot_count == 0:
                click.echo("No usage data found matching the criteria")
                return

            # Display summary header
            click.echo()
            click.echo("Usage Summary")
            click.echo()
            if entity_id:
                click.echo(f"Entity:     {entity_id}")
            if resource:
                click.echo(f"Resource:   {resource}")
            if window:
                click.echo(f"Window:     {window}")
            click.echo(f"Snapshots:  {summary.snapshot_count}")
            if summary.min_window_start and summary.max_window_start:
                click.echo(f"Time Range: {summary.min_window_start} to {summary.max_window_start}")
            click.echo()

            # Table of counters
            headers = ["Limit", "Total", "Average"]
            rows: list[list[str]] = []
            for limit_name in sorted(summary.total.keys()):
                total = summary.total[limit_name]
                avg = summary.average.get(limit_name, 0.0)
                rows.append([limit_name, f"{total:,}", f"{avg:,.2f}"])

            from .visualization import TableRenderer

            renderer = TableRenderer(alignments=["l", "r", "r"])
            click.echo(renderer.render(headers, rows))
            click.echo()

        except ValueError as e:
            click.echo(f"Error: {e}", err=True)
            sys.exit(1)
        except Exception as e:
            click.echo(f"Error: Failed to get usage summary: {e}", err=True)
            sys.exit(1)
        finally:
            await repo.close()

    asyncio.run(_summary())


if __name__ == "__main__":
    cli()
