"""
Caching decorators for CacheStack.

Provides @cached and @invalidate decorators that work with
any BaseCache backend, on both sync and async functions.
"""

from __future__ import annotations

import asyncio
import functools
import inspect
from typing import Any, Callable, Optional

from cachestack.base import BaseCache


def _default_key_builder(func: Callable, args: tuple, kwargs: dict) -> str:
    """Build a cache key from function name and arguments."""
    parts = [func.__module__, func.__qualname__]
    if args:
        parts.append(str(args))
    if kwargs:
        parts.append(str(sorted(kwargs.items())))
    return ":".join(parts)


def cached(
    cache: BaseCache,
    ttl: Optional[int] = None,
    key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None,
    condition: Optional[Callable[[Any], bool]] = None,
) -> Callable:
    """
    Decorator to cache the return value of a function.

    Works on both sync and async functions.

    Args:
        cache: A BaseCache instance to use for storage.
        ttl: Time-to-live in seconds. None means no expiry.
        key_builder: Callable(func, args, kwargs) -> str.
                     Defaults to module + qualname + args.
        condition: Callable(return_value) -> bool.
                   Only cache the result if condition returns True.

    Example:
        @cached(cache=MemoryBackend(), ttl=60)
        async def get_user(user_id: int):
            return await db.fetch_user(user_id)

        @cached(
            cache=RedisBackend(),
            ttl=300,
            key_builder=lambda fn, args, kw: f"user:{args[0]}",
            condition=lambda v: v is not None,
        )
        async def get_profile(user_id: int):
            ...
    """
    _key_builder = key_builder or _default_key_builder

    def decorator(func: Callable) -> Callable:
        is_async = asyncio.iscoroutinefunction(func)

        @functools.wraps(func)
        async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
            key = _key_builder(func, args, kwargs)
            cached_value = await cache.get(key)
            if cached_value is not None:
                return cached_value
            result = await func(*args, **kwargs)
            if condition is None or condition(result):
                await cache.set(key, result, ttl)
            return result

        @functools.wraps(func)
        def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
            key = _key_builder(func, args, kwargs)
            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                loop = None

            def run_coro(coro):
                if loop and loop.is_running():
                    import concurrent.futures
                    with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
                        future = pool.submit(asyncio.run, coro)
                        return future.result()
                else:
                    return asyncio.run(coro)

            cached_value = run_coro(cache.get(key))
            if cached_value is not None:
                return cached_value
            result = func(*args, **kwargs)
            if condition is None or condition(result):
                run_coro(cache.set(key, result, ttl))
            return result

        return async_wrapper if is_async else sync_wrapper

    return decorator


def invalidate(
    cache: BaseCache,
    key_builder: Optional[Callable[[Callable, tuple, dict], str]] = None,
) -> Callable:
    """
    Decorator to invalidate a cache key when a function is called.

    Works on both sync and async functions.

    Args:
        cache: A BaseCache instance.
        key_builder: Callable(func, args, kwargs) -> str.
                     Must match the key_builder used in @cached.

    Example:
        @invalidate(cache=redis_cache, key_builder=lambda fn, a, kw: f"user:{a[0]}")
        async def update_user(user_id: int, data: dict):
            await db.update(user_id, data)
    """
    _key_builder = key_builder or _default_key_builder

    def decorator(func: Callable) -> Callable:
        is_async = asyncio.iscoroutinefunction(func)

        @functools.wraps(func)
        async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
            result = await func(*args, **kwargs)
            key = _key_builder(func, args, kwargs)
            await cache.delete(key)
            return result

        @functools.wraps(func)
        def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
            result = func(*args, **kwargs)
            key = _key_builder(func, args, kwargs)
            try:
                loop = asyncio.get_running_loop()
            except RuntimeError:
                loop = None
            if loop and loop.is_running():
                import concurrent.futures
                with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
                    future = pool.submit(asyncio.run, cache.delete(key))
                    future.result()
            else:
                asyncio.run(cache.delete(key))
            return result

        return async_wrapper if is_async else sync_wrapper

    return decorator
