import { and, count, desc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { bigint, bigserial, index, pgTable, text } from "drizzle-orm/pg-core";
//#region src/inbox-cursor.ts
/**
* Keyset cursor over NUMERIC (createdAtMs, seq) — collation-safe across pg dialects.
* Ported from @platform-modules/comments/src/cursor.ts (spec §5 inbox parity).
*/
var InvalidCursorError = class extends Error {
	name = "InvalidCursorError";
	constructor() {
		super("invalid pagination cursor");
	}
};
const DEFAULT_LIMIT = 20;
const MAX_LIMIT = 200;
const PLUS = /\+/g;
const SLASH = /\//g;
const TRAILING_EQ = /=+$/;
const DASH = /-/g;
const UNDERSCORE = /_/g;
function toBase64Url(value) {
	return btoa(value).replace(PLUS, "-").replace(SLASH, "_").replace(TRAILING_EQ, "");
}
function fromBase64Url(value) {
	let base64 = value.replace(DASH, "+").replace(UNDERSCORE, "/");
	const pad = base64.length % 4;
	if (pad) base64 += "=".repeat(4 - pad);
	return atob(base64);
}
function isKeyset(value) {
	return typeof value === "object" && value !== null && "c" in value && "s" in value && typeof value.c === "number" && typeof value.s === "number" && Number.isFinite(value.c) && Number.isFinite(value.s);
}
function encodeCursor(key) {
	const payload = {
		c: key.createdAtMs,
		s: key.seq
	};
	return toBase64Url(JSON.stringify(payload));
}
function decodeCursor(cursor) {
	try {
		const json = fromBase64Url(cursor);
		const parsed = JSON.parse(json);
		if (!isKeyset(parsed)) throw new InvalidCursorError();
		return {
			createdAtMs: parsed.c,
			seq: parsed.s
		};
	} catch (e) {
		if (e instanceof InvalidCursorError) throw e;
		throw new InvalidCursorError();
	}
}
/** Clamp untrusted limit — non-finite/undefined/<=0 → default; huge → max. Never throws. */
function clampLimit(value, opts) {
	const dflt = opts?.defaultLimit ?? DEFAULT_LIMIT;
	const max = opts?.maxLimit ?? MAX_LIMIT;
	if (value === void 0 || !Number.isFinite(value) || value <= 0) return dflt;
	const t = Math.trunc(value);
	if (t <= 0) return dflt;
	return Math.min(t, max);
}
//#endregion
//#region src/inbox-schema.ts
const notifications = pgTable("notifications", {
	seq: bigserial("seq", { mode: "number" }).notNull(),
	id: text("id").primaryKey(),
	userId: text("user_id").notNull(),
	title: text("title").notNull(),
	body: text("body"),
	href: text("href"),
	kind: text("kind"),
	createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(),
	createdAt: text("created_at").notNull(),
	readAt: text("read_at")
}, (t) => [index("notifications_user_keyset_idx").on(t.userId, t.createdAtMs, t.seq), index("notifications_user_unread_idx").on(t.userId).where(sql`read_at IS NULL`)]);
const notificationsSchema = { notifications };
/** Pure DDL helper — column names/types match the drizzle table above exactly (parity requirement). */
function notificationsTableSql(table = "notifications") {
	return `
CREATE TABLE IF NOT EXISTS ${table} (
  seq        bigserial NOT NULL,
  id         text PRIMARY KEY,
  user_id    text NOT NULL,
  title      text NOT NULL,
  body       text NULL,
  href       text NULL,
  kind       text NULL,
  created_at_ms bigint NOT NULL,
  created_at text NOT NULL,
  read_at    text NULL
);
CREATE INDEX IF NOT EXISTS ${table}_user_keyset_idx ON ${table} (user_id, created_at_ms, seq);
CREATE INDEX IF NOT EXISTS ${table}_user_unread_idx ON ${table} (user_id) WHERE read_at IS NULL;
`.trim();
}
//#endregion
//#region src/inbox.ts
var InboxValidationError = class extends Error {
	field;
	detail;
	name = "InboxValidationError";
	constructor(field, detail) {
		super(`inbox invalid: ${field} — ${detail}`);
		this.field = field;
		this.detail = detail;
	}
};
const MAX_MARK_IDS = 1e3;
function deriveCreatedAt(now) {
	const createdAtMs = Date.parse(now());
	if (Number.isNaN(createdAtMs)) throw new RangeError("deriveCreatedAt: now() returned an unparseable timestamp");
	return {
		createdAtMs,
		createdAt: new Date(createdAtMs).toISOString()
	};
}
function toRecord(row) {
	return {
		id: row.id,
		userId: row.userId,
		title: row.title,
		...row.body != null ? { body: row.body } : {},
		...row.href != null ? { href: row.href } : {},
		...row.kind != null ? { kind: row.kind } : {},
		createdAt: row.createdAt,
		readAt: row.readAt ?? null
	};
}
function createDbNotificationStore(db) {
	return {
		async put(input) {
			const id = crypto.randomUUID();
			const { createdAtMs, createdAt } = deriveCreatedAt(() => (/* @__PURE__ */ new Date()).toISOString());
			await db.insert(notifications).values({
				id,
				userId: input.userId,
				title: input.title,
				body: input.body ?? null,
				href: input.href ?? null,
				kind: input.kind ?? null,
				createdAtMs,
				createdAt,
				readAt: null
			});
			return {
				id,
				userId: input.userId,
				title: input.title,
				...input.body != null ? { body: input.body } : {},
				...input.href != null ? { href: input.href } : {},
				...input.kind != null ? { kind: input.kind } : {},
				createdAt,
				readAt: null
			};
		},
		async list(userId, query = {}) {
			const limit = clampLimit(query.limit);
			const filters = [eq(notifications.userId, userId)];
			if (query.unreadOnly) filters.push(isNull(notifications.readAt));
			if (query.cursor) {
				const key = decodeCursor(query.cursor);
				filters.push(or(lt(notifications.createdAtMs, key.createdAtMs), and(eq(notifications.createdAtMs, key.createdAtMs), lt(notifications.seq, key.seq))));
			}
			const rows = await db.select().from(notifications).where(and(...filters)).orderBy(desc(notifications.createdAtMs), desc(notifications.seq)).limit(limit + 1);
			const hasMore = rows.length > limit;
			const pageRows = hasMore ? rows.slice(0, limit) : rows;
			const items = pageRows.map(toRecord);
			let nextCursor = null;
			if (hasMore && pageRows.length > 0) {
				const last = pageRows[pageRows.length - 1];
				nextCursor = encodeCursor({
					createdAtMs: last.createdAtMs,
					seq: last.seq
				});
			}
			return {
				items,
				nextCursor
			};
		},
		async unreadCount(userId) {
			const [row] = await db.select({ n: count() }).from(notifications).where(and(eq(notifications.userId, userId), isNull(notifications.readAt)));
			return row?.n ?? 0;
		},
		async markRead(userId, ids) {
			const now = (/* @__PURE__ */ new Date()).toISOString();
			if (ids === "all") {
				await db.update(notifications).set({ readAt: now }).where(and(eq(notifications.userId, userId), isNull(notifications.readAt)));
				return;
			}
			if (ids.length === 0) return;
			if (ids.length > MAX_MARK_IDS) throw new InboxValidationError("ids", `exceeds ${MAX_MARK_IDS}`);
			await db.update(notifications).set({ readAt: now }).where(and(eq(notifications.userId, userId), inArray(notifications.id, ids), isNull(notifications.readAt)));
		}
	};
}
/** Persists to NotificationStore — the inbox delivery channel over the shipped notify() pipeline. */
function createInboxChannel(store) {
	return {
		channel: "inbox",
		async send(rendered, recipient) {
			const r = recipient;
			if (!r?.userId || typeof r.userId !== "string") return {
				ok: false,
				error: {
					message: "inbox recipient missing userId",
					retryable: false
				}
			};
			try {
				return {
					ok: true,
					id: (await store.put({
						userId: r.userId,
						title: rendered.subject ?? "notification",
						body: rendered.body,
						href: r.href,
						kind: r.kind
					})).id
				};
			} catch {
				return {
					ok: false,
					error: {
						message: "inbox delivery failed",
						retryable: true
					}
				};
			}
		}
	};
}
//#endregion
export { InboxValidationError, InvalidCursorError, createDbNotificationStore, createInboxChannel, notifications, notificationsSchema, notificationsTableSql };
