/**
 * console「分流台」純函式層（Arcrun#9 收件夾改裝＝全部待辦分流台，2026-07-08 總管派工；
 * 頂層 SDD kb-ingest-architecture R7 消費端）
 *
 * 為什麼獨立成 lib（比照 console-dashboard-model.ts）：route 只做 IO（fetch KBDB entries），
 * 「兩種 entry 契約 → 統一分流項」的解析/分欄/分項判定全在這裡，vitest 直接餵資料驗證。
 *
 * 資料來源二合一（R7）：
 *   - entry_type=todo：Logseq 萃取灌入（Arcrun#8 ingest 線），content JSON 契約（已定）＝
 *     {"text","marker","owner_tier":"ai|collab|leo","project","source","status":"new|done"}
 *   - entry_type=inbox：Telegram 指令（inbox_receive），沿用 {"text","from","status"}
 *
 * 三欄分流（80/15/5，北極星「減 leo 負」）：
 *   ai     = AI 會幫我搞定（80%）
 *   collab = AI 要我做完它接手（15%）
 *   leo    = 非 leo 不可（5%）
 * 無 owner_tier / 看不懂的值（triage engine 還沒判到）→ 歸 collab 欄但標 unclassified，
 * UI 顯「未分流」徽章——不冒領「AI 會搞定」、也不誤判成「非 leo 不可」，且欄位維持三欄
 * 不另開第四欄（80/15/5 的故事不破）。issue 原文的 mira80/collab15/leo5 詞彙當同義吃進來
 * （ingest 端若照 issue 舊詞灌，UI 不至於整欄蒸發）。
 */

import { type KbdbEntry, parseCreatedAtMs, parseJsonContent } from './console-dashboard-model';

export type TriageTier = 'ai' | 'collab' | 'leo';

/** 契約值 → 三欄。issue 原文舊詞（mira80/collab15/leo5）當同義收；其餘＝未分流。 */
const TIER_ALIASES: Record<string, TriageTier> = {
  ai: 'ai',
  collab: 'collab',
  leo: 'leo',
  mira80: 'ai',
  collab15: 'collab',
  leo5: 'leo',
};

export interface TriageItem {
  id: string;
  text: string;
  tier: TriageTier;
  /** true＝owner_tier 缺席/看不懂（triage engine 未判），暫歸 collab 欄＋UI 標「未分流」 */
  unclassified: boolean;
  /** '' = 未分項（UI 過濾 chips 用「未分項」承接） */
  project: string;
  /** todo 來源＝content.source（如 notes）；inbox 一律 telegram（可帶 from） */
  source: string;
  /** Logseq marker（TODO/DOING/LATER/NOW…）；inbox 無＝'' */
  marker: string;
  status: 'new' | 'done';
  origin: 'todo' | 'inbox';
  at: string | number;
  at_ms: number | null;
}

export interface TriageModel {
  /** 全部項（created_at 新→舊）；分欄/過濾在 UI 端做（資料一次給齊，chips 切換零請求） */
  items: TriageItem[];
  /** 出現過的 project（去重、字典序；不含 ''——「未分項」由 UI 依 items 判斷要不要出 chip） */
  projects: string[];
  /** 各欄未完成數（status=new）＋整體計數，供欄位標頭/收件計數用 */
  counts: { ai: number; collab: number; leo: number; unclassified: number; done: number; total: number };
}

function normalizeStatus(v: unknown): 'new' | 'done' {
  return v === 'done' ? 'done' : 'new';
}

function normalizeTier(v: unknown): { tier: TriageTier; unclassified: boolean } {
  const mapped = typeof v === 'string' ? TIER_ALIASES[v.trim()] : undefined;
  return mapped ? { tier: mapped, unclassified: false } : { tier: 'collab', unclassified: true };
}

function str(v: unknown): string {
  return typeof v === 'string' ? v.trim() : '';
}

