from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Any, List, Dict, Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from ..query import Query, QuerySet, QueryBuilder


class DBClient(ABC):
    """General contract for all database clients.

    Timestamp auto-management
    -------------------------
    When ``_auto_timestamps`` is ``True`` (the default), the convenience
    mutation methods automatically inject ``created_at`` and ``updated_at``
    fields with the current UTC time.  Set ``_auto_timestamps = False`` on
    a subclass to opt out.
    """

    _auto_timestamps: bool = True

    @staticmethod
    def _now() -> datetime:
        """Return the current time in UTC.  Override to use a different
        time source (e.g. for testing)."""
        return datetime.now(timezone.utc)

    @abstractmethod
    async def connect(self): ...

    @abstractmethod
    async def disconnect(self): ...

    @abstractmethod
    def table(self, name: str) -> "QueryBuilder": ...

    @abstractmethod
    async def _execute(self, query: "Query") -> "QuerySet": ...

    @abstractmethod
    async def execute_raw(
        self,
        sql: str,
        params: dict[str, Any] | None = None,
        *,
        multi_statement: bool = False,
    ) -> Any:
        """Execute arbitrary raw SQL / native query text.

        Returns:
            * For SELECT-like statements: ``list[dict[str, Any]]`` (rows as dicts).
            * For DML/DDL statements: ``int`` (rowcount), or ``None`` if the
              backend does not report a count.
            * Some backends may return ``None`` for DDL like ``CREATE TABLE``.

        The ``multi_statement`` flag hints that *sql* contains multiple
        statements.  Relational backends should use their native multi-statement
        support (e.g. PostgreSQL accepts ``;``-delimited SQL natively).
        """
        ...

    # -- Gap detection (override in clients that support window functions) --

    async def find_sequential_gaps(
        self,
        table: str,
        sequence_column: str,
        partition_column: str,
        partition_value: Any,
        *,
        min_sequence: int | None = None,
        max_sequence: int | None = None,
        extra_where: list[str] | None = None,
        extra_params: dict[str, Any] | None = None,
    ) -> list[dict[str, Any]]:
        """Find gaps in a sequential integer column, partitioned by another column.

        Uses a ``LAG`` window function to detect missing values in
        *sequence_column* within each *partition_column* group.  Returns
        dicts with keys matching the partition column name,
        ``gap_start``, and ``gap_end``.

        Args:
            table: Table name (unquoted — the client applies quoting).
            sequence_column: The sequential integer column to scan for gaps.
            partition_column: The column to partition by (e.g. ``peer_id``).
            partition_value: The specific partition value to scan.
            min_sequence: Optional lower bound for *sequence_column*.
            max_sequence: Optional upper bound for *sequence_column*.
            extra_where: Additional SQL WHERE clause strings (already
                parameterized with ``:param`` placeholders).
            extra_params: Parameter values for *extra_where* clauses.

        Returns:
            ``[{partition_column: ..., gap_start: int, gap_end: int}, ...]``.
        """
        raise NotImplementedError("find_sequential_gaps() is not supported for this client")

    # -- Internal mutation methods (override in relational clients) --

    async def _bulk_update(self, table: str, ids: List[Any], values: Dict[str, Any]) -> int:
        """Update multiple rows by IDs."""
        raise NotImplementedError("_bulk_update() is not supported for this client")

    async def _bulk_delete(self, table: str, ids: List[Any]) -> int:
        """Delete multiple rows by IDs."""
        raise NotImplementedError("_bulk_delete() is not supported for this client")

    async def _insert_one(
        self,
        table: str,
        data: Dict[str, Any],
        returning: Optional[List[str]] = None
    ) -> Optional[Dict[str, Any]]:
        """Insert a single row into the database."""
        raise NotImplementedError("_insert_one() is not supported for this client")

    async def _insert_many(
        self,
        table: str,
        data: List[Dict[str, Any]],
        returning: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Insert multiple rows into the database."""
        raise NotImplementedError("_insert_many() is not supported for this client")

    async def _update(
        self,
        table: str,
        conditions: Dict[str, Any],
        values: Dict[str, Any],
        limit: Optional[int] = None
    ) -> int:
        """Update rows matching conditions."""
        raise NotImplementedError("_update() is not supported for this client")

    async def _delete(
        self,
        table: str,
        conditions: Dict[str, Any],
        limit: Optional[int] = None
    ) -> int:
        """Delete rows matching conditions."""
        raise NotImplementedError("_delete() is not supported for this client")

    # -- Convenience read methods --

    async def find_one(
        self, 
        table: str, 
        conditions: Optional[Dict[str, Any]] = None
    ) -> Optional[Dict[str, Any]]:
        """
        Find a single record matching the conditions.
        
        Args:
            table: Table name
            conditions: Dictionary of field-value pairs to match
            
        Returns:
            First matching record or None
            
        Example:
            user = await db.find_one("users", {"email": "user@example.com"})
        """
        qb = self.table(table)
        if conditions:
            for field, value in conditions.items():
                qb = qb.where(field, "=", value)
        result = await qb.first()
        return result

    async def find_many(
        self,
        table: str,
        conditions: Optional[Dict[str, Any]] = None,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
        order_by: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """
        Find multiple records matching the conditions.
        
        Args:
            table: Table name
            conditions: Dictionary of field-value pairs to match
            limit: Maximum number of records to return
            offset: Number of records to skip
            order_by: List of fields to order by (prefix with '-' for DESC)
            
        Returns:
            List of matching records
            
        Example:
            users = await db.find_many("users", {"status": "active"}, limit=10)
        """
        qb = self.table(table)
        if conditions:
            for field, value in conditions.items():
                qb = qb.where(field, "=", value)
        if order_by:
            for field in order_by:
                qb = qb.order_by(field)
        if limit:
            qb = qb.limit(limit)
        if offset:
            qb = qb.offset(offset)
        result = await qb.all()
        return result

    async def find_by_id(
        self,
        table: str,
        id: Any,
        id_field: str = "id"
    ) -> Optional[Dict[str, Any]]:
        """
        Find a record by its ID.
        
        Args:
            table: Table name
            id: The ID value to search for
            id_field: Name of the ID field (default: "id")
            
        Returns:
            Matching record or None
            
        Example:
            user = await db.find_by_id("users", 123)
        """
        return await self.find_one(table, {id_field: id})

    async def find_all(
        self,
        table: str,
        limit: Optional[int] = None,
        offset: Optional[int] = None,
        order_by: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """
        Find all records in a table.
        
        Args:
            table: Table name
            limit: Maximum number of records to return
            offset: Number of records to skip
            order_by: List of fields to order by (prefix with '-' for DESC)
            
        Returns:
            List of all records
            
        Example:
            all_users = await db.find_all("users", limit=100)
        """
        return await self.find_many(table, conditions=None, limit=limit, offset=offset, order_by=order_by)

    async def count(
        self,
        table: str,
        conditions: Optional[Dict[str, Any]] = None
    ) -> int:
        """
        Count records matching the conditions.
        
        Args:
            table: Table name
            conditions: Dictionary of field-value pairs to match
            
        Returns:
            Number of matching records
            
        Example:
            active_count = await db.count("users", {"status": "active"})
        """
        qb = self.table(table)
        if conditions:
            for field, value in conditions.items():
                qb = qb.where(field, "=", value)
        return await qb.count()

    async def exists(
        self,
        table: str,
        conditions: Dict[str, Any]
    ) -> bool:
        """
        Check if any records match the conditions.
        
        Args:
            table: Table name
            conditions: Dictionary of field-value pairs to match
            
        Returns:
            True if at least one record exists, False otherwise
            
        Example:
            email_exists = await db.exists("users", {"email": "user@example.com"})
        """
        count = await self.count(table, conditions)
        return count > 0

    # -- Mutation convenience methods --

    def _inject_create_timestamps(self, data: Dict[str, Any]) -> Dict[str, Any]:
        """Inject ``created_at`` and ``updated_at`` into *data* if missing."""
        if not self._auto_timestamps:
            return data
        now = self._now()
        for field in ("created_at", "updated_at"):
            if field not in data:
                data[field] = now
        return data

    def _inject_update_timestamp(self, values: Dict[str, Any]) -> Dict[str, Any]:
        """Inject ``updated_at`` into *values* if missing."""
        if not self._auto_timestamps:
            return values
        if "updated_at" not in values:
            values["updated_at"] = self._now()
        return values

    async def create(
        self,
        table: str,
        data: Dict[str, Any],
        returning: Optional[List[str]] = None
    ) -> Optional[Dict[str, Any]]:
        """Insert a single record."""
        self._inject_create_timestamps(data)
        return await self._insert_one(table, data, returning=returning)

    async def create_many(
        self,
        table: str,
        data: List[Dict[str, Any]],
        returning: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """Insert multiple records."""
        for item in data:
            self._inject_create_timestamps(item)
        return await self._insert_many(table, data, returning=returning)

    async def save(
        self,
        table: str,
        data: Dict[str, Any],
        id_field: str = "id",
        returning: Optional[List[str]] = None
    ) -> Optional[Dict[str, Any]]:
        """Insert or update a record (upsert).

        Only ``updated_at`` is injected — ``created_at`` is never
        overwritten for existing records.
        """
        self._inject_update_timestamp(data)
        record_id = data.get(id_field)
        if record_id is None:
            return await self.create(table, data, returning=returning)

        # Check if the record already exists
        existing = await self.find_by_id(table, record_id, id_field=id_field)
        if existing is not None:
            # Update the existing record
            values = {k: v for k, v in data.items() if k != id_field}
            if values and hasattr(self, "update_by_id"):
                await self.update_by_id(table, record_id, values, id_field=id_field)
            return await self.find_by_id(table, record_id, id_field=id_field)

        # Insert a new record
        return await self.create(table, data, returning=returning)

    async def update(
        self,
        table: str,
        conditions: Dict[str, Any],
        values: Dict[str, Any],
        limit: Optional[int] = None
    ) -> int:
        """Update one or more records matching the conditions."""
        self._inject_update_timestamp(values)
        return await self._update(table, conditions, values, limit=limit)

    async def update_by_id(
        self,
        table: str,
        id: Any,
        values: Dict[str, Any],
        id_field: str = "id"
    ) -> int:
        """Update a single record by its identifier."""
        self._inject_update_timestamp(values)
        return await self.update(table, {id_field: id}, values, limit=1)

    async def bulk_update(
        self,
        table: str,
        ids: List[Any],
        values: Dict[str, Any]
    ) -> int:
        """Update multiple records by their identifiers."""
        self._inject_update_timestamp(values)
        return await self._bulk_update(table, ids, values)

    async def delete(
        self,
        table: str,
        conditions: Dict[str, Any],
        limit: Optional[int] = None
    ) -> int:
        """Delete one or more records matching the conditions."""
        return await self._delete(table, conditions, limit=limit)

    async def delete_by_id(
        self,
        table: str,
        id: Any,
        id_field: str = "id"
    ) -> int:
        """Delete a single record by its identifier."""
        return await self.delete(table, {id_field: id}, limit=1)

    async def bulk_delete(
        self,
        table: str,
        ids: List[Any]
    ) -> int:
        """Delete multiple records by their identifiers."""
        return await self._bulk_delete(table, ids)


class DocumentClient(DBClient):
    """Base class for document databases (Elasticsearch, etc.)"""
    pass


class VectorClient(DBClient):
    """Base class for vector databases (Pinecone, Milvus, etc.)"""
    pass


class OLAPClient(DBClient):
    """Base class for OLAP databases (Apache Doris, ClickHouse, etc.)"""
    pass
