"""Sync command for TestIO MCP CLI.

Provides manual database sync with progress reporting, date filtering,
and status inspection. Complements background sync with explicit control.

Reference: DESIGN-CLI-SYNC-COMMAND.md
"""

import asyncio
import logging
import sys
from datetime import datetime
from typing import Any, NoReturn

from rich.console import Console
from rich.logging import RichHandler
from rich.progress import (
    BarColumn,
    MofNCompleteColumn,
    Progress,
    TaskProgressColumn,
    TextColumn,
    TimeElapsedColumn,
)
from rich.table import Table

from testio_mcp.client import TestIOClient
from testio_mcp.config import Settings
from testio_mcp.database import PersistentCache
from testio_mcp.utilities.date_utils import parse_flexible_date

console = Console()


async def sync_database(
    *,
    since: datetime | None = None,
    product_ids: list[int] | None = None,
    force: bool = False,
    nuke: bool = False,
    refresh: bool = False,
    incremental_only: bool = False,
    dry_run: bool = False,
    verbose: bool = False,
) -> dict[str, Any]:
    """Sync database with optional date filtering.

    Args:
        since: Only sync tests with end_at >= this date
        product_ids: Only sync these product IDs (overrides env var)
        force: Refresh all tests using upsert (non-destructive)
        nuke: Delete database and rebuild (destructive, requires confirmation)
        refresh: (DEPRECATED) Same as default - discover new tests AND update mutable tests
        incremental_only: Fast mode - discover new tests only (skip mutable test refresh)
        dry_run: Preview sync without making changes
        verbose: Show detailed logging

    Returns:
        Sync result dictionary with stats
    """
    # Load settings
    settings = Settings()

    # Show deprecation warning for --refresh flag
    if refresh:
        console.print(
            "[yellow]⚠️  Warning: --refresh flag is deprecated and will be "
            "removed in a future version.[/yellow]"
        )
        console.print(
            "[yellow]   Hybrid refresh (discover new + update mutable) is now "
            "the default behavior.[/yellow]"
        )
        console.print("[yellow]   Use --incremental-only for fast discovery-only mode.\n[/yellow]")

    # Apply product filtering
    if product_ids is None:
        product_ids = settings.TESTIO_PRODUCT_IDS

    # Create client and cache
    async with TestIOClient(
        base_url=settings.TESTIO_CUSTOMER_API_BASE_URL,
        api_token=settings.TESTIO_CUSTOMER_API_TOKEN,
        max_concurrent_requests=settings.MAX_CONCURRENT_API_REQUESTS,
        max_connections=settings.CONNECTION_POOL_SIZE,
        max_keepalive_connections=settings.CONNECTION_POOL_MAX_KEEPALIVE,
        timeout=settings.HTTP_TIMEOUT_SECONDS,
    ) as client:
        cache = PersistentCache(
            client=client,
            db_path=settings.TESTIO_DB_PATH,
            customer_id=settings.TESTIO_CUSTOMER_ID,
        )

        await cache.initialize()

        if verbose:
            console.print(f"[dim]Database: {cache.db_path}[/dim]")
            console.print(f"[dim]Customer ID: {cache.customer_id}[/dim]")
            if product_ids:
                console.print(f"[dim]Product filter: {product_ids}[/dim]")
            if since:
                console.print(f"[dim]Date filter (since): {since.isoformat()}[/dim]")

        # Dry-run mode: Show what would be synced
        if dry_run:
            console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")

            # Fetch products to show what would be synced
            response = await client.get("products")

            if verbose:
                console.print(f"[dim]Raw API response keys: {list(response.keys())}[/dim]")
                console.print(f"[dim]Response count field: {response.get('count', 'N/A')}[/dim]")

            all_products = response.get("products", [])

            if verbose:
                console.print(f"[dim]API returned {len(all_products)} products[/dim]")
                if all_products and len(all_products) > 0:
                    console.print(f"[dim]Sample product: {all_products[0]}[/dim]")

            # Apply product filter and categorize products
            if product_ids:
                products_to_sync = [p for p in all_products if p.get("id") in product_ids]
                products_filtered = [p for p in all_products if p.get("id") not in product_ids]
                if verbose:
                    console.print(
                        f"[dim]Filtered to {len(products_to_sync)} products "
                        f"matching IDs: {product_ids}[/dim]"
                    )
                    console.print(
                        f"[dim]Skipped {len(products_filtered)} products not matching filter[/dim]"
                    )
            else:
                products_to_sync = all_products
                products_filtered = []
                if verbose:
                    console.print(
                        f"[dim]No product filter applied, syncing all "
                        f"{len(products_to_sync)} products[/dim]"
                    )

            # Show preview table
            table = Table(title="Sync Preview")
            table.add_column("Product ID", style="cyan")
            table.add_column("Product Name", style="white")
            table.add_column("Last Sync", style="dim")
            table.add_column("Action")

            # Get env var filter for display (if any)
            settings = Settings()
            env_filter = settings.TESTIO_PRODUCT_IDS

            # Show products that will be synced
            for product in products_to_sync:
                product_id = product.get("id")

                # Get last sync time
                last_sync = await cache._repo.get_product_last_synced(product_id)
                last_sync_display = last_sync if last_sync else "Never"

                # Determine action based on mode flags and whether product exists in DB
                if nuke:
                    action = "Nuclear rebuild (destructive)"
                elif force:
                    action = "Force refresh (upsert)"
                elif refresh:
                    action = "Hybrid refresh (discover + update)"
                else:
                    # Check if product already exists in database
                    product_count = await cache._repo.count_product_tests(product_id)
                    action = "Incremental sync" if product_count > 0 else "Initial sync"

                table.add_row(
                    str(product_id),
                    product.get("name", ""),
                    last_sync_display,
                    f"[green]{action}[/green]",
                )

            # Show filtered products (if any)
            for product in products_filtered:
                product_id = product.get("id")

                # Get last sync time for filtered products too
                last_sync = await cache._repo.get_product_last_synced(product_id)
                last_sync_display = last_sync if last_sync else "Never"

                # Check if filtered by env var or CLI arg
                if env_filter and product_id not in env_filter:
                    filter_reason = "env var"
                else:
                    filter_reason = "CLI arg"

                table.add_row(
                    str(product_id),
                    product.get("name", ""),
                    last_sync_display,
                    f"[dim]Skipped ({filter_reason})[/dim]",
                )

            console.print(table)

            product_word = "products" if len(products_to_sync) != 1 else "product"
            summary = f"\n[bold]Would sync {len(products_to_sync)} {product_word}[/bold]"
            if products_filtered:
                summary += f" [dim](skipped {len(products_filtered)})[/dim]"
            console.print(summary)

            # Close cache connection before returning
            await cache.close()

            # Determine action type for result
            if nuke:
                action_type = "nuclear_rebuild"
            elif force:
                action_type = "force_refresh"
            elif incremental_only:
                action_type = "incremental_sync"
            else:
                # Default is now hybrid refresh (discover new + update mutable)
                action_type = "hybrid_refresh"

            return {
                "status": "dry_run",
                "products_previewed": len(products_to_sync),
                "products_filtered": len(products_filtered),
                "action": action_type,
            }

        # Nuclear rebuild mode: Confirm and delete existing data (STORY-021d)
        if nuke:
            console.print("[yellow]⚠️  DESTRUCTIVE: Nuclear rebuild mode[/yellow]")
            console.print("This will DELETE all local data and resync from scratch.\n")

            # Show current database stats
            total_tests = await cache.count_tests()
            total_products = await cache.count_products()
            db_size_mb = await cache.get_db_size_mb()

            console.print(f"Database: {cache.db_path} ({db_size_mb:.1f} MB)")
            console.print(f"Current data: {total_products} products, {total_tests} tests\n")

            # Manual confirmation required (unless --yes flag used elsewhere)
            confirm = console.input("[bold]Are you ABSOLUTELY sure? Type 'yes' to confirm: [/bold]")
            if confirm.lower() != "yes":
                console.print("[red]Nuclear rebuild cancelled[/red]")
                return {"status": "cancelled", "reason": "nuke_confirmation_declined"}

            # Delete existing data
            if product_ids:
                for product_id in product_ids:
                    await cache.delete_product_tests(product_id)
                console.print(f"[green]✓ Deleted data for {len(product_ids)} products[/green]")
            else:
                # Delete all products (recreate database)
                await cache.close()
                cache.db_path.unlink(missing_ok=True)
                await cache.initialize()
                console.print("[green]✓ Database deleted and recreated[/green]\n")

        # Force refresh mode: Upsert strategy (STORY-021d)
        if force:
            console.print("[cyan]⟳ Force refresh mode: Updating all tests (non-destructive)[/cyan]")
            console.print("This will update existing tests and add new tests using upsert.\n")

            # Fetch products to refresh
            response = await client.get("products")
            all_products = response.get("products", [])

            # Apply product filter
            if product_ids:
                products_to_sync = [p for p in all_products if p.get("id") in product_ids]
            else:
                products_to_sync = all_products

            if not products_to_sync:
                console.print("[yellow]No products to refresh[/yellow]")
                await cache.close()
                return {"status": "success", "products_synced": 0, "total_tests": 0}

            total_updated = 0
            total_added = 0

            # Progress bar for refresh (enhanced with test counts)
            with Progress(
                TextColumn("[progress.description]{task.description}"),
                BarColumn(),
                MofNCompleteColumn(),
                TaskProgressColumn(),
                TextColumn(
                    "[bold blue]{task.fields[tests_processed]} tests processed", justify="right"
                ),
                TimeElapsedColumn(),
                console=console,
            ) as progress:
                task = progress.add_task(
                    "[cyan]Refreshing products...",
                    total=len(products_to_sync),
                    tests_processed=0,  # Custom field for test count
                )

                for product in products_to_sync:
                    product_id = product.get("id")
                    product_name = product.get("name", "")

                    progress.update(
                        task,
                        description=f"[cyan]Refreshing {product_name} ({product_id})...",
                        tests_processed=0,  # Reset count for new product
                    )

                    # Create progress callback for real-time updates (shows current product only)
                    def update_progress(count: int) -> None:
                        progress.update(task, tests_processed=count)

                    # Refresh all tests for product
                    refresh_result = await cache.refresh_all_tests(
                        product_id=product_id,
                        since=since,
                        product_data=product,
                        progress_callback=update_progress,
                    )

                    total_updated += refresh_result.tests_updated
                    total_added += refresh_result.tests_added

                    console.print(
                        f"  {product_name}: {refresh_result.tests_updated} updated, "
                        f"{refresh_result.tests_added} new (total: {refresh_result.total_tests})"
                    )

                    if verbose and refresh_result.errors:
                        console.print(f"    [dim]Errors: {len(refresh_result.errors)}[/dim]")

                    progress.advance(task)

            await cache.close()

            # Success summary
            console.print(
                f"\n[green]✓ Refresh complete: {len(products_to_sync)} products, "
                f"{total_updated} tests updated, {total_added} new tests added[/green]"
            )

            return {
                "status": "success",
                "products_synced": len(products_to_sync),
                "tests_updated": total_updated,
                "tests_added": total_added,
            }

        # Incremental-only mode: Fast discovery (skip mutable test refresh)
        if incremental_only:
            console.print("[cyan]⚡ Fast mode: Discovering new tests only[/cyan]")
            console.print(
                "(Skipping mutable test refresh - use default sync for complete refresh)\n"
            )

            # Perform incremental sync (same as old default behavior)
            response = await client.get("products")
            all_products = response.get("products", [])

            if verbose:
                console.print(f"[dim]API returned {len(all_products)} products[/dim]")

            # Apply product filter
            if product_ids:
                products_to_sync = [p for p in all_products if p.get("id") in product_ids]
                if verbose:
                    console.print(
                        f"[dim]Filtered to {len(products_to_sync)} products "
                        f"matching IDs: {product_ids}[/dim]"
                    )
            else:
                products_to_sync = all_products
                if verbose:
                    console.print(
                        f"[dim]No product filter applied, syncing all "
                        f"{len(products_to_sync)} products[/dim]"
                    )

            if not products_to_sync:
                console.print("[yellow]No products to sync[/yellow]")
                return {"status": "success", "products_synced": 0, "total_tests": 0}

            # Progress bar for sync (enhanced with test counts)
            with Progress(
                TextColumn("[progress.description]{task.description}"),
                BarColumn(),
                MofNCompleteColumn(),
                TaskProgressColumn(),
                TextColumn("[bold blue]{task.fields[tests_fetched]} tests", justify="right"),
                TimeElapsedColumn(),
                console=console,
            ) as progress:
                task = progress.add_task(
                    "[cyan]Syncing products...",
                    total=len(products_to_sync),
                    tests_fetched=0,  # Custom field for test count
                )

                total_new_tests = 0
                total_checked = 0
                total_skipped_tests = 0

                for product in products_to_sync:
                    product_id = product.get("id")
                    product_name = product.get("name", "")

                    # Update progress description
                    progress.update(
                        task,
                        description=f"[cyan]Syncing {product_name} ({product_id})...",
                        tests_fetched=0,  # Reset count for new product
                    )

                    # Create progress callback for real-time updates (shows current product only)
                    def update_progress(count: int) -> None:
                        progress.update(task, tests_fetched=count)

                    # Sync product with date filtering and product metadata (STORY-021a)
                    result = await cache.sync_product_tests(
                        product_id=product_id,
                        since=since,
                        product_data=product,
                        progress_callback=update_progress,
                    )

                    total_new_tests += result.new_tests_count
                    total_checked += result.tests_checked
                    total_skipped_tests += len(result.skipped_tests)

                    # Print running total after each product (always visible)
                    console.print(
                        f"  {product_name}: {result.new_tests_count} new "
                        f"({result.tests_checked} checked, {total_new_tests} total new)"
                    )

                    if verbose:
                        pages = result.boundary_info.get("pages_fetched", 0)
                        hit_known = result.boundary_info.get("hit_known_test", False)
                        console.print(
                            f"    [dim]pages_fetched={pages}, "
                            f"hit_known_test={hit_known}, "
                            f"skipped={len(result.skipped_tests)}[/dim]"
                        )

                    progress.advance(task)

            await cache.close()

            # Success summary
            console.print(
                f"\n[green]✓ Sync complete: {len(products_to_sync)} products, "
                f"{total_checked} checked, {total_new_tests} new, "
                f"{total_skipped_tests} skipped[/green]"
            )

            return {
                "status": "success",
                "products_synced": len(products_to_sync),
                "total_checked": total_checked,
                "total_new_tests": total_new_tests,
                "total_skipped_tests": total_skipped_tests,
            }

        # Default mode: Hybrid refresh (discover new + update mutable) - STORY-021g
        # This is now the default behavior (used to be --refresh only)
        else:
            console.print("[cyan]⟳ Hybrid refresh mode: Discover new + update mutable tests[/cyan]")
            console.print("Part A: Discover new tests (incremental sync)")
            console.print(
                "Part B: Update mutable tests "
                "(customer_finalized, waiting, running, locked, initialized)\n"
            )

            # Fetch products to refresh
            response = await client.get("products")
            all_products = response.get("products", [])

            # Apply product filter
            if product_ids:
                products_to_sync = [p for p in all_products if p.get("id") in product_ids]
            else:
                products_to_sync = all_products

            if not products_to_sync:
                console.print("[yellow]No products to refresh[/yellow]")
                await cache.close()
                return {"status": "success", "products_synced": 0}

            total_new = 0
            total_updated = 0
            total_errors = 0

            # Progress bar for hybrid refresh (enhanced with test counts)
            with Progress(
                TextColumn("[progress.description]{task.description}"),
                BarColumn(),
                MofNCompleteColumn(),
                TaskProgressColumn(),
                TextColumn(
                    "[bold blue]{task.fields[tests_affected]} tests affected", justify="right"
                ),
                TimeElapsedColumn(),
                console=console,
            ) as progress:
                task = progress.add_task(
                    "[cyan]Refreshing products...",
                    total=len(products_to_sync),
                    tests_affected=0,  # Custom field for test count
                )

                for product in products_to_sync:
                    product_id = product.get("id")
                    product_name = product.get("name", "")

                    progress.update(
                        task, description=f"[cyan]Refreshing {product_name} ({product_id})..."
                    )

                    # Hybrid refresh for product
                    hybrid_result = await cache.refresh_active_cycle(
                        product_id=product_id,
                        product_data=product,
                    )

                    total_new += hybrid_result["new_tests_discovered"]
                    total_updated += hybrid_result["mutable_tests_updated"]
                    total_errors += len(hybrid_result["errors"])

                    # Update progress with running test count
                    tests_affected = total_new + total_updated
                    progress.update(task, tests_affected=tests_affected)

                    console.print(
                        f"  {product_name}: {hybrid_result['new_tests_discovered']} new, "
                        f"{hybrid_result['mutable_tests_updated']} updated"
                    )

                    if verbose and hybrid_result["errors"]:
                        console.print(f"    [dim]Errors: {len(hybrid_result['errors'])}[/dim]")

                    progress.advance(task)

            await cache.close()

            # Success summary
            console.print(
                f"\n[green]✓ Hybrid refresh complete: {len(products_to_sync)} products, "
                f"{total_new} new tests, {total_updated} tests updated"
            )
            if total_errors > 0:
                console.print(f"[yellow]⚠️  {total_errors} errors encountered[/yellow]")

            return {
                "status": "success",
                "products_synced": len(products_to_sync),
                "new_tests_discovered": total_new,
                "mutable_tests_updated": total_updated,
                "errors": total_errors,
            }


