"""LLMHosts proxy application factory -- assembles the FastAPI app.

Usage::

    from llmhosts.proxy import create_app

    app = create_app()
    # uvicorn.run(app, host="127.0.0.1", port=4000)

Pass a :class:`~llmhost.config.LLMHostsConfig` to ``create_app`` to enable
authentication, rate limiting, and CORS hardening driven by the
``[security]`` config section.
"""

from __future__ import annotations

import logging
import time
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Any

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from llmhosts import __version__
from llmhosts.config import LLMHostsConfig, SecurityConfig
from llmhosts.dashboard.serve import router as web_dashboard_router
from llmhosts.middleware import CorrelationIdMiddleware, IPRateLimitMiddleware
from llmhosts.proxy.anthropic_routes import router as anthropic_router
from llmhosts.proxy.audit_routes import router as audit_router
from llmhosts.proxy.auth import AuthMiddleware, ProxyKeyStore
from llmhosts.proxy.control_routes import router as control_router
from llmhosts.proxy.dashboard_routes import router as dashboard_router
from llmhosts.proxy.dispatcher import BackendDispatcher
from llmhosts.proxy.errors import ProxyError, generic_error_handler, proxy_error_handler
from llmhosts.proxy.key_routes import router as key_routes_router
from llmhosts.proxy.middleware import RequestIdMiddleware, RequestLoggingMiddleware, SecurityHeadersMiddleware
from llmhosts.proxy.models import ModelInfo, ModelListResponse
from llmhosts.proxy.openai_routes import router as openai_router
from llmhosts.proxy.ratelimit import RateLimitMiddleware
from llmhosts.proxy.topology_routes import router as topology_router
from llmhosts.security.adaptive_rate_limit import AdaptiveRateLimitMiddleware
from llmhosts.security.prompt_guard import PromptGuardMiddleware

if TYPE_CHECKING:
    from collections.abc import AsyncIterator

logger = logging.getLogger(__name__)

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


@asynccontextmanager
async def _lifespan(app: FastAPI) -> AsyncIterator[None]:
    """Manage startup / shutdown of the backend dispatcher."""
    from llmhosts.audit.logger import AuditLogger
    from llmhosts.config import llmhosts_dir
    from llmhosts.hive.karma import KarmaEngine
    from llmhosts.hive.manager import HiveManager
    from llmhosts.router.engine import Router

    # Read config from app state (set by create_app) if available
    pool_config = None
    remit_allowlist: list[str] | None = None
    stored_config = getattr(app.state, "config", None)
    if stored_config is not None:
        pool_config = getattr(stored_config, "connection_pool", None)
        remit = getattr(stored_config, "remit", None)
        if remit is not None and getattr(remit, "remit_allowlist", None):
            remit_allowlist = list(remit.remit_allowlist)

    dispatcher = BackendDispatcher(
        pool_config=pool_config,
        remit_allowlist=remit_allowlist,
    )
    await dispatcher.startup()

    # Attach the Router for intelligent routing and privacy enforcement.
    # Backends discovered during startup are registered with the Router so
    # it can make informed routing decisions (PII detection, cost, locality).
    router = Router()
    for backend_info in dispatcher.list_backends_info():
        router.register_backend(backend_info)
    dispatcher.attach_router(router)
    app.state.router = router

    # Hive karma: when user is in a Hive, attach karma for contribution/consumption and routing.
    data_dir = llmhosts_dir()
    hive_manager = HiveManager(data_dir)
    await hive_manager.initialize()
    if await hive_manager.get_hive() is not None:
        karma_engine = KarmaEngine(hive_manager)
        dispatcher.attach_hive_karma(hive_manager, karma_engine)

    # Audit logger: write request audit trail to SQLite.
    audit_db_path = data_dir / "audit.db"
    audit_logger = AuditLogger(audit_db_path)
    await audit_logger.initialize()
    dispatcher.attach_audit_logger(audit_logger)
    app.state.audit_logger = audit_logger

    # Phase 5: Billing store for marketplace metered billing (Stripe job can read get_since).
    try:
        from llmhosts.billing.store import BillingStore

        billing_db_path = data_dir / "billing.db"
        billing_store = BillingStore(billing_db_path)
        await billing_store.initialize()
        dispatcher.attach_billing_store(billing_store)
        app.state.billing_store = billing_store
        logger.info("BillingStore initialized at %s", billing_db_path)
    except Exception:
        logger.debug("BillingStore startup skipped (non-critical)")

    # RBAC middleware: team plan access control via API key → member lookup.
    from llmhosts.rbac.manager import TeamManager
    from llmhosts.rbac.middleware import RBACMiddleware

    team_manager = TeamManager(data_dir)
    await team_manager.initialize()
    rbac = RBACMiddleware(team_manager)
    app.state.team_manager = team_manager
    app.state.rbac = rbac

    app.state.dispatcher = dispatcher

    # TrustShield Pillar 5: TransparencyDashboard -- write privacy manifest on startup.
    try:
        from llmhosts.security.transparency import TransparencyConfig as _TConfig
        from llmhosts.security.transparency import TransparencyManager

        t_config = _TConfig()
        transparency = TransparencyManager(t_config)
        await transparency.write_manifest()
        app.state.transparency = transparency
        logger.info("TransparencyDashboard: privacy manifest written")
    except Exception:
        logger.debug("TransparencyDashboard startup skipped (non-critical)")

    # Record start time for uptime tracking (used by /control/status)
    app.state.start_time = time.time()

    logger.info("LLMHosts proxy v%s started (privacy enforcement active)", __version__)
    yield
    await audit_logger.close()
    store = getattr(app.state, "billing_store", None)
    if store is not None:
        await store.close()
    await dispatcher.shutdown()
    logger.info("LLMHosts proxy shut down")


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


