from __future__ import annotations

import math
from dataclasses import asdict, dataclass, is_dataclass
from datetime import date, datetime, timedelta
from decimal import Decimal
from enum import Enum
from http import HTTPStatus
from typing import Any, Mapping, Sequence
from uuid import UUID

from ._compat import optional_import
from .logging import logs

JSONResponse = optional_import("fastapi.responses", "JSONResponse")
Response = optional_import("fastapi", "Response")
PydanticValidationError = optional_import("pydantic", "ValidationError")
PandasTimedelta = optional_import("pandas", "Timedelta")
NumpyInteger = optional_import("numpy", "integer")
NumpyFloating = optional_import("numpy", "floating")
NumpyNdarray = optional_import("numpy", "ndarray")
_DEFAULT_FLOAT_PRECISION = object()


@dataclass(frozen=True)
class PaginationRes:
    page: int
    rows: int
    total_rows: int


def value_correction(
    data: Any,
    *,
    mode: str = "response",
    float_precision: int | None | object = _DEFAULT_FLOAT_PRECISION,
    datetime_format: str = "%Y-%m-%d %H:%M:%S",
    date_format: str = "%Y-%m-%d",
    strip_strings: bool = True,
    convert_decimal: bool | None = None,
    convert_datetime: bool | None = None,
    convert_date: bool | None = None,
    convert_timedelta: bool | None = None,
    nan_to_none: bool | None = None,
    convert_uuid: bool | None = None,
    convert_enum: bool | None = None,
    convert_bytes: bool | None = None,
    convert_objects: bool | None = None,
) -> Any:
    response_mode = mode == "response"
    if float_precision is _DEFAULT_FLOAT_PRECISION:
        float_precision = 2 if response_mode else None
    if convert_decimal is None:
        convert_decimal = response_mode
    if convert_datetime is None:
        convert_datetime = response_mode
    if convert_date is None:
        convert_date = response_mode
    if convert_timedelta is None:
        convert_timedelta = response_mode
    if nan_to_none is None:
        nan_to_none = response_mode
    if convert_uuid is None:
        convert_uuid = response_mode
    if convert_enum is None:
        convert_enum = response_mode
    if convert_bytes is None:
        convert_bytes = response_mode
    if convert_objects is None:
        convert_objects = response_mode

    correction_kwargs = {
        "mode": mode,
        "float_precision": float_precision,
        "datetime_format": datetime_format,
        "date_format": date_format,
        "strip_strings": strip_strings,
        "convert_decimal": convert_decimal,
        "convert_datetime": convert_datetime,
        "convert_date": convert_date,
        "convert_timedelta": convert_timedelta,
        "nan_to_none": nan_to_none,
        "convert_uuid": convert_uuid,
        "convert_enum": convert_enum,
        "convert_bytes": convert_bytes,
        "convert_objects": convert_objects,
    }

    # None is JSON-native — return immediately.
    if data is None:
        return data

    # bool must be checked before int since bool is a subclass of int.
    if isinstance(data, bool):
        return data

    if isinstance(data, str):
        return data.strip() if strip_strings else data

    if isinstance(data, bytes):
        if not convert_bytes:
            return data
        return data.decode("utf-8", errors="replace")

    if isinstance(data, UUID):
        if not convert_uuid:
            return data
        return str(data)

    if isinstance(data, Decimal):
        if not convert_decimal:
            return data
        # Recurse so that float rounding and nan_to_none are applied.
        return value_correction(float(data), **correction_kwargs)

    # datetime must be checked before date since datetime is a subclass of date.
    if isinstance(data, datetime):
        if not convert_datetime:
            return data
        return data.strftime(datetime_format)

    if isinstance(data, date):
        if not convert_date:
            return data
        return data.strftime(date_format)

    if isinstance(data, timedelta) or (PandasTimedelta and isinstance(data, PandasTimedelta)):
        if not convert_timedelta:
            return data
        return str(data)

    if isinstance(data, float):
        if nan_to_none and (math.isnan(data) or math.isinf(data)):
            return None
        if float_precision is None:
            return data
        return round(data, int(float_precision))

    if isinstance(data, int):
        return data

    # Optional numpy scalar types — convert to native Python equivalents.
    if NumpyInteger and isinstance(data, NumpyInteger):
        return int(data)
    if NumpyFloating and isinstance(data, NumpyFloating):
        return value_correction(float(data), **correction_kwargs)
    if NumpyNdarray and isinstance(data, NumpyNdarray):
        return value_correction(data.tolist(), **correction_kwargs)

    # Enum — extract the underlying value and recurse so it is also corrected.
    if isinstance(data, Enum):
        if not convert_enum:
            return data
        return value_correction(data.value, **correction_kwargs)

    if isinstance(data, dict):
        return {
            value_correction(key, **correction_kwargs): value_correction(value, **correction_kwargs)
            for key, value in data.items()
        }

    if isinstance(data, list):
        return [value_correction(item, **correction_kwargs) for item in data]

    if isinstance(data, tuple):
        return tuple(value_correction(item, **correction_kwargs) for item in data)

    if isinstance(data, (set, frozenset)):
        return [value_correction(item, **correction_kwargs) for item in sorted(data, key=repr)]

    # Dataclass — convert to dict then recurse.
    if is_dataclass(data) and not isinstance(data, type):
        if not convert_objects:
            return data
        return value_correction(asdict(data), **correction_kwargs)

    # Pydantic v2 / v1 models — convert to dict then recurse.
    if convert_objects:
        model_dump = getattr(data, "model_dump", None)
        if callable(model_dump):
            return value_correction(model_dump(), **correction_kwargs)
        dict_method = getattr(data, "dict", None)
        if callable(dict_method):
            return value_correction(dict_method(), **correction_kwargs)

    return data


