"""
MCP Context Server implementation using FastMCP.

This server provides persistent multimodal context storage capabilities for LLM agents,
enabling shared memory across different conversation threads with support for text and images.
"""

import logging
import sys
import tomllib
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as pkg_version
from pathlib import Path
from typing import Literal
from typing import cast

from fastmcp import FastMCP
from starlette.requests import Request
from starlette.responses import JSONResponse

# ============================================================================
# CRITICAL: Logger Configuration MUST happen BEFORE importing app modules
# that trigger backend loading (app.backends, app.repositories)
# ============================================================================
from app.logger_config import config_logger
from app.settings import get_settings

settings = get_settings()
config_logger(settings.logging.level)
logger = logging.getLogger(__name__)

# Import auth factory for explicit auth configuration
from app.auth import create_auth_provider

# Import startup module for global state access
from app.backends import create_backend
from app.embeddings import create_embedding_provider
from app.errors import ConfigurationError
from app.errors import DependencyError
from app.errors import classify_provider_error

# Import middleware for client serialization compatibility
from app.middleware import JsonStringDeserializerMiddleware
from app.middleware import build_schema_map
from app.migrations import FtsMigrationStatus
from app.migrations import ProviderCheckResult

# Import migration functions from the migrations package
from app.migrations import apply_chunking_migration
from app.migrations import apply_content_hash_migration
from app.migrations import apply_fts_migration
from app.migrations import apply_function_search_path_migration
from app.migrations import apply_jsonb_merge_patch_migration
from app.migrations import apply_semantic_search_migration
from app.migrations import apply_summary_migration
from app.migrations import check_provider_dependencies
from app.migrations import check_vector_storage_dependencies
from app.migrations import estimate_migration_time
from app.migrations import handle_metadata_indexes
from app.migrations import reset_fts_migration_status
from app.repositories import RepositoryContainer

# Import from startup module - global state and initialization
from app.startup import DB_PATH
from app.startup import get_backend
from app.startup import get_embedding_provider
from app.startup import get_reranking_provider
from app.startup import get_summary_provider
from app.startup import init_database
from app.startup import prewarm_ollama_models
from app.startup import propagate_langsmith_settings
from app.startup import set_backend
from app.startup import set_chunking_service
from app.startup import set_embedding_provider
from app.startup import set_repositories
from app.startup import set_reranking_provider
from app.startup import set_summary_provider

# Backward compatibility re-exports for validation utilities
# Tests and external code may import these from app.server
from app.startup.validation import truncate_text
from app.startup.validation import validate_date_param
from app.startup.validation import validate_date_range

# Import validation utilities from startup (for internal use)
from app.startup.validation import validate_pool_timeout_for_embedding

# Import tool functions and registration helpers from app.tools
from app.tools import TOOL_ANNOTATIONS
from app.tools import generate_fts_description
from app.tools import is_tool_disabled
from app.tools import register_tool

# Backward compatibility re-exports
# These are intentionally re-exported for existing code that imports from app.server
__all__ = [
    # From app.migrations
    'FtsMigrationStatus',
    'ProviderCheckResult',
    'estimate_migration_time',
    # From app.startup (for tests)
    'DB_PATH',
    'MAX_IMAGE_SIZE_MB',
    'MAX_TOTAL_SIZE_MB',
    'init_database',
    # From app.tools (for tests)
    'TOOL_ANNOTATIONS',
    'is_tool_disabled',
    # From app.startup.validation (for tests)
    'truncate_text',
    'validate_date_param',
    'validate_date_range',
    # Tool functions (for tests)
    'delete_context',
    'delete_context_batch',
    'fts_search_context',
    'get_context_by_ids',
    'get_statistics',
    'hybrid_search_context',
    'list_threads',
    'search_context',
    'semantic_search_context',
    'store_context',
    'store_context_batch',
    'update_context',
    'update_context_batch',
]
# Import additional startup constants for backward compatibility (used by tests)
from app.startup import MAX_IMAGE_SIZE_MB
from app.startup import MAX_TOTAL_SIZE_MB
from app.tools import delete_context
from app.tools import delete_context_batch
from app.tools import fts_search_context
from app.tools import get_context_by_ids
from app.tools import get_statistics
from app.tools import hybrid_search_context
from app.tools import list_threads
from app.tools import search_context
from app.tools import semantic_search_context
from app.tools import store_context
from app.tools import store_context_batch
from app.tools import update_context
from app.tools import update_context_batch


