// Per-tenant credit ledger — operations on the tenant_credits SQLite
// table. Two buckets per user:
//   purchased_balance — permanent (a la carte purchases + admin grants
//                       + anon-trial carry-over on signup). Never
//                       wiped or refilled.
//   replenish_balance — refillable (initial signup grant + periodic
//                       anniversary refill to tenant_default_credits).
//                       Leftovers at the end of a period are FORFEIT.
//
// Spend order: replenish first, then purchased — refillable bucket
// is "use it or lose it" so it makes sense to burn first.
//
// Multi-mode only. Single-mode doesn't use this table at all.

import { getDb } from "./db.js";
import { getConfigSnapshot } from "./config.js";

const DAY_MS = 24 * 60 * 60 * 1000;

// addMonthClamped(date) — calendar-month add for monthly replenishment.
// Mirrors the relay's same-named helper: Jan 31 → Feb 28/29 (clamped),
// Feb 28 → Mar 28 (preserve day-of-month). Returns a Date.
function addMonthClamped(date) {
  const d = new Date(date.getTime());
  const year = d.getUTCFullYear();
  const month = d.getUTCMonth();
  const day = d.getUTCDate();
  const lastDayOfTargetMonth = new Date(
    Date.UTC(year, month + 2, 0),
  ).getUTCDate();
  const targetDay = Math.min(day, lastDayOfTargetMonth);
  return new Date(
    Date.UTC(
      year,
      month + 1,
      targetDay,
      d.getUTCHours(),
      d.getUTCMinutes(),
      d.getUTCSeconds(),
      d.getUTCMilliseconds(),
    ),
  );
}

// Resolve the next anniversary boundary after `last` for the configured
// period. Returns null if period is "off" (no replenishment).
function nextReplenishAt(lastEpochMs, period) {
  if (period === "off" || !lastEpochMs) return null;
  if (period === "daily") return lastEpochMs + DAY_MS;
  if (period === "weekly") return lastEpochMs + 7 * DAY_MS;
  if (period === "monthly") {
    return addMonthClamped(new Date(lastEpochMs)).getTime();
  }
  return null;
}

// Read the current operator config relevant to credits. Cached per-
// request by getConfigSnapshot (which polls config.js's snapshot).
async function readCreditConfig() {
  const snap = await getConfigSnapshot();
  const period =
    snap?.tenant_credit_replenish_period &&
    ["off", "daily", "weekly", "monthly"].includes(
      snap.tenant_credit_replenish_period,
    )
      ? snap.tenant_credit_replenish_period
      : "off";
  const defaultCredits = Math.max(
    0,
    parseInt(snap?.tenant_default_credits ?? 5, 10) || 0,
  );
  return { period, defaultCredits };
}

// Internal: ensure a tenant_credits row exists for this user. New users
// (just signed up but no row yet — shouldn't happen post-auth-routes-fix
// but defensive) get a row with replenish_balance = current default.
function ensureRow(userId, defaultCredits) {
  const db = getDb();
  const existing = db
    .prepare("SELECT * FROM tenant_credits WHERE user_id = ?")
    .get(userId);
  if (existing) return existing;
  const now = Date.now();
  db.prepare(
    `INSERT INTO tenant_credits
       (user_id, purchased_balance, replenish_balance, last_replenish_at,
        lifetime_granted, lifetime_consumed)
     VALUES (?, 0, ?, ?, ?, 0)`,
  ).run(userId, defaultCredits, now, defaultCredits);
  return db
    .prepare("SELECT * FROM tenant_credits WHERE user_id = ?")
    .get(userId);
}

// Apply periodic refill if due. Returns the (possibly updated) row.
// Anniversary semantics: if last_replenish_at + period_ms <= now, the
// replenish bucket is RESET to defaultCredits (any leftover is
// forfeit), and last_replenish_at is advanced. Idempotent if called
// multiple times in the same period (no-op).
//
// "Multi-period catch-up" rule: if a user has been idle for several
// periods, only ONE refill is applied (we don't stack refills from
// missed periods). They effectively lost the credits for the missed
// days — same as a per-day allowance in any other SaaS.
function maybeReplenish(row, period, defaultCredits) {
  if (period === "off") return row;
  const due = nextReplenishAt(row.last_replenish_at, period);
  if (due === null) return row;
  const now = Date.now();
  if (now < due) return row;
  const db = getDb();
  db.prepare(
    `UPDATE tenant_credits
     SET replenish_balance = ?, last_replenish_at = ?
     WHERE user_id = ?`,
  ).run(defaultCredits, now, row.user_id);
  return {
    ...row,
    replenish_balance: defaultCredits,
    last_replenish_at: now,
  };
}

// ── Public API ──────────────────────────────────────────────────────────

