// echo_engine.jsx — 類似検出エンジン（Jaccard + Claude ハイブリッド）

const TOKEN_RE = /[a-zA-Z]{2,}|[一-龥ぁ-んァ-ヴー々]{2,}/g;
const STOPWORDS = new Set([
  'する', 'した', 'して', 'いる', 'ある', 'ない', 'こと', 'もの', 'ため',
  'the', 'and', 'for', 'with', 'that', 'this', 'from', 'have', 'has',
]);

function tokenize(s) {
  if (!s) return new Set();
  const tokens = (String(s).toLowerCase().match(TOKEN_RE) || [])
    .filter(t => !STOPWORDS.has(t));
  return new Set(tokens);
}

function jaccard(a, b) {
  if (!a.size || !b.size) return 0;
  let inter = 0;
  a.forEach(x => b.has(x) && inter++);
  return inter / (a.size + b.size - inter);
}

// phase1: Jaccardで上位候補を絞る
function jaccardCandidates(text, pastNotes, threshold = 0.15, topK = 3) {
  const w = tokenize(text);
  if (w.size < 2) return [];
  const cands = [];
  for (const p of pastNotes) {
    if (Date.now() - p.ts < 60000) continue; // 直近1分は無視
    const score = jaccard(w, tokenize(p.text));
    if (score > threshold) cands.push({ note: p, rawScore: score });
  }
  cands.sort((a, b) => b.rawScore - a.rawScore);
  return cands.slice(0, topK);
}

// phase2: Claudeで上位候補を意味的に判定
async function claudeRerank(newText, candidates) {
  if (!candidates.length) return null;
  try {
    const listText = candidates.map((c, i) => `[${i}] ${c.note.text}`).join('\n');
    const prompt = `今日のメモと過去のメモを比較して、最も意味的に似ているものを1つ選べ。

今日のメモ: ${newText}

過去のメモ候補:
${listText}

回答は以下のJSONのみ。他のテキストは一切出力するな。
{"index": 0, "score": 75, "reason": "共通の主張"}

- index: 最も似ている候補のインデックス。どれも似ていなければ -1
- score: 0-100の類似度（意味レベル）
- reason: 15字以内の理由`;

    const raw = await window.claude.complete(prompt);
    const m = String(raw).match(/\{[^}]+\}/);
    if (!m) return null;
    const parsed = JSON.parse(m[0]);
    if (parsed.index < 0 || parsed.index >= candidates.length) return null;
    if (parsed.score < 30) return null;
    const picked = candidates[parsed.index];
    return { note: picked.note, score: parsed.score, reason: parsed.reason, rawScore: picked.rawScore };
  } catch (e) {
    // フォールバック: Jaccardスコアをそのまま使う
    const top = candidates[0];
    return { note: top.note, score: Math.round(top.rawScore * 100), reason: 'keyword match', rawScore: top.rawScore };
  }
}

async function findEcho(newText, pastNotes, threshold = 0.15) {
  const candidates = jaccardCandidates(newText, pastNotes, threshold, 3);
  if (!candidates.length) return null;
  return await claudeRerank(newText, candidates);
}

// 分類（タスク / アイデア / メモ）
function classifyLocal(text) {
  const t = text.toLowerCase();
  if (/する$|買う|予約|送る|連絡|電話|払う|todo|do\b|task|契約|締切|〜まで/.test(t)) return 'task';
  if (/かも$|気がする|仮説|思う|アイデア|idea|could|might|hypothesis/.test(t)) return 'idea';
  return 'memo';
}

async function classifyWithClaude(text) {
  try {
    const prompt = `次のメモを task / idea / memo のいずれか一語だけで分類せよ。

判定基準:
- task: 実行可能な行動。「〜する」「買う」「連絡する」など
- idea: 仮説・閃き・気づき。「〜かも」「〜な気がする」など
- memo: 事実の記録・観察。「〜だった」「〜があった」など

メモ: ${text}

出力は task / idea / memo のいずれか1語のみ。説明不要。`;
    const res = await window.claude.complete(prompt);
    const m = String(res).toLowerCase().match(/task|idea|memo/);
    return m ? m[0] : classifyLocal(text);
  } catch (e) {
    return classifyLocal(text);
  }
}

// =========================
// クラスタリング: 全メモを大雑把なテーマに束ねる
// =========================

