from __future__ import annotations
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union

from sqlalchemy.exc import DataError, IntegrityError, StatementError
from storage.accessor import ModelAccessor
from storage.utils import ModelType


__all__ = ["CrudService", "CrudError", "FilterOperation", "Filter"]


# ---------------------------------------------------------
# Filter Types
# ---------------------------------------------------------


class FilterOperation(str, Enum):
    """
    Supported filter operations for find_records queries.

    Comparison operators:
        EQUALS: Equal (field == value)
        NOT_EQUALS: Not equal (field != value)
        GREATER_THAN: Greater than (field > value)
        GREATER_THAN_OR_EQUALS: Greater than or equal (field >= value)
        LESS_THAN: Less than (field < value)
        LESS_THAN_OR_EQUALS: Less than or equal (field <= value)

    Collection operators:
        IN: Value in list (field IN [values])
        NOT_IN: Value not in list (field NOT IN [values])

    String operators:
        CONTAINS: Field contains substring
        STARTS_WITH: Field starts with prefix
        ENDS_WITH: Field ends with suffix
        LIKE: Case-insensitive pattern match (SQL ILIKE)

    Null checks:
        IS_NULL: Field is None
        IS_NOT_NULL: Field is not None
    """

    # Comparison operators
    EQUALS = "eq"
    NOT_EQUALS = "ne"
    GREATER_THAN = "gt"
    GREATER_THAN_OR_EQUALS = "gte"
    LESS_THAN = "lt"
    LESS_THAN_OR_EQUALS = "lte"

    # Collection operators
    IN = "in"
    NOT_IN = "not_in"

    # String operators
    CONTAINS = "contains"
    STARTS_WITH = "startswith"
    ENDS_WITH = "endswith"
    LIKE = "like"

    # Null checks
    IS_NULL = "is_null"
    IS_NOT_NULL = "is_not_null"


class _FilterRequired(TypedDict):
    """Required keys for Filter."""

    field: str
    operation: Union[FilterOperation, str]


class Filter(_FilterRequired, total=False):
    """Type definition for filter dictionaries used in find_records.

    Attributes:
        field: The name of the model field to filter on (required)
        operation: The filter operation to apply - use FilterOperation enum for autocompletion (required)
        value: The value to compare against (optional for IS_NULL/IS_NOT_NULL)
    """

    value: Any


# ---------------------------------------------------------
# Exceptions
# ---------------------------------------------------------


class CrudError(Exception):
    """Base class for CRUD-related errors."""

    def __init__(self, message: str, code: str = "UNKNOWN"):
        super().__init__(message)
        self.code = code


class NotFoundError(CrudError):
    def __init__(self, message: str = "Not found"):
        super().__init__(message, code="NOT_FOUND")


class ConflictError(CrudError):
    def __init__(self, message: str):
        super().__init__(message, code="CONFLICT")


class ValidationError(CrudError):
    def __init__(self, message: str):
        super().__init__(message, code="VALIDATION_ERROR")


class UnsupportedError(CrudError):
    def __init__(self, message: str):
        super().__init__(message, code="UNSUPPORTED_OPERATION")


# ---------------------------------------------------------
# CRUD Service
# ---------------------------------------------------------


