"""
cortex/api/http_server.py — FastAPI HTTP Server for Cortex (Module 13 / M13).

Provides a REST API that mirrors the MCP tools, enabling web dashboard
integration (Nexus UI) and external programmatic access to Cortex memory.

Architecture:
    HTTP Server (FastAPI/uvicorn)
      → CortexPlugin (M13)
        → all other modules

Auth:
    Pass ``X-API-Key: <key>`` header when CORTEX_API_KEY env var is set.
    If CORTEX_API_KEY is not set, auth is disabled (open access — for local dev).

Usage::

    # Start with defaults (localhost:8000)
    cortex-http

    # Custom host/port with API key
    CORTEX_API_KEY=secret cortex-http --host 0.0.0.0 --port 8080

    # Programmatic usage
    import uvicorn
    from cortex.api.http_server import create_app
    app = create_app()
    uvicorn.run(app, host="127.0.0.1", port=8000)

Dependencies: FastAPI, uvicorn, cortex.api.plugin.CortexPlugin.
"""

from __future__ import annotations

import logging
import os
from contextlib import asynccontextmanager
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Optional import guard — fastapi/uvicorn may not be installed
# ---------------------------------------------------------------------------
try:
    from fastapi import FastAPI
    from fastapi.middleware.cors import CORSMiddleware
    import uvicorn
    _FASTAPI_AVAILABLE = True
except ImportError as _e:  # pragma: no cover
    _FASTAPI_AVAILABLE = False
    _IMPORT_ERROR = str(_e)

# Re-export _serialise at module level for backward compatibility
from cortex.api.deps import _serialise  # noqa: F401, E402
from cortex.api.deps import plugin_holder, auth_holder, make_auth_dependency, get_plugin  # noqa: E402
from cortex.api.deps import db_manager_holder, stats_db_holder  # noqa: E402

# Re-export models at module level for backward compatibility with tests
# that do: from cortex.api.http_server import SomeModel
try:
    from cortex.api.models import *  # noqa: F401, F403
except ImportError:
    pass


# ---------------------------------------------------------------------------
# Application factory
# ---------------------------------------------------------------------------

