"""ContextCompressor - 上下文压缩与 Prompt 缓存

长会话自动压缩历史消息，保持 Token 在预算内；
会话内 Prompt 不重复渲染。
"""

import hashlib
import json
import logging
from typing import Any, Protocol, runtime_checkable

logger = logging.getLogger(__name__)


@runtime_checkable
class CompressionStrategy(Protocol):
    """压缩策略协议 — 所有压缩器必须实现此接口"""

    async def compress(self, messages: list[dict]) -> list[dict]:
        """压缩消息列表"""
        ...

    async def compress_tool_result(self, tool_name: str, result: Any) -> str:
        """压缩单个工具输出结果，返回压缩后的字符串"""
        ...

    def is_available(self) -> bool:
        """检查压缩器是否可用"""
        ...


class ContextCompressor:
    """Compress long conversation histories to stay within token budgets"""

    def __init__(
        self,
        llm_gateway: Any = None,
        max_tokens: int = 4000,
        keep_recent: int = 3,
        model: str = "default",
    ):
        self._llm_gateway = llm_gateway
        self._max_tokens = max_tokens
        self._keep_recent = keep_recent
        self._model = model

    def estimate_tokens(self, messages: list[dict]) -> int:
        """Estimate total tokens in message list (rough: 4 chars = 1 token)"""
        total = 0
        for msg in messages:
            content = msg.get("content", "")
            total += len(str(content)) // 4
        return total

    async def compress(self, messages: list[dict], _compression_depth: int = 0) -> list[dict]:
        """Compress messages if they exceed token budget

        Strategy:
        1. Keep system messages unchanged
        2. Keep the most recent N messages unchanged
        3. Compress older messages into a summary using LLM
        """
        if self.estimate_tokens(messages) <= self._max_tokens:
            return messages

        # Separate system messages, old messages, and recent messages
        system_msgs = [m for m in messages if m.get("role") == "system"]
        non_system = [m for m in messages if m.get("role") != "system"]

        if len(non_system) <= self._keep_recent:
            return messages  # Not enough messages to compress

        old_msgs = non_system[:-self._keep_recent]
        recent_msgs = non_system[-self._keep_recent:]

        # Compress old messages
        summary = await self._summarize(old_msgs)

        # Build compressed message list
        compressed = list(system_msgs)
        if summary:
            compressed.append({
                "role": "system",
                "content": f"## Conversation Summary\n{summary}",
            })
        compressed.extend(recent_msgs)

        # Recursive check: if still over budget, compress again
        if self.estimate_tokens(compressed) > self._max_tokens:
            if _compression_depth >= 1:
                # Depth guard: force truncation instead of infinite recursion
                return self._truncate(compressed)
            if len(recent_msgs) > 1:
                # Try keeping fewer recent messages
                return await self._compress_aggressive(messages, _compression_depth=_compression_depth + 1)
            # Last resort: truncate
            return self._truncate(compressed)

        return compressed

    async def _summarize(self, messages: list[dict], max_input_tokens: int = 3200) -> str:
        """Summarize a list of messages using LLM"""
        if not self._llm_gateway:
            # No LLM available, do simple truncation
            return self._simple_summary(messages)

        # Build summary prompt
        conversation_text = "\n".join(
            f"[{m.get('role', 'unknown')}]: {m.get('content', '')}"
            for m in messages
        )

        # Pre-truncate if conversation_text exceeds safe token threshold
        estimated_tokens = len(conversation_text) // 4
        if estimated_tokens > max_input_tokens:
            max_chars = max_input_tokens * 4
            conversation_text = conversation_text[:max_chars] + "\n...[truncated]"

        prompt = (
            "Summarize the following conversation history concisely, "
            "preserving key facts, decisions, and context. "
            "Focus on information that would be needed for continuing the conversation.\n\n"
            f"{conversation_text}"
        )

        try:
            response = await self._llm_gateway.chat(
                messages=[{"role": "user", "content": prompt}],
                model=self._model,
                agent_name="compressor",
                task_type="summarization",
            )
            return response.content
        except Exception as e:
            logger.warning(f"LLM summarization failed, using simple summary: {e}")
            return self._simple_summary(messages)

    def _simple_summary(self, messages: list[dict]) -> str:
        """Simple truncation-based summary when LLM is unavailable"""
        parts = []
        for msg in messages:
            role = msg.get("role", "unknown")
            content = str(msg.get("content", ""))[:200]
            parts.append(f"[{role}]: {content}...")
        return "\n".join(parts)

    async def _compress_aggressive(self, messages: list[dict], _compression_depth: int = 0) -> list[dict]:
        """More aggressive compression when standard compression isn't enough"""
        system_msgs = [m for m in messages if m.get("role") == "system"]
        non_system = [m for m in messages if m.get("role") != "system"]

        # Keep only the last message
        if non_system:
            summary = await self._summarize(non_system[:-1])
            compressed = list(system_msgs)
            if summary:
                compressed.append({
                    "role": "system",
                    "content": f"## Conversation Summary\n{summary}",
                })
            compressed.append(non_system[-1])
            return compressed

        return messages

    def _truncate(self, messages: list[dict]) -> list[dict]:
        """Last resort: truncate long messages"""
        result = []
        for msg in messages:
            content = str(msg.get("content", ""))
            if len(content) > self._max_tokens * 4:
                msg = {**msg, "content": content[:self._max_tokens * 4] + "...[truncated]"}
            result.append(msg)
        return result

    async def compress_tool_result(self, tool_name: str, result: Any) -> str:
        """默认实现：不做压缩，直接返回字符串表示"""
        return str(result)

    def is_available(self) -> bool:
        """ContextCompressor 始终可用"""
        return True


