// Entry CRUD — atomic data + tree (project/workflow via parent_id). Base, D1 only.
import type { Bindings, Entry } from '../types';

function uid(prefix: string): string {
  // deterministic-enough unique id without Math.random in hot path is fine here;
  // crypto.randomUUID is available in Workers runtime.
  return `${prefix}_${crypto.randomUUID()}`;
}

export interface CreateEntryInput {
  content?: string | null;
  entry_type: string;
  owner_id?: string | null;
  parent_id?: string | null;
  page_name?: string | null;
  refs_json?: string;
  tags_json?: string;
  task_status?: string | null;
  confidence?: number | null;
  metadata_json?: string | null;
  id?: string;
}

export async function createEntry(db: D1Database, input: CreateEntryInput): Promise<Entry> {
  const id = input.id ?? uid('e');
  await db
    .prepare(
      `INSERT INTO entries (id, content, entry_type, owner_id, parent_id, page_name, refs_json, tags_json, task_status, confidence, metadata_json)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    )
    .bind(
      id,
      input.content ?? null,
      input.entry_type,
      input.owner_id ?? null,
      input.parent_id ?? null,
      input.page_name ?? null,
      input.refs_json ?? '[]',
      input.tags_json ?? '[]',
      input.task_status ?? null,
      input.confidence ?? null,
      input.metadata_json ?? null,
    )
    .run();
  const row = await getEntry(db, id);
  if (!row) throw new Error('createEntry: insert succeeded but row not found');
  return row;
}

export async function getEntry(db: D1Database, id: string): Promise<Entry | null> {
  const row = await db.prepare('SELECT * FROM entries WHERE id = ?').bind(id).first<Entry>();
  return row ?? null;
}

export interface ListEntriesFilter {
  entry_type?: string;
  owner_id?: string;
  parent_id?: string;
  page_name?: string;   // exact-match lookup (e.g. skill-/example- idempotency key)
  source?: string;      // filter by metadata_json.$.source (ingest envelope source.uri). issue #5.1
  q?: string;           // keyword filter on content (LIKE). Arcrun#3 發現①：list 端點原本完全不吃
                         // search/q，caller 帶了也被靜默丟棄（不是 458K 筆搜不到，是這個 filter 沒接）。
  limit?: number;
  offset?: number;
}

export interface ListEntriesResult {
  entries: Entry[];
  total: number; // 符合本次篩選條件的「全部」筆數（不受 limit/offset 影響）。
                 // Arcrun#3 發現①「count 欄位語意誤導」：舊版 route 把 entries.length（分頁筆數）
                 // 當 count 回傳，容易被誤讀成「總共只有這幾筆」。total 才是真總數，count 仍保留＝本頁筆數。
}

export async function listEntries(db: D1Database, f: ListEntriesFilter = {}): Promise<ListEntriesResult> {
  const conds: string[] = [];
  const params: unknown[] = [];
  if (f.entry_type) { conds.push('entry_type = ?'); params.push(f.entry_type); }
  if (f.owner_id) { conds.push('owner_id = ?'); params.push(f.owner_id); }
  if (f.parent_id) { conds.push('parent_id = ?'); params.push(f.parent_id); }
  if (f.page_name) { conds.push('page_name = ?'); params.push(f.page_name); }
  // source is queryable via SQLite json_extract on the existing metadata_json TEXT column —
  // no new column / no migration (表不變鐵律). Per issue #5.1 (頂層化 source 成可查 filter).
  if (f.source) { conds.push("json_extract(metadata_json, '$.source') = ?"); params.push(f.source); }
  if (f.q) { conds.push('content LIKE ?'); params.push(`%${f.q}%`); }
  const where = conds.length ? `WHERE ${conds.join(' AND ')}` : '';
  const limit = Math.min(f.limit ?? 100, 1000);
  const offset = f.offset ?? 0;
  const [rowsRes, countRow] = await Promise.all([
    db
      .prepare(`SELECT * FROM entries ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
      .bind(...params, limit, offset)
      .all<Entry>(),
    db.prepare(`SELECT COUNT(*) as total FROM entries ${where}`).bind(...params).first<{ total: number }>(),
  ]);
  return { entries: rowsRes.results ?? [], total: countRow?.total ?? 0 };
}

export interface UpdateEntryInput {
  content?: string | null;
  parent_id?: string | null;
  page_name?: string | null;
  refs_json?: string;
  tags_json?: string;
  task_status?: string | null;
  confidence?: number | null;
  metadata_json?: string | null;
}

export async function updateEntry(db: D1Database, id: string, patch: UpdateEntryInput): Promise<Entry | null> {
  const cols: string[] = [];
  const params: unknown[] = [];
  const map: Record<string, unknown> = patch as Record<string, unknown>;
  for (const k of ['content', 'parent_id', 'page_name', 'refs_json', 'tags_json', 'task_status', 'confidence', 'metadata_json']) {
    if (k in map && map[k] !== undefined) { cols.push(`${k} = ?`); params.push(map[k]); }
  }
  if (cols.length === 0) return getEntry(db, id);
  cols.push('updated_at = unixepoch()');
  await db.prepare(`UPDATE entries SET ${cols.join(', ')} WHERE id = ?`).bind(...params, id).run();
  return getEntry(db, id);
}

export async function deleteEntry(db: D1Database, id: string): Promise<void> {
  await db.prepare('DELETE FROM entries WHERE id = ?').bind(id).run();
}

// D1 LIKE keyword search (base; semantic search is the optional embed module).
// entry_type: optional base filter (generic — caller passes any type, base stays type-agnostic).
export async function searchEntries(
  db: D1Database,
  q: string,
  owner_id?: string,
  entry_type?: string,
  limit = 50,
): Promise<Entry[]> {
  const conds = ['content LIKE ?'];
  const params: unknown[] = [`%${q}%`];
  if (owner_id) { conds.push('owner_id = ?'); params.push(owner_id); }
  if (entry_type) { conds.push('entry_type = ?'); params.push(entry_type); }
  const res = await db
    .prepare(`SELECT * FROM entries WHERE ${conds.join(' AND ')} ORDER BY updated_at DESC LIMIT ?`)
    .bind(...params, Math.min(limit, 200))
    .all<Entry>();
  return res.results ?? [];
}