def create_app(
    config_path: Optional[str] = None,
    db_path: Optional[str] = None,
    api_key: Optional[str] = None,
    cors_origins: Optional[List[str]] = None,
) -> "FastAPI":
    """
    Create and configure the FastAPI application.

    Args:
        config_path:  Path to cortex.toml (default: auto-detect).
        db_path:      Override SQLite database path.
        api_key:      Required API key for X-API-Key header auth.
                      If None, falls back to ``CORTEX_API_KEY`` env var.
                      If that is also empty, auth is disabled.
        cors_origins: List of allowed CORS origins. Default: ["*"].

    Returns:
        Configured FastAPI application instance.
    """
    if not _FASTAPI_AVAILABLE:  # pragma: no cover
        raise ImportError(
            f"FastAPI is required for the HTTP server: {_IMPORT_ERROR}\n"
            "Install with: pip install 'evaos[api]'"
        )

    _api_key = api_key or os.getenv("CORTEX_API_KEY") or None
    # Supabase JWT secret — primary auth for hosted deployments (#542)
    _jwt_secret = os.getenv("SUPABASE_JWT_SECRET") or None
    # Configure allowed origins explicitly for security; empty list blocks all cross-origin requests by default.
    _cors_origins = cors_origins or []

    # Warn loudly when no auth is configured (#218)
    if _api_key is None and _jwt_secret is None:
        logger.warning(
            "=" * 60 + "\n"
            "⚠️  NO AUTH CONFIGURED\n"
            "The server is accessible WITHOUT authentication.\n"
            "Set SUPABASE_JWT_SECRET (hosted) or CORTEX_API_KEY (self-host)\n"
            "to enable authentication.\n"
            + "=" * 60
        )

    # ---- Lifespan ----------------------------------------------------------

    @asynccontextmanager
    async def lifespan(app: FastAPI):
        """Initialise CortexPlugin on startup; clean up on shutdown."""
        from cortex.api.plugin import CortexPlugin
        from cortex.config import validate_config as _validate_config
        plugin = CortexPlugin(config_path=config_path, db_path=db_path)
        # Config startup validation (#73)
        config_issues = _validate_config(plugin.config)
        for issue in config_issues:
            logger.warning("CortexConfig validation: %s", issue)
        if plugin.storage is not None:
            await plugin.storage.initialize()
        plugin_holder["plugin"] = plugin
        logger.info("CortexPlugin initialised — HTTP server ready")

        # ---- Debug logging init (#453) -----------------------------------
        try:
            from cortex.debug import init_debug_logging as _init_debug
            _cfg = getattr(plugin, "config", None)
            _debug_enabled = getattr(_cfg, "debug_logging_enabled", False)
            _debug_max = getattr(_cfg, "debug_log_max_entries", 10_000)
            _init_debug(enabled=_debug_enabled, max_entries=_debug_max)
            if _debug_enabled:
                logger.info(
                    "Debug logging enabled — ring buffer capacity: %d", _debug_max
                )
        except Exception as _exc:
            logger.warning("Debug logging init failed (non-fatal): %s", _exc)

        # ---- Hosted-mode: DBManager + StatsDB --------------------------------
        # Hosted mode is active when CORTEX_DATA_DIR is set.
        # Self-host mode (db_path provided, no CORTEX_DATA_DIR) skips both.
        _data_dir = os.getenv("CORTEX_DATA_DIR")
        _hosted_mode = bool(_data_dir)

        if _hosted_mode:
            from cortex.storage.db_manager import DBManager

            _cfg = getattr(plugin, "config", None)
            _max_open = int(
                getattr(_cfg, "db_pool_max_open", None) or 100
            )
            _idle_timeout = float(
                getattr(_cfg, "db_pool_idle_timeout_seconds", None) or 300.0
            )

            _db_manager = DBManager(
                data_dir=_data_dir,
                max_open=_max_open,
                idle_timeout=_idle_timeout,
            )
            await _db_manager.initialize()
            db_manager_holder["manager"] = _db_manager
            logger.info(
                "DBManager initialised (data_dir=%s, max_open=%d, idle_timeout=%gs)",
                _data_dir, _max_open, _idle_timeout,
            )

            from cortex.api.stats_db import StatsDB

            _stats_path = os.getenv("CORTEX_STATS_DB") or f"{_data_dir}/stats.db"
            _stats_db = StatsDB(db_path=_stats_path)
            await _stats_db.initialize()
            stats_db_holder["db"] = _stats_db
            logger.info("StatsDB initialised at %s", _stats_path)

        yield

        # Teardown: close storage if available
        if getattr(plugin, "storage", None) is not None:
            try:
                await plugin.storage.close()
            except Exception as exc:
                logger.warning("Error closing storage on shutdown: %s", exc)

        # Teardown: DBManager
        _mgr = db_manager_holder.get("manager")
        if _mgr is not None:
            try:
                await _mgr.close_all()
            except Exception as exc:
                logger.warning("Error closing DBManager on shutdown: %s", exc)
            db_manager_holder["manager"] = None

        # Teardown: StatsDB
        _sdb = stats_db_holder.get("db")
        if _sdb is not None:
            try:
                await _sdb.close()
            except Exception as exc:
                logger.warning("Error closing StatsDB on shutdown: %s", exc)
            stats_db_holder["db"] = None

        logger.info("CortexPlugin shut down")

    # ---- App ---------------------------------------------------------------

    app = FastAPI(
        title="Cortex HTTP API",
        description=(
            "REST interface for Cortex — identity-preserving cognitive "
            "infrastructure for AI agents."
        ),
        version="1.2.1",
        lifespan=lifespan,
    )

    # ---- CORS --------------------------------------------------------------

    app.add_middleware(
        CORSMiddleware,
        allow_origins=_cors_origins,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # ---- Rate-limiting middleware (#71) ------------------------------------

    from cortex.api.rate_limiter import RateLimiterMiddleware as _RLM

    _rl_rpm = int(os.getenv("CORTEX_RATE_LIMIT_RPM", "60"))
    _rl_burst = int(os.getenv("CORTEX_RATE_LIMIT_BURST", "10"))
    _rl_enabled = _rl_rpm > 0

    app.add_middleware(
        _RLM,
        requests_per_minute=_rl_rpm,
        burst=_rl_burst,
        enabled=_rl_enabled,
    )

    # ---- Usage-tracking middleware (#550) ----------------------------------
    # Only added in hosted mode (when stats_db_holder will be populated).
    # The stats_db instance is resolved lazily from the holder dict at runtime
    # (same pattern as _LazyRequestLogMiddleware) because lifespan hasn't run yet.

    _env_data_dir_check = os.getenv("CORTEX_DATA_DIR")
    if _env_data_dir_check:
        from cortex.api.middleware.usage_tracker import UsageTrackingMiddleware as _UTM

        class _LazyUsageTrackingMiddleware(_UTM):
            """Resolves stats_db lazily from stats_db_holder."""

            def __init__(self, app: Any, *, _stats_db_holder: dict) -> None:
                # Don't call super().__init__ — stats_db not ready yet
                self.app = app
                self._stats_db_holder = _stats_db_holder
                self._stats_db = None

            @property
            def _stats_db(self):  # type: ignore[override]
                return self._stats_db_holder.get("db")

            @_stats_db.setter
            def _stats_db(self, value):
                # Setter required for __init__ in parent; no-op here
                pass

        app.add_middleware(
            _LazyUsageTrackingMiddleware,
            _stats_db_holder=stats_db_holder,
        )

    # ---- Request-logging middleware (#182) ---------------------------------

    from cortex.api.middleware import RequestLogMiddleware as _RLMBase

    class _LazyRequestLogMiddleware(_RLMBase):
        """Resolves storage lazily from the shared plugin holder."""

        def __init__(self, app: Any, *, _plugin_holder: dict, owner_id: str = "default") -> None:
            from starlette.middleware.base import BaseHTTPMiddleware
            BaseHTTPMiddleware.__init__(self, app)
            self._plugin_holder = _plugin_holder
            self._owner_id = owner_id
            self._storage = None

        async def dispatch(self, request: Any, call_next: Any) -> Any:
            plugin = self._plugin_holder.get("plugin")
            if plugin is not None:
                self._storage = getattr(plugin, "storage", None)
                # Fix 3: resolve owner_id lazily from plugin config at request time
                _config = getattr(plugin, "config", None)
                if _config is not None:
                    self._owner_id = getattr(_config, "owner_id", self._owner_id)
            return await super().dispatch(request, call_next)

    app.add_middleware(
        _LazyRequestLogMiddleware,
        _plugin_holder=plugin_holder,
        owner_id="default",
    )

    # ---- Debug request/response logger middleware (#453) -------------------
    # Captures request+response bodies for the debug ring buffer.
    # Added before RequestTracerMiddleware so it runs inside the tracer
    # (tracer is outermost; debug logger sees the already-traced request).
    # Zero-cost no-op when CORTEX_DEBUG is falsy / debug logging disabled.

    from cortex.api.middleware.debug_logger import DebugLoggerMiddleware as _DLM

    app.add_middleware(_DLM)

    # ---- Request-tracing middleware (#453) ---------------------------------
    # Added LAST so it becomes the outermost layer (first to see each request).

    from cortex.api.middleware.request_tracer import RequestTracerMiddleware as _RTM

    app.add_middleware(_RTM)

    # ---- Auth dependency (#542) --------------------------------------------

    # Build AuthProvider when any credential is configured.
    #
    # Behaviour table (backward-compatible):
    #
    #   jwt_secret only  → JWT auth + public-endpoint exemptions, require_auth
    #                       defaults False (override via CORTEX_AUTH_REQUIRE)
    #   api_key only     → Legacy behaviour: all endpoints require the key,
    #                       no public-endpoint exemptions (backward compat)
    #   both configured  → JWT-first with public-endpoint exemptions,
    #                       require_auth defaults False
    #   neither          → Open access (local dev)

    _auth_provider = None
    _require_auth = False
    _public_endpoints: list = []

    if _jwt_secret:
        # JWT mode (hosted) — public endpoints exempt, require_auth configurable
        from cortex.api.auth import AuthProvider
        _auth_provider = AuthProvider(jwt_secret=_jwt_secret, api_key=_api_key)
        _public_endpoints = [
            "/health",
            "/api/v1/health",
            "/metrics",
            "/api/v1/metrics",
            "/docs",
            "/openapi.json",
            "/redoc",
            "/api/v1/config/providers",
        ]
        _require_auth = os.getenv("CORTEX_AUTH_REQUIRE", "false").lower() in (
            "1", "true", "yes"
        )
    elif _api_key:
        # API-key-only mode (self-host) — all endpoints require the key.
        # No public-endpoint exemptions to preserve backward compatibility.
        from cortex.api.auth import AuthProvider
        _auth_provider = AuthProvider(jwt_secret=None, api_key=_api_key)
        _public_endpoints = []
        _require_auth = True  # backward compat: key always required when set

    # Register provider in shared holder so get_current_user can find it
    auth_holder["auth_provider"] = _auth_provider
    auth_holder["require_auth"] = _require_auth
    auth_holder["public_endpoints"] = _public_endpoints

    verify_api_key = make_auth_dependency(
        _api_key,
        auth_provider=_auth_provider,
        require_auth=_require_auth,
        public_endpoints=_public_endpoints,
    )

    # ---- Routes ------------------------------------------------------------

    from cortex.api.routes import register_routes
    register_routes(app, verify_api_key)

    return app


# ---------------------------------------------------------------------------
# CLI entry point  (`cortex-http`)
# ---------------------------------------------------------------------------

def main() -> None:
    """
    Entry point for the ``cortex-http`` CLI command.

    Environment variables:
        CORTEX_API_KEY   — required API key (leave unset for open access)
        CORTEX_DB_PATH   — override SQLite database path
        CORTEX_CONFIG    — path to cortex.toml

    Example::

        CORTEX_API_KEY=secret cortex-http --host 0.0.0.0 --port 8080
    """
    import argparse

    parser = argparse.ArgumentParser(
        prog="cortex-http",
        description="Start the Cortex FastAPI HTTP server",
    )
    parser.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
    parser.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
    parser.add_argument("--reload", action="store_true", help="Enable auto-reload (dev mode)")
    parser.add_argument("--workers", type=int, default=1, help="Number of uvicorn workers")
    parser.add_argument(
        "--log-level",
        default="info",
        choices=["debug", "info", "warning", "error", "critical"],
        help="Log level",
    )
    parser.add_argument("--db-path", default=None, help="Override SQLite DB path")
    parser.add_argument("--config", default=None, help="Path to cortex.toml")
    parser.add_argument(
        "--cors-origins",
        default=None,
        help=(
            "Comma-separated list of allowed CORS origins. "
            "Defaults to none (restrictive). "
            "Set explicitly for development, e.g. --cors-origins http://localhost:3000"
        ),
    )
    args = parser.parse_args()

    if not _FASTAPI_AVAILABLE:  # pragma: no cover
        raise SystemExit(
            "FastAPI is required: pip install 'evaos[api]'"
        )

    cors_origins = (
        [o.strip() for o in args.cors_origins.split(",") if o.strip()]
        if args.cors_origins
        else []
    )

    app = create_app(
        config_path=args.config,
        db_path=args.db_path,
        cors_origins=cors_origins,
    )

    uvicorn.run(
        app,
        host=args.host,
        port=args.port,
        reload=args.reload,
        workers=args.workers if not args.reload else 1,
        log_level=args.log_level,
    )


if __name__ == "__main__":
    main()
