"""Pure logic for talk history replay.

Spec source: vectl step ``talk-history-replay``
Filters response and user_message events from log and formats turn history.
User messages are interleaved with agent responses for full conversation replay.
"""

from __future__ import annotations

from warnings import warn
from typing import Literal

from deal import post, pre


@pre(
    lambda entry, details: (
        entry is not None and (details is None or isinstance(details, dict))
    )
)
@post(lambda result: result is None or isinstance(result, int))
def _coerce_turn_number(entry: dict, details: dict | None) -> int | None:
    """Get turn number from current or legacy log shapes.

    Legacy compatibility source: user request
    ``talk-ux-bugs-v2.talk-ux-bugs-v2-gate-fix-legacy-history``
    requires replay to include legacy events even when fields moved.
    """
    if isinstance(details, dict):
        details_turn_number = details.get("turn_number")
        if isinstance(details_turn_number, int):
            return details_turn_number

    entry_turn_number = entry.get("turn_number")
    if isinstance(entry_turn_number, int):
        return entry_turn_number

    # Legacy compatibility source: user request
    # ``talk-ux-bugs-v2.talk-ux-bugs-v2-gate-fix-legacy-history``
    # requires replay to gracefully handle older ``turn`` field name.
    if isinstance(details, dict):
        details_turn = details.get("turn")
        if isinstance(details_turn, int):
            return details_turn

    entry_turn = entry.get("turn")
    if isinstance(entry_turn, int):
        return entry_turn
    return None


@pre(lambda key: key in {"message_text", "response_text"})
@post(lambda result: isinstance(result, tuple) and len(result) > 0)
def _text_field_candidates(
    key: Literal["message_text", "response_text"],
) -> tuple[str, ...]:
    """Return canonical + legacy text keys for replay compatibility.

    Spec source: user acceptance criteria for
    ``talk-ux-bugs-v2-gate.fix-legacy-history``
    requires fallback mapping for legacy ``content``/``text`` fields.
    """
    if key == "message_text":
        return ("message_text", "content", "text", "message")
    return ("response_text", "content", "text", "response")


@pre(
    lambda entry, details, key="message_text", placeholder="": (
        entry is not None
        and (details is None or isinstance(details, dict))
        and key in {"message_text", "response_text"}
        and isinstance(placeholder, str)
    )
)
@post(lambda result: isinstance(result, str))
def _coerce_text(
    entry: dict,
    details: dict | None,
    key: Literal["message_text", "response_text"],
    placeholder: str,
) -> str:
    """Get text from current or legacy log shapes with placeholder fallback."""
    candidates = _text_field_candidates(key)

    if isinstance(details, dict):
        for candidate_key in candidates:
            details_text = details.get(candidate_key)
            if isinstance(details_text, str):
                if candidate_key != key:
                    warn(
                        (
                            f"Legacy talk history field '{candidate_key}' is deprecated; "
                            f"use '{key}'"
                        ),
                        DeprecationWarning,
                        stacklevel=2,
                    )
                return details_text

    for candidate_key in candidates:
        entry_text = entry.get(candidate_key)
        if isinstance(entry_text, str):
            if candidate_key != key:
                warn(
                    (
                        f"Legacy talk history field '{candidate_key}' is deprecated; "
                        f"use '{key}'"
                    ),
                    DeprecationWarning,
                    stacklevel=2,
                )
            return entry_text
    return placeholder


@pre(lambda entries: entries is not None)
@post(lambda result: isinstance(result, list))
def filter_conversation_events(entries: list[dict]) -> list[dict]:
    """Filter log entries to conversation events (user_message and response).

    Args:
        entries: List of log entry dicts with 'event' field.

    Returns:
        List of conversation event dicts, preserving order.

    >>> filter_conversation_events([])
    []
    >>> filter_conversation_events([{"event": "created"}])
    []
    >>> filter_conversation_events([{"event": "user_message", "details": {"message_text": "hi", "turn_number": 1}}])
    [{'event': 'user_message', 'details': {'message_text': 'hi', 'turn_number': 1}}]
    >>> filter_conversation_events([
    ...     {"event": "created"},
    ...     {"event": "user_message", "details": {"message_text": "hello", "turn_number": 1}},
    ...     {"event": "response", "details": {"response_text": "world", "turn_number": 1}},
    ... ])
    [{'event': 'user_message', 'details': {'message_text': 'hello', 'turn_number': 1}}, {'event': 'response', 'details': {'response_text': 'world', 'turn_number': 1}}]
    """
    return [
        entry for entry in entries if entry.get("event") in {"user_message", "response"}
    ]


