import asyncio
import json
import logging
import secrets
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Optional

try:
    from redis.asyncio import Redis
except ModuleNotFoundError:  # pragma: no cover - local dev environments may not be installed yet
    Redis = None  # type: ignore[assignment]

from config.settings import Settings

logger = logging.getLogger(__name__)

_redis: Optional["Redis"] = None


def redis_key(settings: Settings, *parts: object) -> str:
    prefix = (settings.REDIS_KEY_PREFIX or "remnawave-tg-shop").strip(":")
    clean = [str(part).strip(":") for part in parts if str(part).strip(":")]
    return ":".join([prefix, *clean])


async def get_redis(settings: Settings) -> Optional["Redis"]:
    global _redis
    if not settings.REDIS_URL:
        return None
    if Redis is None:
        logger.warning("REDIS_URL is set but redis package is not installed")
        return None
    if _redis is None:
        _redis = Redis.from_url(
            settings.REDIS_URL,
            encoding="utf-8",
            decode_responses=True,
            health_check_interval=30,
        )
    return _redis


async def close_redis() -> None:
    global _redis
    if _redis is not None:
        await _redis.aclose()
        _redis = None


async def cache_get_json(settings: Settings, key: str) -> Any:
    redis = await get_redis(settings)
    if redis is None:
        return None
    try:
        raw = await redis.get(key)
    except Exception as exc:
        logger.warning("Redis cache get failed for key %s: %s", key, exc)
        return None
    if raw is None:
        return None
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        logger.warning("Invalid JSON in Redis cache key %s", key)
        return None


async def cache_set_json(settings: Settings, key: str, value: Any, ttl_seconds: int) -> None:
    redis = await get_redis(settings)
    if redis is None:
        return
    try:
        await redis.set(key, json.dumps(value, ensure_ascii=False, default=str), ex=ttl_seconds)
    except Exception as exc:
        logger.warning("Redis cache set failed for key %s: %s", key, exc)


async def cache_delete(settings: Settings, *keys: str) -> None:
    redis = await get_redis(settings)
    if redis is None or not keys:
        return
    try:
        await redis.delete(*keys)
    except Exception as exc:
        logger.warning("Redis cache delete failed for %s key(s): %s", len(keys), exc)


async def cache_delete_pattern(settings: Settings, pattern: str) -> int:
    redis = await get_redis(settings)
    if redis is None or not pattern:
        return 0
    deleted = 0
    batch = []
    try:
        async for key in redis.scan_iter(match=pattern, count=100):
            batch.append(key)
            if len(batch) >= 100:
                deleted += int(await redis.delete(*batch))
                batch.clear()
        if batch:
            deleted += int(await redis.delete(*batch))
    except Exception as exc:
        logger.warning("Redis cache pattern delete failed for %s: %s", pattern, exc)
    return deleted


@asynccontextmanager
async def redis_lock(
    settings: Settings,
    name: str,
    *,
    ttl_seconds: int,
) -> AsyncIterator[bool]:
    redis = await get_redis(settings)
    if redis is None:
        yield True
        return

    key = redis_key(settings, "lock", name)
    token = secrets.token_urlsafe(16)
    acquired = bool(await redis.set(key, token, nx=True, ex=ttl_seconds))
    try:
        yield acquired
    finally:
        if acquired:
            script = """
            if redis.call("get", KEYS[1]) == ARGV[1] then
                return redis.call("del", KEYS[1])
            end
            return 0
            """
            try:
                await redis.eval(script, 1, key, token)
            except Exception:
                logger.exception("Failed to release Redis lock %s", key)


async def sleep_or_stop(stop_event: asyncio.Event, seconds: float) -> None:
    try:
        await asyncio.wait_for(stop_event.wait(), timeout=seconds)
    except asyncio.TimeoutError:
        return
