from __future__ import annotations

from typing import Callable, cast

from aiohttp import web

from keiro.gateway.infra.admin import routes_api_keys, routes_billing
from keiro.gateway.infra.app_keys import (
    AUTH_BACKEND_KEY,
    BILLING_STORE_KEY,
    PG_POOL_KEY,
    REQUIRE_ADMIN_KEY,
    SETTINGS_KEY,
)
from keiro.gateway.infra.concurrency import BaseConcurrencyLimiter
from keiro.gateway.infra.config_store import ConfigStore
from keiro.gateway.infra.middleware.access_log import build_access_log_middleware
from keiro.gateway.infra.middleware.auth import (
    InMemoryAuthBackend,
    build_auth_middleware,
)
from keiro.gateway.infra.middleware.concurrency import build_concurrency_middleware
from keiro.gateway.infra.middleware.limits import build_limits_applier
from keiro.gateway.infra.middleware.ratelimit import build_ratelimit_middleware
from keiro.gateway.infra.middleware.tpm import build_tpm_middleware
from keiro.gateway.infra.rate_limit import RateLimiter as GatewayRateLimiter
from keiro.gateway.infra.tpm_limiter import DualTPMLimiter

from ..adapters.runtime import (
    RuntimeOptions,
    _BillingStore,
)
from ..pipeline.container import Services
from ..boot.logging import configure_gateway_logging
from ..boot.settings import get_gateway_settings
from .handlers import register_routes
from .middleware import (
    Metrics,
    bind_auth_context_middleware,
    error_middleware,
    json_body_middleware,
    metrics_middleware,
    request_id_middleware,
)
from . import routes_signup
from .state import CANCELLED_KEY, METRICS_KEY, SERVICES_KEY

import logging

_LOG = logging.getLogger(__name__)

_PERSISTED_KEYS_PATH = "data/registered_keys.jsonl"


def build_app(services: Services, *, options: RuntimeOptions | None = None) -> web.Application:
    """Build the aiohttp application with middleware stack.

    The limiter instances (services.tpm, services.rpm, services.ccy) are the
    same gateway infrastructure types used by the shared middleware — no
    adapter wrapping or unwrapping is needed.
    """
    _ = options  # Options already applied during service bootstrap.
    settings = get_gateway_settings()
    config = ConfigStore(settings.config.tiers_path)
    configure_gateway_logging(settings.logging())

    # The infrastructure types (GatewayRateLimiter, DualTPMLimiter,
    # BaseConcurrencyLimiter) structurally satisfy the port protocols AND
    # are the native types expected by the middleware builders — no casting.
    rate_limiter = cast(GatewayRateLimiter, services.rpm)
    tpm_limiter = cast(DualTPMLimiter, services.tpm)
    concurrency_limiter = cast(BaseConcurrencyLimiter, services.ccy)

    # Create auth backend explicitly so it can be shared between the auth
    # middleware and the self-service signup route.
    auth_backend = _build_auth_backend(settings)

    app = web.Application(
        middlewares=[
            metrics_middleware,
            error_middleware,
            build_access_log_middleware(),
            request_id_middleware,
            json_body_middleware,
            build_auth_middleware(
                backend=auth_backend, config=config, settings=settings,
            ),
            bind_auth_context_middleware,
            build_limits_applier(config, rate_limiter, tpm_limiter, concurrency_limiter),
            build_ratelimit_middleware(rate_limiter),
            build_concurrency_middleware(concurrency_limiter),
            build_tpm_middleware(tpm_limiter),
        ]
    )
    app[SERVICES_KEY] = services
    app[METRICS_KEY] = Metrics()
    app[CANCELLED_KEY] = set()
    app[AUTH_BACKEND_KEY] = auth_backend
    register_routes(app)

    # Health and metrics endpoints (bypass auth/rate-limit via middleware path checks).
    app.router.add_get("/healthz", _health_handler)
    app.router.add_get("/v1/health", _health_handler)
    app.router.add_get("/metrics", _metrics_handler)
    app.router.add_get("/v1/metrics", _metrics_handler)

    # Self-service signup (bypasses auth via middleware path check).
    routes_signup.register(app)

    # Admin endpoints — reuse shared admin routes from gateway infrastructure.
    # Admin routes read SETTINGS_KEY, BILLING_STORE_KEY, PG_POOL_KEY, and
    # REQUIRE_ADMIN_KEY from the app. Wire them using the v1-compatible keys.
    app[SETTINGS_KEY] = settings
    app[REQUIRE_ADMIN_KEY] = _build_require_admin(settings)
    app[PG_POOL_KEY] = None  # Populated by lifecycle hook if Postgres is configured
    _wire_admin_billing(app, services)
    routes_api_keys.register(app)
    routes_billing.register(app)

    return app


