"""Email-activity proposal review over Matrix — the CRM→Matrix leg of the email-capture flow.

The CRM (on the box) drafts a proposed grid note per newly-matched email (local model, no Claude)
and queues it for human review. The CRM is stdlib-only and can't post to Matrix itself, so this
bot PULLS the pending proposals (crm_client.list_email_proposals), posts a review card to the
dedicated review room, and relays the human's in-thread reply back to the CRM. Same draft→approve
discipline as the intake bot: nothing is appended to the grid until a human approves — here OR on
the web Email Capture panel, the two surfaces kept in sync via the CRM's email_proposal_matrix row.

This module is the PURE logic (card rendering, reply grammar, note revision) so it's unit-tested
offline; the async poll/post/reply wiring lives in bot.py (network + Matrix, live-smoke only).
"""
import spark

_YES = {"yes", "y", "approve", "approved", "ok", "confirm", "go", "add", "👍", "✅"}
_NO = {"no", "n", "cancel", "discard", "reject", "skip", "stop", "👎", "❌"}

_SNIPPET_MAX = 400  # email snippet shown on the card; the full body is in the web popup
RULE = "-----------------------"  # top/bottom rule so threads don't bleed together on mobile


def frame(text):
    """Wrap a message in dash rules so each card/reply is visually bounded in the room."""
    return f"{RULE}\n{text}\n{RULE}"


def _truncate(s, n):
    s = (s or "").strip()
    return s if len(s) <= n else s[:n].rstrip() + "…"


def render_card(item):
    """The review card posted to the Matrix review room: who/when + a short email snippet + the
    drafted note. Deliberately compact for mobile — the full scrollable body is in the web Email
    Capture popup. Direction isn't a bare label anymore — the note itself names who emailed whom."""
    name = item.get("investor_name") or "Unknown investor"
    frm = item.get("from_name") or item.get("from_email") or "?"
    lines = [f"📧 Proposed **grid note** for **{name}**"]
    if item.get("email_subject"):
        lines.append(f"· Subject: {item['email_subject']}")
    if item.get("email_date"):
        lines.append(f"· Date: {item['email_date']}")
    lines.append(f"· From: {frm}")
    snippet = _truncate(item.get("snippet"), _SNIPPET_MAX)
    if snippet:
        lines.append(f"· Email: {snippet}")
    lines.append("")
    lines.append(f"📝 Draft note: {item.get('proposed_note') or '(empty)'}")
    lines.append("")
    lines.append("Reply **yes** to add it to the grid, **no** to dismiss, or just tell me how to "
                 "change the note (e.g. *say we discussed the Q3 raise*).")
    return frame("\n".join(lines))


def interpret(text):
    """Classify an in-thread reply: 'approve' | 'reject' | 'revise' (anything else → revise the note)."""
    t = (text or "").strip().lower()
    if t in _YES:
        return "approve"
    if t in _NO:
        return "reject"
    return "revise"


REVISE_SYSTEM = (
    "You revise a single CRM note from a short instruction a venture-fund team member typed. "
    "You are given the CURRENT note and an INSTRUCTION. Apply the instruction and reply with "
    "ONLY a JSON object of the form {\"note\": \"<the full revised note>\"}. Keep it to one or two "
    "factual sentences, no preamble. Output JSON only."
)


def revise_note(note, instruction, parse_fn=spark.parse_json):
    """Re-draft the note via local Qwen from a free-form instruction (no Claude, no scrub — same
    local-only basis as the intake parse). Returns the new note text, or None if the model gave
    nothing usable / unchanged, in which case the caller re-prompts. `parse_fn` is injectable for
    tests."""
    prompt = "CURRENT:\n" + (note or "") + "\n\nINSTRUCTION:\n" + (instruction or "").strip()
    raw = parse_fn(prompt, system=REVISE_SYSTEM, max_tokens=400) or {}
    new = raw.get("note") if isinstance(raw, dict) else None
    new = (new or "").strip()
    if not new or new == (note or "").strip():
        return None
    return new
