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

from pydantic import BaseModel
from surrealdb import Surreal

from .utils import robust, setup_logging

Schema = TypeVar("Schema", bound=BaseModel)
Model = TypeVar("Model", bound=BaseModel)
logger = setup_logging(__name__)


class Repository(Generic[Model, Schema]):
    """
    Repository class for interacting with a database table.

    Args:
        Generic (Model): The pydantic model associated with the database table.
        Generic (Schema): The pydantic model associated with the database table.

    Attributes:
        model (Type[Schema]): The pydantic model associated with the database table.


    Methods:
        create_: Create a new item in the database.
        read_: Read an item from the database.
        update_: Update an item in the database.
        delete_: Delete an item from the database.
        snapshot_: Asynchronously subscribe to live updates from the database table.
    """

    def __init__(self, *, model: Type[Schema]):
        self.model = model

    @property
    def table_name(self) -> str:
        return self.model.__name__

    @robust
    async def create_(self, data: Model, db: Surreal) -> list[Schema]:
        """
        Create a new item in the database.

        Args:
            data (I): The item to be created. As the datamodel contract describes, the id field is not required since it is autogenerated by the database.
                      The type safety of the values is enforced by the pydantic model.
            db (Surreal): The database connection.

        Returns:
            list[S]: A list with the created item.
        """
        response = await db.create(thing=self.table_name, data=data.model_dump())
        return [self.model(**res) for res in response]

    @robust
    async def read_(
        self,
        *,
        db: Surreal,
        id: Optional[str] = None,
        where: Optional[dict[str, Any]] = None,
    ) -> list[Schema]:
        """
        Read an item from the database.

        Args:
            db (Surreal): The database connection.
            id (Optional[str]): The id of the item to be read.
            where (Optional[dict[str, Any]]): The where clause of the item to be read as a dictionary with the keys as the field names and the values as the field values.
                                              The type safety of the values is up to the user.

        Returns:
            list[S]: A list with the item(s) that match(es) the query. If no item matches the query, an empty list is returned.
        """
        if not id and not where:
            response = await db.select(thing=self.table_name)
        elif id:
            response = await db.select(thing=id)
        elif where:
            query = (
                f"SELECT * FROM {self.table_name} WHERE "
                + " AND ".join([f"{key} = '{value}'" for key, value in where.items()])
                + ";"
            )
            response = await db.query(query)
        else:
            logger.error("Invalid query parameters. id: %s, where: %s", id, where)
            return []
        return [self.model(**res) for res in response]

    @robust
    async def update_(self, id: str, data: Schema, db: Surreal) -> list[Schema]:
        """
        Update an item in the database.

        Args:
            id (str): The id of the item to be updated.
            data (S): The full patch of the item to be updated, even including the fields that are not going to be updated.
            db (Surreal): The database connection.

        Returns:
            list[S]: A list with the updated item.
        """
        response = await db.update(id, data.dict())  # type: ignore #[TODO] Migrate to pydantic v2
        if len(response) > 1:
            logger.error("Weird response from the database: %s", response)
            return []
        return [self.model(**res) for res in response]

    @robust
    async def delete_(self, id: str, db: Surreal) -> list[Schema]:
        """
        Delete an item from the database.

        Args:
            id (str): The id of the item to be deleted.
            db (Surreal): The database connection.

        Returns:
            list[S]: Empty list.
        """
        response = await db.delete(id)
        if len(response):
            logger.error("Failed to delete item %s", id)
            return [self.model(**res) for res in response]
        return []
