import hashlib
import html
import re
from datetime import datetime, timezone


def _participant_color(nick):
    """A bright color pinned to a participant — deterministic from the nick, stable across renders.
    Derived from a hash (nick is never interpolated raw) so it's always a safe hsl(...) literal."""
    h = int(hashlib.md5((nick or "").encode("utf-8")).hexdigest(), 16) % 360
    return f"hsl({h}, 80%, 62%)"

def md_to_html(text):
    """Convert a small deterministic markdown subset to HTML.
    
    HTML-escapes the input first, then transforms:
    - inline backtick code -> <code>
    - **bold** -> <strong>
    - *italic* -> <em>
    - fenced triple-backtick blocks -> <pre><code>
    - [text](url) -> <a href> ONLY for http/https URLs
    """
    # Strip NUL first: the FENCED/INLINE placeholder sentinels below use NUL as a delimiter, so a
    # hostile body carrying a forged placeholder could otherwise reach an out-of-range restore
    # (IndexError -> whole-page DoS via build_page's bare except).
    escaped = html.escape(text.replace(chr(0), ""))
    
    # Process fenced code blocks first (they contain raw text that should not be processed)
    # We'll use a placeholder approach
    fenced_parts = []
    def replace_fenced(match):
        content = match.group(1)
        # The content is already HTML-escaped, wrap in pre/code
        fenced_parts.append(f"<pre><code>{content}</code></pre>")
        return f"\x00FENCED{len(fenced_parts)-1}\x00"
    
    # Process fenced blocks (```...```)
    escaped = re.sub(r'```\n?(.*?)```', replace_fenced, escaped, flags=re.DOTALL)
    
    # Process inline code (`...`) - must be done before bold/italic to avoid conflicts
    inline_parts = []
    def replace_inline_code(match):
        content = match.group(1)
        inline_parts.append(f"<code>{content}</code>")
        return f"\x00INLINE{len(inline_parts)-1}\x00"
    escaped = re.sub(r'`([^`]+)`', replace_inline_code, escaped)
    
    # Process links [text](url) - only http/https
    def replace_link(match):
        text = match.group(1)
        url = match.group(2)
        # Only allow http and https URLs
        if url.startswith('http://') or url.startswith('https://'):
            return f'<a href="{url}">{text}</a>'
        else:
            # Return the original markdown text as-is (already escaped)
            return f'[{text}]({url})'
    escaped = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', replace_link, escaped)
    
    # Process bold (**text**)
    escaped = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', escaped)
    
    # Process italic (*text*)
    escaped = re.sub(r'\*([^*]+)\*', r'<em>\1</em>', escaped)
    
    # Restore inline code
    def restore_inline(match):
        idx = int(match.group(1))
        return inline_parts[idx] if idx < len(inline_parts) else match.group(0)
    escaped = re.sub(r'\x00INLINE(\d+)\x00', restore_inline, escaped)
    
    # Restore fenced blocks
    def restore_fenced(match):
        idx = int(match.group(1))
        return fenced_parts[idx] if idx < len(fenced_parts) else match.group(0)
    escaped = re.sub(r'\x00FENCED(\d+)\x00', restore_fenced, escaped)
    
    # THINKING REVEAL: dim the grinder's live progress/reasoning trace lines
    escaped = re.sub(r'(?m)^(\[(?:iter |EMPTY REPLY|reasoning |regression |stall |no-content ).*)$',
                     r'<span class="grind-trace">\1</span>', escaped)
    # THINKING REVEAL (post-hoc): a model's reasoning block, sentinel-wrapped by the agent
    # (⟦think⟧…⟦/think⟧). Dim the WHOLE block -- drift-proof, no per-line allowlist to
    # fall out of sync with what agents emit (unlike the grinder-trace matcher above).
    escaped = re.sub(r'⟦think⟧\n?(.*?)\n?⟦/think⟧',
                     r'<span class="think">💭 \1</span>', escaped, flags=re.S)
    return escaped


def render_transcript(messages):
    # Sort messages by id ascending
    sorted_msgs = sorted(messages, key=lambda m: m["id"])
    
    # Per-type translucent dark tint (~30% translucent on black) — keeps the 5 types visually
    # distinct while letting the Matrix rain behind show through the message boxes.
    type_tint = {
        "chat":   "rgba(0, 30, 12, var(--box-alpha, 0.70))",
        "ask":    "rgba(34, 28, 0, var(--box-alpha, 0.70))",
        "ack":    "rgba(0, 36, 26, var(--box-alpha, 0.70))",
        "reply":  "rgba(22, 0, 38, var(--box-alpha, 0.70))",
        "system": "rgba(26, 26, 26, var(--box-alpha, 0.70))",
    }

    parts = []
    for msg in sorted_msgs:
        mid = msg["id"]
        ts = msg["ts"]
        raw_from = msg["from"]
        frm = html.escape(raw_from)
        to = html.escape(msg["to"])
        body = md_to_html(msg["body"])  # Use md_to_html instead of plain html.escape
        mtype = msg["type"]
        in_reply_to = msg.get("in_reply_to")
        pcolor = _participant_color(raw_from)  # color pinned to the speaker

        # Human-readable timestamp — a forged/absurd ts raises OverflowError/OSError; fall back
        # rather than let it bubble to build_page's bare except and blank the whole page.
        try:
            time_str = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        except (OverflowError, OSError, ValueError, TypeError):
            time_str = html.escape(str(ts))

        tint = type_tint.get(mtype, "rgba(0, 20, 0, var(--box-alpha, 0.70))")
        style = (f"background: {tint}; border-left: 4px solid {pcolor}; padding: 8px; "
                 f"margin: 4px 0; border-radius: 3px; color: #c8ffc8;")

        # id and in_reply_to come straight from json.loads of room lines and must be escaped
        # (a hostile in_reply_to like <img onerror=...> is stored XSS on the page AND every AJAX
        # append, injected via insertAdjacentHTML/innerHTML). pcolor is hash-derived, not raw.
        # data-from (HTML-escaped nick) is what the client-side FROM-filter matches rows against —
        # on the initial render AND on every /api/since AJAX append.
        chunk = f'<div data-id="{html.escape(str(mid))}" data-from="{frm}" style="{style}">'
        chunk += (f'<span class="meta"><b style="color: {pcolor}">{frm}</b> -&gt; {to} '
                  f'<span style="opacity: 0.6">[{html.escape(mtype)}] [{time_str}]</span></span>')
        if in_reply_to is not None:
            chunk += (f' <span class="reply-to" style="opacity: 0.6">'
                      f'(in reply to {html.escape(str(in_reply_to))})</span>')
        chunk += f'<br><span class="body">{body}</span>'
        chunk += '</div>'
        parts.append(chunk)
    
    return "\n".join(parts)