def _http_status_message(response_code: int) -> str:
    try:
        phrase = HTTPStatus(response_code).phrase
    except ValueError:
        return "Request failed."
    return phrase.replace("_", " ")


def _dump_model(model: Any) -> Any:
    if hasattr(model, "model_dump"):
        return model.model_dump()
    if hasattr(model, "dict"):
        return model.dict()
    return model


def _coerce_mapping(value: Any, *, field_name: str) -> dict[str, Any]:
    if isinstance(value, Mapping):
        return dict(value)

    if is_dataclass(value) and not isinstance(value, type):
        return asdict(value)

    dumped = _dump_model(value)
    if isinstance(dumped, Mapping):
        return dict(dumped)

    try:
        return dict(value)
    except (TypeError, ValueError) as exc:
        raise TypeError(f"{field_name} must be a mapping or model instance.") from exc


def _serialize_meta(meta: Any | None) -> tuple[dict[str, Any], list[dict[str, str]] | None]:
    if meta is None:
        return {}, None

    try:
        raw_meta = _coerce_mapping(meta, field_name="meta")
    except TypeError as exc:
        return {}, [{"field": "meta", "message": str(exc) or "Meta data is invalid."}]

    normalized_meta = {str(key): value for key, value in raw_meta.items() if value is not None}
    return value_correction(normalized_meta), None


def _validate_with_schema(schema: Any, item: Any) -> Any:
    if hasattr(schema, "validate_python"):
        return _dump_model(schema.validate_python(item))
    if hasattr(schema, "model_validate"):
        return _dump_model(schema.model_validate(item))
    if hasattr(schema, "parse_obj"):
        return _dump_model(schema.parse_obj(item))
    raise TypeError("schema must be a Pydantic model class or adapter")


def _format_validation_errors(exc: Exception) -> list[dict[str, str]]:
    if not hasattr(exc, "errors"):
        return [{"field": "data", "message": str(exc) or "Validation failed."}]

    formatted_errors: list[dict[str, str]] = []
    for error in exc.errors():  # type: ignore[attr-defined]
        location = [str(part) for part in error.get("loc", []) if part != "__root__"]
        if location and location[0] in {"body", "query", "path"}:
            location = location[1:]
        field_name = ".".join(location) if location else "data"
        formatted_errors.append(
            {
                "field": field_name,
                "message": error.get("msg") or error.get("message") or "Invalid value.",
            }
        )
    return formatted_errors


def _serialize_data(data: Any, schema: Any | None) -> Any:
    if schema is None:
        return value_correction(data)
    if isinstance(data, list):
        return [value_correction(_validate_with_schema(schema, item)) for item in data]
    return value_correction(_validate_with_schema(schema, data))