/** todo entry → 分流項。content 非 JSON（不合契約的殘資料）→ 原文當 text 誠實顯示，不丟棄。 */
export function todoToTriageItem(e: KbdbEntry): TriageItem {
  const j = parseJsonContent(e);
  const { tier, unclassified } = normalizeTier(j?.owner_tier);
  return {
    id: e.id,
    text: str(j?.text) || (e.content ?? ''),
    tier,
    unclassified,
    project: str(j?.project),
    source: str(j?.source),
    marker: str(j?.marker),
    status: normalizeStatus(j?.status),
    origin: 'todo',
    at: e.created_at,
    at_ms: parseCreatedAtMs(e.created_at),
  };
}

/** inbox entry → 分流項（沿用 {"text","from","status"} 契約）。無 tier ＝未分流歸 collab。 */
export function inboxToTriageItem(e: KbdbEntry): TriageItem {
  const j = parseJsonContent(e);
  const from = str(j?.from);
  return {
    id: e.id,
    text: str(j?.text) || (e.content ?? ''),
    tier: 'collab',
    unclassified: true,
    project: '',
    source: from ? `telegram・${from}` : 'telegram',
    marker: '',
    status: normalizeStatus(j?.status),
    origin: 'inbox',
    at: e.created_at,
    at_ms: parseCreatedAtMs(e.created_at),
  };
}

export type TriageCheckAction = 'check' | 'restore';

/**
 * console 勾掉/還原 → 新 content JSON 字串（純函式；route 先 GET 原 entry 再整串回寫，
 * 防 PATCH 蓋掉別的欄位）。
 *
 * ── 雙向銷案語意（leo 2026-07-08 拍板，死循環防呆——萃取端會配合這套規約）──
 *   1. console 勾掉＝**終局**：status:done + checked_via:"console" + checked_at。
 *      即使 Logseq 原文還是 TODO，萃取端（Arcrun#8 ingest 線）看到 checked_via:console
 *      也**絕不復活它**（不改回 new、不重灌覆蓋）——否則「console 勾掉→下輪萃取又出現」
 *      的死循環。
 *   2. Logseq 那邊改 DONE 的，萃取端來 PATCH status:done + checked_via:"logseq"——
 *      同一個 status slot、兩個來源靠 checked_via 溯源，console 只需忠實顯示非 done 項。
 *   3. 還原（誤勾救濟）＝status 回 new、**移除 checked_via/checked_at**（回到「未銷案」
 *      的乾淨狀態，讓兩邊規約重新適用；不留殘欄位讓萃取端誤讀成已銷案）。
 *
 * 防禦：content 非 JSON（不合契約殘資料，與 todoToTriageItem 同一情境）→ 包成
 * {"text":原文} 再上狀態欄——原文不丟、從此畢業成契約 JSON（status 才有地方放）。
 */
export function applyTriageCheck(content: string | null | undefined, action: TriageCheckAction, nowIso: string): string {
  let obj: Record<string, unknown>;
  try {
    const v = JSON.parse(content ?? '');
    obj = v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : { text: content ?? '' };
  } catch {
    obj = { text: content ?? '' };
  }
  if (action === 'restore') {
    const { checked_via: _via, checked_at: _at, ...rest } = obj;
    return JSON.stringify({ ...rest, status: 'new' });
  }
  return JSON.stringify({ ...obj, status: 'done', checked_via: 'console', checked_at: nowIso });
}

/** 二源合一 → 分流台模型。排序＝created_at 新→舊（時間不明沉底）；分欄計數只算未完成。 */
export function buildTriageModel(todoEntries: KbdbEntry[], inboxEntries: KbdbEntry[]): TriageModel {
  const items = [
    ...todoEntries.map(todoToTriageItem),
    ...inboxEntries.map(inboxToTriageItem),
  ].sort((a, b) => (b.at_ms ?? -1) - (a.at_ms ?? -1));
  const projects = [...new Set(items.map((i) => i.project).filter(Boolean))].sort();
  const counts = { ai: 0, collab: 0, leo: 0, unclassified: 0, done: 0, total: items.length };
  for (const it of items) {
    if (it.status === 'done') {
      counts.done++;
      continue;
    }
    counts[it.tier]++;
    if (it.unclassified) counts.unclassified++;
  }
  return { items, projects, counts };
}
