"""Transparent caching policy for unary API calls."""

from __future__ import annotations

import json
from collections import OrderedDict
from dataclasses import dataclass
from typing import Any

from omni.config.models import CacheConfig
from omni.generated.methods import ALL_METHODS
from omni.http.methods import SAFE_MANAGEMENT_METHODS, SAFE_RESOURCE_METHODS
from omni.state import StateStore


@dataclass(slots=True)
class CacheDecision:
    enabled: bool
    ttl: float = 0.0


class FrontCache:
    """Small in-memory front cache for reducing sqlite roundtrips."""

    def __init__(self, max_entries: int) -> None:
        self.max_entries = max_entries
        self._data: OrderedDict[str, str] = OrderedDict()

    def get(self, key: str) -> str | None:
        if key not in self._data:
            return None
        value = self._data.pop(key)
        self._data[key] = value
        return value

    def set(self, key: str, value: str) -> None:
        if key in self._data:
            self._data.pop(key)
        self._data[key] = value
        while len(self._data) > self.max_entries:
            self._data.popitem(last=False)

    def invalidate_prefixes(self, prefixes: list[str]) -> None:
        keys_to_delete = [key for key in self._data if any(key.startswith(p) for p in prefixes)]
        for key in keys_to_delete:
            self._data.pop(key, None)


class CacheController:
    def __init__(self, config: CacheConfig, state: StateStore) -> None:
        self.config = config
        self.state = state
        self.front = FrontCache(max_entries=max(1, min(config.max_entries, 2048)))

    def cache_key(
        self,
        method_key: str,
        request: dict[str, Any],
        metadata: dict[str, str] | None,
        instance_name: str,
    ) -> str:
        req_json = json.dumps(request, sort_keys=True, separators=(",", ":"))
        md_json = json.dumps(metadata or {}, sort_keys=True, separators=(",", ":"))
        return f"{instance_name}:{method_key}:{req_json}:{md_json}"

    def decision(self, method_key: str, stream: bool) -> CacheDecision:
        if not self.config.enabled or stream:
            return CacheDecision(enabled=False)

        if method_key in SAFE_RESOURCE_METHODS or method_key in SAFE_MANAGEMENT_METHODS:
            return CacheDecision(enabled=True, ttl=float(self.config.ttl_defaults.get(method_key, 5.0)))

        return CacheDecision(enabled=False)

    def get(self, key: str) -> dict[str, Any] | None:
        front = self.front.get(key)
        if front is not None:
            return json.loads(front)

        payload = self.state.cache_get(key)
        if payload is None:
            return None

        self.front.set(key, payload)
        return json.loads(payload)

    def set(self, key: str, payload: dict[str, Any], ttl: float) -> None:
        encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"))
        self.front.set(key, encoded)
        self.state.cache_set(key, encoded, ttl)
        self.state.cache_prune_to_limit(self.config.max_entries)

    def invalidate_after_mutation(self, method_key: str, instance_name: str) -> None:
        # Conservative invalidation by high-level service domains.
        prefixes: list[str] = []
        if method_key.startswith("omni.resources.ResourceService"):
            prefixes.append(f"{instance_name}:omni.resources.ResourceService")
        if method_key.startswith("management.ManagementService"):
            prefixes.append(f"{instance_name}:management.ManagementService")

        if prefixes:
            self.front.invalidate_prefixes(prefixes)
            self.state.cache_invalidate_prefixes(prefixes)


def is_mutation(method_key: str) -> bool:
    method = ALL_METHODS.get(method_key)
    if method is None:
        return False

    return not method.idempotent
