"""AUTO-GENERATED by scripts/generate_sync.py - DO NOT EDIT.

Source: config_cache.py

This module provides synchronous versions of the async classes.
Changes should be made to the source file, then regenerated.
"""

import threading
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Literal, cast

from .config_cache import CacheStats as CacheStats
from .limiter import OnUnavailable as OnUnavailable

ConfigSource = Literal["entity", "entity_default", "resource", "system"]
if TYPE_CHECKING:
    from .models import Limit, OnUnavailableAction
_NO_CONFIG: object = object()


@dataclass
class CacheEntry:
    """A cached value with expiration time."""

    value: Any
    expires_at: float


@dataclass
class SyncConfigCache:
    """
    In-memory cache for config data with TTL expiration.

    Provides thread-safe (for sync) and async-safe caching of:
    - System defaults (limits, on_unavailable)
    - Resource defaults (per resource)
    - Entity limits (per entity×resource, with negative caching)

    Thread/async safety:
    - Uses asyncio.Lock for async operations
    - Uses threading.Lock for sync operations
    - Locks are per-cache-instance (no global locking)

    Negative caching:
    - When entity config is not found, caches the "miss" to avoid repeated lookups
    - Uses sentinel _NO_CONFIG to distinguish None from "not cached"

    Args:
        ttl_seconds: Time-to-live for cached entries (0 = disabled)
    """

    ttl_seconds: int = 60
    namespace_id: str = "default"
    _enabled: bool = field(init=False, default=True)
    _system_defaults: CacheEntry | None = field(init=False, default=None)
    _resource_defaults: dict[str, CacheEntry] = field(init=False, default_factory=dict)
    _entity_limits: dict[tuple[str, str, str], CacheEntry] = field(init=False, default_factory=dict)
    _hits: int = field(init=False, default=0)
    _misses: int = field(init=False, default=0)
    _sync_lock: threading.Lock = field(init=False, default_factory=threading.Lock)

    def __post_init__(self) -> None:
        """Initialize derived state after dataclass init."""
        self._enabled = self.ttl_seconds > 0

    def _is_expired(self, entry: CacheEntry) -> bool:
        """Check if a cache entry has expired."""
        return time.time() > entry.expires_at

    def _make_entry(self, value: Any) -> CacheEntry:
        """Create a new cache entry with TTL."""
        return CacheEntry(value=value, expires_at=time.time() + self.ttl_seconds)

    def evict_entity(self, entity_id: str, resource: str) -> None:
        """Evict a single entity+resource config from cache (issue #327).

        Called after set_limits() or delete_limits() to ensure the next
        config resolution reads fresh data from DynamoDB.

        Args:
            entity_id: Entity ID to evict
            resource: Resource name to evict
        """
        self._entity_limits.pop((self.namespace_id, entity_id, resource), None)

    def get_system_defaults(
        self, fetch_fn: Callable[[], tuple[list["Limit"], "OnUnavailableAction | None"]]
    ) -> tuple[list["Limit"], "OnUnavailableAction | None"]:
        """
        Get system defaults, using cache if valid.

        Args:
            fetch_fn: Async function to fetch system defaults from repository

        Returns:
            Tuple of (limits, on_unavailable_str)
        """
        if not self._enabled:
            return fetch_fn()
        with self._sync_lock:
            if self._system_defaults is not None and (not self._is_expired(self._system_defaults)):
                self._hits += 1
                result: tuple[list[Limit], OnUnavailableAction | None] = self._system_defaults.value
                return result
            self._misses += 1
            value = fetch_fn()
            self._system_defaults = self._make_entry(value)
            return value

    def get_resource_defaults(
        self, resource: str, fetch_fn: Callable[[str], list["Limit"]]
    ) -> list["Limit"]:
        """
        Get resource defaults, using cache if valid.

        Args:
            resource: Resource name
            fetch_fn: Async function to fetch resource defaults from repository

        Returns:
            List of limits (empty if none configured)
        """
        if not self._enabled:
            return fetch_fn(resource)
        with self._sync_lock:
            entry = self._resource_defaults.get(resource)
            if entry is not None and (not self._is_expired(entry)):
                self._hits += 1
                return cast(list["Limit"], entry.value)
            self._misses += 1
            value = fetch_fn(resource)
            self._resource_defaults[resource] = self._make_entry(value)
            return value

    def get_entity_limits(
        self, entity_id: str, resource: str, fetch_fn: Callable[[str, str], list["Limit"]]
    ) -> list["Limit"]:
        """
        Get entity limits, using cache if valid (with negative caching).

        Negative caching: If no limits exist for this entity×resource,
        caches the empty result to avoid repeated lookups.

        Args:
            entity_id: Entity ID
            resource: Resource name
            fetch_fn: Async function to fetch entity limits from repository

        Returns:
            List of limits (empty if none configured)
        """
        if not self._enabled:
            return fetch_fn(entity_id, resource)
        cache_key = (self.namespace_id, entity_id, resource)
        with self._sync_lock:
            entry = self._entity_limits.get(cache_key)
            if entry is not None and (not self._is_expired(entry)):
                self._hits += 1
                if entry.value is _NO_CONFIG:
                    return []
                return cast(list["Limit"], entry.value)
            self._misses += 1
            value = fetch_fn(entity_id, resource)
            if value:
                self._entity_limits[cache_key] = self._make_entry(value)
            else:
                self._entity_limits[cache_key] = self._make_entry(_NO_CONFIG)
            return value

    def _check_cache_slot(
        self, slot_type: str, entity_id: str | None = None, resource: str | None = None
    ) -> tuple[bool, Any]:
        """Check a single cache slot for a valid entry.

        Args:
            slot_type: One of "entity", "entity_default", "resource", "system"
            entity_id: Entity ID (for entity/entity_default slots)
            resource: Resource name (for entity/resource slots)

        Returns:
            (is_cached, value) where is_cached indicates if a valid entry exists.
            For entity slots, _NO_CONFIG sentinel means "confirmed no config" (negative cache).
        """
        if slot_type == "entity":
            assert entity_id is not None and resource is not None
            entry = self._entity_limits.get((self.namespace_id, entity_id, resource))
        elif slot_type == "entity_default":
            assert entity_id is not None
            entry = self._entity_limits.get((self.namespace_id, entity_id, "_default_"))
        elif slot_type == "resource":
            assert resource is not None
            entry = self._resource_defaults.get(resource)
        elif slot_type == "system":
            entry = self._system_defaults
        else:
            return (False, None)
        if entry is not None and (not self._is_expired(entry)):
            return (True, entry.value)
        return (False, None)

    def resolve_limits(
        self,
        entity_id: str,
        resource: str,
        batch_fetch_fn: Callable[
            [list[tuple[str, str]]],
            "dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]",
        ],
    ) -> "tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]":
        """
        Resolve limits using batched config fetch with cache awareness.

        Checks all 4 cache slots (entity, entity_default, resource, system),
        collects misses, fetches them in a single BatchGetItem call, populates
        the cache, and returns the highest-precedence match.

        Args:
            entity_id: Entity to resolve limits for
            resource: Resource being accessed
            batch_fetch_fn: Async function to batch-fetch config items by (PK, SK).
                Returns dict mapping (PK, SK) to (limits, on_unavailable) tuples.

        Returns:
            Tuple of (limits, on_unavailable, config_source) where:
            - limits: Resolved limits or None if nothing found
            - on_unavailable: on_unavailable value from system config (if fetched)
            - config_source: "entity", "entity_default", "resource", "system", or None
        """
        if not self._enabled:
            return self._resolve_limits_uncached(entity_id, resource, batch_fetch_fn)
        with self._sync_lock:
            return self._resolve_limits_inner_async(entity_id, resource, batch_fetch_fn)

    def _resolve_limits_inner_async(
        self,
        entity_id: str,
        resource: str,
        batch_fetch_fn: Callable[
            [list[tuple[str, str]]],
            "dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]",
        ],
    ) -> "tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]":
        """Async inner implementation for batched config resolution."""
        from . import schema

        levels, cached_results, miss_keys = self._build_levels_and_check_cache(
            entity_id, resource, schema
        )
        fetched_results: dict[str, Any] = {}
        on_unavailable: OnUnavailableAction | None = None
        if miss_keys:
            fetch_keys = [(pk, sk) for _, pk, sk in miss_keys]
            items = batch_fetch_fn(fetch_keys)
            fetched_results, on_unavailable = self._process_fetched_items(
                miss_keys, items, entity_id, resource
            )
        return self._evaluate_hierarchy(levels, cached_results, fetched_results, on_unavailable)

    def _build_levels_and_check_cache(
        self, entity_id: str, resource: str, schema: Any
    ) -> tuple[
        list[tuple[ConfigSource, str, str]], dict[str, Any], list[tuple[ConfigSource, str, str]]
    ]:
        """Build config levels and check cache for each, returning misses.

        Returns:
            (levels, cached_results, miss_keys)
        """
        include_entity_default = resource != "_default_"
        ns = self.namespace_id
        levels: list[tuple[ConfigSource, str, str]] = [
            ("entity", schema.pk_entity(ns, entity_id), schema.sk_config(resource))
        ]
        if include_entity_default:
            levels.append(
                ("entity_default", schema.pk_entity(ns, entity_id), schema.sk_config("_default_"))
            )
        levels.extend(
            [
                ("resource", schema.pk_resource(ns, resource), schema.sk_config()),
                ("system", schema.pk_system(ns), schema.sk_config()),
            ]
        )
        cached_results: dict[str, Any] = {}
        miss_keys: list[tuple[ConfigSource, str, str]] = []
        for slot_type, pk, sk in levels:
            is_cached, value = self._check_cache_slot(
                slot_type, entity_id=entity_id, resource=resource
            )
            if is_cached:
                self._hits += 1
                cached_results[slot_type] = value
            else:
                self._misses += 1
                miss_keys.append((slot_type, pk, sk))
        return (levels, cached_results, miss_keys)

    def _process_fetched_items(
        self,
        miss_keys: list[tuple[ConfigSource, str, str]],
        items: "dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]",
        entity_id: str,
        resource: str,
    ) -> "tuple[dict[str, Any], OnUnavailableAction | None]":
        """Process fetched items: populate cache and return results.

        Returns:
            (fetched_results, on_unavailable)
        """
        fetched_results: dict[str, Any] = {}
        on_unavailable: OnUnavailableAction | None = None
        for slot_type, pk, sk in miss_keys:
            entry = items.get((pk, sk))
            if entry is not None:
                limits, ou_val = entry
                if slot_type == "system":
                    self._system_defaults = self._make_entry((limits, ou_val))
                    fetched_results[slot_type] = limits
                    on_unavailable = ou_val
                elif slot_type == "resource":
                    self._resource_defaults[resource] = self._make_entry(limits)
                    fetched_results[slot_type] = limits
                elif slot_type == "entity":
                    cache_key = (self.namespace_id, entity_id, resource)
                    if limits:
                        self._entity_limits[cache_key] = self._make_entry(limits)
                    else:
                        self._entity_limits[cache_key] = self._make_entry(_NO_CONFIG)
                    fetched_results[slot_type] = limits
                elif slot_type == "entity_default":
                    cache_key_d = (self.namespace_id, entity_id, "_default_")
                    if limits:
                        self._entity_limits[cache_key_d] = self._make_entry(limits)
                    else:
                        self._entity_limits[cache_key_d] = self._make_entry(_NO_CONFIG)
                    fetched_results[slot_type] = limits
            else:
                ns = self.namespace_id
                if slot_type == "entity":
                    self._entity_limits[ns, entity_id, resource] = self._make_entry(_NO_CONFIG)
                    fetched_results[slot_type] = _NO_CONFIG
                elif slot_type == "entity_default":
                    self._entity_limits[ns, entity_id, "_default_"] = self._make_entry(_NO_CONFIG)
                    fetched_results[slot_type] = _NO_CONFIG
                elif slot_type == "resource":
                    self._resource_defaults[resource] = self._make_entry([])
                    fetched_results[slot_type] = []
                elif slot_type == "system":
                    self._system_defaults = self._make_entry(([], None))
                    fetched_results[slot_type] = []
                    on_unavailable = None
        return (fetched_results, on_unavailable)

    def _evaluate_hierarchy(
        self,
        levels: list[tuple[ConfigSource, str, str]],
        cached_results: dict[str, Any],
        fetched_results: dict[str, Any],
        on_unavailable: "OnUnavailableAction | None",
    ) -> "tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]":
        """Evaluate config hierarchy from cached + fetched results.

        Returns:
            (limits, on_unavailable, config_source)
        """
        if "system" not in fetched_results and "system" in cached_results:
            system_value = cached_results["system"]
            if isinstance(system_value, tuple) and len(system_value) == 2:
                on_unavailable = system_value[1]
        for slot_type, _, _ in levels:
            value = cached_results.get(slot_type) or fetched_results.get(slot_type)
            if value is _NO_CONFIG or value is None:
                continue
            if slot_type == "system" and isinstance(value, tuple):
                limits_val = value[0]
                if limits_val:
                    return (limits_val, on_unavailable, slot_type)
                continue
            if isinstance(value, list) and value:
                return (value, on_unavailable, slot_type)
        return (None, on_unavailable, None)

    def _resolve_limits_uncached(
        self,
        entity_id: str,
        resource: str,
        batch_fetch_fn: Callable[
            [list[tuple[str, str]]],
            "dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]",
        ],
    ) -> "tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]":
        """Resolve limits without caching (TTL=0): batch fetch all 4 levels."""
        from . import schema

        levels, _, _ = self._build_levels_and_check_cache(entity_id, resource, schema)
        fetch_keys = [(pk, sk) for _, pk, sk in levels]
        items = batch_fetch_fn(fetch_keys)
        return self._evaluate_uncached(levels, items)

    def _evaluate_uncached(
        self,
        levels: list[tuple[ConfigSource, str, str]],
        items: "dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]",
    ) -> "tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]":
        """Evaluate hierarchy from batch results without caching."""
        on_unavailable: OnUnavailableAction | None = None
        for slot_type, pk, sk in levels:
            if slot_type == "system":
                entry = items.get((pk, sk))
                if entry is not None:
                    on_unavailable = entry[1]
                break
        for slot_type, pk, sk in levels:
            entry = items.get((pk, sk))
            if entry is None:
                continue
            limits = entry[0]
            if limits:
                return (limits, on_unavailable, slot_type)
        return (None, on_unavailable, None)

    def invalidate(self) -> None:
        """
        Invalidate all cached entries.

        Thread-safe. Call this after config changes to force refresh.
        """
        with self._sync_lock:
            self._system_defaults = None
            self._resource_defaults.clear()
            self._entity_limits.clear()

    def invalidate_async(self) -> None:
        """
        Invalidate all cached entries (async version).

        Async-safe. Call this after config changes to force refresh.
        """
        with self._sync_lock:
            self._system_defaults = None
            self._resource_defaults.clear()
            self._entity_limits.clear()

    def get_stats(self) -> CacheStats:
        """
        Get cache performance statistics.

        Returns:
            CacheStats with hits, misses, size, and TTL
        """
        with self._sync_lock:
            size = (
                (1 if self._system_defaults is not None else 0)
                + len(self._resource_defaults)
                + len(self._entity_limits)
            )
            return CacheStats(
                hits=self._hits, misses=self._misses, size=size, ttl_seconds=self.ttl_seconds
            )

    @property
    def enabled(self) -> bool:
        """Whether caching is enabled (TTL > 0)."""
        return self._enabled
