"""FastAPI application entry point."""

import asyncio
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any

from fastapi import APIRouter, Depends, FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from sqlalchemy import text

from shoreguard.client import ShoreGuardClient
from shoreguard.exceptions import GatewayNotConnectedError

from .auth import (
    bootstrap_admin_user,
    init_auth,
    is_setup_complete,
    require_auth,
    require_role,
)
from .cli import _import_filesystem_gateways, cli  # noqa: F401 — cli re-exported for entry point
from .deps import get_client, resolve_gateway
from .errors import register_error_handlers
from .metrics import metrics_middleware, shoreguard_info
from .metrics import router as metrics_router
from .pages import FRONTEND_DIR
from .pages import router as pages_router
from .routes import (
    approvals,
    audit,
    gateway,
    operations,
    policies,
    providers,
    sandboxes,
    templates,
    webhooks,
)
from .websocket import router as ws_router

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    """Application lifespan — initialise DB, services, and background tasks.

    Args:
        app: The FastAPI application instance.

    Yields:
        None: Control to the application while it is running.

    Raises:
        Exception: If database initialisation fails.
    """
    import os

    from sqlalchemy.orm import sessionmaker as sa_sessionmaker

    import shoreguard.services.gateway as gw_mod
    from shoreguard.db import init_db
    from shoreguard.services.registry import GatewayRegistry

    try:
        engine = init_db()
    except Exception:
        logger.exception("Failed to initialise database")
        raise
    session_factory = sa_sessionmaker(bind=engine)
    registry = GatewayRegistry(session_factory)
    gw_mod.gateway_service = gw_mod.GatewayService(registry)
    logger.info("Gateway service initialised")

    if os.environ.get("SHOREGUARD_LOCAL_MODE"):
        import shoreguard.services.local_gateway as local_mod

        local_mod.local_gateway_manager = local_mod.LocalGatewayManager(gw_mod.gateway_service)
        logger.info("Local gateway mode enabled")

        # Auto-import filesystem gateways so locally managed gateways
        # appear in the DB without a manual import-gateways step.
        imported, skipped = _import_filesystem_gateways(registry)
        if imported:
            logger.info("Auto-imported %d gateway(s) from filesystem", imported)

    # ── Audit ────────────────────────────────────────────────────────────
    import shoreguard.services.audit as audit_mod

    audit_mod.audit_service = audit_mod.AuditService(session_factory)
    logger.info("Audit service initialised")

    # ── Webhooks ────────────────────────────────────────────────────────
    import shoreguard.services.webhooks as webhook_mod

    webhook_mod.webhook_service = webhook_mod.WebhookService(session_factory)
    logger.info("Webhook service initialised")

    # ── Metrics ─────────────────────────────────────────────────────────
    from shoreguard import __version__

    shoreguard_info.info({"version": __version__})

    # ── Auth ─────────────────────────────────────────────────────────────
    init_auth(session_factory)
    bootstrap_admin_user()

    # Hide OpenAPI docs when authentication is enabled to avoid leaking
    # the full API schema to unauthenticated users.
    if is_setup_complete():
        app.openapi_url = None
        app.docs_url = None
        app.redoc_url = None

    # ── Background tasks ─────────────────────────────────────────────────
    async def _cleanup_operations() -> None:
        """Periodically purge expired operations and audit entries."""
        from shoreguard.services.operations import operation_store

        base_interval = 600
        max_interval = 900
        backoff_threshold = 10
        consecutive_failures = 0
        interval = base_interval
        while True:
            await asyncio.sleep(interval)
            try:
                await asyncio.to_thread(operation_store.cleanup)
                if audit_mod.audit_service:
                    await asyncio.to_thread(audit_mod.audit_service.cleanup)
                if webhook_mod.webhook_service:
                    await asyncio.to_thread(webhook_mod.webhook_service.cleanup_old_deliveries)
                consecutive_failures = 0
                interval = base_interval
            except Exception:
                consecutive_failures += 1
                logger.exception(
                    "Operation cleanup failed (consecutive failures: %d)",
                    consecutive_failures,
                )
                if consecutive_failures >= backoff_threshold:
                    interval = min(interval * 2, max_interval)
                    logger.error(
                        "Operation cleanup has failed %d consecutive times, "
                        "backing off to %ds interval",
                        consecutive_failures,
                        interval,
                    )

    async def _health_monitor() -> None:
        """Periodically check health of all registered gateways."""
        base_interval = 30
        max_interval = 300
        backoff_threshold = 10
        consecutive_failures = 0
        interval = base_interval
        while True:
            await asyncio.sleep(interval)
            try:
                await asyncio.to_thread(gw_mod.gateway_service.check_all_health)  # type: ignore[union-attr]
                consecutive_failures = 0
                interval = base_interval
            except Exception:
                consecutive_failures += 1
                logger.exception(
                    "Health monitor error (consecutive failures: %d)",
                    consecutive_failures,
                )
                if consecutive_failures >= backoff_threshold:
                    interval = min(interval * 2, max_interval)
                    logger.error(
                        "Health monitor has failed %d consecutive times, "
                        "backing off to %ds interval",
                        consecutive_failures,
                        interval,
                    )

    cleanup_task = asyncio.create_task(_cleanup_operations())
    health_task = asyncio.create_task(_health_monitor())
    yield
    cleanup_task.cancel()
    health_task.cancel()
    for task in (cleanup_task, health_task):
        try:
            await task
        except asyncio.CancelledError:
            pass
    engine.dispose()
    logger.debug("Database engine disposed")