// Claude にメモ一覧を渡して、クラスタ（テーマ名 + メンバーIDリスト）を返してもらう
async function clusterNotes(notes, { maxClusters = 5 } = {}) {
  if (!notes || notes.length === 0) return { clusters: [], generatedAt: Date.now() };
  // APIの入力を現実的な量に制限
  const listed = notes.slice(-120); // 直近120件まで

  // window.claude が無い環境(本番Vercel等)では即ローカルへ
  if (typeof window === 'undefined' || !window.claude || typeof window.claude.complete !== 'function') {
    return { clusters: [], generatedAt: Date.now(), error: 'no_claude_api' };
  }

  try {
    const listText = listed
      .map(n => `[${n.id}] (${n.type}) ${n.text.slice(0, 120)}`)
      .join('\n');

    const prompt = `次のメモ群を意味・テーマで最大${maxClusters}個のクラスタに分類せよ。

ルール:
- 各クラスタは大雑把でいい。細かく分けるな。
- 1つに属さない独立したメモは "uncategorized" でまとめる
- テーマ名は日本語で短く (10字以内)
- 各クラスタに1行の短い要約を添えて、そのクラスタの核心を書く
- 本人が後で見て役立つかを考える

出力は下のJSONのみ。他は一切出力するな。
{
  "clusters": [
    { "name": "テーマ名", "summary": "1行要約", "ids": ["seed-1", "seed-2"] },
    ...
  ]
}

メモ一覧:
${listText}`;

    const raw = await window.claude.complete(prompt);
    const m = String(raw).match(/\{[\s\S]*\}/);
    if (!m) throw new Error('no JSON in response');
    const parsed = JSON.parse(m[0]);
    if (!parsed.clusters || !Array.isArray(parsed.clusters)) throw new Error('invalid shape');

    // 有効なIDのみに絞る
    const validIds = new Set(listed.map(n => n.id));
    const clusters = parsed.clusters
      .map(c => ({
        name: c.name || '無題',
        summary: c.summary || '',
        ids: (c.ids || []).filter(id => validIds.has(id)),
      }))
      .filter(c => c.ids.length > 0);

    return { clusters, generatedAt: Date.now() };
  } catch (e) {
    return { clusters: [], generatedAt: Date.now(), error: e.message };
  }
}

// クラスタ結果のキャッシュ（DB meta）
async function loadCachedClusters() {
  try {
    const data = await DB.getMeta('clusters');
    if (!data) return null;
    return data;
  } catch { return null; }
}
async function saveCachedClusters(data) {
  try { await DB.setMeta('clusters', data); } catch {}
}

// APIが使えない/失敗した時のローカルフォールバック
// Jaccard類似度で "近いメモ同士" をゆるくまとめ、代表キーワードをクラスタ名にする
function localClusterFallback(notes) {
  if (!notes || notes.length === 0) {
    return { clusters: [], generatedAt: Date.now(), fallback: true };
  }

  // 各ノートのトークン集合をキャッシュ
  const tokenMap = new Map();
  notes.forEach(n => tokenMap.set(n.id, tokenize(n.text)));

  // 類似度しきい値以上でつながるノート同士をUnion-Find的にまとめる
  const THRESHOLD = 0.18;
  const parent = {};
  notes.forEach(n => { parent[n.id] = n.id; });
  const find = (x) => {
    while (parent[x] !== x) {
      parent[x] = parent[parent[x]];
      x = parent[x];
    }
    return x;
  };
  const union = (a, b) => {
    const ra = find(a), rb = find(b);
    if (ra !== rb) parent[ra] = rb;
  };

  for (let i = 0; i < notes.length; i++) {
    for (let j = i + 1; j < notes.length; j++) {
      const score = jaccard(tokenMap.get(notes[i].id), tokenMap.get(notes[j].id));
      if (score >= THRESHOLD) union(notes[i].id, notes[j].id);
    }
  }

  // 同じrootごとにグルーピング
  const groups = {};
  notes.forEach(n => {
    const r = find(n.id);
    if (!groups[r]) groups[r] = [];
    groups[r].push(n);
  });

  // 各グループから代表キーワードを拾って命名
  const clusters = Object.values(groups)
    .filter(g => g.length >= 2) // 1件しかないものはクラスタにしない
    .map(group => {
      // 全メモのトークンを集計して頻度の高いものを拾う
      const freq = new Map();
      group.forEach(n => {
        tokenMap.get(n.id).forEach(tok => {
          freq.set(tok, (freq.get(tok) || 0) + 1);
        });
      });
      const topKeywords = [...freq.entries()]
        .filter(([_, c]) => c >= 2) // 2件以上で登場
        .sort((a, b) => b[1] - a[1])
        .slice(0, 2)
        .map(([tok]) => tok);

      const name = topKeywords.length > 0 ? topKeywords.join(' · ') : `${group.length}件のメモ`;

      return {
        name,
        summary: `${group.length}件が似ている話題`,
        ids: group.map(n => n.id),
      };
    })
    .sort((a, b) => b.ids.length - a.ids.length)
    .slice(0, 6);

  return { clusters, generatedAt: Date.now(), fallback: true };
}

