#app/crud/base_crud.py
from datetime import datetime
from enum import Enum
from fastapi import HTTPException, Query
from typing import Any, Dict, Generic, Optional, Sequence, Type, TypeVar
from uuid import UUID

from psycopg import IntegrityError
from fastapi_pagination.ext.sqlalchemy import paginate
from fastapi_pagination import Params, Page
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import Select, exc, func, inspect, select
from fastapi_pagination.bases import AbstractPage, AbstractParams
from sqlalchemy.orm import DeclarativeMeta

ModelType = TypeVar("ModelType", bound=DeclarativeMeta)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
SchemaType = TypeVar("SchemaType", bound=DeclarativeMeta)
T = TypeVar("T", bound=DeclarativeMeta)


class IOrderEnum(str, Enum):
    ascendent = "ascendent"
    descendent = "descendent"


class MyPage(AbstractPage[T], Generic[T]):
    results: list[T]
    totalResults: int

    __params_type__ = Params

    @classmethod
    def create(
        cls,
        items: Sequence[T],
        params: AbstractParams,
        *,
        total: Optional[int] = None,
        **kwargs: Any
    ) -> Page[T]:
        assert total is not None, "total must be provided"

        return cls(
            results=items,
            totalResults=total,
        )


class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
    def __init__(self, model: type[ModelType]):
        self.model = model
    
    async def get(
        self, *, id: UUID | str, db_session: AsyncSession
    ) -> ModelType | None:

        query = select(self.model).where(self.model.id == id)
        response = await db_session.execute(query)
        return response.scalar_one_or_none()

    async def get_by_ids(
        self,
        *,
        list_ids: list[UUID | str],
        db_session: AsyncSession,
    ) -> list[ModelType] | None:

        response = await db_session.execute(
            select(self.model).where(self.model.id.in_(list_ids))
        )
        return response.scalars().all()

    async def get_count(
        self, *, db_session: AsyncSession
    ) -> ModelType | None:

        response = await db_session.execute(
            select(func.count()).select_from(select(self.model).subquery())
        )
        return response.scalar_one()

    async def get_multi(
        self,
        *,
        skip: int = 0,
        limit: int = 100,
        query: T | Select[T] | None = None,
        db_session: AsyncSession,
    ) -> list[ModelType]:
                        
        if query is None:
            query = select(self.model).offset(skip).limit(limit).order_by(self.model.id)
        response = await db_session.execute(query)
        return response.all()

    async def get_multi_all(
        self,
        *,
        query: T | Select[T] | None = None,
        db_session: AsyncSession,
    ) -> list[ModelType]:

        response = await db_session.execute(query)
        return response.all()
    
    async def get_multi_mappings(
        self,
        *,
        query: T | Select[T] | None = None,
        db_session: AsyncSession,
    ) -> list[ModelType]:
        db_session = db_session or self.db.session
        response = await db_session.execute(query)
        return response.mappings().all()

    async def get_multi_paginated(
        self,
        *,
        params: Params | None = Params(),
        query: T | Select[T] | None = None,
        db_session: AsyncSession,
    ) -> Page[ModelType]:
        
        if query is None:
            query = select(self.model)
        result = await paginate(db_session, query, params)
        return result
    

    async def get_multi_paginated_ordered(
        self,
        *,
        params: Params | None = Params(),
        sort: str | None = None,
        db_session: AsyncSession,
        order: IOrderEnum | None = IOrderEnum.ascendent,
        query: T | Select[T] | None = None,
    ) -> Page[ModelType]:
        
        columns = self.model.__table__.columns

        if sort is None or sort not in columns:
            sort = "id"

        if query is None:
            if order == IOrderEnum.ascendent:
                query = select(self.model).order_by(columns[sort].asc())
            else:
                query = select(self.model).order_by(columns[sort].desc())

        return await paginate(db_session, query, params)

    async def get_multi_ordered(
        self,
        *,
        skip: int = 0,
        limit: int = 100,
        sort: str | None = None,
        order: IOrderEnum | None = IOrderEnum.ascendent,
        query: T | Select[T] | None = None,
        db_session: AsyncSession,
    ) -> list[ModelType]:

        columns = self.model.__table__.columns

        if sort is None or sort not in columns:
            sort = "id"

        if query is None:
            if order == IOrderEnum.ascendent:
                query = select(self.model).offset(skip).limit(limit).order_by(columns[sort].asc())
            else:
                query = select(self.model).offset(skip).limit(limit).order_by(columns[sort].desc())

        response = await db_session.execute(query)
        return response.scalars().all()

    async def create(
        self,
        *,
        obj_in: CreateSchemaType | ModelType,
        created_by_id: UUID | str | None = None,
        db_session: AsyncSession,
    ) -> ModelType:

        if isinstance(obj_in, self.model):
            db_obj = obj_in
        else:
            # compatibile con Pydantic v2
            db_obj = self.model(**obj_in.model_dump())

        if created_by_id and hasattr(db_obj, "created_by_id"):
            setattr(db_obj, "created_by_id", created_by_id)

        try:
            db_session.add(db_obj)
            await db_session.commit()
            await db_session.refresh(db_obj)
        except IntegrityError as e:
            await db_session.rollback()
            raise HTTPException(
                status_code=409,
                detail="Resource already exists",
            ) from e

        return db_obj

    async def update(
        self,
        *,
        obj_current: ModelType,
        obj_in: UpdateSchemaType | dict[str, Any] | ModelType,
        updated_by_id: UUID | str | None = None,
        db_session: AsyncSession,
    ) -> ModelType:        


        if isinstance(obj_in, dict):
            update_data = obj_in
        else:
            update_data = obj_in.model_dump(
                exclude_unset=True
            )
            

        #for field in update_data:
        #    setattr(obj_current, field, update_data[field])
        
        setattr(obj_current, 'jd', update_data)
        if updated_by_id:
            setattr(obj_current, 'updated_by_id', updated_by_id)
            setattr(obj_current, 'updated_updated_atby_id', datetime.now())
        
        db_session.add(obj_current)
        await db_session.commit()
        await db_session.refresh(obj_current)
        return obj_current
    
 
    async def update_model_recursive(
        self,
        *,
        model: Type[T],
        existing_instance: Optional[T],
        obj_in: UpdateSchemaType | dict[str, Any] | ModelType
    ) -> T:
        if isinstance(obj_in, dict):
            data = obj_in
        else:
            data = obj_in.model_dump(exclude_unset=True)

        relationship_fields = {rel.key for rel in model.__mapper__.relationships}
        direct_fields = {k: v for k, v in data.items() if k not in relationship_fields}

        instance = existing_instance or model(**direct_fields)
        if existing_instance:
            for k, v in direct_fields.items():
                setattr(instance, k, v)

        mapper = inspect(model)

        for field_name, value in data.items():
            if field_name not in direct_fields:
                rel = mapper.relationships.get(field_name)
                if not rel:
                    continue
                related_model = rel.entity.class_

                if rel.uselist:  # uno-a-molti
                    existing_items = {getattr(i, "id", None): i for i in getattr(instance, field_name, [])}
                    new_items = []
                    for item in value:
                        item_id = item.get("id")
                        if item_id and item_id in existing_items:
                            updated = await self.update_model_recursive(
                                model=related_model,
                                existing_instance=existing_items[item_id],
                                obj_in=item
                            )
                        else:
                            updated = related_model(**{k: v for k, v in item.items() if not isinstance(v, (dict, list))})
                            for fk in related_model.__table__.foreign_keys:
                                if fk.column.table == model.__table__:
                                    setattr(updated, fk.parent.name, instance.id)
                        new_items.append(updated)
                    setattr(instance, field_name, new_items)

                else:  # uno-a-uno (es. dichiarante, procuratore)
                    sub_instance = getattr(instance, field_name, None)
                    if value:
                        updated_sub = await self.update_model_recursive(
                            model=related_model,
                            existing_instance=sub_instance,
                            obj_in=value
                        )
                        # collega subito la chiave esterna
                        for fk in related_model.__table__.foreign_keys:
                            if fk.column.table == model.__table__:
                                setattr(updated_sub, fk.parent.name, instance.id)
                        setattr(instance, field_name, updated_sub)

        return instance
    

    
    async def update_all(
        self,
        *,
        obj_current: ModelType,
        obj_in: UpdateSchemaType | dict[str, Any] | ModelType,
        updated_by_id: UUID | str | None = None,
        db_session: AsyncSession,
    ) -> ModelType:
                
        if updated_by_id:
            setattr(obj_in,"updated_by_id",updated_by_id) 
            setattr(obj_in,"updated_at",datetime.now()) 
        
        model = type(obj_current)
        istanza = await self.update_model_recursive(model=model,existing_instance=obj_current,obj_in=obj_in)    
        
        db_session.add(istanza)
        await db_session.commit()
        await db_session.refresh(istanza)
        return obj_current
    

    async def remove(
        self, *, id: UUID | str, db_session: AsyncSession
    ) -> ModelType:

        response = await db_session.execute(
            select(self.model).where(self.model.id == id)
        )
        obj = response.scalar_one()
        await db_session.delete(obj)
        await db_session.commit()
        return obj

    async def remove_soft(self, *, id: UUID | str, db_session: AsyncSession) -> ModelType:
        obj = await self.get(id=id, db_session=db_session)
        obj.is_active = False
        obj.deleted_at = datetime.now()
        db_session.add(obj)
        await db_session.commit()
        return obj