async def show_sync_status(*, verbose: bool = False) -> None:
    """Show current database sync status.

    Args:
        verbose: Show detailed sync information
    """
    settings = Settings()

    # Create cache (read-only)
    async with TestIOClient(
        base_url=settings.TESTIO_CUSTOMER_API_BASE_URL,
        api_token=settings.TESTIO_CUSTOMER_API_TOKEN,
        max_concurrent_requests=settings.MAX_CONCURRENT_API_REQUESTS,
        max_connections=settings.CONNECTION_POOL_SIZE,
        max_keepalive_connections=settings.CONNECTION_POOL_MAX_KEEPALIVE,
        timeout=settings.HTTP_TIMEOUT_SECONDS,
    ) as client:
        cache = PersistentCache(
            client=client,
            db_path=settings.TESTIO_DB_PATH,
            customer_id=settings.TESTIO_CUSTOMER_ID,
        )

        await cache.initialize()

        # Gather stats
        total_tests = await cache.count_tests()
        total_products = await cache.count_products()
        db_size_mb = await cache.get_db_size_mb()
        oldest_test_date = await cache.get_oldest_test_date()
        newest_test_date = await cache.get_newest_test_date()

        # Display status table
        table = Table(title="Database Sync Status")
        table.add_column("Metric", style="cyan")
        table.add_column("Value", style="white")

        table.add_row("Database Path", str(cache.db_path))
        table.add_row("Customer ID", str(cache.customer_id))
        table.add_row("Database Size", f"{db_size_mb:.2f} MB")
        table.add_row("Total Products", str(total_products))
        table.add_row("Total Tests", str(total_tests))
        table.add_row("Oldest Test Date", oldest_test_date or "N/A")
        table.add_row("Newest Test Date", newest_test_date or "N/A")

        console.print(table)

        # Verbose: Show per-product stats
        if verbose:
            synced_products = await cache.get_synced_products_info()

            if synced_products:
                console.print("\n[bold]Products Synced:[/bold]")
                product_table = Table()
                product_table.add_column("Product ID", style="cyan")
                product_table.add_column("Test Count", style="white")
                product_table.add_column("Last Synced", style="dim")

                for product_info in synced_products:
                    product_table.add_row(
                        str(product_info["id"]),
                        str(product_info["test_count"]),
                        product_info.get("last_synced", "N/A"),
                    )

                console.print(product_table)

        await cache.close()