def _get_server_version() -> str:
    """Get server version from package metadata or pyproject.toml fallback.

    Returns:
        Version string (e.g., '0.14.0') or 'unknown' if unavailable.
    """
    # Primary: installed package metadata (works for pip, uv, editable installs)
    try:
        return pkg_version('mcp-context-server')
    except PackageNotFoundError:
        pass

    # Fallback: read directly from pyproject.toml (for running from source)
    try:
        pyproject_path = Path(__file__).resolve().parents[1] / 'pyproject.toml'
        if pyproject_path.exists():
            with pyproject_path.open('rb') as f:
                data = tomllib.load(f)
            version = data.get('project', {}).get('version')
            if isinstance(version, str):
                return version
    except Exception:
        pass

    return 'unknown'


# Cache version at module load time
SERVER_VERSION = _get_server_version()


# Backward compatibility: alias for the old private function name
_reset_fts_migration_status = reset_fts_migration_status


# Lifespan context manager for FastMCP
@asynccontextmanager
async def lifespan(mcp: FastMCP[None]) -> AsyncGenerator[None, None]:
    """Manage server lifecycle - initialize on startup, cleanup on shutdown.

    This ensures that the database manager's background tasks run in the
    same event loop as FastMCP, preventing the hanging issue.

    Args:
        mcp: The FastMCP server instance for tool registration

    Yields:
        None: Control is yielded back to FastMCP during server operation
    """
    # Startup
    try:
        # 0) Apply temporary patches for upstream SDK bugs
        # Remove when MCP Python SDK fixes ClosedResourceError handling
        from app.patches import apply_session_crash_patches
        apply_session_crash_patches()

        # Create backend ONCE at the start - used throughout initialization and runtime
        backend = create_backend(backend_type=None, db_path=DB_PATH)
        await backend.initialize()
        set_backend(backend)
        # 1) Ensure schema exists using the shared backend
        await init_database(backend=backend)
        # 2) Handle metadata field indexing (configurable via METADATA_INDEXED_FIELDS)
        await handle_metadata_indexes(backend=backend)
        # 3) Apply semantic search migration if enabled using the shared backend
        await apply_semantic_search_migration(backend=backend)
        # 4) Apply jsonb_merge_patch migration for PostgreSQL (required for metadata_patch)
        await apply_jsonb_merge_patch_migration(backend=backend)
        # 5) Apply function search_path security fix for PostgreSQL
        await apply_function_search_path_migration(backend=backend)
        # 6) Apply FTS migration if enabled
        await apply_fts_migration(backend=backend)
        # 7) Apply chunking migration (1:N embedding relationship)
        await apply_chunking_migration(backend=backend)
        # 8) Apply summary column migration (always runs, column required for search display)
        await apply_summary_migration(backend=backend)
        # 9) Apply content_hash column migration (deduplication optimization)
        await apply_content_hash_migration(backend=backend)
        # 10) Validate pool timeout for embedding operations (PostgreSQL only)
        if backend.backend_type == 'postgresql':
            validate_pool_timeout_for_embedding()
        # 11) Initialize repositories with the backend
        repos = RepositoryContainer(backend)
        set_repositories(repos)

        # 12) Register core tools (annotations from TOOL_ANNOTATIONS in app.tools)
        # Additive tools (create new entries)
        register_tool(mcp, store_context)
        register_tool(mcp, store_context_batch)

        # Read-only tools (no modifications)
        register_tool(mcp, search_context)
        register_tool(mcp, get_context_by_ids)
        register_tool(mcp, list_threads)
        register_tool(mcp, get_statistics)

        # Update tools (destructive, not idempotent)
        register_tool(mcp, update_context)
        register_tool(mcp, update_context_batch)

        # Delete tools (destructive, idempotent)
        register_tool(mcp, delete_context)
        register_tool(mcp, delete_context_batch)

        # 13) Propagate LangSmith settings to os.environ BEFORE embedding provider init
        # This enables LangSmith SDK auto-detection when users configure via .env file
        propagate_langsmith_settings()

        # 14) Initialize embedding generation if enabled (BEFORE semantic search)
        # ENABLE_EMBEDDING_GENERATION controls: provider initialization, embedding generation in store/update
        # ENABLE_SEMANTIC_SEARCH controls: semantic_search_context tool registration ONLY
        if settings.embedding.generation_enabled:
            # Step 1: Check vector storage dependencies (provider-agnostic)
            vector_deps_available = await check_vector_storage_dependencies(backend.backend_type)

            if not vector_deps_available:
                raise ConfigurationError(
                    'ENABLE_EMBEDDING_GENERATION=true but vector storage dependencies not available. '
                    'Fix: Install provider dependencies (e.g., uv sync --extra embeddings-ollama) '
                    'OR set ENABLE_EMBEDDING_GENERATION=false to disable embeddings.',
                )

            # Step 2: Check provider-specific dependencies based on EMBEDDING_PROVIDER
            provider = settings.embedding.provider
            provider_check = await check_provider_dependencies(
                provider, settings.embedding, settings.ollama.host,
                auto_pull=settings.ollama.auto_pull,
                pull_timeout=settings.ollama.pull_timeout,
            )

            if not provider_check['available']:
                install_hint = provider_check.get('install_instructions') or 'Check provider configuration'
                reason = provider_check['reason'] or ''
                # Classify error based on reason (config vs dependency)
                error_class = classify_provider_error(reason)
                raise error_class(
                    f'ENABLE_EMBEDDING_GENERATION=true but {provider} provider dependencies not met. '
                    f'Reason: {reason}. '
                    f'Fix: {install_hint} '
                    f'OR set ENABLE_EMBEDDING_GENERATION=false to disable embeddings.',
                )

            # Step 3: Create and initialize provider
            try:
                embedding_provider = create_embedding_provider()
                await embedding_provider.initialize()

                # Verify provider is available
                if not await embedding_provider.is_available():
                    await embedding_provider.shutdown()
                    raise DependencyError(
                        f'ENABLE_EMBEDDING_GENERATION=true but {embedding_provider.provider_name} '
                        'is not available (service may be down). '
                        'Fix: Ensure the embedding service is running and accessible '
                        'OR set ENABLE_EMBEDDING_GENERATION=false to disable embeddings.',
                    )

                set_embedding_provider(embedding_provider)
                logger.info(
                    f'Embedding generation enabled with provider: {embedding_provider.provider_name} '
                    f'(model: {settings.embedding.model})',
                )

            except ImportError as e:
                raise ConfigurationError(
                    f'ENABLE_EMBEDDING_GENERATION=true but provider import failed: {e}. '
                    f'Fix: Install provider dependencies (e.g., uv sync --extra embeddings-{provider}) '
                    f'OR set ENABLE_EMBEDDING_GENERATION=false to disable embeddings.',
                ) from e
            except (ConfigurationError, DependencyError):
                raise  # Re-raise our specific error types
            except Exception as e:
                # Unknown initialization errors are treated as dependency issues (may recover)
                raise DependencyError(
                    f'ENABLE_EMBEDDING_GENERATION=true but initialization failed: {e}. '
                    f'Fix: Check provider configuration and service availability '
                    f'OR set ENABLE_EMBEDDING_GENERATION=false to disable embeddings.',
                ) from e
        else:
            set_embedding_provider(None)
            logger.info('Embedding generation disabled (ENABLE_EMBEDDING_GENERATION=false)')

        # 15) Initialize reranking provider if enabled
        # Reranking improves search precision by re-scoring results with a cross-encoder
        if settings.reranking.enabled:
            try:
                from app.reranking import create_reranking_provider

                reranking_provider = create_reranking_provider()
                await reranking_provider.initialize()

                # Verify provider is available
                if not await reranking_provider.is_available():
                    await reranking_provider.shutdown()
                    logger.warning(
                        f'Reranking provider {reranking_provider.provider_name} not available. '
                        'Search results will not be reranked.',
                    )
                    set_reranking_provider(None)
                else:
                    set_reranking_provider(reranking_provider)
                    logger.info(
                        f'Reranking enabled with provider: {reranking_provider.provider_name} '
                        f'(model: {reranking_provider.model_name})',
                    )
            except ImportError as e:
                logger.warning(
                    f'Reranking dependencies not installed: {e}. '
                    f'Install with: uv sync --extra reranking. '
                    'Search results will not be reranked.',
                )
                set_reranking_provider(None)
            except Exception as e:
                logger.warning(
                    f'Failed to initialize reranking provider: {e}. '
                    'Search results will not be reranked.',
                )
                set_reranking_provider(None)
        else:
            set_reranking_provider(None)
            logger.info('Reranking disabled (ENABLE_RERANKING=false)')

        # 16) Initialize chunking service if enabled
        # Chunking splits long documents into smaller pieces for better semantic search quality
        if settings.chunking.enabled:
            try:
                from app.services import ChunkingService

                chunking_service = ChunkingService(
                    enabled=settings.chunking.enabled,
                    chunk_size=settings.chunking.size,
                    chunk_overlap=settings.chunking.overlap,
                )
                set_chunking_service(chunking_service)
                logger.info(
                    f'Chunking enabled (size={settings.chunking.size}, '
                    f'overlap={settings.chunking.overlap})',
                )
            except ImportError as e:
                logger.warning(
                    f'Chunking dependencies not installed: {e}. '
                    f'Install with: uv sync --extra embeddings-ollama. '
                    'Text will be embedded as single chunks.',
                )
                set_chunking_service(None)
            except Exception as e:
                logger.warning(
                    f'Failed to initialize chunking service: {e}. '
                    'Text will be embedded as single chunks.',
                )
                set_chunking_service(None)
        else:
            set_chunking_service(None)
            logger.info('Chunking disabled (ENABLE_CHUNKING=false)')

        # 17) Initialize summary provider if enabled
        if settings.summary.generation_enabled:
            # Step 1: Check provider-specific dependencies based on SUMMARY_PROVIDER
            from app.migrations import check_summary_provider_dependencies

            summary_provider_name = settings.summary.provider
            summary_check = await check_summary_provider_dependencies(
                summary_provider_name, settings.summary, settings.ollama.host,
                auto_pull=settings.ollama.auto_pull,
                pull_timeout=settings.ollama.pull_timeout,
            )

            if not summary_check['available']:
                install_hint = summary_check.get('install_instructions') or 'Check provider configuration'
                reason = summary_check['reason'] or ''
                error_class = classify_provider_error(reason)
                raise error_class(
                    f'ENABLE_SUMMARY_GENERATION=true but {summary_provider_name} provider dependencies not met. '
                    f'Reason: {reason}. '
                    f'Fix: {install_hint} '
                    f'OR set ENABLE_SUMMARY_GENERATION=false to disable summaries.',
                )

            # Step 2: Create and initialize provider
            try:
                from app.summary import create_summary_provider

                summary_provider = create_summary_provider()
                await summary_provider.initialize()

                if not await summary_provider.is_available():
                    await summary_provider.shutdown()
                    raise DependencyError(
                        f'ENABLE_SUMMARY_GENERATION=true but {summary_provider.provider_name} '
                        'is not available (service may be down). '
                        'Fix: Ensure the summary service is running and accessible '
                        'OR set ENABLE_SUMMARY_GENERATION=false to disable summaries.',
                    )

                set_summary_provider(summary_provider)
                logger.info(
                    f'Summary generation enabled with provider: {summary_provider.provider_name} '
                    f'(model: {settings.summary.model})',
                )

            except ImportError as e:
                raise ConfigurationError(
                    f'ENABLE_SUMMARY_GENERATION=true but provider import failed: {e}. '
                    f'Fix: Install provider dependencies (e.g., uv sync --extra summary-{settings.summary.provider}) '
                    f'OR set ENABLE_SUMMARY_GENERATION=false to disable summaries.',
                ) from e
            except (ConfigurationError, DependencyError):
                raise
            except Exception as e:
                raise DependencyError(
                    f'ENABLE_SUMMARY_GENERATION=true but initialization failed: {e}. '
                    f'Fix: Check provider configuration and service availability '
                    f'OR set ENABLE_SUMMARY_GENERATION=false to disable summaries.',
                ) from e
        else:
            set_summary_provider(None)
            logger.info('Summary generation disabled (ENABLE_SUMMARY_GENERATION=false)')

        # 18) Pre-warm Ollama models (load into memory for instant first-request response)
        await prewarm_ollama_models()

        # 19) Register semantic search tool if enabled AND embedding provider is available
        # This is a separate check because ENABLE_SEMANTIC_SEARCH only controls tool registration
        if settings.semantic_search.enabled:
            if get_embedding_provider() is not None:
                register_tool(mcp, semantic_search_context)
                logger.info('semantic_search_context registered')
            else:
                # User explicitly set ENABLE_EMBEDDING_GENERATION=false but ENABLE_SEMANTIC_SEARCH=true
                # This is a valid configuration - user wants no embeddings but enabled the flag
                logger.warning(
                    'ENABLE_SEMANTIC_SEARCH=true but ENABLE_EMBEDDING_GENERATION=false - '
                    'semantic_search_context NOT registered (no embedding provider available)',
                )
        else:
            logger.info('Semantic search disabled (ENABLE_SEMANTIC_SEARCH=false)')
            logger.info('semantic_search_context not registered (feature disabled)')

        # 20) Register FTS tool if enabled - ALWAYS register when ENABLE_FTS=true
        # The tool handles graceful degradation during migration
        if settings.fts.enabled:
            # Generate backend-specific FTS description for AI agents
            fts_description = generate_fts_description(
                cast(Literal['sqlite', 'postgresql'], backend.backend_type),
                settings.fts.language,
            )

            # Always register the FTS tool when enabled (DISABLED_TOOLS takes priority)
            # The tool itself checks migration status and returns informative response
            register_tool(mcp, fts_search_context, description=fts_description)

            # Check if FTS is available and log status
            fts_available = await repos.fts.is_available()
            if fts_available:
                logger.info(f'Full-text search enabled and available (backend: {backend.backend_type})')
            else:
                logger.warning('FTS enabled but index may need initialization or migration')
        else:
            logger.info('Full-text search disabled (ENABLE_FTS=false)')
            logger.info('fts_search_context not registered (feature disabled)')

        # 21) Register Hybrid Search tool if enabled AND at least one search mode is available
        if settings.hybrid_search.enabled:
            semantic_available_for_hybrid = (
                settings.semantic_search.enabled and get_embedding_provider() is not None
            )
            fts_available_for_hybrid = settings.fts.enabled

            if semantic_available_for_hybrid or fts_available_for_hybrid:
                # DISABLED_TOOLS takes priority over ENABLE_HYBRID_SEARCH
                register_tool(mcp, hybrid_search_context)
                modes_available = []
                if fts_available_for_hybrid:
                    modes_available.append('fts')
                if semantic_available_for_hybrid:
                    modes_available.append('semantic')
                logger.info(f'hybrid_search_context modes available: {modes_available}')
            else:
                logger.warning(
                    'Hybrid search enabled but no search modes available - feature disabled. '
                    'Enable ENABLE_FTS=true and/or ENABLE_SEMANTIC_SEARCH=true.',
                )
                logger.info('hybrid_search_context not registered (no search modes available)')
        else:
            logger.info('Hybrid search disabled (ENABLE_HYBRID_SEARCH=false)')
            logger.info('hybrid_search_context not registered (feature disabled)')

        # 22) Register schema-aware JSON string deserializer middleware
        # Handles client serialization issues where list/dict params arrive as JSON strings
        # Must run AFTER all tool registrations so schema map includes all tools
        all_tools = await mcp.list_tools(run_middleware=False)
        schema_map = build_schema_map(all_tools)
        if schema_map:
            mcp.add_middleware(JsonStringDeserializerMiddleware(schema_map))
            logger.info(
                'JSON string deserializer middleware registered '
                '(protecting %d tools, %d parameters)',
                len(schema_map),
                sum(len(params) for params in schema_map.values()),
            )
        else:
            logger.info(
                'JSON string deserializer middleware not needed '
                '(no complex params found)',
            )

        logger.info(f'MCP Context Server initialized (backend: {backend.backend_type})')
    except Exception as e:
        logger.error(f'Failed to initialize server: {e}')
        startup_backend = get_backend()
        if startup_backend:
            await startup_backend.shutdown()
        raise

    # Yield control to FastMCP
    yield

    # Shutdown
    logger.info('Shutting down MCP Context Server')
    # At this point, startup succeeded and _backend must be set
    shutdown_backend = get_backend()
    assert shutdown_backend is not None
    try:
        await shutdown_backend.shutdown()
    except Exception as e:
        logger.error(f'Error during shutdown: {e}')
    finally:
        # Shutdown reranking provider if initialized
        shutdown_reranking_provider = get_reranking_provider()
        if shutdown_reranking_provider is not None:
            try:
                await shutdown_reranking_provider.shutdown()
            except Exception as e:
                logger.error(f'Error shutting down reranking provider: {e}')

        # Shutdown embedding provider if initialized
        shutdown_embedding_provider = get_embedding_provider()
        if shutdown_embedding_provider is not None:
            try:
                await shutdown_embedding_provider.shutdown()
            except Exception as e:
                logger.error(f'Error shutting down embedding provider: {e}')

        # Shutdown summary provider if initialized
        shutdown_summary_provider = get_summary_provider()
        if shutdown_summary_provider is not None:
            try:
                await shutdown_summary_provider.shutdown()
            except Exception as e:
                logger.error(f'Error shutting down summary provider: {e}')

        set_backend(None)
        set_repositories(None)
        set_embedding_provider(None)
        set_reranking_provider(None)
        set_chunking_service(None)
        set_summary_provider(None)
    logger.info('MCP Context Server shutdown complete')


