from __future__ import annotations

from typing import Any, Dict, Optional


# ═══════════════════════════════════════════════════════════════════════════
# BASE ERROR
# ═══════════════════════════════════════════════════════════════════════════

class OptropicError(Exception):
    """Base error for all Optropic API errors.

    Every error carries a machine-readable ``code``, an HTTP ``status_code``
    (when applicable), a ``retryable`` flag, optional ``details`` dict, and
    an optional ``request_id`` for support reference.
    """

    def __init__(
        self,
        message: str,
        status_code: Optional[int] = None,
        code: Optional[str] = None,
        *,
        retryable: bool = False,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(message)
        self.message = message
        self.status_code = status_code
        self.code = code
        self.retryable = retryable
        self.details = details
        self.request_id = request_id

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}("
            f"message={self.message!r}, "
            f"status_code={self.status_code!r}, "
            f"code={self.code!r})"
        )

    def to_dict(self) -> Dict[str, Any]:
        """JSON-serializable representation."""
        return {
            "name": self.__class__.__name__,
            "code": self.code,
            "message": self.message,
            "status_code": self.status_code,
            "retryable": self.retryable,
            "details": self.details,
            "request_id": self.request_id,
        }


# ═══════════════════════════════════════════════════════════════════════════
# AUTHENTICATION ERRORS
# ═══════════════════════════════════════════════════════════════════════════