def sync_command_main(args: Any) -> NoReturn:
    """Entry point for sync command.

    Args:
        args: Parsed command-line arguments from argparse
    """
    # Configure logging for CLI with Rich integration
    # Use RichHandler for console output (integrates nicely with progress bars)
    # Also log to file for detailed debugging
    from pathlib import Path

    from testio_mcp.utilities.logging import ShutdownErrorFilter

    settings = Settings()
    log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)

    # Rich handler for console (integrates with progress bars)
    rich_handler = RichHandler(
        console=console,
        rich_tracebacks=True,
        show_time=True,
        show_level=True,
        show_path=False,
    )
    rich_handler.setFormatter(logging.Formatter("%(message)s"))
    rich_handler.addFilter(ShutdownErrorFilter())

    # File handler for debugging
    log_file = Path(settings.LOG_FILE).expanduser()
    log_file.parent.mkdir(parents=True, exist_ok=True)
    file_handler = logging.FileHandler(log_file)
    file_handler.setFormatter(
        logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    )

    # Configure root logger
    root_logger = logging.getLogger()
    root_logger.setLevel(log_level)
    root_logger.handlers.clear()
    root_logger.addHandler(rich_handler)
    root_logger.addHandler(file_handler)

    # Configure package logger
    logger = logging.getLogger("testio_mcp")
    logger.setLevel(log_level)

    # Parse date arguments if provided
    # Priority: --since flag > TESTIO_SYNC_SINCE env var > None
    since: datetime | None = None

    try:
        # Determine which date value to use
        date_value = args.since if args.since else settings.TESTIO_SYNC_SINCE
        date_source = "CLI flag" if args.since else "TESTIO_SYNC_SINCE env var"

        if date_value:
            # Use parse_flexible_date from utilities (supports business terms, ISO, relative dates)
            iso_string = parse_flexible_date(date_value, start_of_day=True)
            # Convert ISO string back to datetime for internal use
            since = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))

            # Log where the date filter came from
            if args.since:
                logger.info(f"Using date filter from {date_source}: {args.since}")
            else:
                logger.info(
                    f"Using date filter from {date_source}: {settings.TESTIO_SYNC_SINCE} "
                    f"(override with --since to change)"
                )
    except Exception as e:
        console.print(f"[red]Error parsing date from {date_source}: {e}[/red]")
        sys.exit(1)

    # Validate mutual exclusivity (STORY-021d, STORY-021g)
    mode_flags = [args.force, args.nuke, args.refresh]
    mode_count = sum(bool(flag) for flag in mode_flags)
    if mode_count > 1:
        console.print("[red]Error: Cannot combine --force, --nuke, and --refresh flags[/red]")
        console.print("[dim]  --force: Non-destructive refresh (upsert strategy)[/dim]")
        console.print("[dim]  --nuke: Destructive rebuild (delete and resync)[/dim]")
        console.print("[dim]  --refresh: Discover new + update mutable tests[/dim]")
        sys.exit(1)

    # Parse product IDs if provided
    product_ids: list[int] | None = None
    if args.product_ids:
        # Validate against env var filter if set
        settings = Settings()
        if settings.TESTIO_PRODUCT_IDS:
            invalid_ids = [
                pid for pid in args.product_ids if pid not in settings.TESTIO_PRODUCT_IDS
            ]
            if invalid_ids:
                console.print(
                    f"[red]Error: Product ID(s) {', '.join(map(str, invalid_ids))} "
                    f"not in TESTIO_PRODUCT_IDS filter[/red]"
                )
                console.print(
                    f"[yellow]Allowed products: "
                    f"{', '.join(map(str, settings.TESTIO_PRODUCT_IDS))}[/yellow]"
                )
                console.print(
                    "[dim]Tip: This ensures manual syncs match your configured "
                    "product filter for scheduled syncs[/dim]"
                )
                sys.exit(1)
        product_ids = args.product_ids

    # Status flag: Show sync status and exit
    if args.status:
        asyncio.run(show_sync_status(verbose=args.verbose))
        sys.exit(0)

    # Sync database
    try:
        result = asyncio.run(
            sync_database(
                since=since,
                product_ids=product_ids,
                force=args.force,
                nuke=args.nuke,
                refresh=args.refresh,
                incremental_only=getattr(args, "incremental_only", False),
                dry_run=args.dry_run,
                verbose=args.verbose,
            )
        )

        # Exit code based on result
        if result.get("status") == "cancelled":
            sys.exit(1)

        sys.exit(0)

    except Exception as e:
        console.print(f"[red]Error during sync: {e}[/red]")
        if args.verbose:
            import traceback

            console.print(traceback.format_exc())
        sys.exit(1)
