import { and, count, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm";
import { bigint, index, integer, jsonb, pgTable, primaryKey, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
//#region src/errors.ts
var ProductValidationError = class extends Error {
	field;
	detail;
	name = "ProductValidationError";
	code = "PRODUCT_VALIDATION";
	constructor(field, detail) {
		super(detail ? `product validation failed: ${field} — ${detail}` : `product validation failed: ${field}`);
		this.field = field;
		this.detail = detail;
	}
};
var NoPriceForCurrencyError = class extends Error {
	currency;
	name = "NoPriceForCurrencyError";
	code = "NO_PRICE_FOR_CURRENCY";
	constructor(currency) {
		super(`no price for currency: ${currency}`);
		this.currency = currency;
	}
};
/**
* Family-canonical discriminated immutable-field error (commerce-conventions C5).
* REPLACES the bespoke per-field `KindImmutableError`. Every commerce sibling reuses
* this exact shape + the single `isImmutableFieldError` guard for its own immutable FKs.
*/
var ImmutableFieldError = class extends Error {
	field;
	name = "ImmutableFieldError";
	code = "IMMUTABLE_FIELD";
	constructor(field) {
		super(`field cannot be changed after create: ${field}`);
		this.field = field;
	}
};
function isProductValidationError(e) {
	return typeof e === "object" && e !== null && e.name === "ProductValidationError" && e.code === "PRODUCT_VALIDATION";
}
function isNoPriceForCurrencyError(e) {
	return typeof e === "object" && e !== null && e.name === "NoPriceForCurrencyError" && e.code === "NO_PRICE_FOR_CURRENCY";
}
function isImmutableFieldError(e) {
	return typeof e === "object" && e !== null && e.name === "ImmutableFieldError" && e.code === "IMMUTABLE_FIELD";
}
//#endregion
//#region src/migrate.ts
var CatalogMigrateError = class extends Error {
	detail;
	name = "CatalogMigrateError";
	code = "CATALOG_MIGRATE";
	constructor(detail) {
		super(`catalog migrate: ${detail}`);
		this.detail = detail;
	}
};
function isCatalogMigrateError(e) {
	return typeof e === "object" && e !== null && e.name === "CatalogMigrateError" && e.code === "CATALOG_MIGRATE";
}
const MIGRATION_STATEMENTS = [
	sql`
    CREATE TABLE IF NOT EXISTS product (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      kind text NOT NULL,
      vendor_id uuid,
      slug text NOT NULL,
      title text NOT NULL,
      description text,
      status text NOT NULL DEFAULT 'draft',
      media jsonb NOT NULL DEFAULT '[]'::jsonb,
      tags jsonb NOT NULL DEFAULT '[]'::jsonb,
      available_from timestamptz(3),
      available_until timestamptz(3),
      created_at timestamptz(3) NOT NULL DEFAULT NOW(),
      updated_at timestamptz(3) NOT NULL DEFAULT NOW()
    )
  `,
	sql`CREATE UNIQUE INDEX IF NOT EXISTS product_vendor_slug_uq ON product (vendor_id, slug)`,
	sql`CREATE INDEX IF NOT EXISTS product_vendor_idx ON product (vendor_id)`,
	sql`CREATE INDEX IF NOT EXISTS product_status_idx ON product (status)`,
	sql`
    CREATE TABLE IF NOT EXISTS variant (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      product_id uuid NOT NULL,
      sku text NOT NULL,
      attributes jsonb NOT NULL DEFAULT '{}'::jsonb,
      created_at timestamptz(3) NOT NULL DEFAULT NOW()
    )
  `,
	sql`CREATE UNIQUE INDEX IF NOT EXISTS variant_product_sku_uq ON variant (product_id, sku)`,
	sql`
    CREATE TABLE IF NOT EXISTS variant_price (
      variant_id uuid NOT NULL,
      currency text NOT NULL,
      amount bigint NOT NULL,
      price_mode text NOT NULL,
      PRIMARY KEY (variant_id, currency)
    )
  `,
	sql`CREATE UNIQUE INDEX IF NOT EXISTS variant_price_variant_currency_uq ON variant_price (variant_id, currency)`,
	sql`
    CREATE TABLE IF NOT EXISTS category (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      parent_id uuid,
      vendor_id uuid,
      name text NOT NULL,
      slug text NOT NULL
    )
  `,
	sql`CREATE UNIQUE INDEX IF NOT EXISTS category_vendor_slug_uq ON category (vendor_id, slug)`,
	sql`
    CREATE TABLE IF NOT EXISTS collection (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      vendor_id uuid,
      name text NOT NULL,
      slug text NOT NULL
    )
  `,
	sql`CREATE UNIQUE INDEX IF NOT EXISTS collection_vendor_slug_uq ON collection (vendor_id, slug)`,
	sql`
    CREATE TABLE IF NOT EXISTS product_category (
      product_id uuid NOT NULL,
      category_id uuid NOT NULL,
      PRIMARY KEY (product_id, category_id)
    )
  `,
	sql`
    CREATE TABLE IF NOT EXISTS product_collection (
      collection_id uuid NOT NULL,
      product_id uuid NOT NULL,
      position integer NOT NULL DEFAULT 0,
      PRIMARY KEY (collection_id, product_id)
    )
  `
];
async function pushSchema(db) {
	try {
		for (const statement of MIGRATION_STATEMENTS) await db.execute(statement);
	} catch (e) {
		throw new CatalogMigrateError(e instanceof Error ? e.message : String(e));
	}
}
//#endregion
//#region src/prices.ts
function resolvePrice(variant, currency) {
	const price = variant.prices.find((p) => p.currency === currency);
	if (!price) throw new NoPriceForCurrencyError(currency);
	return price;
}
//#endregion
//#region src/schema.ts
const product = pgTable("product", {
	id: uuid("id").primaryKey().defaultRandom(),
	kind: text("kind").$type().notNull(),
	vendorId: uuid("vendor_id"),
	slug: text("slug").notNull(),
	title: text("title").notNull(),
	description: text("description"),
	status: text("status").$type().notNull().default("draft"),
	media: jsonb("media").$type().notNull().default(sql`'[]'::jsonb`),
	tags: jsonb("tags").$type().notNull().default(sql`'[]'::jsonb`),
	availableFrom: timestamp("available_from", {
		withTimezone: true,
		precision: 3
	}),
	availableUntil: timestamp("available_until", {
		withTimezone: true,
		precision: 3
	}),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().defaultNow(),
	updatedAt: timestamp("updated_at", {
		withTimezone: true,
		precision: 3
	}).notNull().defaultNow()
}, (t) => [
	uniqueIndex("product_vendor_slug_uq").on(t.vendorId, t.slug),
	index("product_vendor_idx").on(t.vendorId),
	index("product_status_idx").on(t.status)
]);
const variant = pgTable("variant", {
	id: uuid("id").primaryKey().defaultRandom(),
	productId: uuid("product_id").notNull(),
	sku: text("sku").notNull(),
	attributes: jsonb("attributes").$type().notNull().default(sql`'{}'::jsonb`),
	createdAt: timestamp("created_at", {
		withTimezone: true,
		precision: 3
	}).notNull().defaultNow()
}, (t) => [uniqueIndex("variant_product_sku_uq").on(t.productId, t.sku)]);
const variantPrice = pgTable("variant_price", {
	variantId: uuid("variant_id").notNull(),
	currency: text("currency").notNull(),
	amount: bigint("amount", { mode: "bigint" }).notNull(),
	priceMode: text("price_mode").$type().notNull()
}, (t) => [primaryKey({ columns: [t.variantId, t.currency] }), uniqueIndex("variant_price_variant_currency_uq").on(t.variantId, t.currency)]);
const category = pgTable("category", {
	id: uuid("id").primaryKey().defaultRandom(),
	parentId: uuid("parent_id"),
	vendorId: uuid("vendor_id"),
	name: text("name").notNull(),
	slug: text("slug").notNull()
}, (t) => [uniqueIndex("category_vendor_slug_uq").on(t.vendorId, t.slug)]);
const collection = pgTable("collection", {
	id: uuid("id").primaryKey().defaultRandom(),
	vendorId: uuid("vendor_id"),
	name: text("name").notNull(),
	slug: text("slug").notNull()
}, (t) => [uniqueIndex("collection_vendor_slug_uq").on(t.vendorId, t.slug)]);
const productCategory = pgTable("product_category", {
	productId: uuid("product_id").notNull(),
	categoryId: uuid("category_id").notNull()
}, (t) => [primaryKey({ columns: [t.productId, t.categoryId] })]);
const productCollection = pgTable("product_collection", {
	collectionId: uuid("collection_id").notNull(),
	productId: uuid("product_id").notNull(),
	position: integer("position").notNull().default(0)
}, (t) => [primaryKey({ columns: [t.collectionId, t.productId] })]);
const catalogSchema = {
	product,
	variant,
	variantPrice,
	category,
	collection,
	productCategory,
	productCollection
};
//#endregion
//#region src/reads.ts
const SLUG_RE = /^[a-z0-9-]+$/;
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const DEFAULT_PAGE_SIZE = 20;
/** Catalog's OWN cost-justified safe-`IN` bound — never mirror a sibling module's cap. */
const MAX_ENTITY_IDS = 256;
/** A6 visibility floor — enforced in SQL for public reads, never post-filtered in JS. */
function publicVisibilityPredicate(now) {
	return and(eq(product.status, "active"), or(isNull(product.availableFrom), lte(product.availableFrom, now)), or(isNull(product.availableUntil), gt(product.availableUntil, now)));
}
function buildProductFilters(opts) {
	const filters = [];
	if (opts.audience === "public") filters.push(publicVisibilityPredicate(opts.now ?? /* @__PURE__ */ new Date()));
	if (opts.vendorId !== void 0) filters.push(eq(product.vendorId, opts.vendorId));
	return filters;
}
function toVariant(row, prices) {
	return {
		id: row.id,
		productId: row.productId,
		sku: row.sku,
		attributes: row.attributes,
		prices
	};
}
function toProduct(row, variants) {
	return {
		id: row.id,
		kind: row.kind,
		vendorId: row.vendorId ?? null,
		slug: row.slug,
		title: row.title,
		description: row.description ?? void 0,
		status: row.status,
		media: row.media,
		tags: row.tags,
		availableFrom: row.availableFrom ?? null,
		availableUntil: row.availableUntil ?? null,
		createdAt: row.createdAt,
		updatedAt: row.updatedAt,
		variants
	};
}
async function hydrateProducts(db, rows) {
	if (rows.length === 0) return [];
	const productIds = rows.map((r) => r.id);
	const variantRows = await db.select().from(variant).where(inArray(variant.productId, productIds));
	const variantIds = variantRows.map((v) => v.id);
	const priceRows = variantIds.length > 0 ? await db.select().from(variantPrice).where(inArray(variantPrice.variantId, variantIds)) : [];
	const pricesByVariant = /* @__PURE__ */ new Map();
	for (const p of priceRows) {
		const list = pricesByVariant.get(p.variantId) ?? [];
		list.push({
			currency: p.currency,
			amount: p.amount,
			priceMode: p.priceMode
		});
		pricesByVariant.set(p.variantId, list);
	}
	const variantsByProduct = /* @__PURE__ */ new Map();
	for (const v of variantRows) {
		const list = variantsByProduct.get(v.productId) ?? [];
		list.push(toVariant(v, pricesByVariant.get(v.id) ?? []));
		variantsByProduct.set(v.productId, list);
	}
	return rows.map((row) => toProduct(row, variantsByProduct.get(row.id) ?? []));
}
async function hydrateOne(db, row) {
	const [hydrated] = await hydrateProducts(db, [row]);
	return hydrated;
}
function listFilterClauses(filter, db) {
	const clauses = [];
	let entityIdsCapped = false;
	const now = filter.now ?? /* @__PURE__ */ new Date();
	if (filter.audience === "public") clauses.push(publicVisibilityPredicate(now));
	if (filter.kind) clauses.push(eq(product.kind, filter.kind));
	if (filter.status) clauses.push(eq(product.status, filter.status));
	if (filter.vendorId !== void 0) clauses.push(eq(product.vendorId, filter.vendorId));
	if (filter.tag) clauses.push(sql`${product.tags} @> ${JSON.stringify([filter.tag])}::jsonb`);
	if (filter.category) clauses.push(inArray(product.id, db.select({ id: productCategory.productId }).from(productCategory).innerJoin(category, eq(category.id, productCategory.categoryId)).where(eq(category.slug, filter.category))));
	if (filter.collection) clauses.push(inArray(product.id, db.select({ id: productCollection.productId }).from(productCollection).innerJoin(collection, eq(collection.id, productCollection.collectionId)).where(eq(collection.slug, filter.collection))));
	if (filter.entityIds !== void 0) {
		const validAll = filter.entityIds.filter((id) => UUID_RE.test(id));
		const valid = validAll.slice(0, MAX_ENTITY_IDS);
		entityIdsCapped = validAll.length > MAX_ENTITY_IDS;
		if (valid.length === 0) clauses.push(sql`false`);
		else clauses.push(inArray(product.id, valid));
	}
	return {
		clauses,
		entityIdsCapped
	};
}
async function getProductBySlug(db, slug, opts) {
	if (!SLUG_RE.test(slug)) return null;
	const filters = [eq(product.slug, slug), ...buildProductFilters(opts)];
	const [row] = await db.select().from(product).where(and(...filters)).limit(1);
	return row ? hydrateOne(db, row) : null;
}
async function getProductById(db, id, opts) {
	if (!UUID_RE.test(id)) return null;
	const filters = [eq(product.id, id), ...buildProductFilters(opts)];
	const [row] = await db.select().from(product).where(and(...filters)).limit(1);
	return row ? hydrateOne(db, row) : null;
}
async function listProducts(db, filter) {
	const page = Math.max(filter.page ?? 1, 1);
	const pageSize = DEFAULT_PAGE_SIZE;
	const offset = (page - 1) * pageSize;
	const { clauses, entityIdsCapped } = listFilterClauses(filter, db);
	const where = clauses.length ? and(...clauses) : void 0;
	const [totalRow] = await db.select({ value: count() }).from(product).where(where);
	const total = Number(totalRow?.value ?? 0);
	return {
		items: await hydrateProducts(db, await db.select().from(product).where(where).limit(pageSize).offset(offset)),
		total,
		page,
		pageSize,
		...entityIdsCapped ? { entityIdsCapped: true } : {}
	};
}
//#endregion
//#region src/writes.ts
/**
* Throw `ImmutableFieldError(field)` ONLY when the caller explicitly supplies a DIFFERENT
* value for a create-time-frozen field (`provided !== undefined && provided !== existing`).
* Same-value or omitted = idempotent no-op (a full-object resubmit must never throw).
*/
function assertImmutableUnchanged(field, provided, existing) {
	if (provided !== void 0 && provided !== existing) throw new ImmutableFieldError(field);
}
function isUniqueViolation(e) {
	let cur = e;
	while (cur) {
		const code = cur?.code;
		const msg = cur instanceof Error ? cur.message : String(cur);
		if (code === "23505" || /product_vendor_slug_uq|duplicate key|unique constraint/i.test(msg)) return true;
		cur = cur instanceof Error ? cur.cause : void 0;
	}
	return false;
}
async function catchSlugDuplicate(fn) {
	try {
		return await fn();
	} catch (e) {
		if (isUniqueViolation(e)) throw new ProductValidationError("slug", "duplicate");
		throw e;
	}
}
async function upsertProduct(db, input) {
	if (!SLUG_RE.test(input.slug)) throw new ProductValidationError("slug");
	const vendorId = input.vendorId ?? null;
	const now = /* @__PURE__ */ new Date();
	if (input.id) {
		const [existing] = await db.select().from(product).where(eq(product.id, input.id)).limit(1);
		if (existing) {
			assertImmutableUnchanged("kind", input.kind, existing.kind);
			assertImmutableUnchanged("vendorId", input.vendorId, existing.vendorId);
			await catchSlugDuplicate(() => db.update(product).set({
				slug: input.slug,
				title: input.title,
				description: input.description ?? existing.description,
				status: input.status ?? existing.status,
				media: input.media ?? existing.media,
				tags: input.tags ?? existing.tags,
				availableFrom: input.availableFrom !== void 0 ? input.availableFrom : existing.availableFrom,
				availableUntil: input.availableUntil !== void 0 ? input.availableUntil : existing.availableUntil,
				updatedAt: now
			}).where(eq(product.id, input.id)));
			const updated = await getProductById(db, input.id, { audience: "admin" });
			if (!updated) throw new ProductValidationError("id", "not found after update");
			return updated;
		}
	}
	const [created] = await catchSlugDuplicate(() => db.insert(product).values({
		id: input.id,
		kind: input.kind,
		vendorId,
		slug: input.slug,
		title: input.title,
		description: input.description,
		status: input.status ?? "draft",
		media: input.media ?? [],
		tags: input.tags ?? [],
		availableFrom: input.availableFrom ?? null,
		availableUntil: input.availableUntil ?? null,
		createdAt: now,
		updatedAt: now
	}).returning());
	const hydrated = await getProductById(db, created.id, { audience: "admin" });
	if (!hydrated) throw new ProductValidationError("id", "not found after create");
	return hydrated;
}
async function setVariantPrices(db, variantId, prices) {
	await db.delete(variantPrice).where(eq(variantPrice.variantId, variantId));
	if (prices.length === 0) return;
	await db.insert(variantPrice).values(prices.map((p) => ({
		variantId,
		currency: p.currency,
		amount: p.amount,
		priceMode: p.priceMode
	})));
}
async function setProductStatus(db, id, status) {
	await db.update(product).set({
		status,
		updatedAt: /* @__PURE__ */ new Date()
	}).where(eq(product.id, id));
}
//#endregion
export { CatalogMigrateError, ImmutableFieldError, NoPriceForCurrencyError, ProductValidationError, catalogSchema, category, collection, getProductById, getProductBySlug, isCatalogMigrateError, isImmutableFieldError, isNoPriceForCurrencyError, isProductValidationError, listProducts, product, productCategory, productCollection, pushSchema, resolvePrice, setProductStatus, setVariantPrices, upsertProduct, variant, variantPrice };
