#!/usr/bin/env node
// km-wiki-ingest 乾跑（dry-run）驗證器 — 不寫 live、不部署。
// -------------------------------------------------------------
// 註：解析在 live 由 workflow.yaml 的 parse_card = 通用 `code` 零件（sandbox inline JS）承載；
//     本驗證器直接 import card-to-envelope.mjs（＝該 code 節點內聯 JS 的權威來源）跑同一份邏輯，
//     故 dry-run 的 envelope 結果與 code 節點在 live 的輸出等價（Arcrun#10 已單測證明逐欄全等）。
// 對某 repo 的 system-dev/wiki/cards/**/*.md 跑機械解析 + envelope 打包，
// 輸出「將寫入什麼」：① entry 清單（page_name / entry_type / metadata.embed / content_hash）
// ② triplet envelope 清單（每段的 nodes / triplets / source.uri+anchor）
// ③ 每個 graph /triplets/ingest 呼叫的 subrequest 估算（證明壓在 CF 上限下）
// ④ 冪等鍵設計（entry=page_name+content_hash；triplet=source.uri+content_hash）。
//
// 用法：node dry-run.mjs --repo-path <clone路徑> [--repo Leo/notes] [--budget 40] [--json] [--full]
//        --self-test   額外跑「合成超大卡」證明分段生效。

import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { planCard, estimateEnvelopeSubrequests, SUBREQ_CEILING, SUBREQ_BUDGET } from './card-to-envelope.mjs';

function parseArgs(argv) {
  const out = { repo: 'Leo/notes', budget: SUBREQ_BUDGET };
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--repo-path') out.repoPath = argv[++i];
    else if (a === '--repo') out.repo = argv[++i];
    else if (a === '--budget') out.budget = Number(argv[++i]);
    else if (a === '--json') out.json = true;
    else if (a === '--full') out.full = true;
    else if (a === '--self-test') out.selfTest = true;
  }
  return out;
}

function walkCards(dir) {
  const out = [];
  if (!existsSync(dir)) return out;
  for (const e of readdirSync(dir, { withFileTypes: true })) {
    const p = path.join(dir, e.name);
    if (e.isDirectory()) out.push(...walkCards(p));
    else if (e.name.endsWith('.md') && e.name !== '.gitkeep' && !e.name.startsWith('00-INDEX')) out.push(p);
  }
  return out;
}

