// 背景維護寫入的共用 D1 每日額度（Arcrun#85 D69，2026-08-11）。
//
// 為什麼需要這個模組（不是每個 caller 各自算）：
//   D68 已經替「補算向量」的 Workers AI 呼叫量設了每日軟上限（embed.ts
//   DEFAULT_BACKFILL_DAILY_LIMIT），但 leo 逐行複核後指出還有一個沒堵的洞——
//   世代核對（reconcileEmbedGeneration）**不打 AI，卻一樣逐筆寫 D1**（補標 content_hash
//   或重置 is_embedded），47 萬筆候選 ≈ 4.7 倍 D1 免費層 100,000 rows written/日，而它
//   當時零保護。2026-08-11 leo 補了第二刀：**標庫（library backfill）也是同一種操作**
//   ——多筆 D1 row write、不打 AI——若各自設一顆獨立計數器，做標庫時會把 reconcile
//   的閘繞過去（兩者加起來還是可能燒穿同一顆 D1）。
//   ⇒ 兩者必須共用同一顆「今天 D1 背景維護寫入還剩多少」計數器，這裡就是那顆計數器。
//
// 儲存精神完全比照 execution-log.ts checkUsage／embed.ts getBackfillUsageToday：單一
// entries 列/日（entry_type='kbdb_maintenance_usage'），upsert，不新增表（D38）。
//
// 額度怎麼選（不是拍腦袋，比照 execution-log.ts DEFAULT_DAILY_LIMIT 的既有算法）：
//   D1 免費層 100,000 rows written/日。execution_log 自設 20%（20,000）留給知識卡；
//   本模組管的是「背景維護」（reconcile + 標庫 backfill，兩者都是低優先、非使用者
//   當下等待的操作），同樣自設 20%（20,000/日）——不是硬性 Cloudflare 限制，是不讓
//   背景維護把當天寫入額度和知識卡片的正常寫入／execution_log 搶光的自我節制，
//   可用 env.KBDB_MAINTENANCE_DAILY_WRITE_LIMIT 覆寫。
import type { Bindings } from '../types';

export const DEFAULT_MAINTENANCE_DAILY_WRITE_LIMIT = 20000;

export function maintenanceDailyLimit(env: Pick<Bindings, 'KBDB_MAINTENANCE_DAILY_WRITE_LIMIT'>): number {
  const raw = env.KBDB_MAINTENANCE_DAILY_WRITE_LIMIT;
  const n = raw ? parseInt(raw, 10) : NaN;
  return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAINTENANCE_DAILY_WRITE_LIMIT;
}

function utcDay(): string {
  return new Date().toISOString().slice(0, 10);
}

/** 額度計數器 entries id（單一列/日；不分租戶——D1 rows-written 額度是實例級，非租戶級）。 */
function maintenanceUsageId(): string {
  return `kbdb-maintenance-usage:${utcDay()}`;
}

/**
 * 今天背景維護寫入已消耗的筆數。讀取失敗（含壞資料）誠實視為 0（caller 決定是否 fail-open，
 * 精神同 embed.ts getBackfillUsageToday）。
 */
export async function getMaintenanceUsageToday(db: D1Database): Promise<number> {
  const row = await db
    .prepare('SELECT metadata_json FROM entries WHERE id = ?')
    .bind(maintenanceUsageId())
    .first<{ metadata_json: string | null }>();
  if (!row) return 0;
  try {
    const parsed = row.metadata_json ? (JSON.parse(row.metadata_json) as { writes?: number }) : {};
    return Number(parsed.writes) || 0;
  } catch {
    return 0;
  }
}

/** 今天背景維護額度用量 +by（upsert：讀現有列 → +by → UPDATE，不存在則 INSERT，冪等日切）。 */
export async function addMaintenanceUsage(db: D1Database, by: number): Promise<void> {
  if (by <= 0) return;
  const id = maintenanceUsageId();
  const existing = await db
    .prepare('SELECT metadata_json FROM entries WHERE id = ?')
    .bind(id)
    .first<{ metadata_json: string | null }>();
  let prev = 0;
  if (existing) {
    try {
      const parsed = existing.metadata_json ? (JSON.parse(existing.metadata_json) as { writes?: number }) : {};
      prev = Number(parsed.writes) || 0;
    } catch {
      prev = 0;
    }
    await db
      .prepare('UPDATE entries SET metadata_json = ?, updated_at = unixepoch() WHERE id = ?')
      .bind(JSON.stringify({ day: utcDay(), writes: prev + by }), id)
      .run();
  } else {
    await db
      .prepare(`INSERT INTO entries (id, entry_type, metadata_json) VALUES (?, 'kbdb_maintenance_usage', ?)`)
      .bind(id, JSON.stringify({ day: utcDay(), writes: by }))
      .run();
  }
}

export interface MaintenanceBudget {
  limit: number;
  used: number;
  remaining: number;
}

/** 今天還剩多少背景維護 D1 寫入額度（reconcile／標庫 backfill 呼叫前先問這個）。 */
export async function maintenanceBudgetToday(
  env: Pick<Bindings, 'KBDB_MAINTENANCE_DAILY_WRITE_LIMIT'>,
  db: D1Database,
): Promise<MaintenanceBudget> {
  const limit = maintenanceDailyLimit(env);
  let used = 0;
  try {
    used = await getMaintenanceUsageToday(db);
  } catch {
    used = 0; // fail-open：計數器本身故障（含 D1 額度打滿）不該連背景維護都做不了
  }
  return { limit, used, remaining: Math.max(0, limit - used) };
}
