"""Base repository providing common SQLAlchemy patterns.

Provides BaseRepository class and composable mixins for standard CRUD operations.
Mixins require class attributes to be defined by the implementing class:
- _model_class: The SQLAlchemy model class
- _to_domain: Callable to convert ORM model to domain entity
"""

from __future__ import annotations

from collections.abc import Sequence
from typing import TYPE_CHECKING, TypeVar, cast
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.engine import CursorResult
from sqlalchemy.ext.asyncio import AsyncSession

if TYPE_CHECKING:
    from sqlalchemy.orm import DeclarativeBase
    from sqlalchemy.sql import Delete, Select, Update

TModel = TypeVar("TModel", bound="DeclarativeBase")
TExists = TypeVar("TExists")
TEntity = TypeVar("TEntity")


# =============================================================================
# Shared Type Conversion Utilities
# =============================================================================


def bool_or_none(value: object) -> bool | None:
    """Convert value to bool if it is a bool, else return None."""
    return value if isinstance(value, bool) else None


def string_list_or_none(value: object) -> list[str] | None:
    """Convert value to list[str] if it is a list of strings, else return None."""
    if value is None:
        return None
    if isinstance(value, list):
        items = cast(list[object], value)
        return [item for item in items if isinstance(item, str)]
    return None


class BaseRepository:
    """Base class for SQLAlchemy repositories.

    Provides common session management and helper methods for
    executing queries and persisting models.
    """

    def __init__(self, session: AsyncSession) -> None:
        """Initialize repository with database session.

        Args:
            session: SQLAlchemy async session.
        """
        self._session = session

    async def _execute_scalar(
        self,
        stmt: Select[tuple[TModel]],
    ) -> TModel | None:
        """Execute statement and return single scalar result.

        Args:
            stmt: SQLAlchemy select statement.

        Returns:
            Single model instance or None if not found.
        """
        result = await self._session.execute(stmt)
        return result.scalar_one_or_none()

    async def _execute_scalars(
        self,
        stmt: Select[tuple[TModel]],
    ) -> list[TModel]:
        """Execute statement and return all scalar results.

        Args:
            stmt: SQLAlchemy select statement.

        Returns:
            List of model instances.
        """
        result = await self._session.execute(stmt)
        return list(result.scalars().all())

    async def _add_and_flush(self, model: TModel) -> TModel:
        """Add model to session and flush.

        Args:
            model: ORM model instance to persist.

        Returns:
            The persisted model with generated fields populated.
        """
        self._session.add(model)
        await self._session.flush()
        return model

    async def _delete_and_flush(self, model: object) -> None:
        """Delete model from session and flush.

        Args:
            model: ORM model instance to delete.
        """
        await self._session.delete(model)
        await self._session.flush()

    async def _add_all_and_flush(self, models: list[TModel]) -> list[TModel]:
        """Add multiple models to session and flush once.

        Use this for batching inserts to reduce database round-trips.

        Args:
            models: List of ORM model instances to persist.

        Returns:
            The persisted models with generated fields populated.
        """
        self._session.add_all(models)
        await self._session.flush()
        return models

    async def _execute_count(self, stmt: Select[tuple[int]]) -> int:
        """Execute count query and return result.

        Args:
            stmt: SQLAlchemy select statement returning a count.

        Returns:
            Integer count value.
        """
        result = await self._session.execute(stmt)
        return result.scalar_one()

    async def _execute_exists(self, stmt: Select[tuple[TExists]]) -> bool:
        """Check if any rows match the query.

        More efficient than fetching all rows - stops at first match.

        Args:
            stmt: SQLAlchemy select statement.

        Returns:
            True if at least one row exists.
        """
        result = await self._session.execute(stmt.limit(1))
        return result.scalar() is not None

    async def _update_fields(
        self,
        model: TModel,
        **fields: object,
    ) -> TModel:
        """Update model fields and flush.

        Args:
            model: ORM model instance to update.
            **fields: Field name/value pairs to set.

        Returns:
            The updated model.
        """
        for key, value in fields.items():
            setattr(model, key, value)
        await self._session.flush()
        return model

    async def _execute_update(self, stmt: Update) -> int:
        """Execute update statement and return affected row count.

        Args:
            stmt: SQLAlchemy update statement.

        Returns:
            Number of rows affected.
        """
        result = cast(CursorResult[tuple[()]], await self._session.execute(stmt))
        await self._session.flush()
        return result.rowcount

    async def _execute_delete(self, stmt: Delete) -> int:
        """Execute delete statement and return affected row count.

        Args:
            stmt: SQLAlchemy delete statement.

        Returns:
            Number of rows deleted.
        """
        result = cast(CursorResult[tuple[()]], await self._session.execute(stmt))
        await self._session.flush()
        return result.rowcount


