"""
Slot State Cache Layer.
槽位状态缓存层 - 提供会话级槽位状态持久化

[AC-MRS-SLOT-CACHE-01] 多轮状态持久化

Features:
- L1: In-memory cache (process-level, 5 min TTL)
- L2: Redis cache (shared, configurable TTL)
- Automatic fallback on cache miss
- Support for slot value source priority

Key format: slot_state:{tenant_id}:{session_id}
TTL: Configurable (default 30 minutes)
"""

import json
import logging
import time
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any

import redis.asyncio as redis

from app.core.config import get_settings
from app.models.mid.schemas import SlotSource

logger = logging.getLogger(__name__)


@dataclass
class CachedSlotValue:
    """
    缓存的槽位值
    
    Attributes:
        value: 槽位值
        source: 值来源 (user_confirmed, rule_extracted, llm_inferred, default, context)
        confidence: 置信度
        updated_at: 更新时间戳
    """
    value: Any
    source: str
    confidence: float = 1.0
    updated_at: float = field(default_factory=time.time)
    
    def to_dict(self) -> dict[str, Any]:
        return {
            "value": self.value,
            "source": self.source,
            "confidence": self.confidence,
            "updated_at": self.updated_at,
        }
    
    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "CachedSlotValue":
        return cls(
            value=data["value"],
            source=data["source"],
            confidence=data.get("confidence", 1.0),
            updated_at=data.get("updated_at", time.time()),
        )


@dataclass
class CachedSlotState:
    """
    缓存的槽位状态
    
    Attributes:
        filled_slots: 已填充的槽位值字典 {slot_key: CachedSlotValue}
        slot_to_field_map: 槽位到元数据字段的映射
        created_at: 创建时间
        updated_at: 最后更新时间
    """
    filled_slots: dict[str, CachedSlotValue] = field(default_factory=dict)
    slot_to_field_map: dict[str, str] = field(default_factory=dict)
    created_at: float = field(default_factory=time.time)
    updated_at: float = field(default_factory=time.time)
    
    def to_dict(self) -> dict[str, Any]:
        return {
            "filled_slots": {
                k: v.to_dict() for k, v in self.filled_slots.items()
            },
            "slot_to_field_map": self.slot_to_field_map,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
        }
    
    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "CachedSlotState":
        filled_slots = {}
        for k, v in data.get("filled_slots", {}).items():
            if isinstance(v, dict):
                filled_slots[k] = CachedSlotValue.from_dict(v)
            else:
                filled_slots[k] = CachedSlotValue(value=v, source="unknown")
        
        return cls(
            filled_slots=filled_slots,
            slot_to_field_map=data.get("slot_to_field_map", {}),
            created_at=data.get("created_at", time.time()),
            updated_at=data.get("updated_at", time.time()),
        )
    
    def get_simple_filled_slots(self) -> dict[str, Any]:
        """获取简化的已填充槽位字典（仅值）"""
        return {k: v.value for k, v in self.filled_slots.items()}
    
    def get_slot_sources(self) -> dict[str, str]:
        """获取槽位来源字典"""
        return {k: v.source for k, v in self.filled_slots.items()}
    
    def get_slot_confidence(self) -> dict[str, float]:
        """获取槽位置信度字典"""
        return {k: v.confidence for k, v in self.filled_slots.items()}