class CrudService:
    """
    Transport-agnostic CRUD logic for SQLModel components.

    Provides safe, actor-aware CRUD operations. Intended to be reused
    by both FastAPI routers (REST) and RPC bundles (ConnectRPC).
    """

    _FILTER_OPERATIONS: Dict[FilterOperation, Callable[[Any, Any], Any]] = {
        FilterOperation.EQUALS: lambda field, value: field == value,
        FilterOperation.NOT_EQUALS: lambda field, value: field != value,
        FilterOperation.GREATER_THAN: lambda field, value: field > value,
        FilterOperation.GREATER_THAN_OR_EQUALS: lambda field, value: field >= value,
        FilterOperation.LESS_THAN: lambda field, value: field < value,
        FilterOperation.LESS_THAN_OR_EQUALS: lambda field, value: field <= value,
        FilterOperation.IN: lambda field, value: field.in_(value),
        FilterOperation.NOT_IN: lambda field, value: field.notin_(value),
        FilterOperation.CONTAINS: lambda field, value: field.contains(value),
        FilterOperation.STARTS_WITH: lambda field, value: field.startswith(value),
        FilterOperation.ENDS_WITH: lambda field, value: field.endswith(value),
        FilterOperation.LIKE: lambda field, value: field.ilike(value),
        FilterOperation.IS_NULL: lambda field, _: field.is_(None),
        FilterOperation.IS_NOT_NULL: lambda field, _: field.isnot(None),
    }

    def __init__(
        self,
        accessor: ModelAccessor[ModelType],
        *,
        max_records: Optional[int] = None,
    ) -> None:
        self.accessor = accessor
        self.max_records = max_records

    # ---------------- READS ----------------

    def list_records(
        self,
        *,
        include_deleted: bool = False,
        limit: Optional[int] = None,
        offset: int = 0,
        order_by: Optional[str] = None,
        order_desc: bool = False,
    ) -> List[ModelType]:
        """Return records with optional pagination and sorting.

        Args:
            include_deleted: Whether to include soft-deleted records
            limit: Maximum number of records to return (None = no limit)
            offset: Number of records to skip (for pagination)
            order_by: Field name to sort by (must exist on model)
            order_desc: If True, sort descending; otherwise ascending

        Returns:
            List of model instances

        Raises:
            ValidationError: If order_by field doesn't exist on the model
        """
        try:
            return self.accessor.all(
                include_deleted=include_deleted,
                limit=limit,
                offset=offset,
                order_by=order_by,
                order_desc=order_desc,
            )
        except ValueError as e:
            raise ValidationError(str(e)) from e

    def get_record(
        self,
        record_id: int,
        *,
        include_deleted: bool = False,
    ) -> ModelType:  # type: ignore[type-var,misc]
        """Retrieve a single record by ID."""
        obj = self.accessor.get(record_id, include_deleted=include_deleted)
        if not obj:
            raise NotFoundError()
        return obj

    def find_records(
        self,
        filters: List[Filter],
        *,
        include_deleted: bool = False,
        limit: Optional[int] = None,
        offset: int = 0,
        order_by: Optional[str] = None,
        order_desc: bool = False,
    ) -> List[ModelType]:
        """Find records matching filter conditions.

        Args:
            filters: List of Filter dicts with 'field', 'operation' (use FilterOperation enum), and 'value' keys
            include_deleted: Whether to include soft-deleted records
            limit: Maximum number of records to return (None = no limit)
            offset: Number of records to skip (for pagination)
            order_by: Field name to sort by (must exist on model)
            order_desc: If True, sort descending; otherwise ascending

        Returns:
            List of model instances matching all conditions

        Raises:
            ValidationError: If filter is invalid or field doesn't exist

        Example:
            >>> filters = [
            ...     {"field": "name", "operation": FilterOperation.LIKE, "value": "alice%"},
            ...     {"field": "age", "operation": FilterOperation.GREATER_THAN_OR_EQUALS, "value": 18},
            ... ]
            >>> results = crud.find_records(filters)
        """
        try:
            conditions = []
            for filter_dict in filters:
                field_name = filter_dict["field"]
                operation = filter_dict["operation"]
                value = filter_dict.get("value")

                field = getattr(self.accessor.where, field_name, None)
                if field is None:
                    raise ValidationError(f"Field '{field_name}' not found on {self.accessor.model.__name__}")

                condition = self._apply_filter_operation(field, operation, value)
                conditions.append(condition)

            return self.accessor.find(
                *conditions,
                include_deleted=include_deleted,
                limit=limit,
                offset=offset,
                order_by=order_by,
                order_desc=order_desc,
            )
        except (ValueError, KeyError, AttributeError) as e:
            raise ValidationError(str(e)) from e

    def _apply_filter_operation(self, field: Any, operation: Union[FilterOperation, str], value: Any) -> Any:
        """Apply a filter operation to a field.

        Args:
            field: SQLAlchemy field/column
            operation: Operation (use FilterOperation enum for type safety)
            value: Value to compare against

        Returns:
            SQLAlchemy BinaryExpression representing the condition

        Raises:
            ValidationError: If operation is not supported
        """
        # Normalize string to enum if needed
        try:
            operation_enum = FilterOperation(operation) if isinstance(operation, str) else operation
        except ValueError:
            supported = ", ".join(filter_operation.value for filter_operation in FilterOperation)
            raise ValidationError(f"Unsupported filter operation '{operation}'. Supported: {supported}")

        operation_fn = self._FILTER_OPERATIONS[operation_enum]
        try:
            return operation_fn(field, value)
        except (TypeError, AttributeError) as error:
            raise ValidationError(f"Failed to apply '{operation}' operation: {str(error)}") from error

    # ---------------- WRITES ----------------

    def create_record(self, payload: Dict[str, Any], actor: str) -> ModelType:  # type: ignore[type-var,misc]
        """Insert a new record into the database."""
        if self.max_records is not None:
            total = len(self.accessor.all(include_deleted=True))
            if total >= self.max_records:
                raise ConflictError(f"Max {self.max_records} records allowed for {self.accessor.component}")

        obj = self.accessor.model(**payload)
        try:
            result = self.accessor.insert(obj, actor=actor)
            return result
        except (IntegrityError, DataError, StatementError) as e:
            raise ValidationError(str(e)) from e

    def update_record(
        self,
        record_id: int,
        payload: Dict[str, Any],
        actor: str,
    ) -> ModelType:  # type: ignore[type-var,misc]
        """Upsert a record (PUT semantics)."""
        existed = self.accessor.get(record_id, include_deleted=True) is not None

        if not existed and self.max_records is not None:
            total = len(self.accessor.all(include_deleted=True))
            if total >= self.max_records:
                raise ConflictError(f"Max {self.max_records} records allowed for {self.accessor.component}")

        payload_no_id = {k: v for k, v in payload.items() if k != "id"}
        obj = self.accessor.model(id=record_id, **payload_no_id)

        try:
            saved = self.accessor.save(obj, actor=actor)
        except (IntegrityError, DataError, StatementError) as e:
            raise ValidationError(str(e)) from e

        return saved

    def delete_record(self, record_id: int, actor: str) -> None:
        """Delete a record by ID (soft or hard)."""
        ok = self.accessor.delete(record_id, actor=actor)
        if not ok:
            raise NotFoundError()

    def restore_record(self, record_id: int, actor: str) -> ModelType:  # type: ignore[type-var,misc]
        """Restore a soft-deleted record (if supported).

        Returns the restored model instance. If the record was already
        not deleted, returns it unchanged.
        """
        model_cls = self.accessor.model
        if not hasattr(model_cls, "deleted_at"):
            raise UnsupportedError(f"{self.accessor.component} does not support soft delete/restore")

        obj = self.accessor.get(record_id, include_deleted=True)
        if not obj:
            raise NotFoundError()

        if getattr(obj, "deleted_at") is None:
            # already restored, return as-is
            return obj

        self.accessor.restore(record_id, actor=actor)
        # Fetch the restored record to return it
        restored = self.accessor.get(record_id, include_deleted=False)
        if not restored:
            raise NotFoundError("Record not found after restore")
        return restored