// Digest用のローカルフォールバック（直近メモから繰り返しテーマを抽出）
function localDigestFallback(recent) {
  if (!recent || recent.length < 3) return [];

  const tokenMap = new Map();
  recent.forEach(n => tokenMap.set(n.id, tokenize(n.text)));

  const THRESHOLD = 0.18;
  const parent = {};
  recent.forEach(n => { parent[n.id] = n.id; });
  const find = (x) => {
    while (parent[x] !== x) {
      parent[x] = parent[parent[x]];
      x = parent[x];
    }
    return x;
  };
  const union = (a, b) => {
    const ra = find(a), rb = find(b);
    if (ra !== rb) parent[ra] = rb;
  };

  for (let i = 0; i < recent.length; i++) {
    for (let j = i + 1; j < recent.length; j++) {
      const score = jaccard(tokenMap.get(recent[i].id), tokenMap.get(recent[j].id));
      if (score >= THRESHOLD) union(recent[i].id, recent[j].id);
    }
  }

  const groups = {};
  recent.forEach(n => {
    const r = find(n.id);
    if (!groups[r]) groups[r] = [];
    groups[r].push(n);
  });

  return Object.values(groups)
    .filter(g => g.length >= 2)
    .map(group => {
      const freq = new Map();
      group.forEach(n => {
        tokenMap.get(n.id).forEach(tok => {
          freq.set(tok, (freq.get(tok) || 0) + 1);
        });
      });
      const topKeywords = [...freq.entries()]
        .filter(([_, c]) => c >= 2)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 2)
        .map(([tok]) => tok);
      const name = topKeywords.length > 0 ? topKeywords.join(' · ') : `${group.length}件の話題`;
      return {
        name,
        insight: `${group.length}件のメモが近いテーマ`,
        ids: group.map(n => n.id),
      };
    })
    .sort((a, b) => b.ids.length - a.ids.length)
    .slice(0, 3);
}

// キャッシュが新鮮(24h以内)ならそれを返す、古ければ再生成
async function getClusters(notes, { forceRefresh = false } = {}) {
  if (!forceRefresh) {
    const cached = await loadCachedClusters();
    if (cached && (Date.now() - cached.generatedAt) < 24 * 3600 * 1000) {
      return cached;
    }
  }
  const fresh = await clusterNotes(notes);
  // APIが失敗 or 空を返した場合はローカルフォールバック
  if (!fresh.clusters || fresh.clusters.length === 0) {
    const fb = localClusterFallback(notes);
    fb.error = fresh.error; // エラー理由は残す
    await saveCachedClusters(fb);
    return fb;
  }
  await saveCachedClusters(fresh);
  return fresh;
}

// =========================
// Digest（再会サマリ）: 過去1週間のメモから繰り返し出たテーマを抽出
// 「うるさくしない」ことが設計要件。通知なし、バッジなし、Ripen内で静かに表示
// =========================

// この週の日曜を起点にISO週番号的なキーを返す
function currentWeekKey() {
  const now = new Date();
  const sunday = new Date(now);
  sunday.setHours(0, 0, 0, 0);
  sunday.setDate(sunday.getDate() - sunday.getDay());
  return `digest-${sunday.getFullYear()}-${String(sunday.getMonth()+1).padStart(2,'0')}-${String(sunday.getDate()).padStart(2,'0')}`;
}

