/* Agent API view — gives a Claude agent (or any LLM) a clean interface
   to read the current simulation, run scenarios, and get explanations.

   Exposes window.GFAMatrix programmatic API:
     GFAMatrix.simulate(query)         → result JSON
     GFAMatrix.simulateText(spec)      → human-readable summary
     GFAMatrix.recommend(query)        → 3 alternative queries
     GFAMatrix.compare(qA, qB)         → side-by-side delta
     GFAMatrix.taxonomy()              → 560-leaf tree
     GFAMatrix.archetypes(query)       → persona match scores
     GFAMatrix.export()                → full result + AI-readable narrative
*/

const { useState: useAGState, useMemo: useAGMemo, useEffect: useAGEffect } = React;

// === Reading-Trace 스키마(구조화 해석) — 첫답 GEO 방법론의 "Claude Reading Trace"
// (이해함/놓침/인용난이도를 자유텍스트가 아닌 구조화 로그로 남긴다) 착안.
// window.claude.complete는 Anthropic Messages API의 output_config.format(그래머 레벨
// 강제)을 노출하지 않는 sandbox 헬퍼라 진짜 constrained decoding은 못 쓴다 — 대신
// 스키마를 프롬프트로 강제 + 파싱 실패 시 1회 재시도로 "실질적으로 깨끗한 JSON"을 근사한다.
const READING_TRACE_SCHEMA = `{
  "summary": "1-2문장 핵심 요약(문자열)",
  "confidence": "0~100 정수 — 이 해석 자체에 대한 자체평가 확신도",
  "understood": ["입력 데이터에서 명확히 파악한 포인트, 최대 4개"],
  "missed_or_uncertain": ["표본 부족·모호해서 확정 못한 포인트. 없으면 빈 배열"],
  "action": "1문장 실행 권장사항(문자열)"
}`;

function extractJsonObject(text) {
  if (!text) return null;
  const stripped = String(text).replace(/```json\s*|```/g, '').trim();
  const start = stripped.indexOf('{');
  const end = stripped.lastIndexOf('}');
  if (start === -1 || end === -1 || end < start) return null;
  try { return JSON.parse(stripped.slice(start, end + 1)); } catch { return null; }
}

function validateReadingTrace(obj) {
  if (!obj || typeof obj !== 'object') return '응답이 JSON 객체가 아님';
  if (typeof obj.summary !== 'string' || !obj.summary.trim()) return 'summary 누락';
  if (typeof obj.confidence !== 'number' || obj.confidence < 0 || obj.confidence > 100) return 'confidence 범위 이탈(0~100)';
  if (!Array.isArray(obj.understood)) return 'understood가 배열이 아님';
  if (!Array.isArray(obj.missed_or_uncertain)) return 'missed_or_uncertain이 배열이 아님';
  if (typeof obj.action !== 'string' || !obj.action.trim()) return 'action 누락';
  return null;
}