def create_app(config: LLMHostsConfig | None = None) -> FastAPI:
    """Build and return the fully-configured FastAPI application.

    Parameters
    ----------
    config:
        When provided, auth middleware, rate limiting, and CORS are
        configured from the ``[security]`` and ``[server]`` sections.
        When *None*, safe defaults are used (no auth, permissive CORS).
    """
    security = config.security if config else SecurityConfig()

    app = FastAPI(
        title="LLMHosts Proxy",
        description="OpenAI + Anthropic compatible LLM proxy",
        version=__version__,
        lifespan=_lifespan,
    )

    # Store full config so lifespan can read connection_pool settings
    app.state.config = config

    # -- CORS ---------------------------------------------------------------
    # When cors_restrict is True, only allow the origins listed in
    # server.cors_origins. Otherwise keep the permissive default.
    cors_origins: list[str] = ["*"]
    if config and config.security.cors_restrict:
        cors_origins = config.server.cors_origins
    app.add_middleware(
        CORSMiddleware,
        allow_origins=cors_origins,
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # -- Custom middleware ---------------------------------------------------
    # Starlette processes middleware in reverse order of addition.
    # Addition order (last added = outermost = first to run):
    #   SecurityHeaders → CORS → Logging → AdaptiveRateLimit → IPRateLimit → PromptGuard → Auth → Correlation → RequestId
    # Request flow: RequestId → Correlation → Auth → PromptGuard → IPRateLimit → AdaptiveRateLimit → Logging → CORS → SecurityHeaders → handler
    app.add_middleware(SecurityHeadersMiddleware)
    app.add_middleware(RequestLoggingMiddleware)

    # TrustShield Pillar 3: AdaptiveShield -- sliding window with model-weighted costs.
    # Falls back to legacy token bucket if adaptive rate limit is disabled.
    if config and security.rate_limit.enabled:
        from llmhosts.security.adaptive_rate_limit import RateLimitConfig as _ARLConfig

        app.add_middleware(
            AdaptiveRateLimitMiddleware,
            config=_ARLConfig(**security.rate_limit.model_dump()),
        )
    else:
        app.add_middleware(
            RateLimitMiddleware,
            rpm=security.rate_limit_rpm,
            burst=security.rate_limit_burst,
        )

    # IP-based rate limiting for sensitive routes
    app.add_middleware(IPRateLimitMiddleware)

    # TrustShield Pillar 1: PromptGuard -- prompt injection defense.
    # Scans POST requests to /v1/chat/completions and /v1/messages.
    if config and security.prompt_guard.enabled and security.prompt_guard.mode != "off":
        from llmhosts.security.prompt_guard import PromptGuardConfig as _PGConfig

        app.add_middleware(
            PromptGuardMiddleware,
            config=_PGConfig(**security.prompt_guard.model_dump()),
        )

    # Key store for proxy API keys
    from llmhosts.config import llmhosts_dir

    key_store = ProxyKeyStore(data_dir=llmhosts_dir())
    app.state.key_store = key_store

    app.add_middleware(
        AuthMiddleware,
        key_store=key_store,
        require_auth=security.require_auth,
    )
    app.add_middleware(CorrelationIdMiddleware)
    app.add_middleware(RequestIdMiddleware)

    # -- Exception handlers --------------------------------------------------
    app.add_exception_handler(ProxyError, proxy_error_handler)  # type: ignore[arg-type]
    app.add_exception_handler(Exception, generic_error_handler)  # type: ignore[arg-type]

    # -- Routers -------------------------------------------------------------
    app.include_router(openai_router)
    app.include_router(anthropic_router)
    app.include_router(audit_router)
    app.include_router(dashboard_router)
    app.include_router(web_dashboard_router)
    app.include_router(key_routes_router)
    app.include_router(topology_router)
    app.include_router(control_router)

    # -- Built-in endpoints --------------------------------------------------

    @app.get("/health")
    async def health() -> dict[str, Any]:
        """Health check endpoint."""
        return {
            "status": "ok",
            "version": __version__,
            "timestamp": int(time.time()),
        }

    @app.get("/ready")
    async def ready() -> dict[str, Any]:
        """Kubernetes-style readiness probe."""
        return {
            "status": "ready",
            "version": __version__,
        }

    # -- Prometheus metrics endpoint -----------------------------------------
    # Mount Prometheus /metrics endpoint if prometheus-client is available
    try:
        from llmhosts.metrics import create_metrics_app

        if create_metrics_app is not None:
            metrics_app = create_metrics_app()
            app.mount("/metrics", metrics_app)  # type: ignore[arg-type]
            logger.info("Prometheus /metrics endpoint mounted")
    except ImportError:
        logger.debug("prometheus-client not installed -- /metrics endpoint disabled")

    @app.get("/v1/models")
    async def list_models(request: Request) -> JSONResponse:
        """List available models (OpenAI-compatible ``GET /v1/models``)."""
        dispatcher: BackendDispatcher = request.app.state.dispatcher
        raw_models = dispatcher.list_models()
        response = ModelListResponse(data=[ModelInfo(id=m["id"], owned_by=m["owned_by"]) for m in raw_models])
        return JSONResponse(content=response.model_dump())

    @app.get("/api/lan-nodes")
    async def lan_nodes() -> JSONResponse:
        """Return discovered LAN nodes via mDNS."""
        try:
            from llmhosts.discovery.mdns import get_discovered_nodes

            nodes = get_discovered_nodes()
            return JSONResponse(
                content={
                    "nodes": [n.model_dump() for n in nodes],
                    "count": len(nodes),
                }
            )
        except ImportError:
            return JSONResponse(
                content={"nodes": [], "count": 0, "error": "zeroconf not installed"},
            )

    return app
