"""The Architect agent — generates and revises thesis options using Claude.

This is the one step that runs on the frontier model (Claude) rather than the
local Qwen, because sharpening narrative is exactly Claude's strength. Thesis
content is non-sensitive house messaging, so it goes to Claude directly with no
redaction. The agent reads the current thesis and writes new VARIANT drafts back
through architect_tools; nothing it produces is canonical until a human approves.

Flexibility by design (per Grant): a node can hold any number of competing
options at once. generate_options adds N drafts to a node's variant group;
partners keep, refine, branch, or retire them freely.

Requires ANTHROPIC_API_KEY in the environment (wired as a StartOS config secret
on the box). Fails gracefully with a clear message if the key or SDK is absent.
"""
import json
import os
import re
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import architect_tools as at  # noqa: E402

MODEL = os.environ.get("CRM_ARCHITECT_MODEL", "claude-opus-4-8")

VOICE = ("Direct, concrete, conviction-driven. Never use 'bet'/'betting'/'gamble' "
         "language (we invest with conviction). No em dashes. No 'X, not Y' "
         "constructions. No kitchen-sink lists. Plain sentences a serious LP can "
         "verify in their head, with real specifics where possible.")


def _client():
    try:
        from anthropic import Anthropic
    except Exception:
        raise RuntimeError("anthropic SDK not installed in this environment")
    key = os.environ.get("ANTHROPIC_API_KEY")
    if not key:
        raise RuntimeError("ANTHROPIC_API_KEY not configured")
    return Anthropic(api_key=key)


def _render_thesis(thesis):
    """Flatten the current thesis tree into text for the (cacheable) system prompt."""
    line = thesis.get("line", {})
    lines = [f"Thesis line: {line.get('name')} ({line.get('line_key')})"]
    def walk(nodes, depth=0):
        for n in nodes:
            body = (n.get("body") or "").strip()
            tag = n.get("node_type")
            var = f" [variant_group={n['variant_group']}]" if n.get("variant_group") else ""
            lines.append(f"{'  ' * depth}- {tag}{var}: {n.get('title') or ''} {body}".rstrip())
            walk(n.get("children", []), depth + 1)
    walk(thesis.get("tree", []))
    return "\n".join(lines)


def _system(thesis):
    text = ("You are the Architect, the in-house copilot that sharpens Ten31's investment "
            "thesis with the partners. Ten31 invests in critical infrastructure across bitcoin, "
            "AI, energy, and freedom technologies. The spine of the thesis: fiat is being debased "
            "while AI drives the marginal cost of anything reproducible toward zero, so durable "
            "value migrates to what stays provably scarce and verifiable. Bitcoin is the apex form "
            "of that, a fixed-supply, non-debasable, verifiable reserve asset. AI is the abundance "
            "engine and bitcoin is the scarcity anchor, two faces of one megatrend. The throughline "
            "is an asset-value and capital-flow claim: as money debases and AI commoditizes the "
            "reproducible, value accrues to the scarce side of one supply chain and the monetary "
            "premium accrues to bitcoin as the non-debasable reserve asset. This is not a claim "
            "that the world transacts, settles, or clears in bitcoin. The structure runs on three "
            "seams: Energy and Compute (the same scarce firm power feeds both AI and bitcoin), "
            "Debasement and Bitcoin (bitcoin as reserve and as pristine collateral for credit, "
            "never payments), and AI and Data-Ownership (sovereign data and confidential inference, "
            "the own-your-stack and own-your-inference layer). Strike is a financial-services and "
            "reserve re-rate, never a payments story. Proof standard: every proof point must be "
            "falsifiable as scaled substance with a number, never a first-instance milestone. Do "
            "not invent proof and do not over-expose sensitive deal or return specifics. "
            f"VOICE RULES (follow exactly): {VOICE}\n\n"
            "Here is the current working thesis:\n" + _render_thesis(thesis))
    # Cache the thesis context so iterating across requests is cheap.
    return [{"type": "text", "text": text, "cache_control": {"type": "ephemeral"}}]


def _parse_options(text):
    text = re.sub(r"^```(json)?|```$", "", text.strip(), flags=re.MULTILINE).strip()
    m = re.search(r"\[.*\]", text, re.DOTALL)
    if not m:
        return []
    try:
        arr = json.loads(m.group(0))
        return [o for o in arr if isinstance(o, dict) and o.get("text")]
    except json.JSONDecodeError:
        return []


def _call(thesis, user_text, max_tokens=1800):
    resp = _client().messages.create(model=MODEL, max_tokens=max_tokens,
                                     system=_system(thesis),
                                     messages=[{"role": "user", "content": user_text}])
    return "".join(b.text for b in resp.content if getattr(b, "type", None) == "text")


def generate_options(line_key, node_id, n=3, guidance="", db=None):
    """Generate N fresh alternative versions of one node and add them as variant
    drafts in that node's variant group."""
    thesis = at.get_thesis(line_key, db=db)
    if thesis.get("error"):
        return thesis
    node = at.get_node(node_id, db=db)
    if node.get("error"):
        return node

    user = (f'Generate {n} distinct alternative versions of this {node["node_type"]} '
            f'(current text: "{(node.get("body") or "").strip()}").\n'
            f'Guidance from the partner: {guidance.strip() or "sharpen it and make it more concrete"}.\n'
            f'Each option should be genuinely different in angle, not minor rewordings. '
            f'Return ONLY a JSON array of {n} objects, each {{"text": "...", "rationale": "one short line"}}.')
    raw = _call(thesis, user)
    options = _parse_options(raw)[:n]
    if not options:
        return {"error": "no_options_parsed", "raw": raw[:300]}

    group = node.get("variant_group") or node_id
    # Make sure the original node shares the group, so the UI shows them together.
    if not node.get("variant_group"):
        at.upsert_thesis_node(node["line_id"], node["node_type"], node.get("body") or "",
                              title=node.get("title"), node_id=node_id, variant_group=group,
                              change_reason="grouped for variants", db=db)
    created = []
    for opt in options:
        r = at.upsert_thesis_node(node["line_id"], node["node_type"], opt["text"],
                                  title=node.get("title"), parent_id=node.get("parent_id"),
                                  variant_group=group,
                                  meta={"architect_generated": True, "rationale": opt.get("rationale"),
                                        "guidance": guidance}, db=db)
        created.append({"id": r.get("id"), "text": opt["text"], "rationale": opt.get("rationale")})
    return {"node_id": node_id, "variant_group": group, "generated": len(created), "options": created}


def revise(line_key, node_id, feedback, n=2, db=None):
    """Revise one node in light of partner feedback, producing N options that
    address it (kept as variant drafts)."""
    return generate_options(line_key, node_id, n=n,
                            guidance=f"Address this partner feedback directly: {feedback}", db=db)


def status():
    """Whether the Architect can run (key + SDK present)."""
    try:
        _client()
        return {"ready": True, "model": MODEL}
    except Exception as exc:
        return {"ready": False, "reason": str(exc)}
