"""
Dazzle serve command.

Start the development server with frontend and backend.

Port Allocation:
    By default, ports are deterministically assigned based on project name
    to prevent collisions when running multiple Dazzle instances. Each project
    gets a consistent port pair (UI + API) derived from its name hash.

    Override with --port and --api-port for explicit control.
"""

import atexit
import http.server
import os
import socketserver
import tempfile
from pathlib import Path
from typing import Any

import typer

from dazzle.cli.utils import load_project_appspec
from dazzle.core.environment import (
    get_dazzle_env,
    should_enable_test_endpoints,
)
from dazzle.core.errors import DazzleError, ParseError
from dazzle.core.lint import lint_appspec
from dazzle.core.manifest import load_manifest
from dazzle.core.sitespec_loader import load_sitespec, sitespec_exists

from .ports import (
    clear_runtime_file,
    find_available_ports,
    write_runtime_file,
)
from .production import configure_production_logging, validate_production_env


def _validate_infrastructure() -> tuple[str, str]:
    """Validate that required infrastructure env vars are set.

    Returns:
        (database_url, redis_url) tuple.

    Raises:
        SystemExit: If required env vars are missing and
            DAZZLE_SKIP_INFRA_CHECK is not set.
    """
    if os.environ.get("DAZZLE_SKIP_INFRA_CHECK") == "1":
        return os.environ.get("DATABASE_URL", ""), os.environ.get("REDIS_URL", "")

    missing: list[str] = []
    database_url = os.environ.get("DATABASE_URL", "")
    redis_url = os.environ.get("REDIS_URL", "")

    if not database_url:
        missing.append("DATABASE_URL")
    if not redis_url:
        missing.append("REDIS_URL")

    if missing:
        typer.echo("Dazzle requires PostgreSQL + Redis infrastructure.", err=True)
        typer.echo(f"Missing environment variables: {', '.join(missing)}", err=True)
        typer.echo("", err=True)
        typer.echo("Set them in .env or export before running:", err=True)
        typer.echo("  export DATABASE_URL=postgresql://localhost:5432/dazzle_dev", err=True)
        typer.echo("  export REDIS_URL=redis://localhost:6379/0", err=True)
        typer.echo("", err=True)
        typer.echo(
            "Skip this check with DAZZLE_SKIP_INFRA_CHECK=1 (tests only).",
            err=True,
        )
        raise typer.Exit(code=1)

    # Normalize postgres:// → postgresql://
    if database_url.startswith("postgres://"):
        database_url = database_url.replace("postgres://", "postgresql://", 1)

    return database_url, redis_url


def _print_infra_banner(
    database_url: str, redis_url: str, event_tier: str, process_backend: str
) -> None:
    """Print infrastructure status banner after validation passes."""
    typer.echo("Dazzle Infrastructure")
    typer.echo(f"  PostgreSQL: {database_url}")
    typer.echo(f"  Redis:      {redis_url}")
    typer.echo(f"  Event Bus:  {event_tier}")
    typer.echo(f"  Processes:  {process_backend}")
    typer.echo()


