"""Context builder: builds the message list for an LLM call.

Extracted from Agent so message/context construction has a single responsibility.
Agent passes in memory, context manager, and config; this module returns list[Message].
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

from syrin.context import Context
from syrin.context.config import ContextWindowBudget
from syrin.enums import MessageRole
from syrin.types import Message


def build_messages(
    user_input: str,
    *,
    system_prompt: str,
    tools: list[Any],
    conversation_memory: Any = None,
    memory_backend: Any = None,
    persistent_memory: Any = None,
    context_manager: Any = None,
    get_budget: Callable[[], ContextWindowBudget],
    call_context: Context | None = None,
    tracer: Any = None,
) -> list[Message]:
    """Build the message list for the next LLM call.

    Combines system prompt, persistent memory recall, conversation memory,
    and user input; then runs context management (prepare) and returns
    the final list of Message objects.

    Args:
        user_input: The user's message.
        system_prompt: System prompt text.
        tools: List of ToolSpec or tool-like (with to_tool_spec or dict).
        conversation_memory: Optional conversation memory (get_messages()).
        memory_backend: Optional persistent memory backend (search).
        persistent_memory: Optional Memory (top_k for recall).
        context_manager: Context manager with prepare(messages, system_prompt, tools, memory_context, budget, context).
        get_budget: Callable that returns ContextWindowBudget for this call.
        call_context: Optional per-call Context override.
        tracer: Optional tracer for spans (memory.recall).

    Returns:
        List of Message ready for the LLM.
    """
    messages: list[Message] = []
    system_content = system_prompt or ""
    memory_context = ""

    # Persistent memory recall
    if memory_backend is not None and persistent_memory is not None:
        top_k = getattr(persistent_memory, "top_k", 10) or 10
        memories = _in_span(
            tracer,
            "memory.recall",
            {"memory.kind": "persistent"},
            lambda: memory_backend.search(user_input, None, top_k),
            result_attr=("MEMORY_RESULTS_COUNT", len),
        )
        if memories:
            memory_context = "## Relevant Memories:\n"
            for mem in memories:
                type_val = getattr(mem, "type", None)
                type_str = (
                    type_val.value
                    if type_val is not None and hasattr(type_val, "value")
                    else str(type_val)
                    if type_val is not None
                    else "unknown"
                )
                memory_context += f"- [{type_str}] {mem.content}\n"

    if system_content:
        messages.append(Message(role=MessageRole.SYSTEM, content=system_content))

    # Conversation memory
    if conversation_memory is not None:
        mem_messages = _in_span(
            tracer,
            "memory.recall",
            {"memory.kind": "conversation"},
            conversation_memory.get_messages,
            result_attr=("MEMORY_RESULTS_COUNT", len),
        )
        messages.extend(mem_messages)

    messages.append(Message(role=MessageRole.USER, content=user_input))

    # To dicts for context manager
    msg_dicts = []
    for msg in messages:
        role = msg.role
        role_str = role.value if hasattr(role, "value") else str(role)
        msg_dicts.append({"role": role_str, "content": msg.content})

    tool_dicts = []
    for tool in tools:
        if hasattr(tool, "to_tool_spec"):
            tool_dicts.append(tool.to_tool_spec())
        elif isinstance(tool, dict):
            tool_dicts.append(tool)

    if context_manager is None:
        return messages

    budget = get_budget()
    payload = context_manager.prepare(
        messages=msg_dicts,
        system_prompt=system_content,
        tools=tool_dicts,
        memory_context=memory_context,
        budget=budget,
        context=call_context,
    )

    final_messages = []
    for msg_dict in payload.messages:
        msg_data = msg_dict if isinstance(msg_dict, dict) else {}
        role_str = msg_data.get("role", "user")
        try:
            role_enum = MessageRole(role_str) if isinstance(role_str, str) else role_str
        except (ValueError, TypeError):
            role_enum = MessageRole.USER
        final_messages.append(Message(role=role_enum, content=msg_data.get("content", "")))

    return final_messages


def _in_span(
    tracer: Any,
    name: str,
    extra_attrs: dict[str, Any],
    fn: Callable[[], Any],
    result_attr: tuple[str, Callable[[Any], int]] | None = None,
) -> Any:
    """Run fn inside a span if tracer is available. Return fn()."""
    if tracer is None or not hasattr(tracer, "span"):
        return fn()
    try:
        from syrin.observability import SemanticAttributes, SpanKind

        attrs = {SemanticAttributes.MEMORY_OPERATION: "recall", **extra_attrs}
        with tracer.span(name, kind=SpanKind.MEMORY, attributes=attrs) as span:
            result = fn()
            if hasattr(span, "set_attribute") and result_attr is not None:
                attr_name, size_fn = result_attr
                if attr_name == "MEMORY_RESULTS_COUNT":
                    span.set_attribute(
                        getattr(SemanticAttributes, attr_name, attr_name),
                        size_fn(result),
                    )
            return result
    except Exception:
        return fn()
