"""Context compaction logic: trigger check, prompt building, history replacement.

Pure computation functions for determining when compaction is needed,
building the compaction prompt, and applying the summary to message history.

No I/O imports.
"""

from typing import Any

from deal import post, pre
from pydantic_ai.messages import ModelRequest, UserPromptPart

from anima.core.conversation import (
    CompactionHistory,
    CompactionMessage,
    SystemRequestMessage,
)


@pre(
    lambda token_count, context_limit, reserved=20000: (
        token_count >= 0 and context_limit > 0 and reserved >= 0
    )
)
@post(lambda result: isinstance(result, bool))
def is_overflow(token_count: int, context_limit: int, reserved: int = 20000) -> bool:
    """Check if token count exceeds context limit minus reserved buffer.

    Returns True when token_count >= context_limit - reserved.
    This triggers compaction to prevent context overflow.

    Args:
        token_count: Current token usage count. Must be >= 0.
        context_limit: Maximum context window size in tokens. Must be > 0.
        reserved: Buffer tokens to keep free. Must be >= 0. Default 20000.

    Returns:
        True if compaction needed, False otherwise.

    Doctests:
        >>> # Normal case: well under limit
        >>> is_overflow(50000, 200000)
        False

        >>> # Approaching limit
        >>> is_overflow(180000, 200000)
        True

        >>> # At boundary
        >>> is_overflow(180000, 200000, reserved=20000)
        True

        >>> # At limit exactly (should trigger)
        >>> is_overflow(200000, 200000)
        True

        >>> # Custom reserved buffer
        >>> is_overflow(150000, 200000, reserved=50000)
        True

        >>> # Zero reserved means check at exact limit
        >>> is_overflow(199999, 200000, reserved=0)
        False

        >>> is_overflow(200000, 200000, reserved=0)
        True

        >>> # Contract: token_count must be >= 0
        >>> is_overflow(-1, 200000)  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...

        >>> # Contract: context_limit must be > 0
        >>> is_overflow(100000, 0)  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...

        >>> # Contract: reserved must be >= 0
        >>> is_overflow(100000, 200000, reserved=-1)  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...
    """
    # Effective threshold is context_limit minus reserved buffer
    threshold = context_limit - reserved
    return token_count >= threshold