# =============================================================================
# Repository Mixins
# =============================================================================
#
# Composable mixins for standard repository patterns. Mixins require:
# - _model_class: class attribute for the SQLAlchemy model
# - _to_domain: callable (method or staticmethod) to convert model → entity
# - Inheritance from BaseRepository (for session and helper methods)
#
# Usage:
#     class MyRepository(BaseRepository, GetByIdMixin[MyModel, MyEntity]):
#         _model_class = MyModel
#         _to_domain = staticmethod(MyConverter.to_domain)
# =============================================================================


class GetByIdMixin[TModel, TEntity]:
    """Mixin providing standardized get_by_id implementation.

    Requires class attributes:
        _model_class: The SQLAlchemy model class with an 'id' column.

    Requires method:
        _to_domain(model: TModel) -> TEntity: Convert ORM model to domain.

    Must be combined with BaseRepository for access to session.
    """

    _model_class: type[TModel]
    _session: AsyncSession  # From BaseRepository

    def _to_domain(self, model: TModel) -> TEntity:
        """Convert ORM model to domain entity. Override in subclass."""
        raise NotImplementedError

    async def _mixin_get_by_id(self, entity_id: UUID) -> TEntity | None:
        """Retrieve entity by ID with automatic conversion.

        Args:
            entity_id: Entity UUID.

        Returns:
            Domain entity if found, None otherwise.
        """
        stmt = select(self._model_class).where(self._model_class.id == entity_id)  # type: ignore[attr-defined]
        result = await self._session.execute(stmt)
        model = result.scalar_one_or_none()
        return self._to_domain(model) if model else None


class DeleteByIdMixin[TModel]:
    """Mixin providing standardized delete implementation.

    Requires class attributes:
        _model_class: The SQLAlchemy model class with an 'id' column.

    Must be combined with BaseRepository for access to session methods.
    """

    _model_class: type[TModel]
    _session: AsyncSession  # From BaseRepository

    async def _mixin_delete_by_id(self, entity_id: UUID) -> bool:
        """Delete entity by ID.

        Args:
            entity_id: Entity UUID.

        Returns:
            True if deleted, False if not found.
        """
        stmt = select(self._model_class).where(self._model_class.id == entity_id)  # type: ignore[attr-defined]
        result = await self._session.execute(stmt)
        model = result.scalar_one_or_none()
        if not model:
            return False
        await self._session.delete(model)
        await self._session.flush()
        return True


class GetByMeetingMixin[TModel, TEntity]:
    """Mixin for repositories with meeting-scoped entities.

    Requires class attributes:
        _model_class: SQLAlchemy model with 'meeting_id' and 'created_at' columns.

    Requires method:
        _to_domain(model: TModel) -> TEntity: Convert ORM model to domain.

    Must be combined with BaseRepository for access to session methods.
    """

    _model_class: type[TModel]
    _session: AsyncSession  # From BaseRepository

    def _to_domain(self, model: TModel) -> TEntity:
        """Convert ORM model to domain entity. Override in subclass."""
        raise NotImplementedError

    async def _mixin_get_by_meeting(self, meeting_id: UUID) -> Sequence[TEntity]:
        """Get all entities for a specific meeting.

        Args:
            meeting_id: Meeting UUID.

        Returns:
            List of domain entities ordered by creation time.
        """
        stmt = (
            select(self._model_class)
            .where(self._model_class.meeting_id == meeting_id)  # type: ignore[attr-defined]
            .order_by(self._model_class.created_at.asc())  # type: ignore[attr-defined]
        )
        result = await self._session.execute(stmt)
        models = list(result.scalars().all())
        return [self._to_domain(m) for m in models]
