// Entries route — atomic data + tree (project/workflow). Base; embed is OPTIONAL (issue #7).
import { Hono } from 'hono';
import type { Bindings } from '../types';
import {
  createEntry,
  getEntry,
  listEntries,
  updateEntry,
  deleteEntry,
  searchEntries,
} from '../actions/entry-crud';
import { embedEnabled, embedOnWrite, semanticSearch } from '../embed';

export const entryRoutes = new Hono<{ Bindings: Bindings }>();

// library 多值參數（逗號分隔，portal-auth P1，design §3.3）。空值/全空白 → undefined（＝不過濾，
// 行為與未帶參數一字不變——向後相容硬驗收）。
function parseLibraryParam(raw: string | undefined): string[] | undefined {
  if (!raw) return undefined;
  const libs = raw.split(',').map((s) => s.trim()).filter(Boolean);
  return libs.length > 0 ? libs : undefined;
}

// POST /entries — create (entry_type=block/value/project/workflow/...)
entryRoutes.post('/', async (c) => {
  const body = await c.req.json().catch(() => null);
  if (!body || !body.entry_type) return c.json({ success: false, error: 'entry_type required' }, 400);
  const entry = await createEntry(c.env.DB, body);
  // embed-on-write (#7 / #5 第4點)：模組開 + entry 標 embed:true 才做；fire-and-forget，不阻塞回應、失敗不致命。
  if (embedEnabled(c.env)) c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {}));
  return c.json({ success: true, entry });
});

// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source, q/search)
// e.g. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
// e.g. filter by ingest source: ?source=logseq://vault/foo.md (issue #5.1)
// e.g. filter by library（多值逗號分隔，portal-auth P1）: ?library=finance,hr（未標記舊資料歸 general）
// e.g. keyword filter: ?q=遷移 或 ?search=遷移（別名，Arcrun#3 發現①：caller 實測時打的是 search=，
//   舊版完全不接這個 filter；q 與 search 兩個名字都認，避免同一個坑再踩一次）。
// count = 本頁筆數（受 limit 影響）；total = 符合條件全部筆數（不受 limit 影響，見 total 欄位）。
entryRoutes.get('/', async (c) => {
  const { entries, total } = await listEntries(c.env.DB, {
    entry_type: c.req.query('entry_type') || undefined,
    owner_id: c.req.query('owner_id') || undefined,
    parent_id: c.req.query('parent_id') || undefined,
    page_name: c.req.query('page_name') || undefined,
    source: c.req.query('source') || undefined,
    library: parseLibraryParam(c.req.query('library')),
    q: c.req.query('q') || c.req.query('search') || undefined,
    limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
    offset: c.req.query('offset') ? Number(c.req.query('offset')) : undefined,
  });
  return c.json({ success: true, entries, count: entries.length, total });
});

