"""API Gateway dispatch."""

import logging
import os
from dataclasses import dataclass, field
from typing import cast

import jwt
from aws_lambda_typing.events import APIGatewayProxyEventV1
from aws_lambda_typing.responses import APIGatewayProxyResponseV1
from jwt import InvalidTokenError
from pydantic import ValidationError

from ..rest.cors import CORS_ALLOW_ORIGINS, resolve_allow_origin
from ..rest.responses import (
    DEFAULT_CORS_HEADERS,
    bad_request,
    feature_disabled,
    forbidden,
    internal_server_error,
    not_found,
    unauthorized,
)
from ..runtime.context import RequestScope, current_container, set_principal
from ..support.authz import Principal, Role
from ..support.errors import (
    AmbiguousRouteError,
    DomainValidationError,
    FeatureDisabledError,
    ForbiddenError,
    NotFoundError,
    RouteNotFoundError,
    UnauthorizedError,
)
from .contracts import ApiExceptionMapper, ApiRoute, ApiRouteResolver, ICorsHandler, IJwtTokenHandler, IRestHandler
from .models import ApiRequest

log = logging.getLogger(__name__)

DEFAULT_ALLOW_HEADERS = "Content-Type,Authorization,X-Correlation-Id"
DEFAULT_ALLOW_METHODS = "GET,POST,PUT,DELETE,OPTIONS"
DEFAULT_MAX_AGE_SECONDS = "86400"


@dataclass(slots=True, frozen=True)
class DefaultApiRouteResolver(ApiRouteResolver):
    """Resolve one API handler for a request."""

    handlers: tuple[IRestHandler[APIGatewayProxyResponseV1], ...] = ()

    def __init__(self, handlers: list[IRestHandler[APIGatewayProxyResponseV1]] | None = None) -> None:
        object.__setattr__(self, "handlers", tuple(handlers or ()))

    def resolve(self, request: ApiRequest) -> IRestHandler[APIGatewayProxyResponseV1]:
        matches = [handler for handler in self.handlers if _route_matches(handler.route, request)]
        if not matches:
            raise RouteNotFoundError(f"No API handler matched {request.method} {request.resource}")
        if len(matches) > 1:
            names = ", ".join(type(handler).__name__ for handler in matches)
            raise AmbiguousRouteError(f"Multiple API handlers matched {request.method} {request.resource}: {names}")
        return matches[0]


def _route_matches(route: ApiRoute, request: ApiRequest) -> bool:
    method = route.method.upper()
    return (method in {"*", "ANY", request.method}) and route.resource == request.resource


@dataclass(slots=True, frozen=True)
class DefaultCorsPolicy(ICorsHandler):
    """Apply API CORS rules."""

    allowed_origins: tuple[str, ...] = field(default_factory=lambda: tuple(CORS_ALLOW_ORIGINS))

    def preflight(self, request: ApiRequest) -> APIGatewayProxyResponseV1 | None:
        if request.method != "OPTIONS":
            return None
        headers = dict(DEFAULT_CORS_HEADERS)
        headers["Access-Control-Allow-Headers"] = DEFAULT_ALLOW_HEADERS
        headers["Access-Control-Allow-Methods"] = DEFAULT_ALLOW_METHODS
        headers["Access-Control-Max-Age"] = DEFAULT_MAX_AGE_SECONDS
        allow_origin = resolve_allow_origin(request.headers.get("origin"), self.allowed_origins)
        if allow_origin:
            headers["Access-Control-Allow-Origin"] = allow_origin
        return {
            "statusCode": 200,
            "headers": headers,
            "multiValueHeaders": {},
            "body": "",
            "isBase64Encoded": False,
        }

    def apply(self, request: ApiRequest, response: APIGatewayProxyResponseV1) -> APIGatewayProxyResponseV1:
        headers = dict(response.get("headers") or {})
        allow_origin = resolve_allow_origin(request.headers.get("origin"), self.allowed_origins)
        if allow_origin:
            headers["Access-Control-Allow-Origin"] = allow_origin
        response["headers"] = headers
        return response


