"""Conversation context builder and compactor.

This module separates pure context-building logic from I/O operations,
following the Functional Core, Imperative Shell pattern.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Iterable

from .modelrelay import (
    responses_create,
    get_model_context_window,
    get_model_max_output_tokens,
    resolve_root_model,
)
from .prompts import CONVERSATION_SUMMARY_SYSTEM_PROMPT, CONVERSATION_SUMMARY_USER_TEMPLATE

if TYPE_CHECKING:
    from .llm_client import LLMClient

CHARS_PER_TOKEN = 4
SAFETY_MARGIN = 0.9
SUMMARY_FRACTION = 0.25
MIN_SUMMARY_CHARS = 400
MAX_SUMMARY_CHARS = 2000
DEFAULT_CONTEXT_WINDOW = 32000
DEFAULT_MAX_OUTPUT_TOKENS = 4096
SUMMARY_PROMPT_OVERHEAD = 400


# MARK: - Pure Functions (Functional Core)


def _format_messages(messages: Iterable[dict[str, Any]]) -> list[str]:
    """Format messages as labeled strings.

    This is a pure function - no I/O, no side effects.
    """
    formatted: list[str] = []
    for message in messages:
        role = str(message.get("role", "")).lower()
        text = str(message.get("text", "")).strip()
        if not text:
            continue
        label = "User" if role == "user" else "Assistant" if role == "assistant" else "System"
        formatted.append(f"{label}: {text}")
    return formatted


def _context_char_budget(model: str, llm_client: "LLMClient | None" = None) -> int:
    """Calculate the character budget for context.

    Args:
        model: Model identifier
        llm_client: Optional LLM client with credentials for model metadata lookups.
                    Falls back to module-level functions if not provided.
    """
    if llm_client is not None:
        context_window = llm_client.get_model_context_window(model)
        max_output_tokens = llm_client.get_model_max_output_tokens(model)
    else:
        context_window = get_model_context_window(model)
        max_output_tokens = get_model_max_output_tokens(model)

    context_window = context_window or DEFAULT_CONTEXT_WINDOW
    max_output_tokens = max_output_tokens or DEFAULT_MAX_OUTPUT_TOKENS
    available_tokens = max(1, context_window - max_output_tokens)
    return int(available_tokens * CHARS_PER_TOKEN * SAFETY_MARGIN)


def _summary_char_budget(max_context_chars: int) -> int:
    """Calculate the character budget for summary.

    This is a pure function - no I/O, no side effects.
    """
    summary_budget = int(max_context_chars * SUMMARY_FRACTION)
    summary_budget = max(MIN_SUMMARY_CHARS, min(summary_budget, MAX_SUMMARY_CHARS))
    return min(summary_budget, max_context_chars // 2)


def _assemble_context(summary: str | None, recent: list[str]) -> str:
    """Assemble context from summary and recent messages.

    This is a pure function - no I/O, no side effects.
    """
    segments: list[str] = []
    if summary:
        segments.append("Summary so far:")
        segments.append(summary.strip())
    if recent:
        segments.append("Recent conversation:")
        segments.extend(recent)
    return "\n".join(segments).strip()


def _truncate_summary(summary: str | None, max_chars: int) -> str | None:
    """Truncate summary to fit within character limit.

    This is a pure function - no I/O, no side effects.
    """
    if summary is None:
        return None
    trimmed = summary.strip()
    if max_chars <= 0:
        return ""
    if len(trimmed) <= max_chars:
        return trimmed
    trimmed = trimmed[:max_chars].rstrip()
    if trimmed.endswith("."):
        return trimmed
    return trimmed + "..."


def _split_messages_to_fit(
    messages: list[str],
    existing_summary: str,
    max_context_chars: int,
) -> tuple[list[str], list[str]]:
    """Split messages into a chunk that fits and remaining messages.

    This is a pure function - no I/O, no side effects.
    """
    available = max_context_chars - SUMMARY_PROMPT_OVERHEAD - len(existing_summary)
    if available <= 0:
        return messages[:1], messages[1:]

    chunk: list[str] = []
    running = 0
    for entry in messages:
        entry_len = len(entry) + 1
        if running + entry_len > available and chunk:
            break
        running += entry_len
        chunk.append(entry)

    remaining = messages[len(chunk):]
    return chunk, remaining


@dataclass
class SummaryRequest:
    """Request data for a summary LLM call.

    This is a pure data structure representing the request,
    separating request construction from I/O.
    """
    messages: list[dict[str, str]]
    max_tokens: int


def build_summary_request(
    existing_summary: str,
    chunk_messages: list[str],
    max_summary_chars: int,
) -> SummaryRequest:
    """Build a summary request without making any I/O calls.

    This is a pure function - it constructs the request data
    but does not make the API call.

    Args:
        existing_summary: The current summary to extend
        chunk_messages: Messages to summarize
        max_summary_chars: Maximum characters for the summary

    Returns:
        SummaryRequest containing messages and max_tokens
    """
    formatted_messages = "\n".join(chunk_messages)
    user_prompt = CONVERSATION_SUMMARY_USER_TEMPLATE.format(
        existing_summary=existing_summary or "None",
        messages=formatted_messages,
        max_chars=max_summary_chars,
    )
    messages = [
        {"role": "system", "content": CONVERSATION_SUMMARY_SYSTEM_PROMPT},
        {"role": "user", "content": user_prompt},
    ]
    max_tokens = max(128, min(1024, max_summary_chars // CHARS_PER_TOKEN))
    return SummaryRequest(messages=messages, max_tokens=max_tokens)


# MARK: - Imperative Shell (I/O Operations)


def _summarize_chunk(
    existing_summary: str,
    messages: list[str],
    model: str,
    max_summary_chars: int,
    llm_client: "LLMClient | None" = None,
) -> str:
    """Summarize a chunk of messages using an LLM call.

    This is the imperative shell - it orchestrates the pure request
    building with the I/O operation.

    Args:
        existing_summary: Current summary to extend
        messages: Messages to summarize
        model: Model identifier
        max_summary_chars: Maximum characters for summary
        llm_client: Optional LLM client with explicit credentials.
                    Falls back to module-level responses_create if not provided.
    """
    # CORE: Build the request (pure)
    request = build_summary_request(existing_summary, messages, max_summary_chars)

    # SHELL: Make the I/O call using injected client or module-level fallback
    if llm_client is not None:
        summary_text = llm_client.responses_create(
            request.messages,
            model=model,
            temperature=0.0,
            max_tokens=request.max_tokens,
        )
    else:
        summary_text = responses_create(
            request.messages,
            model=model,
            temperature=0.0,
            max_tokens=request.max_tokens,
        )
    return summary_text.strip()


def _summarize_messages(
    summary: str | None,
    messages: list[str],
    model: str,
    max_summary_chars: int,
    max_context_chars: int,
    llm_client: "LLMClient | None" = None,
) -> str:
    """Summarize messages iteratively, handling large message lists.

    This orchestrates multiple summarization rounds if needed.

    Args:
        summary: Existing summary to extend
        messages: Messages to summarize
        model: Model identifier
        max_summary_chars: Maximum characters for summary
        max_context_chars: Maximum characters for context
        llm_client: Optional LLM client with explicit credentials
    """
    existing_summary = summary.strip() if summary else ""
    remaining = list(messages)
    current_summary = existing_summary

    while remaining:
        chunk, remaining = _split_messages_to_fit(
            remaining,
            current_summary,
            max_context_chars,
        )
        current_summary = _summarize_chunk(
            current_summary,
            chunk,
            model,
            max_summary_chars,
            llm_client=llm_client,
        )

    return _truncate_summary(current_summary, max_summary_chars) or ""


def build_conversation_context(
    messages: list[dict[str, Any]],
    summary: str | None,
    model: str | None,
    llm_client: "LLMClient | None" = None,
) -> tuple[str | None, str | None]:
    """Build conversation context, summarizing if needed.

    This is the main entry point that combines pure context building
    with I/O operations for summarization when necessary.

    Args:
        messages: Conversation messages to process
        summary: Existing summary to extend
        model: Model identifier for summarization
        llm_client: Optional LLM client with explicit credentials.
                    Required for PAYGO users where credentials are not in environment.
    """
    resolved_model = resolve_root_model(model)
    formatted = _format_messages(messages)
    if not formatted and not summary:
        return None, summary
    if not formatted and summary:
        return _assemble_context(summary, []), summary

    max_context_chars = _context_char_budget(resolved_model, llm_client=llm_client)
    context_without_summary = "\n".join(formatted)
    if len(context_without_summary) <= max_context_chars:
        return context_without_summary, summary

    summary_budget = _summary_char_budget(max_context_chars)
    available_for_recent = max_context_chars - summary_budget

    recent: list[str] = []
    running = 0
    for entry in reversed(formatted):
        entry_len = len(entry) + 1
        if running + entry_len > available_for_recent and recent:
            break
        running += entry_len
        recent.append(entry)
    recent.reverse()

    split_index = len(formatted) - len(recent)
    older = formatted[:split_index]

    updated_summary = summary
    if older:
        updated_summary = _summarize_messages(
            summary=summary,
            messages=older,
            model=resolved_model,
            max_summary_chars=summary_budget,
            max_context_chars=max_context_chars,
            llm_client=llm_client,
        )

    context = _assemble_context(updated_summary, recent)
    if len(context) > max_context_chars:
        updated_summary = _truncate_summary(updated_summary, max_context_chars - len("\n".join(recent)))
        context = _assemble_context(updated_summary, recent)
        while len(context) > max_context_chars and len(recent) > 1:
            recent.pop(0)
            context = _assemble_context(updated_summary, recent)

    return context, updated_summary
