"""Boundary repair for providers that stream reasoning as discrete summary parts.

Reasoning-summary models (OpenAI gpt-5.x and Responses-API relays onto the chat wire) emit one
``reasoning_content`` delta per *completed* summary part, each opening with a bold heading.
The chat wire lacks the Responses API's ``summary_index`` delimiter (verified live on Nous
Portal ``openai/gpt-5.6-sol``), so plain concatenation glues ``**One****Two**`` into one
half-bold paragraph. The boundary is re-derived from a delta opening a bold heading, matching
the blank-line join Hermes' own Responses adapter does.
"""

from __future__ import annotations

from typing import Any

from agent.message_content import flatten_message_text

__all__ = ["append_streamed_reasoning_detail", "separate_glued_reasoning_blocks"]


def separate_glued_reasoning_blocks(previous: str, delta: Any) -> str:
    """Return *delta*, prefixed with a paragraph break when it glues onto *previous*.

    A break is inserted when *delta* opens a *closed* bold heading and *previous* is mid-line
    (heading butting heading, or prose butting heading). Token-streamed reasoning is left
    alone: its deltas carry their own whitespace, and a fragment that merely opens emphasis
    (``**`` alone) is not a part boundary — summary parts carry the whole heading in one delta.
    """
    # Relays also emit content-part lists/dicts; fragments carry their own whitespace.
    delta = flatten_message_text(delta, sep="")
    glued = previous and delta and not previous[-1].isspace() and delta.startswith("**") and "**" in delta[2:]
    return f"\n\n{delta}" if glued else delta


# reasoning_details entry types whose consecutive fragments are ONE logical block.
_MERGEABLE_DETAIL_TEXT_KEYS = {"reasoning.text": "text", "reasoning.summary": "summary"}
_BACKFILL_DETAIL_KEYS = ("signature", "id", "format", "index")


def streamed_reasoning_detail_text(detail: Any) -> str:
    """Readable text from a detail delta; never expose opaque replay material."""
    dtype = detail.get("type") if isinstance(detail, dict) else getattr(detail, "type", None)
    key = _MERGEABLE_DETAIL_TEXT_KEYS.get(dtype) if isinstance(dtype, str) else None
    if key is None:
        return ""
    text = detail.get(key) if isinstance(detail, dict) else getattr(detail, key, None)
    return text if isinstance(text, str) else ""


def append_streamed_reasoning_detail(details_acc: list, detail: Any) -> None:
    """Accumulate one streamed ``reasoning_details`` delta entry into *details_acc*.

    OpenRouter streams ``reasoning_details`` as word-level deltas: consecutive
    ``reasoning.text`` / ``reasoning.summary`` entries are fragments of one logical
    block and are merged (later fragments backfill ``signature``/``id`` the first
    omitted); encrypted/opaque entries stay discrete. Unmerged, a long thought
    replays as hundreds of one-word entries and providers that validate the
    sequence shape on the next turn reject it. SDK objects are normalized to dicts.
    """
    if not isinstance(detail, dict):
        if hasattr(detail, "model_dump"):
            detail = detail.model_dump(warnings=False)
        elif hasattr(detail, "__dict__"):
            detail = dict(detail.__dict__)
        else:
            return
    dtype = detail.get("type")
    merge_key = _MERGEABLE_DETAIL_TEXT_KEYS.get(dtype)
    last = details_acc[-1] if details_acc else None
    if last is not None and merge_key and last.get("type") == dtype and isinstance(detail.get(merge_key), str):
        last[merge_key] = (last.get(merge_key) or "") + detail[merge_key]
        for k in _BACKFILL_DETAIL_KEYS:
            if last.get(k) in (None, "") and detail.get(k) not in (None, ""):
                last[k] = detail[k]
        return
    details_acc.append(dict(detail))