app = FastAPI(
    title="Shoreguard",
    description="Open source control plane for NVIDIA OpenShell",
    version="0.16.1",
    lifespan=lifespan,
)

register_error_handlers(app)


# ─── Health probes (unauthenticated) ────────────────────────────────────────

health_router = APIRouter(tags=["health"])


@health_router.get("/healthz")
async def healthz() -> dict[str, str]:
    """Liveness probe — returns 200 if the process is running.

    Returns:
        dict[str, str]: Status object with ``{"status": "ok"}``.
    """
    return {"status": "ok"}


@health_router.get("/readyz")
async def readyz() -> JSONResponse:
    """Readiness probe — checks database and service initialisation.

    Returns:
        JSONResponse: 200 with check details when ready, 503 otherwise.
    """
    import shoreguard.services.gateway as gw_mod
    from shoreguard.db import get_engine

    checks: dict[str, str] = {}
    healthy = True

    try:
        engine = get_engine()
        with engine.connect() as conn:
            conn.execute(text("SELECT 1"))
        checks["database"] = "ok"
    except Exception as exc:
        checks["database"] = str(exc)
        healthy = False

    if gw_mod.gateway_service is not None:
        checks["gateway_service"] = "ok"
    else:
        checks["gateway_service"] = "not initialised"
        healthy = False

    status_code = 200 if healthy else 503
    payload = {"status": "ready" if healthy else "not ready", "checks": checks}
    return JSONResponse(content=payload, status_code=status_code)


app.include_router(health_router)
app.include_router(metrics_router)
app.middleware("http")(metrics_middleware)


# ─── Gateway-scoped API routes ──────────────────────────────────────────────
# All sandbox/policy/provider operations are scoped to a specific gateway.
# The resolve_gateway dependency sets a ContextVar so get_client() returns
# the correct client — route handlers need zero changes.

gw_api = APIRouter(
    prefix="/api/gateways/{gw}",
    dependencies=[Depends(resolve_gateway), Depends(require_auth)],
)
gw_api.include_router(sandboxes.router, prefix="/sandboxes", tags=["sandboxes"])
gw_api.include_router(policies.router, tags=["policies"])
gw_api.include_router(approvals.router, prefix="/sandboxes", tags=["approvals"])
gw_api.include_router(providers.router, prefix="/providers", tags=["providers"])


@gw_api.get("/health", response_model=None)
async def gw_health(gw: str) -> dict[str, Any] | JSONResponse:
    """Return gateway health status.

    Args:
        gw: The gateway name.

    Returns:
        dict[str, Any] | JSONResponse: Health info or 503 if disconnected.
    """
    from .deps import _get_gateway_service

    try:
        client = _get_gateway_service().get_client(name=gw)
        return await asyncio.to_thread(client.health)
    except GatewayNotConnectedError:
        return JSONResponse(
            status_code=503,
            content={"status": "disconnected", "detail": f"Gateway '{gw}' not connected"},
        )