@pre(
    lambda message_history, custom_prompt=None: (
        isinstance(message_history, list)
        and len(message_history) > 0
        and (
            custom_prompt is None
            or (isinstance(custom_prompt, str) and len(custom_prompt.strip()) > 0)
        )
    )
)
@post(lambda result: isinstance(result, str) and len(result) > 0)
def build_compaction_prompt(
    message_history: CompactionHistory, custom_prompt: str | None = None
) -> str:
    """Build the prompt sent to LLM to generate a conversation summary.

    If custom_prompt is provided, use it as the base template for building
    the compaction request. The conversation history is always embedded into
    the prompt so the summarizer can operate with `message_history=[]`.

    The prompt instructs the model to preserve:
    - Key decisions
    - Tool results referenced later
    - User preferences
    - Task context

    Args:
        message_history: The conversation to summarize (must be non-empty).
        custom_prompt: Optional custom prompt template. None uses default.

    Returns:
        A non-empty prompt string for the LLM.

    Doctests:
        >>> # Build with default prompt
        >>> messages = [{"role": "user", "content": "Hello"}]
        >>> prompt = build_compaction_prompt(messages)
        >>> len(prompt) > 0
        True
        >>> "summary" in prompt.lower()
        True

        >>> # Build with custom prompt template (history placeholder replaced)
        >>> custom = "Summarize this conversation: {history}"
        >>> prompt = build_compaction_prompt(messages, custom_prompt=custom)
        >>> "Hello" in prompt
        True
        >>> "{history}" in prompt
        False

        >>> # Default prompt contains key instructions
        >>> prompt = build_compaction_prompt(messages)
        >>> "decision" in prompt.lower()
        True
        >>> "tool result" in prompt.lower() or "tool_result" in prompt.lower().replace(" ", "_")
        True

        >>> # Contract: message_history must be non-empty
        >>> build_compaction_prompt([])  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...
    """

    def _render_message(msg: CompactionMessage) -> str:
        """Render a message object into compact text.

        This is intentionally lossy: compaction needs the semantic content,
        not full fidelity serialization.
        """
        # Common dict shape: {"role": ..., "content": ...}
        if isinstance(msg, dict):
            content = msg.get("content")
            if isinstance(content, str):
                return content

        # PydanticAI message objects: ModelRequest/ModelResponse
        parts = getattr(msg, "parts", None)
        if isinstance(parts, list):
            texts: list[str] = []
            for part in parts:
                content = getattr(part, "content", None)
                if isinstance(content, str) and content.strip():
                    texts.append(content)
            if texts:
                return "\n".join(texts)

        # Fallback: stable string conversion
        return str(msg)

    rendered_lines: list[str] = []
    for i, m in enumerate(message_history):
        rendered_lines.append(f"[{i}] {_render_message(m)}")
    rendered_history = "\n".join(rendered_lines)

    if custom_prompt is not None:
        # Treat custom_prompt as a template: replace {history} if present,
        # otherwise append the rendered history.
        if "{history}" in custom_prompt:
            return custom_prompt.replace("{history}", rendered_history)
        return f"{custom_prompt.strip()}\n\nConversation to summarize:\n{rendered_history}".strip()

    # Default compaction prompt embeds the rendered history directly.
    return f"""Summarize the following conversation while preserving:
1. Key decisions and conclusions reached
2. Tool results that may be referenced in future messages
3. User preferences and constraints mentioned
4. Task context and current objective

Generate a concise summary that captures essential information for continuation.

The summary should be structured with clear sections:
- Decisions: Major conclusions or choices made
- Context: Task, objective, and constraints
- References: Tool results/data that may be needed later
- Preferences: User preferences and constraints

Conversation to summarize:
{rendered_history}

Generate the summary now:"""


@pre(
    lambda original_system_request, summary_text, recent_messages: (
        original_system_request is not None
        and summary_text is not None
        and len(summary_text.strip()) > 0
        and isinstance(recent_messages, list)
    )
)
@post(lambda result: isinstance(result, list) and len(result) >= 2)
def apply_compaction(
    original_system_request: SystemRequestMessage,
    summary_text: str,
    recent_messages: CompactionHistory,
) -> CompactionHistory:
    """Build new message_history after compaction.

    Constructs: [original_system_request, summary_as_user_message, ...recent_messages]

    The summary is wrapped in XML tags for future compaction to identify:
    <compaction-summary>\n{summary_text}\n</compaction-summary>

    Args:
        original_system_request: The original system request (ModelRequest).
        summary_text: The summary text from LLM (must be non-empty).
        recent_messages: Recent messages to preserve after compaction.

    Returns:
        New message history starting with system request and summary.

    Note:
        This function constructs ModelRequest with UserPromptPart internally
        using imports from pydantic_ai.messages (top-level import, not I/O).

    Doctests:
        >>> # Identity and exact length are enforced by runtime assertions.
        >>> system = ModelRequest(parts=[UserPromptPart(content="system")])
        >>> recent = [ModelRequest(parts=[UserPromptPart(content="recent")])]
        >>> compacted = apply_compaction(system, "summary", recent)
        >>> compacted[0] is system
        True
        >>> len(compacted) == 2 + len(recent)
        True

        >>> # Contract: summary_text must be non-empty
        >>> sys_req = ModelRequest(parts=[UserPromptPart(content="system")])
        >>> apply_compaction(sys_req, "", [])  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...
    """
    # Wrap summary in XML tags
    summary_content = f"<compaction-summary>\n{summary_text}\n</compaction-summary>"

    # Create summary message
    summary_message = ModelRequest(parts=[UserPromptPart(content=summary_content)])

    # Construct new history
    compacted_history = [original_system_request, summary_message] + list(
        recent_messages
    )

    # Contract-like invariants involving derived expected length and identity.
    assert compacted_history[0] is original_system_request
    assert len(compacted_history) == 2 + len(recent_messages)

    return compacted_history