function gitCommit(repoPath) {
  try {
    return execFileSync('git', ['-C', repoPath, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
  } catch { return undefined; }
}

const args = parseArgs(process.argv.slice(2));
if (!args.repoPath) {
  console.error('用法: node dry-run.mjs --repo-path <clone路徑> [--repo Leo/notes] [--budget 40] [--json] [--full] [--self-test]');
  process.exit(1);
}

const cardsRoot = path.join(args.repoPath, 'system-dev', 'wiki', 'cards');
const cardPaths = walkCards(cardsRoot);
const commit = gitCommit(args.repoPath);

const report = { repo: args.repo, commit, budget: args.budget, ceiling: SUBREQ_CEILING, cards: [], totals: {} };
let totEntries = 0, totEnvelopes = 0, totTriplets = 0, totNodes = 0, maxSub = 0, over = 0;

for (const p of cardPaths) {
  const rel = path.relative(args.repoPath, p);
  const md = readFileSync(p, 'utf8');
  const plan = planCard(md, rel, args.repo, { budget: args.budget, commit });
  totEntries++;
  totEnvelopes += plan.envelopes.length;
  const cardTriplets = plan.envelopes.reduce((s, e) => s + e.triplets.length, 0);
  const cardNodes = plan.envelopes.reduce((s, e) => s + e.nodes.length, 0);
  totTriplets += cardTriplets;
  totNodes += cardNodes;
  for (const e of plan.envelopes) {
    maxSub = Math.max(maxSub, e._estSubrequests);
    if (e._estSubrequests > SUBREQ_CEILING) over++;
  }
  report.cards.push({
    relPath: rel,
    canonical: plan.meta.canonical,
    entry: {
      page_name: plan.entry.page_name,
      entry_type: plan.entry.entry_type,
      'metadata.embed': plan.entry.metadata.embed,
      content_hash: plan.entry.metadata.content_hash.slice(0, 12) + '…',
      content_bytes: Buffer.byteLength(plan.entry.content, 'utf8'),
      tags: plan.entry.tags,
    },
    envelopeCount: plan.envelopes.length,
    envelopes: plan.envelopes.map((e) => ({
      'source.uri': e.source.uri,
      'source.anchor': e.source.anchor ?? null,
      nodes: e.nodes.length,
      triplets: e.triplets.length,
      est_subrequests: e._estSubrequests,
      under_ceiling: e._estSubrequests <= SUBREQ_CEILING,
      sample_triplets: e.triplets.slice(0, args.full ? 999 : 4).map((t) => `${t.subject} >> ${t.predicate} >> ${t.object} (${t.confidence})`),
      sample_nodes: e.nodes.slice(0, args.full ? 999 : 4).map((n) => `${n.name}${n.gloss ? ' — ' + n.gloss.slice(0, 30) + '…' : ''}`),
    })),
  });
}

report.totals = {
  cards: totEntries,
  entries_to_upsert: totEntries,
  triplet_envelopes: totEnvelopes,
  total_triplets: totTriplets,
  total_nodes: totNodes,
  max_est_subrequests_single_call: maxSub,
  ceiling: SUBREQ_CEILING,
  budget: args.budget,
  any_envelope_over_ceiling: over,
};

// --- self-test：合成一張「超大卡」（20 邊 + 22 節點）證明單 envelope 會爆、分段後每段壓在預算下 ---
if (args.selfTest) {
  const entities = [];
  const edges = [];
  for (let i = 0; i < 22; i++) entities.push(`- **實體${i}**（別名${i}）— 這是實體 ${i} 的一句描述。`);
  for (let i = 0; i < 20; i++) edges.push(`- 實體${i} >> 關聯到 >> 實體${i + 1}`);
  const bigCard = `---\ntags: [壓測]\ngloss: 合成超大卡，測分段。\n---\n# 合成超大卡\n\n← [[notes/00-INDEX]]\n\n## 實體\n${entities.join('\n')}\n\n## 關聯\n### 內文知識關係\n${edges.join('\n')}\n`;
  const plan = planCard(bigCard, 'system-dev/wiki/cards/notes/合成超大卡.md', args.repo, { budget: args.budget });
  const single = estimateEnvelopeSubrequests(plan.tripletCount, plan.nodeCount);
  report.self_test = {
    note: '合成 20 邊 / 22 節點 的超大卡',
    if_single_envelope_est_subrequests: single,
    would_crash_single: single > SUBREQ_CEILING,
    segmented_into: plan.envelopes.length,
    per_segment: plan.envelopes.map((e) => ({
      uri: e.source.uri, anchor: e.source.anchor, triplets: e.triplets.length, nodes: e.nodes.length, est_subrequests: e._estSubrequests, under_ceiling: e._estSubrequests <= SUBREQ_CEILING,
    })),
    all_segments_under_ceiling: plan.envelopes.every((e) => e._estSubrequests <= SUBREQ_CEILING),
  };
}

if (args.json) {
  console.log(JSON.stringify(report, null, 2));
  process.exit(0);
}

// --- 人類可讀輸出 ---
const L = (s = '') => console.log(s);
L(`\n================ km-wiki-ingest DRY-RUN（不寫 live）================`);
L(`repo=${report.repo}  commit=${(commit || '(none)').slice(0, 12)}  budget=${args.budget}  CF_ceiling=${SUBREQ_CEILING}`);
L(`卡片來源根：${path.relative(args.repoPath, cardsRoot)}  找到 ${cardPaths.length} 張卡\n`);

for (const c of report.cards) {
  L(`── 卡片：${c.relPath}`);
  L(`   ENTRY（base POST /entries 或 kbdb_upsert_block，冪等鍵 page_name）：`);
  L(`     page_name     = ${c.entry.page_name}`);
  L(`     entry_type    = ${c.entry.entry_type}`);
  L(`     metadata.embed= ${c.entry['metadata.embed']}   content_hash=${c.entry.content_hash}   bytes=${c.entry.content_bytes}`);
  L(`     tags          = ${JSON.stringify(c.entry.tags)}`);
  L(`   TRIPLET ENVELOPE(s)（POST graph /triplets/ingest；分段數=${c.envelopeCount}）：`);
  for (const e of c.envelopes) {
    L(`     • uri=${e['source.uri']}${e['source.anchor'] ? '  anchor=' + e['source.anchor'] : ''}`);
    L(`       nodes=${e.nodes}  triplets=${e.triplets}  est_subrequests=${e.est_subrequests}  ≤ceiling? ${e.under_ceiling ? 'YES' : 'NO ⚠️'}`);
    for (const t of e.sample_triplets) L(`         - ${t}`);
    if (e.sample_nodes.length) L(`         nodes: ${e.sample_nodes.join(' | ')}`);
  }
  L('');
}

L(`================ 彙總 ================`);
for (const [k, v] of Object.entries(report.totals)) L(`  ${k.padEnd(34)} = ${v}`);
L(`  冪等設計：`);
L(`    entry   → page_name（穩定鍵）+ metadata.content_hash（比對是否改動 → 未改 skip、改動 PATCH 重嵌）`);
L(`    triplet → source.uri + source.content_hash（graph 現役 per-source 冪等；同 hash 整包 no-op）`);
L(`    分段    → 各段獨立 source.uri（#segNN）→ 各自獨立冪等，繞開 per-source content_hash 整包 skip`);

if (report.self_test) {
  L(`\n================ SELF-TEST：超大檔分段 ================`);
  const st = report.self_test;
  L(`  ${st.note}`);
  L(`  若不分段（單 envelope）估算 subrequest = ${st.if_single_envelope_est_subrequests} → 會炸? ${st.would_crash_single ? 'YES（> ' + SUBREQ_CEILING + '）' : 'no'}`);
  L(`  分段後段數 = ${st.segmented_into}，每段：`);
  for (const s of st.per_segment) L(`    - ${s.anchor}: triplets=${s.triplets} nodes=${s.nodes} est=${s.est_subrequests} ≤ceiling? ${s.under_ceiling ? 'YES' : 'NO ⚠️'}`);
  L(`  全部段壓在上限下？ ${st.all_segments_under_ceiling ? 'YES ✅' : 'NO ⚠️'}`);
}
L('');