class SetInferenceRequest(BaseModel):
    """Request body for setting cluster inference configuration.

    Attributes:
        provider_name: Name of the inference provider.
        model_id: Identifier of the model to use.
        verify: Whether to verify the configuration before applying.
        timeout_secs: Per-route request timeout in seconds (0 = default 60s).
    """

    provider_name: str
    model_id: str
    verify: bool = True
    timeout_secs: int = 0


@gw_api.get("/inference")
async def get_inference(gw: str, client: ShoreGuardClient = Depends(get_client)) -> dict[str, Any]:
    """Return current cluster inference configuration.

    Args:
        gw: The gateway name.
        client: The ShoreGuardClient for this gateway.

    Returns:
        dict[str, Any]: Current inference provider and model settings.
    """
    return await asyncio.to_thread(client.get_cluster_inference)


@gw_api.put("/inference", dependencies=[Depends(require_role("operator"))])
async def set_inference(
    gw: str,
    body: SetInferenceRequest,
    request: Request,
    client: ShoreGuardClient = Depends(get_client),
) -> dict[str, Any]:
    """Update cluster inference configuration.

    Args:
        gw: The gateway name.
        body: The inference configuration to apply.
        request: The incoming HTTP request (for audit logging).
        client: The ShoreGuardClient for this gateway.

    Returns:
        dict[str, Any]: Updated inference configuration.
    """
    actor = getattr(request.state, "user_id", "unknown")
    logger.info(
        "Inference config updated (gateway=%s, provider=%s, model=%s, actor=%s)",
        gw,
        body.provider_name,
        body.model_id,
        actor,
    )
    result = await asyncio.to_thread(
        client.set_cluster_inference,
        provider_name=body.provider_name,
        model_id=body.model_id,
        verify=body.verify,
        timeout_secs=body.timeout_secs,
    )
    from shoreguard.services.audit import audit_log
    from shoreguard.services.webhooks import fire_webhook

    await audit_log(
        request,
        "inference.update",
        "inference",
        gw,
        gateway=gw,
        detail={"provider": body.provider_name, "model": body.model_id},
    )
    await fire_webhook(
        "inference.updated",
        {
            "gateway": gw,
            "provider": body.provider_name,
            "model": body.model_id,
            "actor": actor,
        },
    )
    return result


app.include_router(gw_api)


# ─── Global API routes (not gateway-scoped) ─────────────────────────────────

app.include_router(
    gateway.router,
    prefix="/api/gateway",
    tags=["gateway"],
    dependencies=[Depends(require_auth)],
)

# Presets are local YAML files, not gateway-scoped — mount only preset
# routes globally.  The sandbox-scoped policy routes (/sandboxes/{name}/policy/*)
# are already mounted under gw_api and must NOT be duplicated at the global level.
app.include_router(
    policies.preset_router,
    prefix="/api",
    tags=["policies-global"],
    dependencies=[Depends(require_auth)],
)

app.include_router(
    operations.router,
    prefix="/api/operations",
    tags=["operations"],
    dependencies=[Depends(require_auth)],
)

app.include_router(
    audit.router,
    prefix="/api/audit",
    tags=["audit"],
    dependencies=[Depends(require_auth), Depends(require_role("admin"))],
)

app.include_router(
    webhooks.router,
    prefix="/api/webhooks",
    tags=["webhooks"],
    dependencies=[Depends(require_auth), Depends(require_role("admin"))],
)

app.include_router(
    templates.router,
    prefix="/api/sandbox-templates",
    tags=["templates"],
    dependencies=[Depends(require_auth)],
)


# ─── WebSocket, pages, and static files ─────────────────────────────────────

app.include_router(ws_router)
app.include_router(pages_router)

# Serve static files (CSS, JS, images)
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")


if __name__ == "__main__":
    cli()
