import { r as isInsufficientBalanceError, t as InsufficientBalanceError } from "./errors-CvXxEMgH.js";
import { bigint, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
import { and, eq, getTableColumns, gte, sql } from "drizzle-orm";
//#region src/schema.ts
const ledgerEntries = pgTable("ledger_entries", {
	id: uuid("id").primaryKey().defaultRandom(),
	delta: bigint("delta", { mode: "bigint" }).notNull(),
	currency: text("currency"),
	reason: text("reason").notNull(),
	ref: jsonb("ref").$type(),
	idempotencyKey: text("idempotency_key").notNull(),
	createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
}, (table) => [uniqueIndex("ledger_entries_idempotency_key_uq").on(table.idempotencyKey)]);
/** Optional default balance projection — hosts may use their own column instead. */
const walletBalances = pgTable("wallet_balances", {
	ownerId: text("owner_id").primaryKey(),
	balance: bigint("balance", { mode: "bigint" }).notNull().default(0n),
	updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
});
const ledgerSchema = {
	ledgerEntries,
	walletBalances
};
//#endregion
//#region src/append.ts
async function appendEntry(tx, input) {
	const rows = await tx.insert(ledgerEntries).values({
		delta: input.delta,
		currency: input.currency ?? null,
		reason: input.reason,
		ref: input.ref ?? null,
		idempotencyKey: input.key
	}).onConflictDoNothing({ target: ledgerEntries.idempotencyKey }).returning({ id: ledgerEntries.id });
	return {
		inserted: rows.length > 0,
		id: rows[0]?.id ?? null
	};
}
//#endregion
//#region src/balance.ts
function columnPropertyName(table, column) {
	const columns = getTableColumns(table);
	for (const [key, col] of Object.entries(columns)) if (col === column) return key;
	throw new Error("balanceColumn is not part of the target table");
}
async function getBalance(querier, target) {
	if (target.kind === "wallet") {
		const [row] = await querier.select({ balance: walletBalances.balance }).from(walletBalances).where(eq(walletBalances.ownerId, target.ownerId)).limit(1);
		return row?.balance ?? 0n;
	}
	const [row] = await querier.select({ balance: target.balanceColumn }).from(target.table).where(target.where).limit(1);
	return coerceBalance(row?.balance) ?? 0n;
}
/**
* Cross-driver bigint coercion. pglite returns a `bigint`; neon-http
* deserializes a bigint column as a string. Numbers are unexpected for a
* `{ mode: 'bigint' }` column but coerced for safety. `null`/`undefined`
* (no row) → `null` so the caller can distinguish "no row" from a real `0`.
*/
function coerceBalance(value) {
	if (typeof value === "bigint") return value;
	if (typeof value === "string" || typeof value === "number") return BigInt(value);
	return null;
}
async function guardedDebitUpdate(tx, target, amount) {
	if (amount <= 0n) throw new Error("guardedDebitUpdate: amount must be positive");
	if (target.kind === "wallet") {
		const updated = await tx.update(walletBalances).set({
			balance: sql`${walletBalances.balance} - ${amount}`,
			updatedAt: /* @__PURE__ */ new Date()
		}).where(and(eq(walletBalances.ownerId, target.ownerId), gte(walletBalances.balance, amount))).returning({ balance: walletBalances.balance });
		if (updated.length === 0) return null;
		return coerceBalance(updated[0]?.balance);
	}
	const balanceKey = columnPropertyName(target.table, target.balanceColumn);
	const updated = await tx.update(target.table).set({ [balanceKey]: sql`${target.balanceColumn} - ${amount}` }).where(and(target.where, gte(target.balanceColumn, amount))).returning({ balance: target.balanceColumn });
	if (updated.length === 0) return null;
	return coerceBalance(updated[0]?.balance);
}
//#endregion
//#region src/debit.ts
/**
* Optimistic guarded debit: idempotent ledger insert FIRST, then a guarded
* `UPDATE … WHERE balance >= amount`. On a duplicate key (retry) the balance
* mutation is skipped; on insufficient balance it throws and the just-inserted
* ledger row must roll back.
*
* MUST run inside a transaction (`db.transaction((tx) => debit(tx, …))`). The
* insert-first/throw-on-insufficient atomicity depends on the throw rolling the
* ledger row back — passing a non-transactional `Querier` autocommits the insert
* and leaves an orphan entry with the balance unmutated.
*/
async function debit(tx, input) {
	if (input.amount <= 0n) throw new Error("debit: amount must be positive");
	const { inserted } = await appendEntry(tx, {
		key: input.key,
		delta: -input.amount,
		reason: input.reason,
		ref: input.ref
	});
	if (!inserted) return {
		inserted: false,
		balance: await getBalance(tx, input.target)
	};
	const nextBalance = await guardedDebitUpdate(tx, input.target, input.amount);
	if (nextBalance == null) throw new InsufficientBalanceError();
	return {
		inserted: true,
		balance: nextBalance
	};
}
//#endregion
//#region src/debit-with-read.ts
/**
* Pessimistic debit: `SELECT … FOR UPDATE` the target row(s), let the caller
* `fn` decide sufficiency/split from the locked rows, then idempotent ledger
* insert FIRST and apply the caller's mutation. On a duplicate key (retry) the
* mutation is skipped; a caller `fn` that throws (insufficient) leaves no
* partial state.
*
* MUST run inside a transaction (`db.transaction((tx) => debitWithRead(tx, …))`).
* The `FOR UPDATE` lock and the insert-first/rollback atomicity only hold inside
* a transaction — a non-transactional `Querier` neither holds the lock nor rolls
* back the inserted ledger row if `fn` or `apply` throws.
*/
async function debitWithRead(tx, input, fn) {
	const plan = await fn(await tx.select().from(input.lock.table).where(input.lock.where).for("update"));
	const { inserted, id } = await appendEntry(tx, {
		key: input.key,
		delta: plan.delta,
		reason: input.reason,
		ref: input.ref
	});
	if (!inserted) return {
		inserted: false,
		id: null
	};
	await plan.apply(tx);
	return {
		inserted: true,
		id
	};
}
//#endregion
export { InsufficientBalanceError, appendEntry, debit, debitWithRead, getBalance, isInsufficientBalanceError, ledgerEntries, ledgerSchema, walletBalances };