@dataclass(slots=True, frozen=True)
class JwtTokenHandler(IJwtTokenHandler):
    """Authenticate a request with a JWT token."""

    public_key: str
    issuer: str = field(default_factory=lambda: os.getenv("AUTH_JWT_ISSUER", "stock-agent").strip() or "stock-agent")
    audience: str = field(
        default_factory=lambda: os.getenv("AUTH_JWT_AUDIENCE", "stock-agent-api").strip() or "stock-agent-api"
    )

    def authenticate(self, request: ApiRequest) -> Principal:
        token = self._extract_bearer_token(request.headers)
        if not token:
            return Principal.guest()
        try:
            claims = jwt.decode(
                token,
                self.public_key,
                algorithms=["RS256"],
                issuer=self.issuer,
                audience=self.audience,
                options={"require": ["exp", "iat", "nbf", "iss", "aud"]},
            )
        except InvalidTokenError as exc:
            raise UnauthorizedError("Invalid token") from exc
        return self._principal_from_claims(claims)

    def __call__(self, event: APIGatewayProxyEventV1 | ApiRequest) -> None:
        request = event if isinstance(event, ApiRequest) else ApiRequest.from_event(cast(APIGatewayProxyEventV1, event))
        set_principal(self.authenticate(request))

    def _principal_from_claims(self, claims: dict[str, object]) -> Principal:
        try:
            name = claims["name"]
            role = claims["role"]
            tenant_id = claims["tenant_id"]
        except KeyError as exc:
            raise UnauthorizedError("Missing identity claims") from exc
        if not isinstance(name, str) or not isinstance(role, str) or not isinstance(tenant_id, str):
            raise UnauthorizedError("Missing or invalid identity claims")
        try:
            return Principal.of(name=name, role=Role(role), tenant_id=tenant_id)
        except (DomainValidationError, TypeError, ValueError) as exc:
            raise UnauthorizedError("Missing or invalid identity claims") from exc

    def _extract_bearer_token(self, headers: dict[str, str]) -> str | None:
        auth_value = headers.get("authorization")
        if not auth_value:
            return None
        parts = auth_value.strip().split(" ", 1)
        if len(parts) != 2 or parts[0].lower() != "bearer":
            raise UnauthorizedError("Authorization header must be of type Bearer")
        return parts[1].strip()


@dataclass(slots=True, frozen=True)
class DefaultApiExceptionMapper(ApiExceptionMapper):
    """Map request exceptions to API responses."""

    def map_exception(self, exc: Exception) -> APIGatewayProxyResponseV1:
        if isinstance(exc, ValidationError):
            log.warning("Validation error: %s", exc)
            return bad_request("Invalid input", details=exc.errors())
        if isinstance(exc, DomainValidationError):
            log.warning("Domain validation error: %s", exc)
            return bad_request("Invalid input", details=[str(exc)])
        if isinstance(exc, (RouteNotFoundError, NotFoundError)):
            log.warning("Not found error: %s", exc)
            return not_found("Not found", details=[str(exc)])
        if isinstance(exc, ForbiddenError):
            log.warning("Forbidden error: %s", exc)
            return forbidden("Forbidden", details=[str(exc)])
        if isinstance(exc, FeatureDisabledError):
            log.warning("Feature disabled: %s", exc)
            return feature_disabled("Feature disabled", details=[str(exc)])
        if isinstance(exc, UnauthorizedError):
            log.warning("Unauthorized error: %s", exc)
            return unauthorized("Unauthorized", details=[str(exc)])
        if isinstance(exc, AmbiguousRouteError):
            log.exception("Ambiguous route configuration: %s", exc)
            return internal_server_error(details=[str(exc)])
        log.exception("Unhandled API error: %s", exc)
        return internal_server_error()


@dataclass(slots=True, frozen=True)
class ApiGatewayEventHandler:
    """Dispatch an API Gateway event."""

    route_resolver: ApiRouteResolver
    authenticator: IJwtTokenHandler
    exception_mapper: ApiExceptionMapper
    cors_policy: ICorsHandler

    def handle(self, event: APIGatewayProxyEventV1 | ApiRequest) -> APIGatewayProxyResponseV1:
        request = event if isinstance(event, ApiRequest) else ApiRequest.from_event(event)
        with RequestScope(
            correlation_id=request.correlation_id,
            principal=Principal.guest(),
            container=current_container(),
        ):
            try:
                response = self._dispatch(request)
            except Exception as exc:
                response = self.exception_mapper.map_exception(exc)
        return self.cors_policy.apply(request, response)

    def _dispatch(self, request: ApiRequest) -> APIGatewayProxyResponseV1:
        preflight = self.cors_policy.preflight(request)
        if preflight is not None:
            return preflight
        set_principal(self.authenticator.authenticate(request))
        handler = self.route_resolver.resolve(request)
        return handler.handle(request)


__all__ = [
    "ApiGatewayEventHandler",
    "DefaultApiExceptionMapper",
    "DefaultApiRouteResolver",
    "DefaultCorsPolicy",
    "JwtTokenHandler",
]