@pre(lambda messages: isinstance(messages, list) and len(messages) >= 2)
@post(lambda result: result >= 1)
def find_compaction_boundary(messages: list[Any]) -> int:
    """Find the index that splits old vs recent messages.

    Recent = all messages since last compaction summary.
    Old = everything before the last compaction summary.

    If no prior compaction found, keep last ~30% of messages (minimum 2).

    Args:
        messages: Full message history (must have at least 2 messages).

    Returns:
        Index where old messages end and recent messages begin.
        Always returns index >= 1 (at least one old message preserved).

    Doctests:
        >>> # With compaction summary present
        >>> from pydantic_ai.messages import ModelRequest, ModelResponse, UserPromptPart, TextPart
        >>> messages = [
        ...     ModelRequest(parts=[UserPromptPart(content="system")]),
        ...     ModelRequest(parts=[UserPromptPart(content="<compaction-summary>\\nOld summary\\n</compaction-summary>")]),
        ...     ModelResponse(parts=[TextPart(content="assistant response")]),
        ...     ModelRequest(parts=[UserPromptPart(content="new user message")]),
        ... ]
        >>> idx = find_compaction_boundary(messages)
        >>> idx == 2  # Returns index after compaction summary
        True

        >>> # Without compaction summary, use 30% rule
        >>> messages_no_compact = [
        ...     ModelRequest(parts=[UserPromptPart(content="system")]),
        ...     ModelRequest(parts=[UserPromptPart(content="msg1")]),
        ...     ModelResponse(parts=[TextPart(content="msg2")]),
        ...     ModelRequest(parts=[UserPromptPart(content="msg3")]),
        ...     ModelRequest(parts=[UserPromptPart(content="msg4")]),
        ... ]
        >>> idx = find_compaction_boundary(messages_no_compact)
        >>> idx >= 2  # At least 30% (minimum 2)
        True
        >>> idx < len(messages_no_compact)
        True

        >>> # Minimum 2 messages preserved when no compaction
        >>> small_messages = [
        ...     ModelRequest(parts=[UserPromptPart(content="msg1")]),
        ...     ModelRequest(parts=[UserPromptPart(content="msg2")]),
        ... ]
        >>> idx = find_compaction_boundary(small_messages)
        >>> idx == 1  # Only 1 old message, 1 recent (minimum)
        True

        >>> # Exact 30% case (10 messages, keep last 3)
        >>> ten_messages = [
        ...     ModelRequest(parts=[UserPromptPart(content=f"msg{i}")])
        ...     for i in range(10)
        ... ]
        >>> idx = find_compaction_boundary(ten_messages)
        >>> idx == 7  # 10 * 0.7 = 7, keep last 3 (30%)
        True

        >>> # Contract: messages must have at least 2 items
        >>> find_compaction_boundary([ModelRequest(parts=[UserPromptPart(content="one")])])  # doctest: +IGNORE_EXCEPTION_DETAIL
        Traceback (most recent call last):
        ...
        PreContractError: ...
    """
    # Search for last compaction summary
    for i in range(len(messages) - 1, -1, -1):
        msg = messages[i]
        # Check if message has parts with compaction-summary tag
        if hasattr(msg, "parts"):
            for part in msg.parts:
                if hasattr(part, "content") and isinstance(part.content, str):
                    if "<compaction-summary>" in part.content:
                        # Return index AFTER the summary message
                        return i + 1

    # No compaction found: use 30% rule
    # Keep last 30% as recent (minimum 2 messages)
    # Ensure at least 1 old message remains (result >= 1)
    total = len(messages)
    recent_count = max(2, int(total * 0.3))
    # Cap recent_count to ensure at least 1 old message
    recent_count = min(recent_count, total - 1)

    # Return index where recent messages begin
    return total - recent_count


# Re-export for module public API
__all__ = [
    "is_overflow",
    "build_compaction_prompt",
    "apply_compaction",
    "find_compaction_boundary",
]
