"""Shell orchestration for context compaction.

Spec source: context-compaction.implement-agent-compaction-shell.
"""

from __future__ import annotations

from typing import Any, Sequence

from pydantic_ai import Agent
from pydantic_ai.messages import ModelMessage
from returns.result import Failure, Result, Success

from anima.core.conversation import CompactionConfig
from anima.core.execution.state import TokenUsage
from anima.core.compaction import (
    apply_compaction,
    build_compaction_prompt,
    find_compaction_boundary,
    is_overflow,
)
from anima.core.prune import prune_tool_outputs
from anima.core.schemas import AnimaError, InternalError
from anima.shell.state import append_log


# @invar:allow shell_result: Spec requires bool trigger helper in shell module
# @shell_orchestration: Compaction trigger decision stays with shell orchestration
def should_compact(
    token_usage: TokenUsage,
    compaction_config: CompactionConfig,
    model_context_limit: int,
) -> bool:
    """Determine whether automatic compaction should run."""
    if not compaction_config.auto:
        return False

    # Use last_request_prompt_tokens (the actual input size of the most recent
    # model request) instead of cumulative prompt_tokens. The cumulative total
    # grows with every model request in the session and does NOT reflect the
    # actual context window usage of the next request.
    # last_request_prompt_tokens is set by track_token_usage after each model
    # call and represents the true "how full is the context window right now?"
    return is_overflow(
        token_usage.last_request_prompt_tokens,
        model_context_limit,
        compaction_config.reserved,
    )


async def run_compaction(
    agent: Agent[Any],
    message_history: Sequence[ModelMessage],
    compaction_config: CompactionConfig,
    persona_compaction_prompt: str | None,
    instance_id: str,
) -> Result[list[ModelMessage], AnimaError]:
    """Run one compaction cycle and return compacted message history."""
    history = list(message_history)
    boundary_index = find_compaction_boundary(history)

    history_for_summary = history
    if compaction_config.prune:
        history_for_summary = prune_tool_outputs(history, boundary_index)

    old_messages = history_for_summary[:boundary_index]
    recent_messages = history_for_summary[boundary_index:]

    prompt_override = persona_compaction_prompt or compaction_config.prompt
    compaction_prompt = build_compaction_prompt(old_messages, prompt_override)

    try:
        summary_result = await agent.run(compaction_prompt, message_history=[])
    except Exception as exc:
        # Best-effort logging: compaction failures should be visible to operators,
        # but logging must never mask the underlying failure.
        try:
            _ = append_log(
                instance_id,
                "compaction_error",
                {
                    "error": str(exc),
                    "old_count": len(message_history),
                    "boundary_index": boundary_index,
                },
            )
        except Exception:
            pass
        return Failure(InternalError(f"Compaction LLM call failed: {exc}"))

    summary_text = str(summary_result.output)
    new_history = apply_compaction(
        original_system_request=history[0],
        summary_text=summary_text,
        recent_messages=recent_messages,
    )

    _ = append_log(
        instance_id,
        "compaction",
        {
            "old_count": len(message_history),
            "new_count": len(new_history),
            "summary_length": len(summary_text),
        },
    )

    return Success(new_history)


__all__ = ["run_compaction", "should_compact"]
