"""Helper functions and models for constructing API Gateway Lambda Proxy responses."""

import base64
import json
from datetime import date, datetime
from decimal import Decimal
from http import HTTPStatus
from typing import Any, Generic, Mapping, TypeVar
from uuid import UUID

from aws_lambda_typing.responses import APIGatewayProxyResponseV1
from pydantic import BaseModel, ConfigDict, Field

from ..support.serialization import to_primitive

# ---- CORS defaults ----
DEFAULT_CORS_HEADERS: dict[str, str] = {
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
}

DEFAULT_EXPOSE_HEADERS = "Location, ETag"


def _with_cors(
    base: Mapping[str, str] | None,
    *,
    expose_headers: str | None = None,
) -> dict[str, str]:
    """Merge user headers over CORS defaults and optionally set expose headers."""

    merged = dict(DEFAULT_CORS_HEADERS)
    if base:
        merged.update(base)
    if expose_headers:
        if "Access-Control-Expose-Headers" in merged:
            merged["Access-Control-Expose-Headers"] = merged["Access-Control-Expose-Headers"] + "," + expose_headers
        else:
            merged["Access-Control-Expose-Headers"] = expose_headers
    return merged


class LambdaProxyResponse(BaseModel):
    statusCode: int = Field(..., ge=100, le=599)
    headers: dict[str, str] = Field(default_factory=dict)
    multiValueHeaders: dict[str, list[str]] | None = None
    body: str
    isBase64Encoded: bool = False

    model_config = ConfigDict(extra="forbid")

    def to_response(self) -> APIGatewayProxyResponseV1:
        return {
            "statusCode": self.statusCode,
            "headers": self.headers,
            "multiValueHeaders": self.multiValueHeaders or {},
            "body": self.body,
            "isBase64Encoded": self.isBase64Encoded,
        }


class ErrorResponse(BaseModel):
    code: str
    message: str
    details: Any = None


T = TypeVar("T")


class ApiResponse(BaseModel, Generic[T]):
    status: int
    body: T
    headers: dict[str, str] = Field(default_factory=lambda: {"Content-Type": "application/json"})
    multi_value_headers: dict[str, list[str]] | None = None
    is_base64: bool = False

    model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")

    def to_lambda(self) -> LambdaProxyResponse:
        body_str, is_b64 = _serialize_body(self.body, self.is_base64)
        return LambdaProxyResponse(
            statusCode=self.status,
            headers=self.headers,
            multiValueHeaders=self.multi_value_headers,
            body=body_str,
            isBase64Encoded=is_b64,
        )


def _serialize_body(body: Any, is_base64: bool) -> tuple[str, bool]:
    if isinstance(body, (bytes, bytearray)):
        return base64.b64encode(body).decode("ascii"), True
    if isinstance(body, BaseModel):
        return json.dumps(to_primitive(body), ensure_ascii=False), is_base64
    if isinstance(body, str):
        return body, is_base64
    if isinstance(body, int | float | bool) or body is None:
        return json.dumps(body), is_base64
    return json.dumps(to_primitive(body), default=_json_default, ensure_ascii=False), is_base64


def _json_default(obj: Any) -> Any:
    if isinstance(obj, BaseModel):
        return obj.model_dump(mode="json", by_alias=True)
    if isinstance(obj, datetime | date):
        return obj.isoformat()
    if isinstance(obj, UUID):
        return str(obj)
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")


def _merge_headers(base: Mapping[str, str] | None, default: Mapping[str, str]) -> dict[str, str]:
    merged = dict(default)
    if base:
        merged.update(base)
    return merged


def ok(data: T, *, headers: Mapping[str, str] | None = None) -> APIGatewayProxyResponseV1:
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[T](status=HTTPStatus.OK.value, body=data, headers=hdrs)
    return resp.to_lambda().to_response()


