/**
 * Safe integer-cents helpers. Money is stored as decimal strings in SQLite
 * (Prisma BigInt is limited to signed 64-bit ~9.22e18).
 */

export function toCentsBigInt(value) {
  if (typeof value === 'bigint') return value;
  if (value == null || value === '') return 0n;
  if (typeof value === 'number') {
    if (!Number.isFinite(value)) return 0n;
    return BigInt(Math.trunc(value));
  }
  const s = String(value).trim();
  if (!s) return 0n;
  // Accept plain ints or decimals (truncate toward zero)
  const m = s.match(/^([+-]?\d+)/);
  if (!m) return 0n;
  try {
    return BigInt(m[1]);
  } catch {
    return 0n;
  }
}

/** JSON-safe cents (string) so clients keep full precision. */
export function centsJson(value) {
  return toCentsBigInt(value).toString();
}

/**
 * Parse a credits amount (may include decimals) into integer cents, BigInt-safe.
 * "15.5" → 1550n, "15000000000000" → 1500000000000000n.
 * Returns null if invalid / negative.
 */
export function creditsToCents(raw) {
  if (raw == null || raw === '') return null;
  if (typeof raw === 'bigint') {
    return raw < 0n ? null : raw * 100n;
  }
  if (typeof raw === 'number') {
    if (!Number.isFinite(raw) || raw < 0) return null;
    // Prefer string path when outside safe integer range after *100
    if (!Number.isSafeInteger(Math.round(raw * 100))) {
      return creditsToCents(String(raw));
    }
    return BigInt(Math.round(raw * 100));
  }
  const s = String(raw).trim().replace(',', '.');
  if (!s) return null;
  const m = s.match(/^(\d+)(?:\.(\d{1,2})\d*)?$/);
  if (!m) return null;
  try {
    const whole = BigInt(m[1]);
    const frac = m[2] ? m[2].padEnd(2, '0') : '00';
    return whole * 100n + BigInt(frac);
  } catch {
    return null;
  }
}

/** Format cents as a decimal credits string without Number precision loss. */
export function centsToCreditsString(cents) {
  const bi = toCentsBigInt(cents);
  const neg = bi < 0n;
  const abs = neg ? -bi : bi;
  const whole = abs / 100n;
  const frac = (abs % 100n).toString().padStart(2, '0');
  const trimmed = frac === '00' ? whole.toString() : `${whole}.${frac}`.replace(/0+$/, '').replace(/\.$/, '');
  return `${neg ? '-' : ''}${trimmed}`;
}

/**
 * Multiply cents by (1 + pct/100), floored. Uses integer math only.
 */
export function applyPercentFloor(cents, pct) {
  const base = toCentsBigInt(cents);
  const p = Math.max(0, Math.floor(Number(pct) || 0));
  if (base <= 0n) return 0n;
  if (p <= 0) return base;
  return (base * BigInt(100 + p)) / 100n;
}

/**
 * Multiply cents by a float rate (e.g. 0.1), floored, via fixed-point (1e6).
 */
export function applyRateFloor(cents, rate) {
  const base = toCentsBigInt(cents);
  if (base <= 0n) return 0n;
  const r = Number(rate);
  if (!Number.isFinite(r) || r <= 0) return 0n;
  const scale = 1_000_000;
  const rateInt = Math.max(0, Math.floor(r * scale));
  return (base * BigInt(rateInt)) / BigInt(scale);
}

export function minBigInt(a, b) {
  return a <= b ? a : b;
}

export function maxBigInt(a, b) {
  return a >= b ? a : b;
}

/** Add delta to a stored cents string/bigint; returns new value as string. */
export function addCents(current, delta) {
  return (toCentsBigInt(current) + toCentsBigInt(delta)).toString();
}

/** SQLite INTEGER is signed 64-bit — beyond this, use CAS (string BigInt). */
const SQLITE_INT64_MAX = 9223372036854775807n;

function fitsSqliteInt64(value) {
  const v = toCentsBigInt(value);
  return v >= 0n && v <= SQLITE_INT64_MAX;
}

