"""API key authentication that resolves tenants and enforces rate limits."""

from __future__ import annotations
import json
import time
import hashlib

from typing import Annotated

from fastapi import Header, Request, Depends

import jwt
from jwt import InvalidTokenError
from jwt.algorithms import RSAAlgorithm

from hieromem_api.instrumentation.prometheus import record_rate_limit_block
from hieromem_api.rate_limit import get_rate_limiter
from hieromem_api.settings import get_settings, APISettings
from hieromem_api.tenants import Tenant, get_tenant_registry
from hieromem_api.rbac import Permission, Role, has_permission
from hieromem_api.vault import read_kv_secret, fetch_jwks


def _hash_key(api_key: str) -> str:
    return hashlib.sha256(api_key.encode("utf-8")).hexdigest()


def _load_vault_api_keys(settings: APISettings) -> dict[str, str]:
    """Return a mapping of api_key_hash -> tenant_id from Vault."""

    if not settings.use_vault:
        return {}

    data = read_kv_secret(settings, settings.vault_api_key_path)
    return {v: k for k, v in data.items() if v}


class OIDCVerifier:
    """Validates OIDC bearer tokens using provider JWKS."""

    def __init__(self, settings: APISettings) -> None:
        self._settings = settings
        self._jwks: dict[str, object] | None = None
        self._last_fetch: float = 0.0

    def _load_jwks(self) -> dict[str, object]:
        now = time.time()
        if self._jwks and now - self._last_fetch < self._settings.oidc_cache_ttl:
            return self._jwks

        jwks_url = self._settings.oidc_jwks_url
        if not jwks_url:
            issuer = self._settings.oidc_issuer
            if not issuer:
                raise RuntimeError("OIDC issuer or JWKS URL must be configured")
            jwks_url = issuer.rstrip("/") + "/.well-known/jwks.json"

        self._jwks = fetch_jwks(jwks_url)
        self._last_fetch = now
        return self._jwks

    def verify(self, token: str) -> dict[str, object]:
        options = {"verify_aud": bool(self._settings.oidc_audience)}
        jwks = self._load_jwks()
        issuer = self._settings.oidc_issuer
        audience = self._settings.oidc_audience or self._settings.oidc_client_id

        unverified_header = jwt.get_unverified_header(token)
        kid = unverified_header.get("kid")
        keys = jwks.get("keys", []) if isinstance(jwks, dict) else []
        rsa_key = next((key for key in keys if key.get("kid") == kid), None)
        if rsa_key is None:
            raise InvalidTokenError("Unable to find matching JWK for token")

        alg = rsa_key.get("alg", "RS256")
        key = RSAAlgorithm.from_jwk(json.dumps(rsa_key))

        return jwt.decode(
            token,
            key,
            algorithms=[alg],
            audience=audience,
            issuer=issuer,
            options=options,
        )


_OIDC_VERIFIER: OIDCVerifier | None = None


def _get_oidc_verifier(settings: APISettings) -> OIDCVerifier:
    global _OIDC_VERIFIER
    if _OIDC_VERIFIER is None:
        _OIDC_VERIFIER = OIDCVerifier(settings)
    return _OIDC_VERIFIER


def _authenticate_oidc_token(token: str, settings: APISettings) -> Tenant:
    verifier = _get_oidc_verifier(settings)
    claims = verifier.verify(token)
    tenant_id = str(
        claims.get(settings.oidc_tenant_claim)
        or claims.get("sub")
        or "oidc-user"
    )
    role = str(claims.get(settings.oidc_role_claim, Role.VIEWER.value))
    return Tenant(
        tenant_id=tenant_id,
        api_key="oidc",
        api_key_hash=_hash_key(token),
        role=role,
        allow_unauthenticated=False,
        enforce_namespace_isolation=True,
    )


def _dev_tenant_from_settings(api_key: str) -> Tenant:
    return Tenant(
        tenant_id="dev",
        api_key=api_key,
        api_key_hash=_hash_key(api_key),
        allow_unauthenticated=False,
        enforce_namespace_isolation=False,
    )


def _client_ip_from_request(request: Request) -> str | None:
    forwarded_for = request.headers.get("X-Forwarded-For")
    if forwarded_for:
        return forwarded_for.split(",")[0].strip()
    if request.client:
        return request.client.host
    return None