def main() -> None:
    """Main entry point for the MCP Context Server.

    Supports both stdio (default) and HTTP transport modes:
    - stdio: Default for local process spawning (uv run mcp-context-server)
    - http: For Docker/remote deployments (set MCP_TRANSPORT=http)

    Initialization and shutdown are handled by the lifespan context manager.

    Exit codes follow BSD sysexits.h convention for supervisor integration:
    - 0: Normal shutdown
    - 69 (EX_UNAVAILABLE): External dependency unavailable (supervisor may retry with backoff)
    - 78 (EX_CONFIG): Configuration error (supervisor should NOT restart)
    - 1: General error (unknown cause)
    """
    try:
        # Log server version first (before any subsystem messages)
        logger.info(f'MCP Context Server v{SERVER_VERSION}')

        # Determine transport mode early (controls auth and health endpoint)
        transport = settings.transport.transport
        logger.info(f'Transport: {transport.upper()}')

        # Initialize authentication provider only for HTTP transports
        # Auth has no effect on stdio (MCP specification: local process communication)
        if transport == 'stdio':
            if settings.auth.provider != 'none':
                logger.warning(
                    'MCP_AUTH_PROVIDER=%s is configured but has no effect on stdio transport. '
                    'Authentication is only applicable to HTTP transports.',
                    settings.auth.provider,
                )
            auth_provider = None
        else:
            auth_provider = create_auth_provider()

        # Resolve server instructions (env var override or default)
        from app.instructions import resolve_instructions

        instructions_text = resolve_instructions(settings.instructions)

        # Create FastMCP server with lifespan management and explicit auth
        # mask_error_details=False exposes validation errors for LLM autocorrection
        mcp = FastMCP(
            name='mcp-context-server',
            version=SERVER_VERSION,
            instructions=instructions_text or None,
            lifespan=lifespan,
            mask_error_details=False,
            auth=auth_provider,
        )

        if transport == 'stdio':
            mcp.run(transport=transport, show_banner=False)
        else:
            # Register health check endpoint for container orchestration (HTTP only)
            async def _health_handler(_: Request) -> JSONResponse:
                """Health check endpoint for Docker/Kubernetes liveness probes."""
                return JSONResponse({'status': 'ok'})

            mcp.custom_route('/health', methods=['GET'])(_health_handler)

            host = settings.transport.host
            port = settings.transport.port
            logger.info(f'Server URL: http://{host}:{port}/mcp')

            if transport in ('http', 'streamable-http'):
                stateless_http = settings.transport.stateless_http
                if not stateless_http:
                    logger.warning(
                        'Stateless HTTP mode: disabled. Server-side session tracking is active. '
                        'This requires sticky sessions for horizontal scaling.',
                    )
                mcp.run(
                    transport=cast(Literal['stdio', 'http', 'sse', 'streamable-http'], transport),
                    host=host,
                    port=port,
                    stateless_http=stateless_http,
                    show_banner=False,
                )
            else:
                # SSE transport: stateless_http parameter is silently ignored by FastMCP
                mcp.run(
                    transport=cast(Literal['stdio', 'http', 'sse', 'streamable-http'], transport),
                    host=host,
                    port=port,
                    show_banner=False,
                )

    except KeyboardInterrupt:
        logger.info('Server shutdown requested')
    except ConfigurationError as e:
        # Configuration errors: missing packages, invalid settings, missing API keys
        # Exit code 78 (EX_CONFIG) signals supervisor NOT to restart
        logger.critical(f'Configuration error (will not retry): {e}')
        sys.exit(ConfigurationError.EXIT_CODE)
    except DependencyError as e:
        # Dependency errors: service down, model not pulled, network issues
        # Exit code 69 (EX_UNAVAILABLE) allows supervisor to retry with backoff
        logger.error(f'Dependency unavailable (may retry): {e}')
        sys.exit(DependencyError.EXIT_CODE)
    except Exception as e:
        logger.error(f'Server error: {e}')
        sys.exit(1)


if __name__ == '__main__':
    main()