async function generateDigest(notes) {
  const now = Date.now();
  const WEEK = 7 * 24 * 3600 * 1000;
  const recent = notes.filter(n => (now - n.ts) < WEEK && n.status !== 'resting');

  if (recent.length < 3) {
    return { themes: [], generatedAt: now, empty: true };
  }

  // window.claude が無い環境 → ローカルでテーマを作る
  if (typeof window === 'undefined' || !window.claude || typeof window.claude.complete !== 'function') {
    return { themes: localDigestFallback(recent), generatedAt: now, weekKey: currentWeekKey(), fallback: true };
  }

  try {
    const listText = recent
      .slice(-30)
      .map(n => `[${n.id}] ${n.text.slice(0, 100)}`)
      .join('\n');

    const prompt = `次は、ユーザーが先週投下したメモ群だ。この中から、繰り返し現れている"テーマ"を最大3個抽出せよ。

ルール:
- 1つのメモしか該当しないテーマは選ぶな。最低2つ以上のメモに共通するテーマだけ
- テーマ名は静かで短い日本語1行（10字以内）
- 1行の短い気づき（本人が後で読んで「ああ、そうだな」と思える視点）を添える
- 命令・評価・催促の言葉は使うな（例: 進めるべき、やるべき、遅れている、等は禁止）
- 各テーマに関連するメモIDを並べる

出力は下のJSONのみ。他は一切出力するな。
{
  "themes": [
    { "name": "テーマ名", "insight": "1行の気づき", "ids": ["id1", "id2"] }
  ]
}

メモ一覧:
${listText}`;

    const raw = await window.claude.complete(prompt);
    const m = String(raw).match(/\{[\s\S]*\}/);
    if (!m) throw new Error('no JSON');
    const parsed = JSON.parse(m[0]);
    if (!parsed.themes || !Array.isArray(parsed.themes)) throw new Error('bad shape');

    const validIds = new Set(recent.map(n => n.id));
    const themes = parsed.themes
      .map(th => ({
        name: th.name || '',
        insight: th.insight || '',
        ids: (th.ids || []).filter(id => validIds.has(id)),
      }))
      .filter(th => th.ids.length >= 2 && th.name)
      .slice(0, 3);

    // AIが返したテーマが空ならローカルへフォールバック
    if (themes.length === 0) {
      return { themes: localDigestFallback(recent), generatedAt: now, weekKey: currentWeekKey(), fallback: true };
    }
    return { themes, generatedAt: now, weekKey: currentWeekKey() };
  } catch (e) {
    // Claude API失敗時もローカルで救う
    return { themes: localDigestFallback(recent), generatedAt: now, error: e.message, fallback: true };
  }
}

async function loadCachedDigest() {
  try { return await DB.getMeta('digest'); } catch { return null; }
}
async function saveCachedDigest(data) {
  try { await DB.setMeta('digest', data); } catch {}
}

// 今週のdigestを取得。週が変わったら再生成、それ以外はキャッシュ
async function getDigest(notes, { forceRefresh = false } = {}) {
  const weekKey = currentWeekKey();
  if (!forceRefresh) {
    const cached = await loadCachedDigest();
    if (cached && cached.weekKey === weekKey) return cached;
  }
  const fresh = await generateDigest(notes);
  fresh.weekKey = weekKey;
  await saveCachedDigest(fresh);
  return fresh;
}

// 週末(金・土・日)ならバナー出してOKの判定
function isDigestSeason() {
  const d = new Date().getDay();
  return d === 5 || d === 6 || d === 0;
}

// digest を見たフラグ（バナー抑制用）
async function markDigestSeen() {
  try { await DB.setMeta('digestSeen', { weekKey: currentWeekKey(), ts: Date.now() }); } catch {}
}
async function isDigestSeen() {
  try {
    const seen = await DB.getMeta('digestSeen');
    return seen && seen.weekKey === currentWeekKey();
  } catch { return false; }
}

// カレンダー自動紐付け（将来Google Calendar APIに差し替え）
function maybeLinkCalendar(text) {
  const t = text.toLowerCase();
  if (/meta|インタビュー|interview|phone screen|licensing|ライセンシング/i.test(t)) return 'Meta phone screen';
  if (/sd先生|発音|レッスン|授業/.test(t)) return 'English class';
  if (/star|面接|q1|q2|q3/i.test(t)) return 'Interview prep';
  return null;
}

Object.assign(window, {
  tokenize, jaccard, jaccardCandidates, claudeRerank,
  findEcho, classifyLocal, classifyWithClaude, maybeLinkCalendar,
  clusterNotes, getClusters, loadCachedClusters, saveCachedClusters,
  localClusterFallback, localDigestFallback,
  generateDigest, getDigest, loadCachedDigest, saveCachedDigest,
  isDigestSeason, markDigestSeen, isDigestSeen, currentWeekKey,
});