class AuthenticationError(OptropicError):
    """Raised when the API key is missing or invalid (HTTP 401)."""

    def __init__(
        self,
        message: str = "Authentication failed",
        code: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(
            message=message,
            status_code=401,
            code=code or "INVALID_API_KEY",
            retryable=False,
            details=details,
            request_id=request_id,
        )


class ForbiddenError(OptropicError):
    """Raised when the caller lacks permission (HTTP 403)."""

    def __init__(
        self,
        message: str = "Forbidden",
        code: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(
            message=message,
            status_code=403,
            code=code or "INSUFFICIENT_PERMISSIONS",
            retryable=False,
            details=details,
            request_id=request_id,
        )


# ═══════════════════════════════════════════════════════════════════════════
# VALIDATION ERRORS
# ═══════════════════════════════════════════════════════════════════════════

class ValidationError(OptropicError):
    """Raised when the request payload fails validation (HTTP 400)."""

    def __init__(
        self,
        message: str = "Validation error",
        code: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(
            message=message,
            status_code=400,
            code=code or "VALIDATION_ERROR",
            retryable=False,
            details=details,
            request_id=request_id,
        )


class InvalidSerialError(ValidationError):
    """Raised when a serial number is invalid."""

    def __init__(
        self,
        serial: str = "",
        message: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.serial = serial
        super().__init__(
            message=message or f'Invalid serial number: "{serial}".',
            code="INVALID_SERIAL",
            details={"serial": serial, **(details or {})},
            request_id=request_id,
        )


class InvalidGTINError(ValidationError):
    """Raised when a GTIN is invalid."""

    def __init__(
        self,
        gtin: str = "",
        message: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.gtin = gtin
        super().__init__(
            message=message or f'Invalid GTIN: "{gtin}".',
            code="INVALID_GTIN",
            details={"gtin": gtin, **(details or {})},
            request_id=request_id,
        )


class InvalidCodeError(ValidationError):
    """Raised when a code format is invalid."""

    def __init__(
        self,
        invalid_code: str = "",
        message: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.invalid_code = invalid_code
        super().__init__(
            message=message or f'Invalid code format: "{invalid_code}".',
            code="INVALID_CODE_FORMAT",
            details={"code": invalid_code, **(details or {})},
            request_id=request_id,
        )


# ═══════════════════════════════════════════════════════════════════════════
# RESOURCE ERRORS
# ═══════════════════════════════════════════════════════════════════════════

class NotFoundError(OptropicError):
    """Raised when the requested resource does not exist (HTTP 404)."""

    def __init__(
        self,
        message: str = "Not found",
        code: Optional[str] = None,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(
            message=message,
            status_code=404,
            code=code or "NOT_FOUND",
            retryable=False,
            details=details,
            request_id=request_id,
        )


class CodeNotFoundError(NotFoundError):
    """Raised when a code is not found."""

    def __init__(
        self,
        searched_code: str = "",
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.searched_code = searched_code
        super().__init__(
            message=f'Code not found: "{searched_code}".',
            code="CODE_NOT_FOUND",
            details={"code": searched_code, **(details or {})},
            request_id=request_id,
        )


class BatchNotFoundError(NotFoundError):
    """Raised when a batch is not found."""

    def __init__(
        self,
        batch_id: str = "",
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.batch_id = batch_id
        super().__init__(
            message=f'Batch not found: "{batch_id}".',
            code="BATCH_NOT_FOUND",
            details={"batchId": batch_id, **(details or {})},
            request_id=request_id,
        )


# ═══════════════════════════════════════════════════════════════════════════
# STATE ERRORS
# ═══════════════════════════════════════════════════════════════════════════

class RevokedCodeError(OptropicError):
    """Raised when a code has been revoked."""

    def __init__(
        self,
        revoked_code: str = "",
        reason: str = "Unknown reason",
        revoked_at: str = "",
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.revoked_code = revoked_code
        self.reason = reason
        self.revoked_at = revoked_at
        super().__init__(
            message=f"Code has been revoked: {reason}",
            status_code=410,
            code="CODE_REVOKED",
            retryable=False,
            details={
                "code": revoked_code,
                "reason": reason,
                "revokedAt": revoked_at,
                **(details or {}),
            },
            request_id=request_id,
        )


# ═══════════════════════════════════════════════════════════════════════════
# RATE LIMITING ERRORS
# ═══════════════════════════════════════════════════════════════════════════

class RateLimitError(OptropicError):
    """Raised when the caller has exceeded the allowed request rate (HTTP 429)."""

    def __init__(
        self,
        message: str = "Rate limit exceeded",
        code: Optional[str] = None,
        *,
        retry_after: Optional[int] = None,
        limit: int = 0,
        remaining: int = 0,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.retry_after = retry_after or 60
        self.limit = limit
        self.remaining = remaining
        super().__init__(
            message=message,
            status_code=429,
            code=code or "RATE_LIMITED",
            retryable=True,
            details={
                "retryAfter": self.retry_after,
                "limit": limit,
                "remaining": remaining,
                **(details or {}),
            },
            request_id=request_id,
        )


class QuotaExceededError(OptropicError):
    """Raised when the monthly quota is exceeded (HTTP 402)."""

    def __init__(
        self,
        quota_limit: int = 0,
        quota_used: int = 0,
        resets_at: str = "",
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.quota_limit = quota_limit
        self.quota_used = quota_used
        self.resets_at = resets_at
        super().__init__(
            message=f"Monthly quota exceeded ({quota_used}/{quota_limit}). Resets at {resets_at}.",
            status_code=402,
            code="QUOTA_EXCEEDED",
            retryable=False,
            details={
                "quotaLimit": quota_limit,
                "quotaUsed": quota_used,
                "resetsAt": resets_at,
                **(details or {}),
            },
            request_id=request_id,
        )


# ═══════════════════════════════════════════════════════════════════════════
# NETWORK ERRORS
# ═══════════════════════════════════════════════════════════════════════════

class NetworkError(OptropicError):
    """Raised when a network error occurs."""

    def __init__(
        self,
        message: str = "Network error occurred",
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(
            message=message,
            status_code=None,
            code="NETWORK_ERROR",
            retryable=True,
            details=details,
            request_id=request_id,
        )


class TimeoutError(OptropicError):
    """Raised when a request times out."""

    def __init__(
        self,
        timeout_ms: int = 30000,
        *,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        self.timeout_ms = timeout_ms
        super().__init__(
            message=f"Request timed out after {timeout_ms}ms.",
            status_code=408,
            code="TIMEOUT",
            retryable=True,
            details={"timeoutMs": timeout_ms, **(details or {})},
            request_id=request_id,
        )


class ServiceUnavailableError(OptropicError):
    """Raised when the service is temporarily unavailable (HTTP 503)."""

    def __init__(
        self,
        message: str = "Service temporarily unavailable",
        *,
        retry_after: Optional[int] = None,
        details: Optional[Dict[str, Any]] = None,
        request_id: Optional[str] = None,
    ) -> None:
        super().__init__(
            message=message,
            status_code=503,
            code="SERVICE_UNAVAILABLE",
            retryable=True,
            details={"retryAfter": retry_after, **(details or {})},
            request_id=request_id,
        )


# ═══════════════════════════════════════════════════════════════════════════
# ERROR FACTORY
# ═══════════════════════════════════════════════════════════════════════════

def create_error_from_response(
    status_code: int,
    body: Dict[str, Any],
) -> OptropicError:
    """Create an appropriate error from an API response.

    Used internally by the client to map HTTP responses to typed errors.
    """
    code = body.get("code", "UNKNOWN_ERROR")
    message = body.get("message", "An unknown error occurred")
    details = body.get("details")
    request_id = body.get("requestId")

    error_map = {
        "INVALID_API_KEY": lambda: AuthenticationError(message, code, details=details, request_id=request_id),
        "EXPIRED_API_KEY": lambda: AuthenticationError(message, code, details=details, request_id=request_id),
        "INSUFFICIENT_PERMISSIONS": lambda: ForbiddenError(message, code, details=details, request_id=request_id),
        "VALIDATION_ERROR": lambda: ValidationError(message, code, details=details, request_id=request_id),
        "INVALID_SERIAL": lambda: InvalidSerialError(
            (details or {}).get("serial", ""), message, details=details, request_id=request_id,
        ),
        "INVALID_GTIN": lambda: InvalidGTINError(
            (details or {}).get("gtin", ""), message, details=details, request_id=request_id,
        ),
        "INVALID_CODE_FORMAT": lambda: InvalidCodeError(
            (details or {}).get("code", ""), message, details=details, request_id=request_id,
        ),
        "CODE_NOT_FOUND": lambda: CodeNotFoundError(
            (details or {}).get("code", ""), details=details, request_id=request_id,
        ),
        "BATCH_NOT_FOUND": lambda: BatchNotFoundError(
            (details or {}).get("batchId", ""), details=details, request_id=request_id,
        ),
        "CODE_REVOKED": lambda: RevokedCodeError(
            (details or {}).get("code", ""),
            (details or {}).get("reason", "Unknown reason"),
            (details or {}).get("revokedAt", ""),
            details=details, request_id=request_id,
        ),
        "RATE_LIMITED": lambda: RateLimitError(
            message, code,
            retry_after=(details or {}).get("retryAfter"),
            limit=(details or {}).get("limit", 0),
            remaining=(details or {}).get("remaining", 0),
            details=details, request_id=request_id,
        ),
        "QUOTA_EXCEEDED": lambda: QuotaExceededError(
            (details or {}).get("quotaLimit", 0),
            (details or {}).get("quotaUsed", 0),
            (details or {}).get("resetsAt", ""),
            details=details, request_id=request_id,
        ),
        "NETWORK_ERROR": lambda: NetworkError(message, details=details, request_id=request_id),
        "TIMEOUT": lambda: TimeoutError(
            (details or {}).get("timeoutMs", 30000), details=details, request_id=request_id,
        ),
        "SERVICE_UNAVAILABLE": lambda: ServiceUnavailableError(
            message,
            retry_after=(details or {}).get("retryAfter"),
            details=details, request_id=request_id,
        ),
    }

    factory = error_map.get(code)
    if factory:
        return factory()

    # Fallback: map by HTTP status code for unknown error codes
    status_map = {
        400: lambda: ValidationError(message, code, details=details, request_id=request_id),
        401: lambda: AuthenticationError(message, code, details=details, request_id=request_id),
        403: lambda: ForbiddenError(message, code, details=details, request_id=request_id),
        404: lambda: NotFoundError(message, code, details=details, request_id=request_id),
        429: lambda: RateLimitError(message, code, details=details, request_id=request_id),
        503: lambda: ServiceUnavailableError(message, details=details, request_id=request_id),
    }

    status_factory = status_map.get(status_code)
    if status_factory:
        return status_factory()

    return OptropicError(
        message=message,
        status_code=status_code,
        code=code,
        retryable=status_code >= 500,
        details=details,
        request_id=request_id,
    )