def created(
    data: T,
    location: str | None = None,
    *,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    base = _merge_headers(headers, {"Content-Type": "application/json"})
    if location:
        base = dict(base)
        base["Location"] = location
        hdrs = _with_cors(base, expose_headers=DEFAULT_EXPOSE_HEADERS)
    else:
        hdrs = _with_cors(base, expose_headers=DEFAULT_EXPOSE_HEADERS)
    resp = ApiResponse[T](status=HTTPStatus.CREATED.value, body=data, headers=hdrs)
    return resp.to_lambda().to_response()


def accepted(data: T | None = None, *, headers: Mapping[str, str] | None = None) -> APIGatewayProxyResponseV1:
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    payload: Any = data if data is not None else {}
    resp = ApiResponse[Any](status=HTTPStatus.ACCEPTED.value, body=payload, headers=hdrs)
    return resp.to_lambda().to_response()


def no_content(*, headers: Mapping[str, str] | None = None) -> APIGatewayProxyResponseV1:
    hdrs = _with_cors(headers or {})
    resp = ApiResponse[str](status=HTTPStatus.NO_CONTENT.value, body="", headers=hdrs)
    return resp.to_lambda().to_response()


def bad_request(
    message: str,
    *,
    code: str = "BadRequest",
    details: Any = None,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    err = ErrorResponse(code=code, message=message, details=details)
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[ErrorResponse](status=HTTPStatus.BAD_REQUEST.value, body=err, headers=hdrs)
    return resp.to_lambda().to_response()


def not_found(
    message: str = "Not Found",
    *,
    code: str = "NotFound",
    details: Any = None,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    err = ErrorResponse(code=code, message=message, details=details)
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[ErrorResponse](status=HTTPStatus.NOT_FOUND.value, body=err, headers=hdrs)
    return resp.to_lambda().to_response()


def unauthorized(
    message: str = "Unauthorized",
    *,
    code: str = "Unauthorized",
    details: Any = None,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    err = ErrorResponse(code=code, message=message, details=details)
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[ErrorResponse](status=HTTPStatus.UNAUTHORIZED.value, body=err, headers=hdrs)
    return resp.to_lambda().to_response()


def forbidden(
    message: str = "Forbidden",
    *,
    code: str = "Forbidden",
    details: Any = None,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    err = ErrorResponse(code=code, message=message, details=details)
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[ErrorResponse](status=HTTPStatus.FORBIDDEN.value, body=err, headers=hdrs)
    return resp.to_lambda().to_response()


def feature_disabled(
    message: str = "Feature disabled",
    *,
    code: str = "FeatureDisabled",
    details: Any = None,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    err = ErrorResponse(code=code, message=message, details=details)
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[ErrorResponse](status=HTTPStatus.FORBIDDEN.value, body=err, headers=hdrs)
    return resp.to_lambda().to_response()


def internal_server_error(
    message: str = "Internal Server Error",
    *,
    code: str = "InternalServerError",
    details: Any = None,
    headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    err = ErrorResponse(code=code, message=message, details=details)
    hdrs = _with_cors(_merge_headers(headers, {"Content-Type": "application/json"}))
    resp = ApiResponse[ErrorResponse](status=HTTPStatus.INTERNAL_SERVER_ERROR.value, body=err, headers=hdrs)
    return resp.to_lambda().to_response()


def file_response(
    data: bytes,
    *,
    content_type: str,
    filename: str | None = None,
    status: int = HTTPStatus.OK.value,
    extra_headers: Mapping[str, str] | None = None,
) -> APIGatewayProxyResponseV1:
    base = {"Content-Type": content_type}
    if filename:
        base["Content-Disposition"] = f'attachment; filename="{filename}"'
    if extra_headers:
        base.update(extra_headers)
    hdrs = _with_cors(base, expose_headers=DEFAULT_EXPOSE_HEADERS)
    resp = ApiResponse[bytes](status=status, body=data, headers=hdrs, is_base64=True)
    return resp.to_lambda().to_response()


__all__ = [
    "DEFAULT_CORS_HEADERS",
    "DEFAULT_EXPOSE_HEADERS",
    "ApiResponse",
    "ErrorResponse",
    "LambdaProxyResponse",
    "accepted",
    "bad_request",
    "created",
    "feature_disabled",
    "file_response",
    "forbidden",
    "internal_server_error",
    "no_content",
    "not_found",
    "ok",
    "unauthorized",
]