function insufficientBalanceError(message = 'Insufficient balance') {
  const err = new Error(message);
  err.status = 400;
  err.code = 'INSUFFICIENT_BALANCE';
  return err;
}

function balanceConflictError() {
  const err = new Error('Balance update conflict, retry');
  err.status = 409;
  err.code = 'BALANCE_CONFLICT';
  return err;
}

async function findUserOrThrow(tx, userId) {
  const user = await tx.user.findUnique({ where: { id: userId } });
  if (!user) {
    const err = new Error('User not found');
    err.status = 401;
    throw err;
  }
  return user;
}

/**
 * Atomically deduct cost: `balance = balance - cost` WHERE `balance >= cost`.
 * SQL arithmetic when values fit SQLite INTEGER; otherwise optimistic CAS.
 * @returns {Promise<user>}
 */
export async function spendUserBalance(tx, userId, cost, { message } = {}) {
  const costBi = toCentsBigInt(cost);
  if (costBi < 0n) throw new Error('cost must be non-negative');
  if (costBi === 0n) return findUserOrThrow(tx, userId);

  const costStr = costBi.toString();

  for (let attempt = 0; attempt < 12; attempt += 1) {
    const user = await findUserOrThrow(tx, userId);
    const bal = toCentsBigInt(user.balance);
    if (bal < costBi) throw insufficientBalanceError(message);

    if (fitsSqliteInt64(bal) && fitsSqliteInt64(costBi)) {
      const rows = await tx.$executeRaw`
        UPDATE "User"
        SET balance = CAST((CAST(balance AS INTEGER) - CAST(${costStr} AS INTEGER)) AS TEXT)
        WHERE id = ${userId}
          AND CAST(balance AS INTEGER) >= CAST(${costStr} AS INTEGER)
      `;
      if (rows > 0) return findUserOrThrow(tx, userId);
      continue;
    }

    const next = (bal - costBi).toString();
    const updated = await tx.user.updateMany({
      where: { id: userId, balance: user.balance },
      data: { balance: next },
    });
    if (updated.count === 1) return { ...user, balance: next };
  }

  throw balanceConflictError();
}

/**
 * Atomically credit amount: `balance = balance + amount`.
 * @returns {Promise<user>}
 */
export async function creditUserBalance(tx, userId, amount) {
  const amt = toCentsBigInt(amount);
  if (amt < 0n) throw new Error('amount must be non-negative');
  if (amt === 0n) return findUserOrThrow(tx, userId);

  const amtStr = amt.toString();

  for (let attempt = 0; attempt < 12; attempt += 1) {
    const user = await findUserOrThrow(tx, userId);
    const bal = toCentsBigInt(user.balance);
    const nextBi = bal + amt;

    if (fitsSqliteInt64(bal) && fitsSqliteInt64(amt) && fitsSqliteInt64(nextBi)) {
      const rows = await tx.$executeRaw`
        UPDATE "User"
        SET balance = CAST((CAST(balance AS INTEGER) + CAST(${amtStr} AS INTEGER)) AS TEXT)
        WHERE id = ${userId}
      `;
      if (rows > 0) return findUserOrThrow(tx, userId);
      continue;
    }

    const next = nextBi.toString();
    const updated = await tx.user.updateMany({
      where: { id: userId, balance: user.balance },
      data: { balance: next },
    });
    if (updated.count === 1) return { ...user, balance: next };
  }

  throw balanceConflictError();
}

/**
 * Apply signed delta (BigInt-safe). Negative → atomic spend; positive → credit.
 * `currentBalance` is ignored (kept for call-site compatibility).
 * @returns {Promise<user>}
 */
export async function adjustUserBalance(tx, userId, _currentBalance, delta) {
  const d = toCentsBigInt(delta);
  if (d < 0n) return spendUserBalance(tx, userId, -d);
  if (d > 0n) return creditUserBalance(tx, userId, d);
  return findUserOrThrow(tx, userId);
}
