"""
Custom exceptions for bulk operations.

These exceptions provide domain-specific error handling for bulk operations,
with structured error information for better API responses.
"""

from rest_framework.exceptions import APIException
from rest_framework import status


class BulkOperationError(APIException):
    """Base exception for all bulk operation errors."""

    status_code = status.HTTP_400_BAD_REQUEST
    default_detail = "A bulk operation error occurred."
    default_code = "bulk_operation_error"


class DuplicateKeyError(BulkOperationError):
    """
    Raised when duplicate unique_field combinations found in request.

    Attributes:
        duplicates: List of duplicate unique_field combinations
    """

    default_detail = "Duplicate unique field combinations found in request."
    default_code = "duplicate_key"

    def __init__(self, duplicates, detail=None, code=None):
        """
        Initialize with list of duplicates.

        Args:
            duplicates: List of dicts containing duplicate unique field values
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.duplicates = duplicates
        if detail is None:
            detail = f"{self.default_detail} Found {len(duplicates)} duplicate(s)."
        super().__init__(detail, code)


class MissingUniqueFieldError(BulkOperationError):
    """
    Raised when required unique_field missing from data.

    Attributes:
        field: Name of missing field
        index: Position in data where field is missing
        indices: List of all positions where field is missing
    """

    default_detail = "Required unique field is missing from data."
    default_code = "missing_unique_field"

    def __init__(self, field, index=None, indices=None, detail=None, code=None):
        """
        Initialize with missing field information.

        Args:
            field: Name of the missing field
            index: Single index where field is missing (optional)
            indices: List of indices where field is missing (optional)
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.field = field
        self.index = index
        self.indices = indices or ([index] if index is not None else [])

        if detail is None:
            if self.indices:
                detail = f'Field "{field}" is missing at indices: {self.indices}'
            else:
                detail = f'Field "{field}" is required for this operation.'
        super().__init__(detail, code)


class BatchSizeExceededError(BulkOperationError):
    """
    Raised when request exceeds MAX_BATCH_SIZE.

    Attributes:
        size: Actual size of request
        max_size: Maximum allowed size
    """

    default_detail = "Request batch size exceeds maximum allowed."
    default_code = "batch_size_exceeded"

    def __init__(self, size, max_size, detail=None, code=None):
        """
        Initialize with size information.

        Args:
            size: Actual batch size
            max_size: Maximum allowed batch size
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.size = size
        self.max_size = max_size

        if detail is None:
            detail = f"Batch size {size} exceeds maximum allowed size of {max_size}."
        super().__init__(detail, code)


class BulkValidationError(BulkOperationError):
    """
    Raised when bulk data fails validation.

    Attributes:
        errors: Dict of {index: error_details}
    """

    default_detail = "Bulk data validation failed."
    default_code = "bulk_validation_error"
    status_code = status.HTTP_400_BAD_REQUEST

    def __init__(self, errors, detail=None, code=None):
        """
        Initialize with validation errors.

        Args:
            errors: Dictionary mapping indices to error details
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.errors = errors

        if detail is None:
            error_count = len(errors)
            detail = f"Validation failed for {error_count} item(s)."
        super().__init__(detail, code)

    def get_full_details(self):
        """
        Get full error details including per-item errors.

        Returns:
            Dict with overall message and per-index errors
        """
        return {"message": str(self.detail), "code": self.default_code, "errors": self.errors}


class PartialBulkError(BulkOperationError):
    """
    Raised when some operations succeed but others fail.

    This exception is used when the failure strategy allows partial success.

    Attributes:
        successful: List of successful instances
        failed: List of failed items
        errors: Dict of {index: error}
    """

    default_detail = "Bulk operation completed with partial failures."
    default_code = "partial_bulk_error"
    status_code = status.HTTP_207_MULTI_STATUS

    def __init__(self, successful, failed, errors, detail=None, code=None):
        """
        Initialize with success and failure information.

        Args:
            successful: List of successfully processed instances
            failed: List of items that failed processing
            errors: Dictionary mapping indices to error details
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.successful = successful
        self.failed = failed
        self.errors = errors

        if detail is None:
            success_count = len(successful)
            fail_count = len(failed)
            detail = f"{success_count} succeeded, {fail_count} failed."
        super().__init__(detail, code)

    def to_response_data(self):
        """
        Convert to structured response data.

        Returns:
            Dict suitable for HTTP response
        """
        return {
            "message": str(self.detail),
            "code": self.default_code,
            "successful": len(self.successful),
            "failed": len(self.failed),
            "errors": self.errors,
        }


class UnsupportedBulkOperation(BulkOperationError):
    """
    Raised when operation not supported for this model/viewset.
    """

    default_detail = "This bulk operation is not supported."
    default_code = "unsupported_operation"
    status_code = status.HTTP_405_METHOD_NOT_ALLOWED

    def __init__(self, operation=None, detail=None, code=None):
        """
        Initialize with operation information.

        Args:
            operation: Name of the unsupported operation
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.operation = operation

        if detail is None and operation:
            detail = f"Bulk {operation} operation is not supported."
        super().__init__(detail, code)


class ObjectNotFoundError(BulkOperationError):
    """
    Raised when objects required for update are not found.

    This is used specifically for bulk update operations where
    all objects must exist.
    """

    default_detail = "One or more objects not found."
    default_code = "object_not_found"
    status_code = status.HTTP_404_NOT_FOUND

    def __init__(self, missing_keys, detail=None, code=None):
        """
        Initialize with missing key information.

        Args:
            missing_keys: List of unique field combinations that weren't found
            detail: Optional custom error message
            code: Optional custom error code
        """
        self.missing_keys = missing_keys

        if detail is None:
            detail = f"{len(missing_keys)} object(s) not found for update."
        super().__init__(detail, code)