@pre(lambda entries: entries is not None)
@post(lambda result: isinstance(result, list))
def filter_response_events(entries: list[dict]) -> list[dict]:
    """Filter log entries to only response events.

    Args:
        entries: List of log entry dicts with 'event' field.

    Returns:
        List of response event dicts, preserving order.

    >>> filter_response_events([])
    []
    >>> filter_response_events([{"event": "created"}])
    []
    >>> filter_response_events([{"event": "response", "details": {"response_text": "hi", "turn_number": 1}}])
    [{'event': 'response', 'details': {'response_text': 'hi', 'turn_number': 1}}]
    >>> filter_response_events([
    ...     {"event": "created"},
    ...     {"event": "response", "details": {"response_text": "hello", "turn_number": 1}},
    ...     {"event": "state_transition"},
    ...     {"event": "response", "details": {"response_text": "world", "turn_number": 2}},
    ... ])
    [{'event': 'response', 'details': {'response_text': 'hello', 'turn_number': 1}}, {'event': 'response', 'details': {'response_text': 'world', 'turn_number': 2}}]
    """
    return [entry for entry in entries if entry.get("event") == "response"]


@pre(lambda conversation_entries: conversation_entries is not None)
@post(lambda result: isinstance(result, list))
def format_conversation_turns(
    conversation_entries: list[dict],
) -> list[tuple[str, int, str]]:
    """Format conversation events as (speaker, turn_number, text) tuples.

    Args:
        conversation_entries: List of conversation event dicts (user_message or response).

    Returns:
        List of (speaker, turn_number, text) tuples where speaker is "user" or "agent".
        User messages return message_text (or placeholder if missing).
        Agent responses return response_text (or placeholder if missing).

    >>> format_conversation_turns([])
    []
    >>> format_conversation_turns([
    ...     {"event": "user_message", "details": {"message_text": "hi", "turn_number": 1}},
    ...     {"event": "response", "details": {"response_text": "hello", "turn_number": 1}},
    ... ])
    [('user', 1, 'hi'), ('agent', 1, 'hello')]
    >>> format_conversation_turns([{"event": "user_message"}])
    []
    >>> format_conversation_turns([{"event": "user_message", "details": {"turn_number": 1}}])
    [('user', 1, '[no message text]')]
    >>> format_conversation_turns([{"event": "response", "details": {}}])
    []
    >>> format_conversation_turns([{"event": "response", "details": {"turn_number": 2}}])
    [('agent', 2, '[no response text]')]
    >>> format_conversation_turns([{"event": "user_message", "turn_number": 1}])
    [('user', 1, '[no message text]')]
    >>> format_conversation_turns([{"event": "response", "turn_number": 1, "response_text": "ok"}])
    [('agent', 1, 'ok')]
    >>> format_conversation_turns([{"event": "user_message", "details": {"content": "legacy", "turn": 3}}])
    [('user', 3, 'legacy')]
    """
    turns = []
    for entry in conversation_entries:
        details = entry.get("details")
        details_dict: dict | None = details if isinstance(details, dict) else None

        turn_number = _coerce_turn_number(entry, details_dict)
        if turn_number is None:
            continue

        event = entry.get("event")
        if event == "user_message":
            message_text = _coerce_text(
                entry,
                details_dict,
                key="message_text",
                placeholder="[no message text]",
            )
            turns.append(("user", turn_number, message_text))
        elif event == "response":
            response_text = _coerce_text(
                entry,
                details_dict,
                key="response_text",
                placeholder="[no response text]",
            )
            turns.append(("agent", turn_number, response_text))
    return turns


@pre(lambda response_entries: response_entries is not None)
@post(lambda result: isinstance(result, list))
def format_history_turns(response_entries: list[dict]) -> list[tuple[int, str]]:
    """Format response events as (turn_number, text) tuples.

    Args:
        response_entries: List of response event dicts.

    Returns:
        List of (turn_number, response_text) tuples.

    >>> format_history_turns([])
    []
    >>> format_history_turns([{"event": "response", "details": {"response_text": "hi", "turn_number": 1}}])
    [(1, 'hi')]
    >>> format_history_turns([
    ...     {"event": "response", "details": {"response_text": "hello", "turn_number": 1}},
    ...     {"event": "response", "details": {"response_text": "world", "turn_number": 2}},
    ... ])
    [(1, 'hello'), (2, 'world')]
    >>> format_history_turns([{"event": "response"}])
    []
    >>> format_history_turns([{"event": "response", "details": {}}])
    []
    """
    turns = []
    for entry in response_entries:
        details = entry.get("details")
        if not isinstance(details, dict):
            continue
        turn_number = details.get("turn_number")
        response_text = details.get("response_text")
        if isinstance(turn_number, int) and isinstance(response_text, str):
            turns.append((turn_number, response_text))
    return turns
