from typing import Any, Callable, Generic, Type, TypeVar

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.orm import Session

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


class CRUDRouterFactory(
    Generic[ModelType, CreateSchemaType, UpdateSchemaType, ResponseSchemaType]
):
    """Factory to create standard CRUD routes"""

    def __init__(
        self,
        model: Type[ModelType],
        create_schema: Type[CreateSchemaType],
        update_schema: Type[UpdateSchemaType],
        response_schema: Type[ResponseSchemaType],
        db_dependency: Callable,
        auth_dependency: Callable,
        prefix: str,
        tags: list[str],
        id_field: str = "key",
    ):
        self.model = model
        self.create_schema = create_schema
        self.update_schema = update_schema
        self.response_schema = response_schema
        self.db_dependency = db_dependency
        self.auth_dependency = auth_dependency
        self.id_field = id_field
        self.router = APIRouter(prefix=prefix, tags=tags)
        self._register_routes()

    def _register_routes(self):
        """Register all CRUD routes"""

        @self.router.get("/", response_model=list[self.response_schema])
        def list_items(
            skip: int = 0,
            limit: int = 100,
            db: Session = Depends(self.db_dependency),
            current_user: dict = Depends(self.auth_dependency),
        ):
            items = db.query(self.model).offset(skip).limit(limit).all()
            return items

        @self.router.get(f"/{{{self.id_field}}}", response_model=self.response_schema)
        def get_item(
            db: Session = Depends(self.db_dependency),
            current_user: dict = Depends(self.auth_dependency),
            **kwargs,
        ):
            item_id = kwargs.get(self.id_field)
            obj = (
                db.query(self.model)
                .filter(getattr(self.model, self.id_field) == item_id)
                .first()
            )
            if not obj:
                raise HTTPException(status_code=404, detail="Not found")
            return obj

        @self.router.post("/", response_model=self.response_schema)
        def create_item(
            data: Any,
            db: Session = Depends(self.db_dependency),
            current_user: dict = Depends(self.auth_dependency),
        ):
            obj = self.model(**data.dict())
            db.add(obj)
            db.commit()
            db.refresh(obj)
            return obj

        @self.router.put(f"/{{{self.id_field}}}", response_model=self.response_schema)
        def update_item(
            data: Any,
            db: Session = Depends(self.db_dependency),
            current_user: dict = Depends(self.auth_dependency),
            **kwargs,
        ):
            item_id = kwargs.get(self.id_field)
            obj = (
                db.query(self.model)
                .filter(getattr(self.model, self.id_field) == item_id)
                .first()
            )
            if not obj:
                raise HTTPException(status_code=404, detail="Not found")

            for field, value in data.dict(exclude_unset=True).items():
                setattr(obj, field, value)

            db.commit()
            db.refresh(obj)
            return obj

        @self.router.delete(f"/{{{self.id_field}}}", status_code=204)
        def delete_item(
            db: Session = Depends(self.db_dependency),
            current_user: dict = Depends(self.auth_dependency),
            **kwargs,
        ):
            item_id = kwargs.get(self.id_field)
            obj = (
                db.query(self.model)
                .filter(getattr(self.model, self.id_field) == item_id)
                .first()
            )
            if not obj:
                raise HTTPException(status_code=404, detail="Not found")

            db.delete(obj)
            db.commit()
            return None
