"""Read-only prompt index and bounded transcript jumps; no transcript-wide payload hydration."""

from __future__ import annotations

import re
from contextlib import contextmanager

from agent.compaction_display import project_compaction_message_for_display
from agent.context_compressor import user_originated_turn_view
from hermes_state_messages import DISPLAY_VISIBLE_SQL


_SYNTHETIC_PROMPT = re.compile(
    r"^\s*(?:\[IMPORTANT: Background process |\[ASYNC (?:DELEGATION )?(?:BATCH )?COMPLETE\b|"
    r"A background fan-out of \d+ subagent\(s\) you dispatched earlier has finished\.|"
    r"A background subagent you dispatched earlier has finished\.)",
    re.IGNORECASE,
)


def _prompt_preview(db, content, display_kind, summary):
    message = project_compaction_message_for_display({
        "role": "user", "content": db._decode_content(content),
        "display_kind": display_kind, "_compressed_summary": bool(summary),
    })
    if message is None or user_originated_turn_view(message) is None:
        return ""
    content = message.get("content")
    if isinstance(content, list):
        content = " ".join(
            part if isinstance(part, str) else part.get("text", "")
            for part in content if isinstance(part, (str, dict)))
    if not isinstance(content, str):
        return ""
    text = " ".join(content.split())
    if not text or _SYNTHETIC_PROMPT.match(text):
        return ""
    return text if len(text) <= 120 else text[:119].rstrip() + "…"


@contextmanager
def _snapshot(db):
    # The count and page must see the same compaction/rewind generation.
    with db._read_ctx() as conn:
        conn.execute("BEGIN")
        try:
            yield conn
        finally:
            if conn.in_transaction:
                conn.execute("ROLLBACK")


def _display_rows_sql(conn, session_id, *, users_only=False):
    """Return only representative ids and their stable first-row order, never bodies.

    Legacy stores cannot backfill on a GET. SQL groups their payload identities in
    SQLite; only user content crosses the Python boundary for carrier normalization.
    Current stores use the durable display index, including protected-tail copies.
    """
    filters = (" AND role = 'user'" if users_only else "") + DISPLAY_VISIBLE_SQL
    indexed = conn.execute(
        "SELECT 1 FROM messages WHERE session_id = ? AND (active = 1 OR compacted = 1) "
        f"{filters} AND (display_order IS NULL OR display_identity IS NULL) LIMIT 1",
        (session_id,),
    ).fetchone() is None
    if indexed:
        return f"""WITH display_rows AS (
            SELECT (SELECT candidate.id FROM messages candidate
                    WHERE candidate.session_id = :sid
                      AND candidate.display_order = m.display_order
                      AND (candidate.active = 1 OR candidate.compacted = 1){DISPLAY_VISIBLE_SQL}
                    ORDER BY candidate.active DESC, candidate.id DESC LIMIT 1) AS row_id,
                   m.display_order AS sort_id
            FROM messages m WHERE session_id = :sid AND (active = 1 OR compacted = 1){filters}
            GROUP BY m.display_order
        )"""
    return f"""WITH ranked AS (
        SELECT id, MIN(id) OVER identity AS sort_id,
               ROW_NUMBER() OVER (identity ORDER BY active DESC, id DESC) AS preference
        FROM messages WHERE session_id = :sid AND (active = 1 OR compacted = 1){filters}
        WINDOW identity AS (PARTITION BY role,
            CASE WHEN role = 'user' THEN timeline_identity_content(content, display_kind) ELSE content END,
            timestamp, tool_call_id, tool_calls, tool_name)
    ), display_rows AS (SELECT id AS row_id, sort_id FROM ranked WHERE preference = 1)"""