def serve_command(
    manifest: str = typer.Option("dazzle.toml", "--manifest", "-m"),
    port: int = typer.Option(None, "--port", "-p", help="Frontend port (auto-assigned if not set)"),
    api_port: int = typer.Option(
        None, "--api-port", help="Backend API port (auto-assigned if not set)"
    ),
    host: str = typer.Option("127.0.0.1", "--host", help="Host to bind to"),
    ui_only: bool = typer.Option(False, "--ui-only", help="Serve UI only (static files)"),
    backend_only: bool = typer.Option(
        False,
        "--backend-only",
        help="Serve backend API only (no frontend UI)",
    ),
    test_mode: bool | None = typer.Option(
        None,
        "--test-mode/--no-test-mode",
        help="Enable test endpoints. Default based on DAZZLE_ENV (enabled in development/test).",
    ),
    dev_mode: bool | None = typer.Option(
        None,
        "--dev-mode/--no-dev-mode",
        help="Enable dev control plane. Default based on DAZZLE_ENV (enabled in development only).",
    ),
    watch: bool = typer.Option(
        False,
        "--watch",
        "-w",
        help="Enable hot reload: watch DSL files and auto-refresh browser on changes",
    ),
    watch_source: bool = typer.Option(
        False,
        "--watch-source",
        "-W",
        help="Also watch framework source files (Python, CSS, JS). Implies --watch.",
    ),
    local: bool = typer.Option(
        False,
        "--local",
        help="Skip Docker infrastructure; require DATABASE_URL and REDIS_URL to be set manually",
    ),
    graphql: bool = typer.Option(
        False,
        "--graphql",
        help="Enable GraphQL endpoint at /graphql (requires strawberry-graphql)",
    ),
    database_url: str = typer.Option(
        "",
        "--database-url",
        help="PostgreSQL URL. Also reads DATABASE_URL env var.",
    ),
    auto_mock: bool | None = typer.Option(
        None,
        "--mock/--no-mock",
        help="Auto-start vendor mocks for API packs without credentials. Default: enabled in local mode.",
    ),
    workers: int | None = typer.Option(
        None,
        "--workers",
        help="Number of uvicorn workers (default: 1).",
    ),
    local_assets: bool | None = typer.Option(
        None,
        "--local-assets/--cdn-assets",
        help="Serve JS/CSS from local installation instead of CDN. Default: local in dev, CDN in --production.",
    ),
    production: bool = typer.Option(
        False,
        "--production",
        help="Production mode: bind 0.0.0.0, require DATABASE_URL, JSON logging, no dev features.",
    ),
) -> None:
    """
    Serve Dazzle app (backend API + UI with live data).

    By default, starts Postgres + Redis in Docker and runs the app locally.
    Use --local to skip Docker infrastructure (you must set DATABASE_URL
    and REDIS_URL yourself).

    Runs:
    - FastAPI backend on api-port (default 8000) with PostgreSQL persistence
    - Jinja2/HTMX frontend on port (default 3000)
    - Auto-migration for schema changes
    - Interactive API docs at http://host:api-port/docs

    Examples:
        dazzle serve                    # Start infra in Docker, run app locally
        dazzle serve --local            # No Docker; bring your own Postgres + Redis
        dazzle serve --watch            # With hot reload (DSL file watching)
        dazzle serve --backend-only     # API server only (for separate frontend)
        dazzle serve --port 4000        # Frontend on 4000
        dazzle serve --api-port 9000    # API on 9000
        dazzle serve --ui-only          # Static UI only (no API)
        dazzle serve --no-test-mode     # Disable E2E test endpoints
        dazzle serve --graphql          # Enable GraphQL at /graphql

    Hot reload (--watch):
        Watch DSL files for changes and auto-refresh browser.

    Related commands:
        dazzle stop                     # Stop dev infrastructure
        dazzle logs                     # View container logs
    """
    # Resolve project path from manifest
    manifest_path = Path(manifest).resolve()
    project_root = manifest_path.parent

    # Load manifest to get auth config and project name
    mf = None
    dev_config_override_test: bool | None = None
    try:
        mf = load_manifest(manifest_path)
        from dazzle.core.manifest import check_framework_version

        try:
            if mf:
                check_framework_version(mf)
        except SystemExit:
            raise
        auth_enabled = mf.auth.enabled
        project_name = mf.name or project_root.name
        # Get manifest dev config overrides (v0.24.0)
        dev_config_override_test = mf.dev.test_endpoints
    except Exception:
        auth_enabled = False
        project_name = project_root.name

    # Production mode: override settings for deployment
    if production:
        configure_production_logging()
        database_url_prod, redis_url_prod = validate_production_env()

        # Check for DSL files
        from dazzle.core.fileset import discover_dsl_files as _discover

        try:
            mf_check = load_manifest(manifest_path)
            dsl_files = _discover(project_root, mf_check)
        except Exception:
            dsl_files = []
        if not dsl_files:
            typer.echo(
                "No DSL files found in current directory. "
                "Run dazzle serve --production from your project root.",
                err=True,
            )
            raise typer.Exit(code=1)

        # Override settings
        host = "0.0.0.0"
        port_env = os.environ.get("PORT")
        if port_env:
            try:
                port = int(port_env)
            except ValueError:
                pass
        database_url = database_url_prod
        redis_url = redis_url_prod or ""
        os.environ["DATABASE_URL"] = database_url
        if redis_url:
            os.environ["REDIS_URL"] = redis_url

        # Disable dev features
        local = True  # Skip Docker infrastructure management
        watch = False
        watch_source = False
        enable_dev_mode = False
        enable_test_mode = False
        auto_mock = False

        # Production mode: refuse to start with pending migrations
        try:
            from alembic import command
            from alembic.config import Config as AlembicConfig
            from alembic.util.exc import CommandError

            alembic_dir = Path(__file__).resolve().parents[3] / "dazzle_back" / "alembic"
            cfg = AlembicConfig(str(alembic_dir / "alembic.ini"))
            cfg.set_main_option("script_location", str(alembic_dir))
            cfg.set_main_option("sqlalchemy.url", database_url)

            try:
                command.check(cfg)
            except CommandError:
                typer.echo(
                    "Cannot start in production mode: pending migrations detected. "
                    "Run 'dazzle db migrate' first.",
                    err=True,
                )
                raise typer.Exit(code=1)
        except ImportError:
            pass  # Alembic not installed, skip check

        env = get_dazzle_env()
    else:
        # Resolve dev_mode and test_mode based on:
        # 1. CLI option (if explicitly set)
        # 2. Manifest [dev] section (if set)
        # 3. Environment defaults (based on DAZZLE_ENV)
        # (v0.24.0 - environment-aware dev features)
        env = get_dazzle_env()

        if dev_mode is None:
            # CLI not set - enable in development env
            enable_dev_mode = env.value == "development"
        else:
            # CLI explicitly set - honor it
            enable_dev_mode = dev_mode

        if test_mode is None:
            # CLI not set - use manifest or environment default
            enable_test_mode = should_enable_test_endpoints(dev_config_override_test)
        else:
            # CLI explicitly set - honor it
            enable_test_mode = test_mode

        # Resolve DATABASE_URL: CLI flag → env → dazzle.toml → default
        from dazzle.core.manifest import resolve_database_url

        database_url = resolve_database_url(mf, explicit_url=database_url)
        redis_url = os.environ.get("REDIS_URL", "")

    # Allocate ports based on project name (deterministic hashing)
    # This prevents collisions when running multiple Dazzle instances
    allocation = find_available_ports(
        project_name=project_name,
        ui_port=port,
        api_port=api_port,
        host=host,
    )
    port = allocation.ui_port
    api_port = allocation.api_port

    # Show port allocation info if auto-assigned
    if allocation.ui_port != 3000 or allocation.api_port != 8000:
        typer.echo(f"Port allocation for '{project_name}':")
        typer.echo(f"  UI:  {allocation.ui_port}")
        typer.echo(f"  API: {allocation.api_port}")
        typer.echo()

    # Write runtime file for port discovery by E2E tests
    write_runtime_file(project_root, allocation)

    # Clean up runtime file on exit
    def cleanup_runtime() -> None:
        clear_runtime_file(project_root)

    atexit.register(cleanup_runtime)

    # --watch-source implies --watch
    if watch_source:
        watch = True

    # Docker infrastructure mode: start Postgres + Redis in Docker,
    # then run the app locally (same as --local but with Docker-provided services).
    if not local and not ui_only and not backend_only:
        try:
            from dazzle_ui.runtime.docker.utils import is_docker_available

            if is_docker_available():
                from dazzle.cli.runtime_impl.docker import (
                    start_dev_infrastructure,
                    stop_dev_infrastructure,
                )

                typer.echo("Starting dev infrastructure (Postgres + Redis) in Docker...")
                try:
                    infra_db_url, infra_redis_url = start_dev_infrastructure(
                        project_root=project_root,
                        project_name=project_name,
                    )
                    # Set env vars so the local app connects to Docker services
                    os.environ["DATABASE_URL"] = infra_db_url
                    os.environ["REDIS_URL"] = infra_redis_url
                    database_url = infra_db_url
                    redis_url = infra_redis_url

                    typer.echo("  PostgreSQL: ready")
                    typer.echo("  Redis:      ready")
                    typer.echo()

                    # Register shutdown to stop Docker services
                    def _stop_infra() -> None:
                        typer.echo("Stopping dev infrastructure...")
                        stop_dev_infrastructure(project_root)

                    atexit.register(_stop_infra)

                except RuntimeError as exc:
                    typer.echo(f"Docker infrastructure failed: {exc}", err=True)
                    typer.echo(
                        "Falling back to local mode (ensure DATABASE_URL and REDIS_URL are set)"
                    )
                    typer.echo()
            else:
                typer.echo("Docker not available, falling back to local mode")
                typer.echo("Install Docker for automatic Postgres + Redis setup")
                typer.echo()
        except ImportError:
            pass  # Docker runtime not available, fall through to local

    # Ensure env var is set for subcomponents that read it
    if database_url:
        os.environ["DATABASE_URL"] = database_url

    # Validate required infrastructure (PostgreSQL + Redis)
    validated_db_url, validated_redis_url = _validate_infrastructure()
    # Use validated URL if not already set
    if not database_url and validated_db_url:
        database_url = validated_db_url
        os.environ["DATABASE_URL"] = database_url
    if not redis_url and validated_redis_url:
        redis_url = validated_redis_url

    # Local mode execution
    try:
        from dazzle_back.runtime import FASTAPI_AVAILABLE
        from dazzle_ui.converters import compute_persona_default_routes, convert_appspec_to_ui
        from dazzle_ui.runtime import run_unified_server
    except ImportError as e:
        typer.echo(f"Dazzle runtime not available: {e}", err=True)
        typer.echo("Install with: pip install dazzle-dsl[serve]", err=True)
        raise typer.Exit(code=1)

    if not FASTAPI_AVAILABLE and not ui_only:
        typer.echo("FastAPI not installed. Use --ui-only or install:", err=True)
        typer.echo("  pip install fastapi uvicorn", err=True)
        raise typer.Exit(code=1)

    # Load AppSpec
    root = project_root

    try:
        appspec = load_project_appspec(root)

        errors, _ = lint_appspec(appspec)
        if errors:
            typer.echo("Cannot serve; spec has validation errors:", err=True)
            for err in errors:
                typer.echo(f"  ERROR: {err}", err=True)
            raise typer.Exit(code=1)

    except (ParseError, DazzleError) as e:
        typer.echo(f"Error loading spec: {e}", err=True)
        raise typer.Exit(code=1)

    # Auto-start vendor mocks if enabled (v0.32.0)
    mock_orch = None
    should_mock = auto_mock if auto_mock is not None else local
    if should_mock and not ui_only:
        try:
            from dazzle.testing.vendor_mock.orchestrator import (
                MockOrchestrator,
                discover_packs_from_appspec,
            )

            packs = discover_packs_from_appspec(appspec)
            if packs:
                mock_orch = MockOrchestrator.from_appspec(
                    appspec, base_port=9001, project_root=project_root
                )
                mock_orch.start()
                for _name, mock in mock_orch.vendors.items():
                    typer.echo(f"  • Mock: {mock.provider} on :{mock.port} ({mock.env_var})")
                # Store orchestrator for MCP tool access
                try:
                    from dazzle.mcp.server.state import set_mock_orchestrator

                    set_mock_orchestrator(mock_orch)
                except Exception:
                    pass  # MCP server may not be running
        except Exception as e:
            typer.echo(f"  Warning: Vendor mock setup failed: {e}", err=True)

    # Load SiteSpec if available (v0.16.0)
    sitespec_data = None
    if sitespec_exists(project_root):
        try:
            sitespec = load_sitespec(project_root)
            sitespec_data = sitespec.model_dump()
            typer.echo(f"  • SiteSpec: loaded ({len(sitespec.pages)} pages)")
        except Exception as e:
            typer.echo(f"Warning: Failed to load sitespec.yaml: {e}", err=True)
            # Continue without SiteSpec - it's optional

    if ui_only:
        # Serve UI only with static preview files
        from dazzle_ui.runtime.static_preview import generate_preview_files

        with tempfile.TemporaryDirectory() as tmpdir:
            preview_files = generate_preview_files(appspec, tmpdir)
            # Create an index.html that links to all previews
            if preview_files:
                # Copy the first list file as index.html
                first = preview_files[0]
                (Path(tmpdir) / "index.html").write_text(first.read_text())

            os.chdir(tmpdir)
            handler = http.server.SimpleHTTPRequestHandler
            typer.echo(f"\nServing Dazzle UI preview at http://{host}:{port}")
            typer.echo(f"  {len(preview_files)} preview files generated")
            typer.echo("Press Ctrl+C to stop\n")

            with socketserver.TCPServer((host, port), handler) as httpd:
                try:
                    httpd.serve_forever()
                except KeyboardInterrupt:
                    typer.echo("\nStopped.")
        return

    if backend_only:
        # Serve backend API only (no frontend UI)
        from dazzle_ui.runtime import run_backend_only

        typer.echo(f"Starting Dazzle backend for '{appspec.name}'...")
        typer.echo(f"  • {len(appspec.domain.entities)} entities")
        typer.echo(f"  • {len(appspec.surfaces)} surfaces")
        typer.echo("  • Database: PostgreSQL (DATABASE_URL)")
        if enable_test_mode:
            typer.echo("  • Test mode: ENABLED (/__test__/* endpoints available)")
        if graphql:
            typer.echo("  • GraphQL: ENABLED (/graphql endpoint)")
        typer.echo()
        typer.echo(f"API: http://{host}:{api_port}")
        typer.echo(f"Docs: http://{host}:{api_port}/docs")
        if graphql:
            typer.echo(f"GraphQL: http://{host}:{api_port}/graphql")
        typer.echo()

        run_backend_only(
            appspec=appspec,
            port=api_port,
            enable_test_mode=enable_test_mode,
            enable_dev_mode=enable_dev_mode,
            enable_graphql=graphql,
            host=host,
            sitespec_data=sitespec_data,
            project_root=project_root,
            redis_url=redis_url,
            workers=workers,
        )
        return

    # Full combined server with API + UI
    typer.echo(f"Starting Dazzle server for '{appspec.name}'...")

    # Convert specs (pass shell config from manifest)
    assert mf is not None, "Manifest must be loaded for combined server mode"
    ui_spec = convert_appspec_to_ui(appspec, shell_config=mf.shell)

    typer.echo(f"  • {len(appspec.domain.entities)} entities")
    typer.echo(f"  • {len(appspec.surfaces)} surfaces")
    typer.echo(f"  • {len(ui_spec.workspaces)} workspaces")
    typer.echo("  • Database: PostgreSQL (DATABASE_URL)")

    # Extract personas and scenarios for dev control plane (v0.8.5)
    # Compute default routes from workspace access rules (v0.23.0)
    persona_routes = compute_persona_default_routes(appspec.personas, appspec.workspaces)
    personas = [
        {
            "id": p.id,
            "label": p.label,
            "description": p.description,
            "goals": p.goals,
            "default_route": persona_routes.get(p.id),
        }
        for p in appspec.personas
    ]
    scenarios = [
        {
            "id": s.id,
            "name": s.name,
            "description": s.description,
            "demo_fixtures": [{"entity": f.entity, "records": f.records} for f in s.demo_fixtures],
            "seed_data_path": s.seed_data_path,
            "persona_entries": [
                {
                    "persona_id": e.persona_id,
                    "start_route": e.start_route,
                    "seed_script": e.seed_script,
                }
                for e in s.persona_entries
            ],
        }
        for s in appspec.scenarios
    ]

    if personas:
        typer.echo(f"  • {len(personas)} personas")
    if scenarios:
        typer.echo(f"  • {len(scenarios)} scenarios")

    typer.echo()

    # Run combined server
    # Show environment and mode status (v0.24.0)
    typer.echo(f"  • Environment: {env.value.upper()}")
    if enable_dev_mode:
        typer.echo("  • Dev mode: ENABLED (use --no-dev-mode or DAZZLE_ENV=production to disable)")
    if enable_test_mode:
        typer.echo("  • Test mode: ENABLED (/__test__/* endpoints available)")

    # Show hot reload status
    if watch:
        typer.echo("  • Hot reload: ENABLED (watching DSL files)")

    # Print infrastructure banner
    if redis_url:
        event_tier = "Redis Streams"
        process_backend = "Celery + Beat"
    elif database_url:
        event_tier = "PostgreSQL"
        process_backend = "EventBus (PostgreSQL)"
    else:
        event_tier = "Memory"
        process_backend = "None (set REDIS_URL or DATABASE_URL)"
    _print_infra_banner(database_url, redis_url, event_tier, process_backend)

    # Build theme overrides from manifest (v0.16.0)
    theme_overrides: dict[str, Any] = {}
    if mf.theme.colors:
        theme_overrides["colors"] = mf.theme.colors
    if mf.theme.shadows:
        theme_overrides["shadows"] = mf.theme.shadows
    if mf.theme.spacing:
        theme_overrides["spacing"] = mf.theme.spacing
    if mf.theme.radii:
        theme_overrides["radii"] = mf.theme.radii
    if mf.theme.custom:
        theme_overrides["custom"] = mf.theme.custom

    # Resolve local_assets: env var > CLI flag > default (local in dev, CDN in prod)
    if local_assets is None:
        env_val = os.environ.get("DAZZLE_LOCAL_ASSETS")
        if env_val is not None:
            use_local_assets = env_val == "1"
        else:
            use_local_assets = not production
    else:
        use_local_assets = local_assets

    run_unified_server(
        appspec=appspec,
        ui_spec=ui_spec,
        port=port,
        enable_test_mode=enable_test_mode,
        enable_dev_mode=enable_dev_mode,
        enable_auth=auth_enabled,
        auth_config=mf.auth if auth_enabled else None,
        host=host,
        enable_watch=watch,
        watch_source=watch_source,
        project_root=project_root,
        personas=personas,
        scenarios=scenarios,
        sitespec_data=sitespec_data,
        theme_preset=mf.theme.preset,
        theme_overrides=theme_overrides if theme_overrides else None,
        redis_url=redis_url,
        workers=workers,
        tenant_config=mf.tenant if mf.tenant.isolation != "none" else None,
        local_assets=use_local_assets,
    )
