"""Cache service: blockchain data caching with TTL expiry.

Uses the BlockchainCache model (PostgreSQL/JSONB) to cache
on-chain data like WoT size, Smith size, and TechComm size.
Avoids repeated RPC calls to the Duniter node.
"""

from __future__ import annotations

import logging
from datetime import datetime, timedelta, timezone

from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.cache import BlockchainCache

logger = logging.getLogger(__name__)


async def get_cached(key: str, db: AsyncSession) -> dict | None:
    """Retrieve a cached value by key if it has not expired.

    Parameters
    ----------
    key:
        The cache key to look up.
    db:
        Async database session.

    Returns
    -------
    dict | None
        The cached value as a dict, or None if missing/expired.
    """
    result = await db.execute(
        select(BlockchainCache).where(
            BlockchainCache.cache_key == key,
            BlockchainCache.expires_at > datetime.now(timezone.utc),
        )
    )
    entry = result.scalar_one_or_none()

    if entry is None:
        logger.debug("Cache miss pour la cle '%s'", key)
        return None

    logger.debug("Cache hit pour la cle '%s'", key)
    return entry.cache_value


async def set_cached(
    key: str,
    value: dict,
    db: AsyncSession,
    ttl_seconds: int = 3600,
) -> None:
    """Store a value in the cache with the given TTL.

    If the key already exists, it is replaced (upsert).

    Parameters
    ----------
    key:
        The cache key.
    value:
        The value to store (must be JSON-serializable).
    db:
        Async database session.
    ttl_seconds:
        Time-to-live in seconds (default: 1 hour).
    """
    now = datetime.now(timezone.utc)
    expires_at = now + timedelta(seconds=ttl_seconds)

    # Check if key already exists
    result = await db.execute(
        select(BlockchainCache).where(BlockchainCache.cache_key == key)
    )
    existing = result.scalar_one_or_none()

    if existing is not None:
        existing.cache_value = value
        existing.fetched_at = now
        existing.expires_at = expires_at
        logger.debug("Cache mis a jour pour la cle '%s' (TTL=%ds)", key, ttl_seconds)
    else:
        entry = BlockchainCache(
            cache_key=key,
            cache_value=value,
            fetched_at=now,
            expires_at=expires_at,
        )
        db.add(entry)
        logger.debug("Cache cree pour la cle '%s' (TTL=%ds)", key, ttl_seconds)

    await db.commit()


async def invalidate(key: str, db: AsyncSession) -> None:
    """Remove a specific cache entry by key.

    Parameters
    ----------
    key:
        The cache key to invalidate.
    db:
        Async database session.
    """
    await db.execute(
        delete(BlockchainCache).where(BlockchainCache.cache_key == key)
    )
    await db.commit()
    logger.debug("Cache invalide pour la cle '%s'", key)


async def cleanup_expired(db: AsyncSession) -> int:
    """Remove all expired cache entries.

    Parameters
    ----------
    db:
        Async database session.

    Returns
    -------
    int
        Number of entries removed.
    """
    result = await db.execute(
        delete(BlockchainCache).where(
            BlockchainCache.expires_at <= datetime.now(timezone.utc)
        )
    )
    await db.commit()

    count = result.rowcount
    if count > 0:
        logger.info("Nettoyage cache: %d entrees expirees supprimees", count)
    return count