// GET /entries/search?q=...&owner_id=...&source=...&entry_type=...&library=...&mode=keyword|semantic
//   - mode=keyword（預設）：D1 LIKE（base，永遠可用）。
//   - mode=semantic：需 embed 模組開（Vectorize+AI binding）。未開 → 降級 keyword + capability_hint 告知缺能力（#7 發現閉環）。
//   - entry_type：base 通用 filter（caller 傳任意 type，如 workflow；base 不寫死語意，workflow-discovery Q4）。
//   - library：多值庫 filter（逗號分隔，portal-auth P1）。keyword 走 json_extract＋NULL→general；
//     semantic 走 Vectorize $in。未帶＝全庫（行為不變）。
//   - source：keyword 走 json_extract 謂詞（#66——#5.1 只接了 list 那半，這裡原本解析完即丟）；
//     semantic 走 Vectorize metadata filter（原本就有）。
//   - top_k / min_score（#67，semantic 專用）：topK 可調（預設 20、上限 100）＋分數閾值
//     （預設 0＝不過濾）。未帶＝行為與舊版一致（向後相容）；semantic 回應的 entry 另附 score
//     欄讓 caller 自裁（加欄不改形，keyword 路徑不受影響）。
entryRoutes.get('/search', async (c) => {
  const q = c.req.query('q');
  if (!q) return c.json({ success: false, error: 'q required' }, 400);
  const owner_id = c.req.query('owner_id') || undefined;
  const source = c.req.query('source') || undefined;
  const entry_type = c.req.query('entry_type') || undefined;
  const library = parseLibraryParam(c.req.query('library'));
  const mode = c.req.query('mode') === 'semantic' ? 'semantic' : 'keyword';
  // 數字參數防呆：非數字/非正 → 當沒帶（回預設），不 400——與其他 filter「壞值靜默忽略」一致。
  const topKNum = Number(c.req.query('top_k'));
  const top_k = Number.isFinite(topKNum) && topKNum > 0 ? Math.floor(topKNum) : undefined;
  const minScoreNum = Number(c.req.query('min_score'));
  const min_score = Number.isFinite(minScoreNum) && minScoreNum > 0 ? minScoreNum : undefined;

  if (mode === 'semantic') {
    const hits = await semanticSearch(c.env, q, {
      owner_id, source, entry_type, library, topK: top_k, min_score,
    });
    if (hits === null) {
      // 模組沒開：誠實降級 keyword + 告知「叫 CC 幫你開 vectorize」（不假裝有語義）。
      const entries = await searchEntries(c.env.DB, q, owner_id, entry_type, undefined, library, source);
      return c.json({
        success: true,
        entries,
        count: entries.length,
        mode: 'keyword',
        requested_mode: 'semantic',
        capability_hint:
          '語義查詢需先開 vectorize（embed 模組）。叫 CC「幫我開語義查詢」即可（設 kbdb_embed:true + redeploy）。本次已降級關鍵字搜尋。',
      });
    }
    // hydrate vector hits → 完整 entry（保持回應形狀與 keyword 一致）。
    // #67：entry 附 score（相似分數）——加欄不改形，既有 caller 不解析多的欄位不受影響。
    const entries = (
      await Promise.all(
        hits.map(async (h) => {
          const e = await getEntry(c.env.DB, h.id);
          return e ? { ...e, score: h.score } : null;
        }),
      )
    ).filter((e): e is NonNullable<typeof e> => e !== null);
    return c.json({ success: true, entries, count: entries.length, mode: 'semantic' });
  }

  const entries = await searchEntries(c.env.DB, q, owner_id, entry_type, undefined, library, source);
  return c.json({ success: true, entries, count: entries.length, mode: 'keyword' });
});

// GET /entries/:id
entryRoutes.get('/:id', async (c) => {
  const entry = await getEntry(c.env.DB, c.req.param('id'));
  if (!entry) return c.json({ success: false, error: 'not found' }, 404);
  return c.json({ success: true, entry });
});

// PATCH /entries/:id
entryRoutes.patch('/:id', async (c) => {
  const body = await c.req.json().catch(() => ({}));
  const entry = await updateEntry(c.env.DB, c.req.param('id'), body);
  if (!entry) return c.json({ success: false, error: 'not found' }, 404);
  // 內容改了 → 重 embed（保持向量新鮮）。embedOnWrite 內部自會檢查模組開 + entry 是否 embeddable。
  if (embedEnabled(c.env) && body.content !== undefined) {
    c.executionCtx.waitUntil(embedOnWrite(c.env, entry).catch(() => {}));
  }
  return c.json({ success: true, entry });
});

// DELETE /entries/:id
entryRoutes.delete('/:id', async (c) => {
  // 模組開 → 連帶刪向量（避免孤兒向量）。失敗不致命。
  if (embedEnabled(c.env)) {
    c.executionCtx.waitUntil(c.env.VECTORIZE!.deleteByIds([c.req.param('id')]).then(() => {}).catch(() => {}));
  }
  await deleteEntry(c.env.DB, c.req.param('id'));
  return c.json({ success: true });
});