// getOrInit(userId) — fetch the tenant_credits row, lazily refilling if
// the period boundary has passed. Returns the canonical shape for
// callers that just want to display + compute totals.
export async function getOrInit(userId) {
  if (!userId) return null;
  const { period, defaultCredits } = await readCreditConfig();
  let row = ensureRow(userId, defaultCredits);
  row = maybeReplenish(row, period, defaultCredits);
  return {
    user_id: row.user_id,
    purchased: row.purchased_balance,
    replenish: row.replenish_balance,
    total: row.purchased_balance + row.replenish_balance,
    last_replenish_at: row.last_replenish_at,
    lifetime_granted: row.lifetime_granted,
    lifetime_consumed: row.lifetime_consumed,
    period, // surfaces config in returned shape for UI hints
  };
}

// gateAndDebit(userId) — atomic: refill if due, check total > 0, debit
// one credit (replenish first, then purchased). Returns
// { ok: true, total, source: "replenish"|"purchased" } on success,
// { ok: false, reason: "no_credits", total: 0 } if nothing available.
export async function gateAndDebit(userId) {
  if (!userId) return { ok: false, reason: "no_user_id" };
  const state = await getOrInit(userId);
  if (!state) return { ok: false, reason: "no_user_id" };
  if (state.total <= 0) {
    return { ok: false, reason: "no_credits", total: 0 };
  }
  const db = getDb();
  const tx = db.transaction(() => {
    if (state.replenish > 0) {
      db.prepare(
        `UPDATE tenant_credits
         SET replenish_balance = replenish_balance - 1,
             lifetime_consumed = lifetime_consumed + 1
         WHERE user_id = ?`,
      ).run(userId);
      return "replenish";
    }
    db.prepare(
      `UPDATE tenant_credits
       SET purchased_balance = purchased_balance - 1,
           lifetime_consumed = lifetime_consumed + 1
       WHERE user_id = ?`,
    ).run(userId);
    return "purchased";
  });
  const source = tx();
  // Re-read for the new total. Cheap — same row, no replenish-check
  // needed since we just touched it.
  const fresh = db
    .prepare(
      "SELECT purchased_balance, replenish_balance FROM tenant_credits WHERE user_id = ?",
    )
    .get(userId);
  return {
    ok: true,
    total: (fresh.purchased_balance || 0) + (fresh.replenish_balance || 0),
    source,
  };
}

// addPurchased(userId, amount) — increment the permanent bucket. Used
// by admin grants AND a la carte purchase apply AND anon-trial
// carry-over on signup. Increments lifetime_granted too.
//
// We don't replenish-check here — adding to the permanent bucket
// shouldn't trigger a refill side-effect. The refill happens lazily
// on the next getOrInit() call.
export function addPurchased(userId, amount) {
  if (!userId || !Number.isFinite(amount) || amount <= 0) return null;
  const db = getDb();
  const existing = db
    .prepare("SELECT user_id FROM tenant_credits WHERE user_id = ?")
    .get(userId);
  if (existing) {
    db.prepare(
      `UPDATE tenant_credits
       SET purchased_balance = purchased_balance + ?,
           lifetime_granted = lifetime_granted + ?
       WHERE user_id = ?`,
    ).run(amount, amount, userId);
  } else {
    // Row didn't exist — initialize WITHOUT a replenishable seed
    // (this user hasn't been through the signup flow on this Recap;
    // probably an admin granting credits before they sign in).
    const now = Date.now();
    db.prepare(
      `INSERT INTO tenant_credits
         (user_id, purchased_balance, replenish_balance, last_replenish_at,
          lifetime_granted, lifetime_consumed)
       VALUES (?, ?, 0, ?, ?, 0)`,
    ).run(userId, amount, now, amount);
  }
  return db
    .prepare("SELECT * FROM tenant_credits WHERE user_id = ?")
    .get(userId);
}

// seedSignup(userId, amount?) — initialize a tenant_credits row at
// signup. Seeds replenish_balance with the configured default
// (overridable for testing), sets last_replenish_at = now so the
// first refill boundary is computed correctly.
export async function seedSignup(userId, amountOverride) {
  if (!userId) return null;
  const { defaultCredits } = await readCreditConfig();
  const amount =
    typeof amountOverride === "number" && amountOverride >= 0
      ? amountOverride
      : defaultCredits;
  const db = getDb();
  const existing = db
    .prepare("SELECT user_id FROM tenant_credits WHERE user_id = ?")
    .get(userId);
  if (existing) return existing; // don't re-seed
  const now = Date.now();
  db.prepare(
    `INSERT INTO tenant_credits
       (user_id, purchased_balance, replenish_balance, last_replenish_at,
        lifetime_granted, lifetime_consumed)
     VALUES (?, 0, ?, ?, ?, 0)`,
  ).run(userId, amount, now, amount);
  return db
    .prepare("SELECT * FROM tenant_credits WHERE user_id = ?")
    .get(userId);
}