class SlotStateCache:
    """
    [AC-MRS-SLOT-CACHE-01] 槽位状态缓存层
    
    提供会话级槽位状态持久化，支持：
    - L1: 内存缓存（进程级，5分钟 TTL）
    - L2: Redis 缓存（共享，可配置 TTL）
    - 自动降级（Redis 不可用时仅使用内存缓存）
    - 槽位值来源优先级合并
    
    Key format: slot_state:{tenant_id}:{session_id}
    TTL: Configurable via settings.slot_state_cache_ttl (default 1800 seconds = 30 minutes)
    """
    
    _local_cache: dict[str, tuple[CachedSlotState, float]] = {}
    _local_cache_ttl = 300
    
    SOURCE_PRIORITY = {
        SlotSource.USER_CONFIRMED.value: 100,
        "user_confirmed": 100,
        SlotSource.RULE_EXTRACTED.value: 80,
        "rule_extracted": 80,
        SlotSource.LLM_INFERRED.value: 60,
        "llm_inferred": 60,
        "context": 40,
        SlotSource.DEFAULT.value: 20,
        "default": 20,
        "unknown": 0,
    }
    
    def __init__(self, redis_client: redis.Redis | None = None):
        self._redis = redis_client
        self._settings = get_settings()
        self._enabled = self._settings.redis_enabled
        self._cache_ttl = getattr(self._settings, "slot_state_cache_ttl", 1800)
    
    async def _get_client(self) -> redis.Redis | None:
        """Get or create Redis client."""
        if not self._enabled:
            return None
        if self._redis is None:
            try:
                self._redis = redis.from_url(
                    self._settings.redis_url,
                    encoding="utf-8",
                    decode_responses=True,
                )
            except Exception as e:
                logger.warning(f"[SlotStateCache] Failed to connect to Redis: {e}")
                self._enabled = False
                return None
        return self._redis
    
    def _make_key(self, tenant_id: str, session_id: str) -> str:
        """Generate cache key."""
        return f"slot_state:{tenant_id}:{session_id}"
    
    def _make_local_key(self, tenant_id: str, session_id: str) -> str:
        """Generate local cache key."""
        return f"{tenant_id}:{session_id}"
    
    def _get_source_priority(self, source: str) -> int:
        """Get priority for a source."""
        return self.SOURCE_PRIORITY.get(source, 0)
    
    async def get(
        self,
        tenant_id: str,
        session_id: str,
    ) -> CachedSlotState | None:
        """
        Get cached slot state (L1 -> L2).
        
        Args:
            tenant_id: Tenant ID for isolation
            session_id: Session ID
            
        Returns:
            CachedSlotState or None if not found
        """
        local_key = self._make_local_key(tenant_id, session_id)
        if local_key in self._local_cache:
            state, timestamp = self._local_cache[local_key]
            if time.time() - timestamp < self._local_cache_ttl:
                logger.debug(f"[SlotStateCache] L1 hit: {local_key}")
                return state
            else:
                del self._local_cache[local_key]
        
        client = await self._get_client()
        if client is None:
            return None
        
        key = self._make_key(tenant_id, session_id)
        
        try:
            data = await client.get(key)
            if data:
                logger.debug(f"[SlotStateCache] L2 hit: {key}")
                state_dict = json.loads(data)
                state = CachedSlotState.from_dict(state_dict)
                
                self._local_cache[local_key] = (state, time.time())
                
                return state
            return None
        except Exception as e:
            logger.warning(f"[SlotStateCache] Failed to get from cache: {e}")
            return None
    
    async def set(
        self,
        tenant_id: str,
        session_id: str,
        state: CachedSlotState,
    ) -> bool:
        """
        Set slot state to cache (L1 + L2).
        
        Args:
            tenant_id: Tenant ID for isolation
            session_id: Session ID
            state: CachedSlotState to cache
            
        Returns:
            True if successful
        """
        local_key = self._make_local_key(tenant_id, session_id)
        state.updated_at = time.time()
        self._local_cache[local_key] = (state, time.time())
        
        client = await self._get_client()
        if client is None:
            return False
        
        key = self._make_key(tenant_id, session_id)
        
        try:
            await client.setex(
                key,
                self._cache_ttl,
                json.dumps(state.to_dict(), default=str),
            )
            logger.debug(f"[SlotStateCache] Set cache: {key}")
            return True
        except Exception as e:
            logger.warning(f"[SlotStateCache] Failed to set cache: {e}")
            return False
    
    async def merge_and_set(
        self,
        tenant_id: str,
        session_id: str,
        new_slots: dict[str, CachedSlotValue],
        slot_to_field_map: dict[str, str] | None = None,
    ) -> CachedSlotState:
        """
        Merge new slot values with cached state and save.
        
        Priority: user_confirmed > rule_extracted > llm_inferred > context > default
        
        Args:
            tenant_id: Tenant ID
            session_id: Session ID
            new_slots: New slot values to merge
            slot_to_field_map: Slot to field mapping
            
        Returns:
            Updated CachedSlotState
        """
        state = await self.get(tenant_id, session_id)
        if state is None:
            state = CachedSlotState()
        
        for slot_key, new_value in new_slots.items():
            if slot_key in state.filled_slots:
                existing = state.filled_slots[slot_key]
                existing_priority = self._get_source_priority(existing.source)
                new_priority = self._get_source_priority(new_value.source)
                
                if new_priority >= existing_priority:
                    state.filled_slots[slot_key] = new_value
                    logger.debug(
                        f"[SlotStateCache] Slot '{slot_key}' updated: "
                        f"{existing.source}({existing_priority}) -> "
                        f"{new_value.source}({new_priority})"
                    )
            else:
                state.filled_slots[slot_key] = new_value
                logger.debug(
                    f"[SlotStateCache] Slot '{slot_key}' added: "
                    f"source={new_value.source}, value={new_value.value}"
                )
        
        if slot_to_field_map:
            state.slot_to_field_map.update(slot_to_field_map)
        
        await self.set(tenant_id, session_id, state)
        
        return state
    
    async def delete(
        self,
        tenant_id: str,
        session_id: str,
    ) -> bool:
        """
        Delete slot state from cache (L1 + L2).
        
        Args:
            tenant_id: Tenant ID for isolation
            session_id: Session ID
            
        Returns:
            True if successful
        """
        local_key = self._make_local_key(tenant_id, session_id)
        if local_key in self._local_cache:
            del self._local_cache[local_key]
        
        client = await self._get_client()
        if client is None:
            return False
        
        key = self._make_key(tenant_id, session_id)
        
        try:
            await client.delete(key)
            logger.debug(f"[SlotStateCache] Deleted cache: {key}")
            return True
        except Exception as e:
            logger.warning(f"[SlotStateCache] Failed to delete cache: {e}")
            return False
    
    async def clear_slot(
        self,
        tenant_id: str,
        session_id: str,
        slot_key: str,
    ) -> bool:
        """
        Clear a specific slot from cached state.
        
        Args:
            tenant_id: Tenant ID
            session_id: Session ID
            slot_key: Slot key to clear
            
        Returns:
            True if successful
        """
        state = await self.get(tenant_id, session_id)
        if state is None:
            return True
        
        if slot_key in state.filled_slots:
            del state.filled_slots[slot_key]
            await self.set(tenant_id, session_id, state)
            logger.debug(f"[SlotStateCache] Cleared slot: {slot_key}")
        
        return True
    
    async def close(self) -> None:
        """Close Redis connection."""
        if self._redis:
            await self._redis.close()


_slot_state_cache: SlotStateCache | None = None


def get_slot_state_cache() -> SlotStateCache:
    """Get singleton SlotStateCache instance."""
    global _slot_state_cache
    if _slot_state_cache is None:
        _slot_state_cache = SlotStateCache()
    return _slot_state_cache
