"""
PostgreSQL cache backend using asyncpg with connection pooling.

Schema (auto-created on first use):

    CREATE TABLE IF NOT EXISTS cachestack (
        key        TEXT PRIMARY KEY,
        value      BYTEA NOT NULL,
        expires_at TIMESTAMPTZ
    );
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import Any, Dict, Optional

from cachestack.base import BaseCache, BackendUnavailableError, CacheError
from cachestack.serializers import BaseSerializer, PickleSerializer


@dataclass
class PostgresConfig:
    """Configuration for the PostgreSQL backend.

    Attributes:
        dsn: PostgreSQL connection string.
        serializer: Serializer for values.
        ttl: Default TTL in seconds.
        table: Table name to use.
        min_pool_size: Minimum connection pool size.
        max_pool_size: Maximum connection pool size.
        namespace: Key prefix.
        silent: Suppress errors if True.
    """
    dsn: str = "postgresql://localhost/cache"
    serializer: BaseSerializer = field(default_factory=PickleSerializer)
    ttl: Optional[int] = None
    table: str = "cachestack"
    min_pool_size: int = 2
    max_pool_size: int = 10
    namespace: str = "cachestack"
    silent: bool = False


class PostgresBackend(BaseCache):
    """
    Async PostgreSQL cache backend using asyncpg.

    Uses a connection pool for high concurrency. TTL is managed
    via a timestamptz column. Expired entries are lazily deleted on read
    and can be proactively purged via purge_expired().

    Args:
        config: PostgresConfig instance.
    """

    def __init__(self, config: Optional[PostgresConfig] = None) -> None:
        self._config = config or PostgresConfig()
        self._pool: Any = None
        self._hits = 0
        self._misses = 0
        self._errors = 0

    def _prefixed(self, key: str) -> str:
        return f"{self._config.namespace}:{key}"

    async def _get_pool(self) -> Any:
        """Lazily initialize the asyncpg connection pool."""
        if self._pool is None:
            try:
                import asyncpg
            except ImportError as e:
                raise ImportError(
                    "asyncpg is required for PostgresBackend. "
                    "Install it with: pip install cachestack[postgres]"
                ) from e
            try:
                self._pool = await asyncpg.create_pool(
                    dsn=self._config.dsn,
                    min_size=self._config.min_pool_size,
                    max_size=self._config.max_pool_size,
                )
                await self._ensure_table()
            except Exception as e:
                raise BackendUnavailableError(
                    f"Failed to connect to Postgres: {e}"
                ) from e
        return self._pool

    async def _ensure_table(self) -> None:
        """Create the cache table if it doesn't exist."""
        pool = self._pool
        async with pool.acquire() as conn:
            await conn.execute(f"""
                CREATE TABLE IF NOT EXISTS {self._config.table} (
                    key        TEXT PRIMARY KEY,
                    value      BYTEA NOT NULL,
                    expires_at TIMESTAMPTZ
                );
                CREATE INDEX IF NOT EXISTS idx_{self._config.table}_expires
                    ON {self._config.table} (expires_at)
                    WHERE expires_at IS NOT NULL;
            """)

    async def get(self, key: str) -> Optional[Any]:
        """Retrieve a value by key, deleting it if expired."""
        try:
            pool = await self._get_pool()
            async with pool.acquire() as conn:
                row = await conn.fetchrow(
                    f"""
                    SELECT value, expires_at
                    FROM {self._config.table}
                    WHERE key = $1
                    """,
                    self._prefixed(key),
                )
                if row is None:
                    self._misses += 1
                    return None
                if row["expires_at"] is not None:
                    import datetime
                    now = datetime.datetime.now(datetime.timezone.utc)
                    if row["expires_at"] < now:
                        await conn.execute(
                            f"DELETE FROM {self._config.table} WHERE key = $1",
                            self._prefixed(key),
                        )
                        self._misses += 1
                        return None
                self._hits += 1
                return self._config.serializer.loads(bytes(row["value"]))
        except (BackendUnavailableError, ImportError):
            raise
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return None
            raise CacheError(f"PostgresBackend get failed: {e}") from e

    async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        """Store a value by key with optional TTL."""
        try:
            pool = await self._get_pool()
            raw = self._config.serializer.dumps(value)
            effective_ttl = ttl or self._config.ttl
            expires_at = None
            if effective_ttl:
                import datetime
                expires_at = (
                    datetime.datetime.now(datetime.timezone.utc)
                    + datetime.timedelta(seconds=effective_ttl)
                )
            async with pool.acquire() as conn:
                await conn.execute(
                    f"""
                    INSERT INTO {self._config.table} (key, value, expires_at)
                    VALUES ($1, $2, $3)
                    ON CONFLICT (key) DO UPDATE
                        SET value = EXCLUDED.value,
                            expires_at = EXCLUDED.expires_at
                    """,
                    self._prefixed(key),
                    raw,
                    expires_at,
                )
        except (BackendUnavailableError, ImportError):
            raise
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return
            raise CacheError(f"PostgresBackend set failed: {e}") from e

    async def delete(self, key: str) -> None:
        """Delete a key."""
        try:
            pool = await self._get_pool()
            async with pool.acquire() as conn:
                await conn.execute(
                    f"DELETE FROM {self._config.table} WHERE key = $1",
                    self._prefixed(key),
                )
        except (BackendUnavailableError, ImportError):
            raise
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return
            raise CacheError(f"PostgresBackend delete failed: {e}") from e

    async def exists(self, key: str) -> bool:
        """Check if a key exists and is not expired."""
        return (await self.get(key)) is not None

    async def clear(self) -> None:
        """Delete all keys under this namespace."""
        try:
            pool = await self._get_pool()
            async with pool.acquire() as conn:
                await conn.execute(
                    f"DELETE FROM {self._config.table} WHERE key LIKE $1",
                    f"{self._config.namespace}:%",
                )
        except (BackendUnavailableError, ImportError):
            raise
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return
            raise CacheError(f"PostgresBackend clear failed: {e}") from e

    async def purge_expired(self) -> int:
        """
        Proactively delete all expired entries.

        Returns:
            Number of entries deleted.
        """
        try:
            pool = await self._get_pool()
            import datetime
            now = datetime.datetime.now(datetime.timezone.utc)
            async with pool.acquire() as conn:
                result = await conn.execute(
                    f"""
                    DELETE FROM {self._config.table}
                    WHERE expires_at IS NOT NULL AND expires_at < $1
                    """,
                    now,
                )
            deleted = int(result.split()[-1])
            return deleted
        except Exception as e:
            self._errors += 1
            if self._config.silent:
                return 0
            raise CacheError(f"PostgresBackend purge failed: {e}") from e

    async def close(self) -> None:
        """Close the connection pool."""
        if self._pool:
            await self._pool.close()
            self._pool = None

    async def stats(self) -> Dict[str, Any]:
        """Return cache statistics."""
        pool_size = 0
        if self._pool:
            pool_size = self._pool.get_size()
        return {
            "backend": "postgres",
            "table": self._config.table,
            "namespace": self._config.namespace,
            "hits": self._hits,
            "misses": self._misses,
            "errors": self._errors,
            "pool_size": pool_size,
        }
