from typing import Any, Generic, TypeVar

from pydantic import BaseModel
from sqlalchemy import select, func, delete as sa_delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.inspection import inspect

ModelType = TypeVar("ModelType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)


class MissingSessionError(RuntimeError):
    pass


class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
    def __init__(self, model: type[ModelType], *, config=None):
        self.model = model
        self.config = config

    # -------------------------
    # SESSION
    # -------------------------

    def _db(self, db: AsyncSession | None) -> AsyncSession:
        if db:
            return db

        if self.config and hasattr(self.config, "current_db_session"):
            return self.config.current_db_session()

        raise MissingSessionError(f"No DB session for {self.model.__name__}")


    # -------------------------
    # USER (audit)
    # -------------------------

    def _current_user_id(self) -> str | None:
        if self.config and hasattr(self.config, "current_user_id"):
            return self.config.current_user_id()
        return None

    def _apply_create_audit(self, obj):
        user_id = self._current_user_id()
        if not user_id:
            return

        if hasattr(obj, "created_by") and not getattr(obj, "created_by", None):
            obj.created_by = user_id

        if hasattr(obj, "updated_by"):
            obj.updated_by = user_id

    def _apply_update_audit(self, obj):
        user_id = self._current_user_id()
        if user_id and hasattr(obj, "updated_by"):
            obj.updated_by = user_id

    # -------------------------
    # PK
    # -------------------------

    @property
    def pk_column(self):
        mapper = inspect(self.model)
        pk = mapper.primary_key

        if len(pk) != 1:
            raise ValueError("Only single PK supported")

        return pk[0]

    # -------------------------
    # GET
    # -------------------------

    async def get(self, id: Any, *, db: AsyncSession | None = None):
        db = self._db(db)
        return await db.get(self.model, id)

    async def get_multi(
        self,
        *,
        offset: int = 0,
        limit: int = 100,
        db: AsyncSession | None = None,
    ):
        db = self._db(db)

        stmt = select(self.model).offset(offset).limit(limit)
        result = await db.execute(stmt)
        return result.scalars().all()

    async def count(self, *, db: AsyncSession | None = None):
        db = self._db(db)

        stmt = select(func.count()).select_from(self.model)
        result = await db.execute(stmt)
        return result.scalar_one()

    # -------------------------
    # CREATE
    # -------------------------

    async def create(
        self,
        *,
        obj_in: CreateSchemaType | dict,
        db: AsyncSession | None = None,
        commit: bool = True,
    ):
        db = self._db(db)

        data = (
            obj_in.model_dump(exclude_unset=True)
            if isinstance(obj_in, BaseModel)
            else obj_in
        )

        db_obj = self.model(**data)

        self._apply_create_audit(db_obj)

        db.add(db_obj)

        if commit:
            await db.commit()
            await db.refresh(db_obj)
        else:
            await db.flush()

        return db_obj

    # -------------------------
    # UPDATE
    # -------------------------

    async def update(
        self,
        *,
        db_obj,
        obj_in,
        db: AsyncSession | None = None,
        commit: bool = True,
    ):
        db = self._db(db)

        data = (
            obj_in.model_dump(exclude_unset=True)
            if isinstance(obj_in, BaseModel)
            else obj_in
        )

        for k, v in data.items():
            setattr(db_obj, k, v)

        self._apply_update_audit(db_obj)

        db.add(db_obj)

        if commit:
            await db.commit()
            await db.refresh(db_obj)
        else:
            await db.flush()

        return db_obj

    # -------------------------
    # DELETE
    # -------------------------

    async def delete(
        self,
        *,
        id: Any,
        db: AsyncSession | None = None,
        commit: bool = True,
    ):
        db = self._db(db)

        obj = await db.get(self.model, id)
        if not obj:
            return None

        await db.delete(obj)

        if commit:
            await db.commit()
        else:
            await db.flush()

        return obj

    async def delete_where(
        self,
        *,
        where,
        db: AsyncSession | None = None,
        commit: bool = True,
    ):
        db = self._db(db)

        stmt = sa_delete(self.model).where(where)
        result = await db.execute(stmt)

        if commit:
            await db.commit()
        else:
            await db.flush()

        return result.rowcount or 0