def create_compressor(config: dict[str, Any] | None = None) -> CompressionStrategy | None:
    """根据配置创建压缩器实例

    Args:
        config: 压缩配置字典，支持以下字段：
            - enabled: bool, 是否启用压缩（默认 False）
            - provider: "headroom" | "summary", 压缩提供者
            - max_tokens: int, token 预算（summary 模式）
            - keep_recent: int, 保留最近 N 条消息（summary 模式）
            - 其他 provider 特定配置

    Returns:
        CompressionStrategy 实例，或 None（未启用时）
    """
    if not config or not config.get("enabled", False):
        return None

    provider = config.get("provider", "summary")

    if provider == "headroom":
        try:
            from agentkit.core.headroom_compressor import HeadroomCompressor
            compressor = HeadroomCompressor(config)
            if compressor.is_available():
                return compressor
            logger.warning(
                "HeadroomCompressor not available (headroom-ai not installed?). "
                "Falling back to ContextCompressor."
            )
        except ImportError:
            logger.warning(
                "HeadroomCompressor module not available. "
                "Falling back to ContextCompressor."
            )
        # Fallback to summary compressor
        return ContextCompressor(
            max_tokens=config.get("max_tokens", 4000),
            keep_recent=config.get("keep_recent", 3),
        )

    # Default: summary-based compression
    return ContextCompressor(
        max_tokens=config.get("max_tokens", 4000),
        keep_recent=config.get("keep_recent", 3),
    )


def render_cached(template, variables: dict[str, Any] | None = None) -> list[dict[str, str]]:
    """Render PromptTemplate with caching - returns cached result for same variables"""
    cache_key = hashlib.md5(
        json.dumps(variables or {}, sort_keys=True).encode()
    ).hexdigest()

    if not hasattr(template, '_render_cache'):
        template._render_cache = {}

    if cache_key in template._render_cache:
        return template._render_cache[cache_key]

    result = template.render(variables=variables)
    template._render_cache[cache_key] = result
    return result


def clear_cache(template) -> None:
    """Clear the render cache on a PromptTemplate instance"""
    if hasattr(template, '_render_cache'):
        template._render_cache.clear()