def _register_functions(db, conn):
    from agent.context_compressor import split_user_originated_turn

    def identity_content(content, display_kind):
        handoff, live = split_user_originated_turn({
            "role": "user", "content": db._decode_content(content), "display_kind": display_kind})
        return db._encode_content(live.get("content")) if handoff is not None and live is not None else content

    conn.create_function("timeline_identity_content", 2, identity_content, deterministic=True)
    conn.create_function("timeline_preview", 3,
                         lambda content, kind, summary: _prompt_preview(db, content, kind, summary),
                         deterministic=True)


def get_session_messages_around(db, session_id, row_id, *, limit=120):
    """Read at most *limit* display rows starting at an exact human prompt.

    Existence probes/counts contain ids only. Full payloads are fetched only for
    the selected bounded page, even when the anchor is deep in a transcript.
    """
    with _snapshot(db) as conn:
        _register_functions(db, conn)
        anchor = conn.execute(
            "SELECT content, display_kind, _compressed_summary FROM messages "
            "WHERE session_id = ? AND id = ? AND role = 'user' AND (active = 1 OR compacted = 1)",
            (session_id, row_id),
        ).fetchone()
        if anchor is None or not _prompt_preview(db, *anchor):
            return None
        sql = _display_rows_sql(conn, session_id)
        params = {"sid": session_id, "row_id": row_id, "limit": limit}
        selected = conn.execute(sql + """
            SELECT sort_id FROM display_rows WHERE row_id = :row_id
        """, params).fetchone()
        if selected is None:
            return None
        params["start"] = selected["sort_id"]
        counts = conn.execute(sql + """
            SELECT COUNT(*) AS total, COALESCE(SUM(sort_id < :start), 0) AS offset FROM display_rows
        """, params).fetchone()
        rows = conn.execute(sql + """
            SELECT m.* FROM (SELECT row_id, sort_id FROM display_rows
                            WHERE sort_id >= :start ORDER BY sort_id LIMIT :limit) AS page
            JOIN messages m ON m.id = page.row_id ORDER BY page.sort_id
        """, params).fetchall()
    messages = [db._row_to_message_dict(row, warn_context="timeline jump", summary_flag=True) for row in rows]
    return {"messages": messages, "pagination": {
        "row_id": row_id, "limit": limit, "returned": len(messages), "order": "oldest",
        "offset": counts["offset"], "total": counts["total"],
        "has_older": counts["offset"] > 0,
        "has_newer": counts["offset"] + len(messages) < counts["total"],
    }}


def get_session_timeline(db, session_id, *, limit=500, after_row_id=0):
    """Chronological prompts. Cursor is the first physical row id of a logical turn."""
    with _snapshot(db) as conn:
        _register_functions(db, conn)
        sql = _display_rows_sql(conn, session_id, users_only=True) + """,
            prompts AS MATERIALIZED (
                SELECT row_id, sort_id, m.timestamp,
                       timeline_preview(m.content, m.display_kind, m._compressed_summary) AS preview
                FROM display_rows JOIN messages m ON m.id = row_id
            ), eligible AS MATERIALIZED (SELECT * FROM prompts WHERE preview <> '')
        """
        params = {"sid": session_id, "after": after_row_id, "limit": limit + 1}
        rows = conn.execute(sql + """
            SELECT row_id, sort_id, timestamp, preview, (SELECT COUNT(*) FROM eligible) AS total
            FROM eligible WHERE sort_id > :after ORDER BY sort_id LIMIT :limit
        """, params).fetchall()
        total = rows[0]["total"] if rows else conn.execute(
            sql + "SELECT COUNT(*) FROM eligible", params).fetchone()[0]
    has_more = len(rows) > limit
    page = rows[:limit]
    return {
        "entries": [{"row_id": row["row_id"], "preview": row["preview"], "timestamp": row["timestamp"]}
                    for row in page],
        "pagination": {"limit": limit, "after_row_id": after_row_id, "returned": len(page),
                       "total": total, "has_more": has_more,
                       "next_cursor": page[-1]["sort_id"] if has_more else None},
    }