def _enforce_rate_limit(tenant: Tenant, request: Request) -> None:
    settings = get_settings()
    limiter = get_rate_limiter()
    limit = tenant.rate_limit_per_minute or settings.default_rate_limit_per_minute
    client_ip = _client_ip_from_request(request)

    result = limiter.check(
        tenant.api_key_hash or tenant.tenant_id,
        limit,
        ip=client_ip,
        ip_limit=settings.ip_rate_limit_per_minute,
        burst_multiplier=settings.rate_limit_burst_multiplier,
        base_penalty_seconds=settings.rate_limit_backoff_base_seconds,
        max_penalty_seconds=settings.rate_limit_max_backoff_seconds,
    )

    if not result.allowed:
        record_rate_limit_block(tenant.tenant_id)
        from hieromem_api.errors import RateLimitExceededError
        raise RateLimitExceededError(
            retry_after=result.retry_after or 60,
            details={
                "tenant_id": tenant.tenant_id,
                "limit": limit,
                "ip": client_ip,
                "key": result.key,
                "violations": result.violations,
            },
        )


def _extract_bearer_token(header_value: object) -> str | None:
    """Return the bearer token from an Authorization header value.

    When called directly (e.g., in unit tests), the FastAPI ``Header`` default
    instance may be passed instead of a string. Guard against non-string values
    so we don't attempt string operations on descriptor objects.
    """

    if not isinstance(header_value, str):
        return None

    parts = header_value.split()
    if len(parts) == 2 and parts[0].lower() == "bearer":
        return parts[1]
    return None


def require_api_key(
    request: Request,
    x_api_key: Annotated[str | None, Header(alias="X-API-Key")] = None,
    authorization: Annotated[str | None, Header(alias="Authorization")] = None,
) -> Tenant:
    """Resolve the tenant associated with the request's API key.
    Checks SQLite registry first, then falls back to Vault stored keys.
    """
    settings = get_settings()
    registry = get_tenant_registry()
    tenant: Tenant | None = None

    # Load Vault keys once per request if needed
    vault_keys = _load_vault_api_keys(settings)

    # Attempt OIDC authentication first if enabled
    bearer_token = _extract_bearer_token(authorization)
    oidc_error: Exception | None = None
    if bearer_token and settings.oidc_enabled:
        try:
            tenant = _authenticate_oidc_token(bearer_token, settings)
        except Exception as exc:  # pragma: no cover - depends on provider runtime
            oidc_error = exc

    if tenant is None and x_api_key:
        # Hash the key before looking it up
        key_hash = _hash_key(x_api_key)
        tenant = registry.get_by_hash(key_hash)

        if tenant is None and settings.api_key_header and x_api_key == settings.api_key_header:
            tenant = _dev_tenant_from_settings(x_api_key)

        if tenant is None and key_hash in vault_keys:
            tenant_id = vault_keys[key_hash]
            tenant = Tenant(
                tenant_id=tenant_id,
                api_key=x_api_key,
                api_key_hash=key_hash,
                allow_unauthenticated=False,
                enforce_namespace_isolation=False,
            )

        if tenant is None:
            from hieromem_api.errors import UnauthorizedError
            raise UnauthorizedError()
    else:
        default_tenant = registry.default_tenant()
        if settings.api_key_header:
            from hieromem_api.errors import UnauthorizedError
            raise UnauthorizedError()
        if registry.has_api_keys() and not default_tenant.allow_unauthenticated:
            from hieromem_api.errors import UnauthorizedError
            raise UnauthorizedError()
        tenant = default_tenant

    if tenant is None and oidc_error:
        from hieromem_api.errors import UnauthorizedError
        raise UnauthorizedError()

    if not tenant or not tenant.is_active():
        from hieromem_api.errors import ForbiddenError
        raise ForbiddenError(
            "Tenant disabled", details={"tenant_id": tenant.tenant_id})

    _enforce_rate_limit(tenant, request)
    request.state.tenant = tenant
    request.state.tenant_id = tenant.tenant_id
    return tenant


def require_permission(permission: Permission):
    """
    Create a FastAPI dependency that enforces permission-based access control.

    Usage:
        @router.post("/endpoint")
        def my_endpoint(
            tenant: Tenant = Depends(require_api_key),
            _perm_check: None = Depends(require_permission(Permission.WRITE_MEMORY))
        ):
            ...
    """
    def check_permission(tenant: Tenant = Depends(require_api_key)) -> None:
        """Dependency that checks if tenant has required permission."""
        if not has_permission(tenant.role, permission):
            from hieromem_api.errors import ForbiddenError
            raise ForbiddenError(
                f"Insufficient permissions. Required permission: {permission.value}"
            )
    return check_permission


def require_role(*allowed_roles: str):
    """
    Create a FastAPI dependency that enforces role-based access control.
    Deprecated: Use require_permission instead where possible.
    """
    def check_role(tenant: Tenant = Depends(require_api_key)) -> None:
        """Dependency that checks if tenant has required role."""
        if tenant.role not in allowed_roles:
            from hieromem_api.errors import ForbiddenError
            raise ForbiddenError(
                f"Insufficient permissions. Required role: {', '.join(allowed_roles)}"
            )
    return check_role


__all__ = ["require_api_key", "require_role", "require_permission"]