def _is_integer_value(value: Any) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def _serialize_pagination(pagination: Any) -> tuple[dict[str, Any] | None, list[dict[str, str]] | None]:
    try:
        raw_pagination = _coerce_mapping(pagination, field_name="pagination")
    except TypeError as exc:
        return None, [{"field": "pagination", "message": str(exc) or "Pagination is invalid."}]

    required_fields = ("page", "rows", "total_rows")
    errors: list[dict[str, str]] = []

    for field_name in required_fields:
        if field_name not in raw_pagination:
            errors.append({"field": f"pagination.{field_name}", "message": "Field is required."})
            continue

        value = raw_pagination[field_name]
        if not _is_integer_value(value):
            errors.append({"field": f"pagination.{field_name}", "message": "Must be an integer."})
            continue

        if field_name == "page" and value < 1:
            errors.append({"field": "pagination.page", "message": "Must be greater than or equal to 1."})
        elif field_name in {"rows", "total_rows"} and value < 0:
            errors.append({"field": f"pagination.{field_name}", "message": "Must be greater than or equal to 0."})

    if errors:
        return None, errors

    normalized_pagination = {field_name: raw_pagination[field_name] for field_name in required_fields}
    for key, value in raw_pagination.items():
        if key not in normalized_pagination:
            normalized_pagination[key] = value

    return value_correction(normalized_pagination), None


def _set_response_code(payload: dict[str, Any], response_code: int) -> None:
    payload["response_code"] = response_code


def _set_error_aliases(
    payload: dict[str, Any],
    *,
    message: str,
    details: Sequence[Mapping[str, Any]] | None = None,
) -> None:
    payload["error_message"] = message
    if details:
        payload["errors"] = [dict(item) for item in details]


def _set_validation_error(
    payload: dict[str, Any],
    *,
    message: str,
    details: Sequence[Mapping[str, Any]],
    response_code: int = 422,
    log_payload: object | None = None,
) -> int:
    normalized_details = [dict(item) for item in details]
    logs(log_payload if log_payload is not None else normalized_details, type="error")
    payload["success"] = False
    _set_response_code(payload, response_code)
    payload["error"] = {
        "code": "validation_error",
        "message": message,
        "details": normalized_details,
    }
    _set_error_aliases(payload, message=message, details=normalized_details)
    return response_code


def create_response(
    response_code: int,
    data: Any = None,
    schema: Any | None = None,
    pagination: Any | None = None,
    error_message: str | None = None,
    error_code: str | None = None,
    details: Sequence[Mapping[str, Any]] | None = None,
    meta: Any | None = None,
    *,
    as_json_response: bool = True,
) -> Any:
    if response_code == 204 and as_json_response:
        if Response is None:
            raise RuntimeError("FastAPI must be installed to return a Response object.")
        return Response(status_code=204)

    payload: dict[str, Any] = {
        "success": 200 <= response_code < 300,
        "meta": {},
    }
    _set_response_code(payload, response_code)

    normalized_meta, meta_errors = _serialize_meta(meta)
    payload["meta"] = normalized_meta
    if meta_errors:
        response_code = _set_validation_error(
            payload,
            message="Meta data did not match the expected structure.",
            details=meta_errors,
        )

    if data is not None and "error" not in payload:
        try:
            payload["data"] = _serialize_data(data, schema)
        except Exception as exc:  # noqa: BLE001
            if PydanticValidationError and isinstance(exc, PydanticValidationError):
                validation_errors = _format_validation_errors(exc)
            else:
                validation_errors = [{"field": "data", "message": str(exc) or "Validation failed."}]

            response_code = _set_validation_error(
                payload,
                message="Response data did not match the expected schema.",
                details=validation_errors,
                log_payload={"raw_error": str(exc), "details": validation_errors},
            )

    if pagination is not None and "error" not in payload:
        normalized_pagination, pagination_errors = _serialize_pagination(pagination)
        if pagination_errors:
            response_code = _set_validation_error(
                payload,
                message="Pagination data did not match the expected structure.",
                details=pagination_errors,
            )
        else:
            payload["pagination"] = normalized_pagination

    if "error" not in payload and (error_message or response_code >= 400):
        payload["success"] = False
        _set_response_code(payload, response_code)
        error_obj: dict[str, Any] = {
            "code": error_code or "api_error",
            "message": error_message or _http_status_message(response_code),
        }
        if details:
            error_obj["details"] = [dict(item) for item in details]
        payload["error"] = error_obj
        _set_error_aliases(
            payload,
            message=error_obj["message"],
            details=error_obj.get("details"),
        )

    if not as_json_response:
        return payload

    if JSONResponse is None:
        raise RuntimeError("FastAPI must be installed to return a JSONResponse object.")
    return JSONResponse(content=payload, status_code=response_code)
