import { and, desc, eq, gte, ilike, lt, lte, or } from "drizzle-orm";
import { index, jsonb, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
//#region src/errors.ts
var InvalidCursorError = class extends Error {
	name = "InvalidCursorError";
	constructor() {
		super("INVALID_CURSOR");
	}
};
//#endregion
//#region src/cursor.ts
function toBase64Url(value) {
	return btoa(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function fromBase64Url(value) {
	let base64 = value.replace(/-/g, "+").replace(/_/g, "/");
	const pad = base64.length % 4;
	if (pad) base64 += "=".repeat(4 - pad);
	return atob(base64);
}
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function isAuditCursor(value) {
	return typeof value === "object" && value !== null && "createdAt" in value && "id" in value && typeof value.createdAt === "string" && typeof value.id === "string" && !Number.isNaN(Date.parse(value.createdAt)) && UUID_RE.test(value.id);
}
function encodeCursor(row) {
	const payload = {
		createdAt: row.createdAt.toISOString(),
		id: row.id
	};
	return toBase64Url(JSON.stringify(payload));
}
function decodeCursor(cursor) {
	try {
		const json = fromBase64Url(cursor);
		const parsed = JSON.parse(json);
		if (!isAuditCursor(parsed)) throw new InvalidCursorError();
		return parsed;
	} catch (error) {
		if (error instanceof InvalidCursorError) throw error;
		throw new InvalidCursorError();
	}
}
//#endregion
//#region src/schema.ts
const auditLog = pgTable("audit_log", {
	id: uuid("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
	actorId: text("actor_id"),
	actorType: text("actor_type"),
	actorLabel: text("actor_label"),
	action: text("action").notNull(),
	entityType: text("entity_type").notNull(),
	entityId: text("entity_id").notNull(),
	metadata: jsonb("metadata").$type(),
	ip: text("ip"),
	tenantId: text("tenant_id"),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
}, (table) => [
	index("audit_log_tenant_created_idx").on(table.tenantId, desc(table.createdAt)),
	index("audit_log_tenant_entity_idx").on(table.tenantId, table.entityType, table.entityId),
	index("audit_log_tenant_actor_idx").on(table.tenantId, table.actorId)
]);
const auditSchema = { auditLog };
//#endregion
//#region src/list-audit.ts
function toAuditRecord(row) {
	return {
		id: row.id,
		actorId: row.actorId,
		actorType: row.actorType,
		actorLabel: row.actorLabel,
		action: row.action,
		entityType: row.entityType,
		entityId: row.entityId,
		metadata: row.metadata,
		ip: row.ip,
		tenantId: row.tenantId,
		createdAt: row.createdAt
	};
}
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 200;
/** Clamp untrusted `limit`: non-finite → default; else trunc into [1, MAX_LIMIT]. */
function clampLimit(value) {
	if (value === void 0 || !Number.isFinite(value)) return DEFAULT_LIMIT;
	return Math.min(Math.max(Math.trunc(value), 1), MAX_LIMIT);
}
/** Reject an Invalid Date at the boundary rather than emit a cryptic driver RangeError. */
function assertValidDate(value, field) {
	if (Number.isNaN(value.getTime())) throw new RangeError(`listAudit: filter.${field} is not a valid Date`);
}
function buildFilterConditions(filter) {
	const conditions = [];
	if (filter.tenant !== void 0) conditions.push(eq(auditLog.tenantId, filter.tenant));
	if (filter.actor !== void 0) conditions.push(eq(auditLog.actorId, filter.actor));
	if (filter.entityType !== void 0) conditions.push(eq(auditLog.entityType, filter.entityType));
	if (filter.entityId !== void 0) conditions.push(eq(auditLog.entityId, filter.entityId));
	if (filter.action !== void 0) conditions.push(eq(auditLog.action, filter.action));
	if (filter.from !== void 0) {
		assertValidDate(filter.from, "from");
		conditions.push(gte(auditLog.createdAt, filter.from));
	}
	if (filter.to !== void 0) {
		assertValidDate(filter.to, "to");
		conditions.push(lte(auditLog.createdAt, filter.to));
	}
	if (filter.q !== void 0) {
		const pattern = `%${filter.q}%`;
		conditions.push(or(ilike(auditLog.action, pattern), ilike(auditLog.entityType, pattern), ilike(auditLog.actorLabel, pattern)));
	}
	if (filter.cursor !== void 0) {
		const cursor = decodeCursor(filter.cursor);
		const cursorCreatedAt = new Date(cursor.createdAt);
		conditions.push(or(lt(auditLog.createdAt, cursorCreatedAt), and(eq(auditLog.createdAt, cursorCreatedAt), lt(auditLog.id, cursor.id))));
	}
	return conditions;
}
/**
* List audit records, newest first, with keyset pagination (cursor = `(created_at_ms, id)`).
*
* `tenant` is OPTIONAL (single-tenant hosts store `tenant_id` null). When provided it
* constrains correctly; when omitted the query spans ALL tenants — scoping is the host's
* responsibility. On a multi-tenant audit surface, always pass `tenant` (fail-open default).
*/
async function listAudit(db, filter = {}) {
	const limit = clampLimit(filter.limit);
	const conditions = buildFilterConditions(filter);
	const whereClause = conditions.length > 0 ? and(...conditions) : void 0;
	const rows = await db.select().from(auditLog).where(whereClause).orderBy(desc(auditLog.createdAt), desc(auditLog.id)).limit(limit + 1);
	if (rows.length <= limit) return {
		items: rows.map(toAuditRecord),
		nextCursor: null
	};
	const kept = rows.slice(0, limit);
	const last = kept[kept.length - 1];
	return {
		items: kept.map(toAuditRecord),
		nextCursor: encodeCursor({
			createdAt: last.createdAt,
			id: last.id
		})
	};
}
//#endregion
//#region src/log-audit.ts
/**
* Append an audit record. **Never throws** — a write failure is swallowed and
* returned as `{ logged: false, error }` so it cannot break the operation being logged.
*
* CONTRACT (boundary spec amendment B): because failures are swallowed, this is the
* *decoupled* form — call it **after the business commit, or fire-and-forget**.
* Do NOT call it inside the caller's business transaction: a failed insert aborts the
* whole Postgres tx while the swallow hides it, so the mutation can commit with the
* audit row silently dropped. For transaction-atomic audit, insert into the exported
* `auditLog` schema directly within your own tx (a throw there correctly rolls back).
*/
async function logAudit(db, event) {
	try {
		const [row] = await db.insert(auditLog).values({
			actorId: event.actorId ?? null,
			actorType: event.actorType ?? null,
			actorLabel: event.actorLabel ?? null,
			action: event.action,
			entityType: event.entityType,
			entityId: event.entityId,
			metadata: event.metadata ?? null,
			ip: event.ip ?? null,
			tenantId: event.tenantId ?? null
		}).returning({ id: auditLog.id });
		return {
			logged: true,
			id: row.id
		};
	} catch (error) {
		return {
			logged: false,
			error
		};
	}
}
//#endregion
export { InvalidCursorError, auditLog, auditSchema, decodeCursor, encodeCursor, listAudit, logAudit };