def _build_auth_backend(settings: object) -> InMemoryAuthBackend | object:
    """Create the auth backend and load any persisted self-service keys."""
    from pathlib import Path

    # Duck-type: accept any settings object with auth.issued_keys
    # (both infra.settings.GatewaySettings and boot.settings.GatewaySettings).
    auth = getattr(settings, "auth", None)
    if auth is None or not hasattr(auth, "issued_keys"):
        return InMemoryAuthBackend()

    if settings.auth.mode.lower() == "postgres":
        # Postgres backend is created inside build_auth_middleware; return
        # None so the middleware falls through to its own constructor.
        # Signup will be unavailable in Postgres mode (appropriate — use
        # the admin API instead).
        return InMemoryAuthBackend(
            settings.auth.issued_keys, default_tier=settings.auth.default_tier,
        )

    backend = InMemoryAuthBackend(
        settings.auth.issued_keys, default_tier=settings.auth.default_tier,
    )
    # Load keys created via self-service signup on previous runs.
    keys_path = Path(_PERSISTED_KEYS_PATH)
    loaded = backend.load_persisted_keys(keys_path)
    if loaded:
        _LOG.info("Restored %d self-service keys from %s", loaded, keys_path)
    # Also populate the email dedup tracker.
    routes_signup.load_persisted_emails(keys_path)
    return backend


def _build_require_admin(
    settings: object,
) -> "Callable[[web.Request], None]":
    """Build an admin-guard callable that validates X-Admin-Token."""
    token: str | None = None
    admin = getattr(settings, "admin", None)
    if admin is not None:
        token = getattr(admin, "token", None)

    def require_admin(request: web.Request) -> None:
        if not token:
            raise web.HTTPForbidden(text="Admin endpoints disabled")
        hdr = request.headers.get("X-Admin-Token")
        if hdr != token:
            raise web.HTTPUnauthorized(text="Invalid admin token")

    return require_admin


def _wire_admin_billing(app: web.Application, services: Services) -> None:
    """Expose the legacy billing store through the admin-route app key."""
    billing_port = services.billing
    if isinstance(billing_port, _BillingStore):
        app[BILLING_STORE_KEY] = billing_port.underlying
    else:
        # Fallback: store the port itself (admin routes may get a type mismatch
        # but this keeps startup from failing in unusual configurations).
        app[BILLING_STORE_KEY] = billing_port


async def _health_handler(_request: web.Request) -> web.Response:
    return web.json_response({"status": "ok"})


async def _metrics_handler(request: web.Request) -> web.Response:
    from ..infra.metrics import METRICS as PROVIDER_METRICS

    metrics: Metrics = request.app[METRICS_KEY]
    basic = metrics.as_prometheus()
    provider = PROVIDER_METRICS.render_text()
    body = f"{basic}\n{provider}\n" if provider else f"{basic}\n"
    return web.Response(text=body, content_type="text/plain")


__all__ = ["build_app", "SERVICES_KEY", "METRICS_KEY"]
