from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass
from typing import Any, Dict, Hashable, Optional

@dataclass
class CacheItem:
    value: Any
    expires_at: float

class InflightDedup:
    """
    Prevents duplicate in-flight work for the same key.
    Also provides optional TTL cache of results.
    """
    def __init__(self, ttl_seconds: float = 0.0):
        self.ttl = float(ttl_seconds)
        self._inflight: Dict[Hashable, asyncio.Future] = {}
        self._cache: Dict[Hashable, CacheItem] = {}
        self._lock = asyncio.Lock()

    def _now(self) -> float:
        return time.monotonic()

    def _get_cache(self, key: Hashable):
        if self.ttl <= 0:
            return None
        item = self._cache.get(key)
        if not item:
            return None
        if item.expires_at <= self._now():
            self._cache.pop(key, None)
            return None
        return item.value

    async def run(self, key: Hashable, coro_factory):
        # Cache fast path
        cached = self._get_cache(key)
        if cached is not None:
            return cached

        async with self._lock:
            cached = self._get_cache(key)
            if cached is not None:
                return cached

            fut = self._inflight.get(key)
            if fut is not None and not fut.done():
                return await fut

            fut = asyncio.get_event_loop().create_future()
            self._inflight[key] = fut

        try:
            val = await coro_factory()
            if self.ttl > 0:
                self._cache[key] = CacheItem(value=val, expires_at=self._now() + self.ttl)
            fut.set_result(val)
            return val
        except Exception as e:
            if not fut.done():
                fut.set_exception(e)
            raise
        finally:
            async with self._lock:
                cur = self._inflight.get(key)
                if cur is fut:
                    self._inflight.pop(key, None)

    async def purge(self):
        if self.ttl <= 0:
            return
        now = self._now()
        async with self._lock:
            for k in list(self._cache.keys()):
                if self._cache[k].expires_at <= now:
                    self._cache.pop(k, None)