# https://chatgpt.com/share/e/69ca1d35-3154-800d-9fa4-c1aed736d70a

from __future__ import annotations

import logging
from datetime import datetime
from typing import Any, Generic, TypeVar, TYPE_CHECKING

from pydantic import BaseModel
from sqlalchemy import Select, delete as sa_delete, exists, func, inspect, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from fastapi_pagination import Params
from fastapi_pagination.ext.sqlalchemy import paginate

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from gisweb_tenants.tenant_db import TenantDbConfig


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


class MissingDbError(RuntimeError):
    pass


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

    # -------------------------
    # DB
    # -------------------------

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

        if self.config is None:
            raise MissingDbError(f"No active DB for {self.model.__name__}")

        return self.config.current_db()

    # -------------------------
    # AUDIT
    # -------------------------

    def _current_user_id(self) -> str | None:
        if self.config is None:
            return None
        return self.config.current_user_id()

    def _apply_create_audit(self, obj: ModelType) -> None:
        user_id = self._current_user_id()
        if not user_id:
            return

        if hasattr(obj, "created_by") and not getattr(obj, "created_by", None):
            setattr(obj, "created_by", user_id)
        if hasattr(obj, "created_by_id") and not getattr(obj, "created_by_id", None):
            setattr(obj, "created_by_id", user_id)

        if hasattr(obj, "updated_by"):
            setattr(obj, "updated_by", user_id)
        if hasattr(obj, "updated_by_id"):
            setattr(obj, "updated_by_id", user_id)

    def _apply_update_audit(self, obj: ModelType) -> None:
        user_id = self._current_user_id()
        if not user_id:
            return

        if hasattr(obj, "updated_by"):
            setattr(obj, "updated_by", user_id)
        if hasattr(obj, "updated_by_id"):
            setattr(obj, "updated_by_id", user_id)

        if hasattr(obj, "updated_at"):
            setattr(obj, "updated_at", datetime.now())

    # -------------------------
    # HOOKS
    # -------------------------

    def _prepare_create_data(
        self,
        obj_in: CreateSchemaType | dict[str, Any],
    ) -> dict[str, Any]:
        return self._to_dict(obj_in)

    def _prepare_update_data(
        self,
        obj_in: UpdateSchemaType | dict[str, Any],
    ) -> dict[str, Any]:
        return self._to_dict(obj_in)

    async def _before_create(
        self,
        *,
        db_obj: ModelType,
        db: AsyncSession,
    ) -> None:
        return None

    async def _after_create(
        self,
        *,
        db_obj: ModelType,
        db: AsyncSession,
    ) -> None:
        return None

    async def _before_update(
        self,
        *,
        db_obj: ModelType,
        update_data: dict[str, Any],
        db: AsyncSession,
    ) -> None:
        return None

    async def _after_update(
        self,
        *,
        db_obj: ModelType,
        db: AsyncSession,
    ) -> None:
        return None

    async def _before_delete(
        self,
        *,
        db_obj: ModelType,
        db: AsyncSession,
    ) -> None:
        return None

    async def _after_delete(
        self,
        *,
        db_obj: ModelType,
        db: AsyncSession,
    ) -> None:
        return None

    # -------------------------
    # DATA
    # -------------------------

    def _to_dict(self, obj_in: BaseModel | dict[str, Any]) -> dict[str, Any]:
        if isinstance(obj_in, BaseModel):
            return obj_in.model_dump(exclude_unset=True)
        return dict(obj_in)

    # -------------------------
    # PK / COLUMNS
    # -------------------------

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

        if len(pk) != 1:
            raise ValueError(f"{self.model.__name__}: only single-column PK supported")

        return pk[0]

    def get_column(self, name: str):
        columns = self.model.__table__.columns
        if name not in columns:
            raise ValueError(f"{self.model.__name__}: invalid sort column '{name}'")
        return columns[name]

    # -------------------------
    # QUERY HELPERS
    # -------------------------

    def base_select(self) -> Select:
        return select(self.model)

    def apply_ordering(
        self,
        stmt: Select,
        *,
        sort: str | None = None,
        order: str = "asc",
        default_sort: str = "id",
    ) -> Select:
        sort_name = sort or default_sort
        column = self.get_column(sort_name)
        return stmt.order_by(column.asc() if order == "asc" else column.desc())

    def apply_offset_limit(
        self,
        stmt: Select,
        *,
        offset: int = 0,
        limit: int | None = None,
    ) -> Select:
        stmt = stmt.offset(offset)
        if limit is not None:
            stmt = stmt.limit(limit)
        return stmt

    async def count_query(
        self,
        stmt: Select,
        *,
        db: AsyncSession | None = None,
    ) -> int:
        db = self._db(db)
        count_stmt = select(func.count()).select_from(stmt.order_by(None).subquery())
        result = await db.execute(count_stmt)
        return int(result.scalar_one())

    # -------------------------
    # EXECUTION HELPERS
    # -------------------------

    async def execute_scalars(
        self,
        *,
        stmt: Select,
        db: AsyncSession | None = None,
    ) -> list[Any]:
        db = self._db(db)
        result = await db.execute(stmt)
        return list(result.scalars().all())

    async def execute_rows(
        self,
        *,
        stmt: Select,
        db: AsyncSession | None = None,
    ) -> list[Any]:
        db = self._db(db)
        result = await db.execute(stmt)
        return list(result.all())

    async def execute_mappings(
        self,
        *,
        stmt: Select,
        db: AsyncSession | None = None,
    ) -> list[dict[str, Any]]:
        db = self._db(db)
        result = await db.execute(stmt)
        return [dict(row) for row in result.mappings().all()]

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

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

    async def get_required(
        self,
        id: Any,
        *,
        db: AsyncSession | None = None,
    ) -> ModelType:
        obj = await self.get(id, db=db)
        if obj is None:
            raise ValueError(f"{self.model.__name__} with id={id} not found")
        return obj

    async def get_by_ids(
        self,
        *,
        ids: list[Any],
        db: AsyncSession | None = None,
    ) -> list[ModelType]:
        db = self._db(db)
        stmt = self.base_select().where(self.pk_column.in_(ids))
        result = await db.execute(stmt)
        return list(result.scalars().all())

    async def get_multi(
        self,
        *,
        offset: int = 0,
        limit: int = 100,
        stmt: Select | None = None,
        db: AsyncSession | None = None,
    ) -> list[ModelType]:
        db = self._db(db)

        if stmt is None:
            stmt = self.base_select()
            stmt = self.apply_ordering(stmt, default_sort=self.pk_column.name)
            stmt = self.apply_offset_limit(stmt, offset=offset, limit=limit)

        result = await db.execute(stmt)
        return list(result.scalars().all())

    async def get_multi_ordered(
        self,
        *,
        offset: int = 0,
        limit: int = 100,
        sort: str | None = None,
        order: str = "asc",
        stmt: Select | None = None,
        db: AsyncSession | None = None,
    ) -> list[ModelType]:
        db = self._db(db)

        if stmt is None:
            stmt = self.base_select()

        stmt = self.apply_ordering(
            stmt,
            sort=sort,
            order=order,
            default_sort=self.pk_column.name,
        )
        stmt = self.apply_offset_limit(stmt, offset=offset, limit=limit)

        result = await db.execute(stmt)
        return list(result.scalars().all())

    async def get_multi_paginated(
        self,
        *,
        params: Params | None = None,
        stmt: Select | None = None,
        db: AsyncSession | None = None,
    ):
        db = self._db(db)

        if stmt is None:
            stmt = self.base_select()
            stmt = self.apply_ordering(stmt, default_sort=self.pk_column.name)

        return await paginate(db, stmt, params or Params())

    async def get_multi_paginated_ordered(
        self,
        *,
        params: Params | None = None,
        sort: str | None = None,
        order: str = "asc",
        stmt: Select | None = None,
        db: AsyncSession | None = None,
    ):
        db = self._db(db)

        if stmt is None:
            stmt = self.base_select()

        stmt = self.apply_ordering(
            stmt,
            sort=sort,
            order=order,
            default_sort=self.pk_column.name,
        )

        return await paginate(db, stmt, params or Params())

    async def count(
        self,
        *,
        stmt: Select | None = None,
        db: AsyncSession | None = None,
    ) -> int:
        db = self._db(db)
        if stmt is None:
            stmt = self.base_select()
        return await self.count_query(stmt, db=db)

    async def exists(
        self,
        id: Any,
        *,
        db: AsyncSession | None = None,
    ) -> bool:
        db = self._db(db)
        stmt = select(exists().where(self.pk_column == id))
        result = await db.execute(stmt)
        return bool(result.scalar())

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

    async def create(
        self,
        *,
        obj_in: CreateSchemaType | dict[str, Any] | ModelType,
        db: AsyncSession | None = None,
        commit: bool = True,
        refresh: bool = True,
    ) -> ModelType:
        db = self._db(db)

        if isinstance(obj_in, self.model):
            db_obj = obj_in
        else:
            data = self._prepare_create_data(obj_in)
            db_obj = self.model(**data)

        self._apply_create_audit(db_obj)
        await self._before_create(db_obj=db_obj, db=db)

        db.add(db_obj)

        try:
            if commit:
                await db.commit()
                if refresh:
                    await db.refresh(db_obj)
            else:
                await db.flush()
        except IntegrityError:
            await db.rollback()
            raise

        await self._after_create(db_obj=db_obj, db=db)
        return db_obj

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

    async def update(
        self,
        *,
        db_obj: ModelType,
        obj_in: UpdateSchemaType | dict[str, Any],
        db: AsyncSession | None = None,
        commit: bool = True,
        refresh: bool = True,
        exclude_fields: set[str] | None = None,
    ) -> ModelType:
        db = self._db(db)
        update_data = self._prepare_update_data(obj_in)
        exclude_fields = exclude_fields or set()

        for key, value in update_data.items():
            if key in exclude_fields:
                continue
            if hasattr(db_obj, key):
                setattr(db_obj, key, value)

        self._apply_update_audit(db_obj)
        await self._before_update(db_obj=db_obj, update_data=update_data, db=db)

        db.add(db_obj)

        try:
            if commit:
                await db.commit()
                if refresh:
                    await db.refresh(db_obj)
            else:
                await db.flush()
        except IntegrityError:
            await db.rollback()
            raise

        await self._after_update(db_obj=db_obj, db=db)
        return db_obj

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

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

        db_obj = await self.get(id, db=db)
        if db_obj is None:
            return None

        await self._before_delete(db_obj=db_obj, db=db)
        await db.delete(db_obj)

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

        await self._after_delete(db_obj=db_obj, db=db)
        return db_obj

    async def delete_where(
        self,
        *,
        where,
        db: AsyncSession | None = None,
        commit: bool = True,
    ) -> int:
        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 int(result.rowcount or 0)

    # -------------------------
    # SOFT DELETE
    # -------------------------

    async def soft_delete(
        self,
        *,
        id: Any,
        db: AsyncSession | None = None,
        commit: bool = True,
        active_field: str = "is_active",
        deleted_at_field: str = "deleted_at",
    ) -> ModelType | None:
        db = self._db(db)

        db_obj = await self.get(id, db=db)
        if db_obj is None:
            return None

        if hasattr(db_obj, active_field):
            setattr(db_obj, active_field, False)

        if hasattr(db_obj, deleted_at_field):
            setattr(db_obj, deleted_at_field, datetime.now())

        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