function AgentAPIView({ query, target, exposure }) {
  const [out, setOut] = useAGState('');
  const [busy, setBusy] = useAGState(false);
  const [aiSummary, setAiSummary] = useAGState('');
  const [aiTrace, setAiTrace] = useAGState(null);
  const [aiBusy, setAiBusy] = useAGState(false);

  const result = useAGMemo(() => window.SimEngine.evalQuery(query), [query]);

  // 카테고리 민감도(P0/P1/P2) — 결정론 엔진 leave-one-out. LLM 미개입.
  const sensitivity = useAGMemo(() => {
    if (!window.SimEngine?.leafSensitivity) return [];
    try { return window.SimEngine.leafSensitivity(query); } catch { return []; }
  }, [query]);

  const apiJson = useAGMemo(() => {
    if (!result) return null;
    return {
      version: 'gfa-pm-lab.v0.3',
      generatedAt: new Date().toISOString(),
      input: {
        query: query.map(g => ({ op: g.op, leaves: g.leaves })),
        target,
        exposure,
      },
      output: {
        예상도달인원: result.finalReach,
        월노출수: result.impressions,
        클릭률: result.ctr,
        노출당비용: result.cpm,
        전환율: result.conv,
        예상전환건수: result.convs,
        타겟정확도: result.validity,
        경쟁도: result.compete,
        월예상지출: result.budget,
        성별비율_여성: Math.round(result.skew * 100),
        연령분포: Object.fromEntries(window.SimEngine.AGES.map((a, i) => [a, +(result.ageDist[i]*100).toFixed(1)])),
        지면효과: Object.fromEntries(window.SimEngine.PLACEMENTS.map((p, i) => [p.label, +(result.placeNorm[i]*100).toFixed(1)])),
        시간대효과: Object.fromEntries(window.SimEngine.TIME_BINS.map((t, i) => [t.label, +(result.timeDist[i]*100).toFixed(1)])),
      },
    };
  }, [query, target, exposure, result]);

  function runTest() {
    setBusy(true);
    setOut('');
    const tests = [];
    function pass(name) { tests.push({ name, ok: true }); }
    function fail(name, why) { tests.push({ name, ok: false, why }); }

    try {
      const tx = window.GFAMatrix.taxonomy();
      if (tx.length > 0) pass('taxonomy 트리 로드'); else fail('taxonomy 트리 로드', '비어 있음');
    } catch (e) { fail('taxonomy 트리 로드', e.message); }

    try {
      const sample = [{ op: 'OR', leaves: ['관심사 > 뷰티 > 스킨케어', '관심사 > 뷰티 > 메이크업/색조'] }];
      const r = window.GFAMatrix.simulate(sample);
      if (r && r.finalReach > 0) pass('단순 OR 쿼리 시뮬레이션 (도달 > 0)');
      else fail('단순 OR 쿼리 시뮬레이션', '결과 비정상');
    } catch (e) { fail('단순 OR 쿼리 시뮬레이션', e.message); }

    try {
      const recs = window.GFAMatrix.recommend([{ op: 'OR', leaves: ['관심사 > 게임 > 모바일 게임'] }]);
      if (Array.isArray(recs) && recs.length >= 3) pass('추천 3종 생성');
      else fail('추천 3종 생성', `length=${recs?.length}`);
    } catch (e) { fail('추천 3종 생성', e.message); }

    try {
      const text = window.GFAMatrix.simulateText({
        leaves: ['관심사 > 라이프 이벤트 > 결혼'],
        op: 'OR',
      });
      if (text && text.length > 20) pass('자연어 요약 출력');
      else fail('자연어 요약 출력', '문자열 너무 짧음');
    } catch (e) { fail('자연어 요약 출력', e.message); }

    try {
      const ax = window.GFAMatrix.archetypes([{ op: 'OR', leaves: ['관심사 > 게임 > PC 게임'] }]);
      if (Array.isArray(ax) && ax.length === 6) pass('아키타입 6종 매칭');
      else fail('아키타입 6종 매칭', `len=${ax?.length}`);
    } catch (e) { fail('아키타입 6종 매칭', e.message); }

    try {
      const cmp = window.GFAMatrix.compare(
        [{ op: 'OR', leaves: ['관심사 > 뷰티 > 스킨케어'] }],
        [{ op: 'OR', leaves: ['관심사 > 게임 > PC 게임'] }]
      );
      if (cmp && cmp.diff) pass('두 쿼리 비교');
      else fail('두 쿼리 비교', 'diff 없음');
    } catch (e) { fail('두 쿼리 비교', e.message); }

    try {
      const sens = window.GFAMatrix.sensitivity([
        { op: 'OR', leaves: ['관심사 > 뷰티 > 스킨케어', '관심사 > 게임 > PC 게임'] },
      ]);
      if (Array.isArray(sens) && sens.length === 2 && sens.every(s => ['P0','P1','P2'].includes(s.tier)))
        pass('카테고리 민감도 분석(P0/P1/P2)');
      else fail('카테고리 민감도 분석(P0/P1/P2)', `len=${sens?.length}`);
    } catch (e) { fail('카테고리 민감도 분석(P0/P1/P2)', e.message); }

    const allPass = tests.every(t => t.ok);
    setOut(JSON.stringify({ status: allPass ? 'PASS' : 'FAIL', tests }, null, 2));
    setBusy(false);
  }

  function copyJson() {
    if (!apiJson) return;
    window.safeCopy(JSON.stringify(apiJson, null, 2));
  }

  async function explainWithClaude() {
    if (!result || !window.claude?.complete) return;
    setAiBusy(true);
    setAiSummary('');
    setAiTrace(null);

    const topImpact = sensitivity.slice(0, 3)
      .map(s => `${s.leaf}(${s.tier}, Δvalidity ${s.deltaValidity})`)
      .join(', ') || '없음';

    function buildPrompt(correction) {
      return `당신은 디지털 광고 미디어 플래너입니다. 다음 네이버 GFA 광고 시뮬레이션 결과를 분석해,
아래 스키마와 정확히 일치하는 JSON 객체 "하나만" 출력하세요. 설명·마크다운·코드블록 없이
순수 JSON 텍스트만 반환합니다.

스키마:
${READING_TRACE_SCHEMA}

카테고리 민감도(제거 시 영향이 큰 순, 결정론 엔진 산출): ${topImpact}

시뮬레이션 결과:
${JSON.stringify(apiJson, null, 2)}${correction ? '\n\n(이전 응답이 스키마와 맞지 않았습니다: ' + correction + ' — JSON 객체만, 다른 텍스트 없이 다시 출력하세요.)' : ''}`;
    }

    let trace = null;
    let lastReply = '';
    let lastErr = '';
    try {
      for (let attempt = 0; attempt < 2 && !trace; attempt++) {
        lastReply = await window.claude.complete(buildPrompt(attempt === 0 ? '' : lastErr));
        const parsed = extractJsonObject(lastReply);
        lastErr = validateReadingTrace(parsed);
        if (!lastErr) trace = parsed;
      }
      if (trace) setAiTrace(trace);
      else setAiSummary('AI 해설 실패: 스키마 검증 2회 실패(' + lastErr + '). 원문:\n' + lastReply);
    } catch (e) {
      setAiSummary('AI 호출 실패: ' + e.message);
    }
    setAiBusy(false);
  }

  return (
    <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 14 }} data-screen-label="06 Agent">
      <div className="panel">
        <div className="panel-head">
          <h3>현재 시뮬레이션 — JSON 출력<span className="tag">window.GFAMatrix.export()</span></h3>
          <div style={{ display: 'flex', gap: 6 }}>
            <button className="btn" data-size="sm" onClick={copyJson}>📋 복사</button>
          </div>
        </div>
        <div className="panel-body">
          <pre className="agent-pre">
{apiJson ? JSON.stringify(apiJson, null, 2) : '// 쿼리를 먼저 구성하세요'}
          </pre>
        </div>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14 }}>
        <div className="panel">
          <div className="panel-head">
            <h3>AI 자연어 해설<span className="tag">claude.complete</span></h3>
            <button className="btn" data-variant="primary" data-size="sm"
                    onClick={explainWithClaude} disabled={!result || aiBusy}>
              {aiBusy ? <><span className="spin"></span> 해설 생성 중…</> : '🪄 해설 요청'}
            </button>
          </div>
          <div className="panel-body">
            {aiTrace ? (
              <div style={{ fontSize: 13, lineHeight: 1.65, color: 'var(--text)' }}>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 8 }}>
                  <strong style={{ fontSize: 14 }}>{aiTrace.summary}</strong>
                  <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10.5, color: 'var(--text-mute)', whiteSpace: 'nowrap' }}>
                    확신도 {aiTrace.confidence}%
                  </span>
                </div>
                {aiTrace.understood?.length > 0 && (
                  <div style={{ marginBottom: 8 }}>
                    <div style={{ fontSize: 11, color: 'var(--plant)', fontWeight: 600, marginBottom: 3 }}>파악한 포인트</div>
                    <ul style={{ margin: 0, paddingLeft: 18 }}>
                      {aiTrace.understood.map((u, i) => <li key={i} style={{ fontSize: 12.5 }}>{u}</li>)}
                    </ul>
                  </div>
                )}
                {aiTrace.missed_or_uncertain?.length > 0 && (
                  <div style={{ marginBottom: 8 }}>
                    <div style={{ fontSize: 11, color: 'var(--text-mute)', fontWeight: 600, marginBottom: 3 }}>확정 보류 · 불확실</div>
                    <ul style={{ margin: 0, paddingLeft: 18 }}>
                      {aiTrace.missed_or_uncertain.map((u, i) => <li key={i} style={{ fontSize: 12.5, color: 'var(--text-mute)' }}>{u}</li>)}
                    </ul>
                  </div>
                )}
                {aiTrace.action && (
                  <div style={{ fontSize: 12.5, color: 'var(--cyan)', borderTop: '1px dashed var(--border)', paddingTop: 6 }}>
                    ▸ {aiTrace.action}
                  </div>
                )}
              </div>
            ) : aiSummary ? (
              <p style={{ fontSize: 14, lineHeight: 1.7, color: 'var(--text)', margin: 0, whiteSpace: 'pre-wrap' }}>
                {aiSummary}
              </p>
            ) : (
              <div style={{ color: 'var(--text-mute)', fontSize: 12 }}>
                🪄 버튼을 누르면 Claude가 현재 시뮬레이션 결과를 구조화된 스키마(요약·확신도·파악한 점·불확실한 점·권장사항)로 해설합니다.
                <br/><br/>
                <strong>외부 에이전트 활용:</strong><br/>
                <code style={{ fontFamily: 'var(--font-mono)', fontSize: 11 }}>
                  const text = await window.GFAMatrix.simulateText({ leaves, op })
                </code>
              </div>
            )}
          </div>
        </div>

        <div className="panel">
          <div className="panel-head">
            <h3>자가 테스트<span className="tag">엔진 6종 점검</span></h3>
            <button className="btn" data-variant="primary" data-size="sm" onClick={runTest} disabled={busy}>
              {busy ? <><span className="spin"></span> 실행 중…</> : '▶ 테스트 실행'}
            </button>
          </div>
          <div className="panel-body">
            <pre className="agent-pre" style={{ maxHeight: 260 }}>{out || '// 테스트 실행 결과가 여기에 표시됩니다.\n// 에이전트가 페이지 로드 직후 GFAMatrix.runTests() 로 자동 점검할 수 있습니다.'}</pre>
          </div>
        </div>
      </div>

      <div className="panel">
        <div className="panel-head">
          <h3>카테고리 민감도 분석<span className="tag">leave-one-out · 결정론</span></h3>
        </div>
        <div className="panel-body">
          {sensitivity.length > 0 ? (
            <>
              <p style={{ fontSize: 11.5, color: 'var(--text-mute)', margin: '0 0 8px' }}>
                각 카테고리를 쿼리에서 하나씩 제외했을 때 타겟정확도·전환적합도가 얼마나 떨어지는지 측정한 값입니다.
                P0는 제거 시 영향이 가장 큰(=핵심) 카테고리, P2는 빼도 영향이 작은 카테고리입니다. 전부 결정론 엔진(evalQuery) 재계산 — LLM 미개입.
              </p>
              <table className="api-tbl">
                <thead><tr><th>순위</th><th>카테고리</th><th>Δ타겟정확도</th><th>Δ전환적합도</th><th>Δ도달%</th><th>티어</th></tr></thead>
                <tbody>
                  {sensitivity.slice(0, 8).map((s, i) => (
                    <tr key={s.leaf}>
                      <td>{i + 1}</td>
                      <td style={{ fontSize: 11.5 }}>{window.SimEngine.shortLabel ? window.SimEngine.shortLabel(s.leaf) : s.leaf}</td>
                      <td style={{ fontFamily: 'var(--font-mono)' }}>{s.deltaValidity >= 0 ? '-' : '+'}{Math.abs(s.deltaValidity)}</td>
                      <td style={{ fontFamily: 'var(--font-mono)' }}>{s.deltaFitness >= 0 ? '-' : '+'}{Math.abs(s.deltaFitness)}</td>
                      <td style={{ fontFamily: 'var(--font-mono)' }}>{s.deltaReachPct}%</td>
                      <td>
                        <span style={{
                          fontFamily: 'var(--font-mono)', fontSize: 10.5, fontWeight: 700, padding: '1px 7px', borderRadius: 4,
                          color: s.tier === 'P0' ? 'var(--signal)' : s.tier === 'P1' ? 'var(--cyan)' : 'var(--text-mute)',
                          background: s.tier === 'P0' ? 'oklch(0.2 0.06 30 / 0.35)' : 'transparent',
                        }}>{s.tier}</span>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </>
          ) : (
            <div style={{ color: 'var(--text-mute)', fontSize: 12 }}>// 쿼리에 카테고리를 2개 이상 담으면 상대 영향력을 비교할 수 있습니다.</div>
          )}
        </div>
      </div>

      <div className="panel">
        <div className="panel-head"><h3>에이전트 활용 가이드<span className="tag">API 레퍼런스</span></h3></div>
        <div className="panel-body" style={{ fontSize: 13, lineHeight: 1.7 }}>
          <p>이 페이지는 <strong>외부 LLM 에이전트가 직접 호출</strong>할 수 있도록 <code>window.GFAMatrix</code> 글로벌 API를 노출합니다.
            iframe에 페이지를 띄운 뒤 <code>eval_js</code> 또는 <code>postMessage</code>로 호출하세요.</p>
          <h4 style={{ color: 'var(--signal)', margin: '14px 0 6px' }}>주요 메서드</h4>
          <table className="api-tbl">
            <thead><tr><th>메서드</th><th>설명</th><th>반환값</th></tr></thead>
            <tbody>
              <tr><td><code>GFAMatrix.simulate(query)</code></td><td>쿼리 → 도달·CPM·CTR 등 전체 지표</td><td>객체</td></tr>
              <tr><td><code>GFAMatrix.simulateText(spec)</code></td><td>한국어 요약 1-2문장</td><td>문자열</td></tr>
              <tr><td><code>GFAMatrix.recommend(query)</code></td><td>확장·집중·다양화 3종 추천</td><td>배열[3]</td></tr>
              <tr><td><code>GFAMatrix.compare(qA, qB)</code></td><td>두 쿼리 핵심지표 차이</td><td>객체</td></tr>
              <tr><td><code>GFAMatrix.archetypes(query)</code></td><td>6개 페르소나 매칭 점수</td><td>배열[6]</td></tr>
              <tr><td><code>GFAMatrix.taxonomy()</code></td><td>560종 카테고리 트리</td><td>트리</td></tr>
              <tr><td><code>GFAMatrix.runTests()</code></td><td>엔진 자가진단</td><td>객체</td></tr>
              <tr><td><code>GFAMatrix.sensitivity(query)</code></td><td>카테고리별 제거 영향력(P0/P1/P2), 결정론</td><td>배열</td></tr>
              <tr><td><code>GFAMatrix.export()</code></td><td>현재 화면 시뮬레이션 결과 전체</td><td>객체</td></tr>
            </tbody>
          </table>

          <h4 style={{ color: 'var(--cyan)', margin: '14px 0 6px' }}>쿼리 구조</h4>
          <pre className="agent-pre" style={{ marginTop: 0 }}>
{`{
  "query": [
    { "op": "OR",  "leaves": ["관심사 > 뷰티 > 스킨케어", "관심사 > 뷰티 > 메이크업/색조"] },
    { "op": "AND", "leaves": ["관심사 > 라이프 이벤트 > 결혼"] }
  ]
}
// (A∪B) ∩ C 형태로 해석됨`}</pre>

          <h4 style={{ color: 'var(--plant)', margin: '14px 0 6px' }}>호출 예 — 클로드 에이전트 시점</h4>
          <pre className="agent-pre" style={{ marginTop: 0 }}>
{`// 1. 페이지를 iframe에 띄움
// 2. window.GFAMatrix 가 글로벌에 노출됨
const r = window.GFAMatrix.simulate([
  { op: 'OR', leaves: ['관심사 > 게임 > 모바일 게임'] }
]);
// r.finalReach → 예상 도달 인원
// r.validity   → 0~100 점 타겟 정확도
// r.recommendation → 자연어 한 줄 인사이트`}</pre>
        </div>
      </div>

      <style>{`
        .agent-pre {
          background: oklch(0.08 0.01 240 / 0.6);
          border: 1px solid rgba(255,240,220,0.08);
          padding: 10px 12px;
          font-family: var(--font-mono);
          font-size: 11px;
          line-height: 1.6;
          color: var(--bone-200);
          margin: 0;
          overflow: auto;
          max-height: 400px;
          white-space: pre;
        }
        .api-tbl {
          width: 100%;
          border-collapse: collapse;
          margin: 4px 0 12px;
        }
        .api-tbl th, .api-tbl td {
          text-align: left;
          padding: 6px 10px;
          border-bottom: 1px dashed var(--border);
          font-size: 12px;
        }
        .api-tbl th { color: var(--text-dim); font-weight: 500; font-size: 11px; }
        .api-tbl code {
          font-family: var(--font-mono);
          font-size: 11px;
          color: var(--signal);
          background: oklch(0.08 0.01 240 / 0.5);
          padding: 2px 6px;
        }
        code {
          font-family: var(--font-mono);
          font-size: 12px;
          color: var(--signal);
          background: oklch(0.08 0.01 240 / 0.4);
          padding: 1px 4px;
        }
      `}</style>
    </div>
  );
}

// === Install the public API on window =================================
function installGFAMatrixAPI() {
  const SE = window.SimEngine;
  if (!SE) return;

  function summarize(result) {
    if (!result) return '쿼리 결과가 없습니다.';
    const reach = SE.fmtN(result.finalReach);
    const v = result.validity;
    const c = result.compete;
    const fStr = v >= 70 ? '우수' : v >= 50 ? '양호' : '개선 필요';
    const cStr = c >= 70 ? '경합' : c >= 50 ? '중간' : '여유';
    return `예상 도달 ${reach}명, 클릭률 ${result.ctr}%, 전환율 ${result.conv}%, 노출당 비용 ₩${result.cpm.toLocaleString()}. 타겟 정확도 ${v}점(${fStr}), 경쟁도 ${c}점(${cStr}). 월 예상 지출 약 ${SE.fmtKRW(result.budget)}.`;
  }

  window.GFAMatrix = {
    version: 'gfa-pm-lab.v0.3',
    taxonomy() { return window.TAXONOMY; },
    leafCount() { return window.TOTAL_LEAVES; },
    simulate(query) {
      const r = SE.evalQuery(query);
      if (r) r.recommendation = summarize(r);
      return r;
    },
    simulateText({ leaves, op = 'OR' }) {
      const q = [{ op, leaves }];
      return summarize(SE.evalQuery(q));
    },
    recommend(query) { return SE.aiRecommend(query, window.TAXONOMY); },
    compare(qA, qB) {
      const a = SE.evalQuery(qA);
      const b = SE.evalQuery(qB);
      if (!a || !b) return null;
      const keys = ['finalReach', 'ctr', 'cpm', 'conv', 'validity', 'compete', 'budget'];
      const diff = {};
      for (const k of keys) {
        diff[k] = {
          A: a[k], B: b[k],
          delta: b[k] - a[k],
          pct: a[k] ? +((b[k] - a[k]) / a[k] * 100).toFixed(1) : null,
        };
      }
      return { A: a, B: b, diff };
    },
    archetypes(query) { return SE.rankArchetypes(query); },
    sensitivity(query) { return SE.leafSensitivity ? SE.leafSensitivity(query) : []; },
    runTests() {
      const tests = [];
      try {
        const t = this.taxonomy();
        tests.push({ name: 'taxonomy', ok: t.length > 0 });
      } catch (e) { tests.push({ name: 'taxonomy', ok: false, err: e.message }); }
      try {
        const r = this.simulate([{ op: 'OR', leaves: ['관심사 > 뷰티 > 스킨케어'] }]);
        tests.push({ name: 'simulate', ok: r?.finalReach > 0 });
      } catch (e) { tests.push({ name: 'simulate', ok: false, err: e.message }); }
      try {
        const r = this.recommend([{ op: 'OR', leaves: ['관심사 > 게임 > PC 게임'] }]);
        tests.push({ name: 'recommend', ok: r.length === 3 });
      } catch (e) { tests.push({ name: 'recommend', ok: false, err: e.message }); }
      try {
        const s = this.sensitivity([{ op: 'OR', leaves: ['관심사 > 뷰티 > 스킨케어', '관심사 > 게임 > PC 게임'] }]);
        tests.push({ name: 'sensitivity', ok: Array.isArray(s) && s.length === 2 });
      } catch (e) { tests.push({ name: 'sensitivity', ok: false, err: e.message }); }
      return { status: tests.every(t => t.ok) ? 'PASS' : 'FAIL', tests };
    },
    export() {
      // The most recent simulation rendered on screen
      const app = window.__currentApp;
      if (!app) return null;
      const r = SE.evalQuery(app.query);
      if (r) r.recommendation = summarize(r);
      return {
        version: 'gfa-pm-lab.v0.3',
        input: app,
        output: r,
      };
    },
  };
}

Object.assign(window, { AgentAPIView, installGFAMatrixAPI });
// sim-engine.js는 일반 <script>로 먼저 로드되므로 이 시점에 window.SimEngine이 이미 존재한다
// (index.html 로드 순서: sim-engine.js → agent-api.jsx). 기존엔 이 호출이 어디서도 실행되지
// 않아 window.GFAMatrix가 항상 undefined였다(사전 존재 버그 — 이 세션에서 발견·수정).
if (window.SimEngine) installGFAMatrixAPI();
