from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, Optional, Type


@dataclass
class ItsAIError(Exception):
    status_code: Optional[int]
    type: Optional[str]
    message: str
    payload: Optional[Dict[str, Any]] = None

    def __str__(self) -> str:  # pragma: no cover - trivial
        sc = f" status={self.status_code}" if self.status_code is not None else ""
        tp = f" type={self.type}" if self.type else ""
        return f"ItsAIError{sc}{tp}: {self.message}"


class NetworkError(ItsAIError):
    pass


class ApiError(ItsAIError):
    """High-level API error attached to successful HTTP responses."""

    def __init__(
            self,
            *,
            code: Optional[str],
            message: str,
            details: Optional[Dict[str, Any]] = None,
            status_code: Optional[int] = None,
    ) -> None:
        payload = details if isinstance(details, dict) else None
        super().__init__(status_code=status_code, type=code, message=message, payload=payload)

    @property
    def code(self) -> Optional[str]:
        return self.type

    @property
    def details(self) -> Optional[Dict[str, Any]]:
        return self.payload


class KeyDoesNotExist(ItsAIError):
    pass


class ApiKeyDoesNotExist(ItsAIError):
    pass


class RichLimit(ItsAIError):
    pass


class LowWords(ItsAIError):
    pass


class ManyWords(ItsAIError):
    pass


class RateLimitExceeded(ItsAIError):
    pass


class BatchLimitExceeded(ItsAIError):
    pass


class BatchLimit(ItsAIError):
    pass


class OnlyEnglish(ItsAIError):
    pass


class Blacklist(ItsAIError):
    pass


class ValidationError(ItsAIError):
    pass


class NotFound(ItsAIError):
    pass


class PermissionDenied(ItsAIError):
    pass


class NotAcceptable(ItsAIError):
    pass


class AuthenticationFailed(ItsAIError):
    pass


class ServerError(ItsAIError):
    pass


class ServiceUnavailable(ItsAIError):
    pass


class RequestTimeout(ItsAIError):
    pass


class NoResultFound(ItsAIError):
    pass


class AnalysisNotFound(ItsAIError):
    pass


class PlagiarismCheckNotFound(ItsAIError):
    pass


class AnalysisIdRequired(ItsAIError):
    pass


class ExecutionLimitExceeded(ItsAIError):
    pass


class InvalidSignature(ItsAIError):
    pass


class EventNameRequired(ItsAIError):
    pass


TYPE_TO_ERROR: Dict[str, Type[ItsAIError]] = {
    "key_not_exist": KeyDoesNotExist,
    "api_key_does_not_exist": ApiKeyDoesNotExist,
    "rich_limit": RichLimit,
    "low_words": LowWords,
    "many_words": ManyWords,
    "rate_limit": RateLimitExceeded,
    "batch_limit_exceed": BatchLimitExceeded,
    "batch_limit": BatchLimit,
    "only_english": OnlyEnglish,
    "blacklist": Blacklist,
    "validation": ValidationError,
    "not_found": NotFound,
    "permission_denied": PermissionDenied,
    "not_acceptable": NotAcceptable,
    "authentication_failed": AuthenticationFailed,
    "server_error": ServerError,
    "service_unavailable": ServiceUnavailable,
    "request_timeout": RequestTimeout,
    "no_result_found": NoResultFound,
    "analysis_not_found": AnalysisNotFound,
    "plagiarism_check_not_found": PlagiarismCheckNotFound,
    "analysis_id_required": AnalysisIdRequired,
    "execution_limit_exceeded": ExecutionLimitExceeded,
    "invalid_signature": InvalidSignature,
    "event_name_required": EventNameRequired,
}


def error_class_from_code(error_code: str) -> Optional[Type[ItsAIError]]:
    if ":" in error_code:
        err_type, err_name = error_code.split(":")[:2]
        return TYPE_TO_ERROR.get(err_name) or TYPE_TO_ERROR.get(err_type) or TYPE_TO_ERROR.get(error_code)
    return TYPE_TO_ERROR.get(error_code)


def error_from_type(
        error_type: Optional[str],
        status_code: Optional[int],
        message: str,
        payload: Optional[Dict[str, Any]] = None,
) -> ItsAIError:
    exc_cls: Type[ItsAIError] = ItsAIError
    if isinstance(error_type, str):
        found_cls = error_class_from_code(error_type)
        if found_cls is not None:
            exc_cls = found_cls

    return exc_cls(status_code=status_code, type=error_type, message=message, payload=payload)


def build_error_from_http(
        status_code: int,
        body_text: Optional[str],
        json_body: Optional[Dict[str, Any]],
) -> ItsAIError:
    if not isinstance(json_body, dict):
        # Non-JSON or unexpected
        if body_text is None or not body_text.strip():
            message = "Non-JSON error response"
        else:
            message = body_text.strip()

        # Map by status as fallback
        if status_code == 400:
            return ValidationError(status_code=status_code, type="validation", message=message)
        if status_code == 401:
            return AuthenticationFailed(status_code=status_code, type="authentication_failed", message=message)
        if status_code == 403:
            return PermissionDenied(status_code=status_code, type="permission_denied", message=message)
        if status_code == 404:
            return NotFound(status_code=status_code, type="not_found", message=message)
        if status_code == 406:
            return NotAcceptable(status_code=status_code, type="not_acceptable", message=message)
        if status_code == 408:
            return RequestTimeout(status_code=status_code, type="request_timeout", message=message)
        if status_code == 429:
            return RateLimitExceeded(status_code=status_code, type="rate_limit", message=message)
        if 500 <= status_code < 600:
            if status_code in (503, 504):
                return ServiceUnavailable(status_code=status_code, type="service_unavailable", message=message)
            return ServerError(status_code=status_code, type="server_error", message=message)

        return ItsAIError(status_code=status_code, type=None, message=message)

    embedded_error = json_body.get("error")
    if not isinstance(embedded_error, dict):
        return ServerError(status_code=500, type="unknown_response",
                           message=f"Server response can't be parsed: {body_text}")

    # 'type' for backward capability
    error_type = embedded_error.get("code") or json_body.get("type")
    message = str(
        embedded_error.get("message")
        or embedded_error.get("error")
        or json_body.get("message")
        or json_body.get("error")
        or "API error"
    )
    embedded_error.pop("message", None)
    embedded_error.pop("code", None)
    # Backward capability
    embedded_error.pop("type", None)
    payload = embedded_error
    return error_from_type(error_type=error_type, status_code=status_code, message=message, payload=payload)

