import { a as __exportAll } from "./runner-K8XK101B.mjs";
import { d as invalidateTaxonomyObjectCache } from "./object-cache-Bok5j2ae.mjs";
import { ulid } from "ulidx";

//#region src/database/repositories/taxonomy.ts
var taxonomy_exports = /* @__PURE__ */ __exportAll({ TaxonomyRepository: () => TaxonomyRepository });
/**
* Taxonomy repository for categories, tags, and other classification.
*
* Terms are per-locale. Translations of the same term share a `translation_group`
* ULID. `content_taxonomies.taxonomy_id` stores the translation_group so a single
* association spans every locale of a post.
*
* The repository does not resolve locale fallbacks on its own — callers supply
* the locale they want. Runtime helpers and handlers use `getFallbackChain()`
* from `i18n/config` when they need fallback behaviour.
*/
var TaxonomyRepository = class {
	constructor(db) {
		this.db = db;
	}
	/**
	* Create a new taxonomy term. When `translationOf` is set the new row joins
	* the source term's translation_group; otherwise a fresh group is minted
	* (matching the migration backfill pattern `translation_group = id`).
	*/
	async create(input) {
		const id = ulid();
		const parentInput = input.parentId === void 0 || input.parentId === "" ? null : input.parentId;
		const parentId = parentInput ? await this.resolveParentRef(parentInput) : null;
		let translationGroup = id;
		if (input.translationOf) {
			const source = await this.findById(input.translationOf);
			if (source?.translationGroup) translationGroup = source.translationGroup;
		}
		await this.db.insertInto("taxonomies").values({
			id,
			name: input.name,
			slug: input.slug,
			label: input.label,
			parent_id: parentId,
			data: input.data ? JSON.stringify(input.data) : null,
			...input.locale !== void 0 ? { locale: input.locale } : {},
			translation_group: translationGroup
		}).execute();
		invalidateTaxonomyObjectCache();
		const taxonomy = await this.findById(id);
		if (!taxonomy) throw new Error("Failed to create taxonomy");
		return taxonomy;
	}
	async findById(id) {
		const row = await this.db.selectFrom("taxonomies").selectAll().where("id", "=", id).executeTakeFirst();
		return row ? this.rowToTaxonomy(row) : null;
	}
	/**
	* Find a term by (name, slug). When `locale` is provided, filter by it.
	* When omitted, returns the lowest-locale-code match (deterministic across
	* calls). Mirrors `ContentRepository.findBySlug`.
	*/
	async findBySlug(name, slug, locale) {
		let query = this.db.selectFrom("taxonomies").selectAll().where("name", "=", name).where("slug", "=", slug);
		if (locale !== void 0) query = query.where("locale", "=", locale);
		const row = await query.orderBy("locale", "asc").executeTakeFirst();
		return row ? this.rowToTaxonomy(row) : null;
	}
	/**
	* Get all terms for a taxonomy (e.g., all categories).
	*
	* `id asc` is a stable tiebreaker for terms that share a label. Without it
	* the SQL ordering is implementation-defined when labels match, which
	* breaks keyset pagination over `(label, id)`.
	*/
	async findByName(name, options = {}) {
		let query = this.db.selectFrom("taxonomies").selectAll().where("name", "=", name).orderBy("label", "asc").orderBy("id", "asc");
		if (options.locale !== void 0) query = query.where("locale", "=", options.locale);
		if (options.parentId !== void 0) if (options.parentId === null) query = query.where("parent_id", "is", null);
		else query = query.where("parent_id", "=", options.parentId);
		return (await query.execute()).map((row) => this.rowToTaxonomy(row));
	}
	/**
	* Children of a term. Accepts a term id OR a translation_group and resolves
	* to the group, since `parent_id` stores the parent's translation_group.
	* Pass `locale` to scope to one locale's tree (children share the parent's
	* group across locales); omit it to find children in every locale (used to
	* block deletes that would orphan a sibling translation's subtree).
	*/
	async findChildren(parentIdOrGroup, locale) {
		const group = await this.resolveTranslationGroup(parentIdOrGroup);
		if (!group) return [];
		let query = this.db.selectFrom("taxonomies").selectAll().where("parent_id", "=", group).orderBy("label", "asc").orderBy("id", "asc");
		if (locale !== void 0) query = query.where("locale", "=", locale);
		return (await query.execute()).map((row) => this.rowToTaxonomy(row));
	}
	/**
	* Every translation sibling of a term (including itself), identified by
	* their shared `translation_group`.
	*/
	async findTranslations(translationGroup) {
		return (await this.db.selectFrom("taxonomies").selectAll().where("translation_group", "=", translationGroup).orderBy("locale", "asc").execute()).map((row) => this.rowToTaxonomy(row));
	}
	async update(id, input) {
		if (!await this.findById(id)) return null;
		const updates = {};
		if (input.slug !== void 0) updates.slug = input.slug;
		if (input.label !== void 0) updates.label = input.label;
		if (input.parentId !== void 0) updates.parent_id = input.parentId === "" || input.parentId === null ? null : await this.resolveParentRef(input.parentId);
		if (input.data !== void 0) updates.data = JSON.stringify(input.data);
		if (Object.keys(updates).length > 0) {
			await this.db.updateTable("taxonomies").set(updates).where("id", "=", id).execute();
			invalidateTaxonomyObjectCache();
		}
		return this.findById(id);
	}
	async delete(id) {
		const term = await this.findById(id);
		if (!term) return false;
		if (term.translationGroup) {
			if ((await this.db.selectFrom("taxonomies").select("id").where("translation_group", "=", term.translationGroup).where("id", "!=", id).execute()).length === 0) await this.db.deleteFrom("content_taxonomies").where("taxonomy_id", "=", term.translationGroup).execute();
		}
		const result = await this.db.deleteFrom("taxonomies").where("id", "=", id).executeTakeFirst();
		invalidateTaxonomyObjectCache();
		return (result.numDeletedRows ?? 0n) > 0n;
	}
	async attachToEntry(collection, entryId, taxonomyId) {
		const group = await this.resolveTranslationGroup(taxonomyId);
		if (!group) return;
		const row = {
			collection,
			entry_id: entryId,
			taxonomy_id: group
		};
		await this.db.insertInto("content_taxonomies").values(row).onConflict((oc) => oc.doNothing()).execute();
		invalidateTaxonomyObjectCache();
	}
	async detachFromEntry(collection, entryId, taxonomyId) {
		const group = await this.resolveTranslationGroup(taxonomyId);
		if (!group) return;
		await this.db.deleteFrom("content_taxonomies").where("collection", "=", collection).where("entry_id", "=", entryId).where("taxonomy_id", "=", group).execute();
		invalidateTaxonomyObjectCache();
	}
	/**
	* Taxonomy terms assigned to a content entry, resolved into a specific locale.
	* Terms whose translation_group lacks a row in the requested locale are
	* omitted — callers wanting fallback behaviour apply it themselves.
	*/
	async getTermsForEntry(collection, entryId, taxonomyName, locale) {
		let query = this.db.selectFrom("content_taxonomies").innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id").selectAll("taxonomies").where("content_taxonomies.collection", "=", collection).where("content_taxonomies.entry_id", "=", entryId);
		if (taxonomyName) query = query.where("taxonomies.name", "=", taxonomyName);
		if (locale !== void 0) query = query.where("taxonomies.locale", "=", locale);
		return (await query.orderBy("taxonomies.locale", "asc").execute()).map((row) => this.rowToTaxonomy(row));
	}
	/**
	* Replace all assignments of a given taxonomy for one content entry.
	* Term ids OR translation_groups are accepted and normalised to groups.
	*/
	async setTermsForEntry(collection, entryId, taxonomyName, termIds) {
		const groups = [];
		for (const id of termIds) {
			const group = await this.resolveTranslationGroup(id);
			if (group) groups.push(group);
		}
		const newGroups = new Set(groups);
		const current = await this.db.selectFrom("content_taxonomies").innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id").select(["content_taxonomies.taxonomy_id as group"]).distinct().where("content_taxonomies.collection", "=", collection).where("content_taxonomies.entry_id", "=", entryId).where("taxonomies.name", "=", taxonomyName).execute();
		const currentGroups = new Set(current.map((r) => r.group));
		const toRemove = [...currentGroups].filter((g) => !newGroups.has(g));
		if (toRemove.length > 0) await this.db.deleteFrom("content_taxonomies").where("collection", "=", collection).where("entry_id", "=", entryId).where("taxonomy_id", "in", toRemove).execute();
		const toAdd = [...newGroups].filter((g) => !currentGroups.has(g));
		if (toAdd.length > 0) await this.db.insertInto("content_taxonomies").values(toAdd.map((taxonomy_id) => ({
			collection,
			entry_id: entryId,
			taxonomy_id
		}))).onConflict((oc) => oc.doNothing()).execute();
		if (toRemove.length > 0 || toAdd.length > 0) invalidateTaxonomyObjectCache();
	}
	async clearEntryTerms(collection, entryId) {
		const result = await this.db.deleteFrom("content_taxonomies").where("collection", "=", collection).where("entry_id", "=", entryId).executeTakeFirst();
		const removed = Number(result.numDeletedRows ?? 0);
		if (removed > 0) invalidateTaxonomyObjectCache();
		return removed;
	}
	/**
	* Copy every term assignment from one content entry to another. Used when
	* creating a translation of a post so the new translation inherits the
	* source's term assignments. Safe to call when the source has no terms.
	*/
	async copyEntryTerms(collection, sourceEntryId, targetEntryId) {
		const rows = await this.db.selectFrom("content_taxonomies").select(["taxonomy_id"]).where("collection", "=", collection).where("entry_id", "=", sourceEntryId).execute();
		if (rows.length === 0) return;
		await this.db.insertInto("content_taxonomies").values(rows.map((r) => ({
			collection,
			entry_id: targetEntryId,
			taxonomy_id: r.taxonomy_id
		}))).onConflict((oc) => oc.doNothing()).execute();
		invalidateTaxonomyObjectCache();
	}
	/**
	* Count content entries that use any translation of this term. Accepts
	* either a term id or a translation_group — we normalise to the group.
	*/
	async countEntriesWithTerm(termIdOrGroup) {
		const group = await this.resolveTranslationGroup(termIdOrGroup);
		if (!group) return 0;
		const result = await this.db.selectFrom("content_taxonomies").select((eb) => eb.fn.count("entry_id").as("count")).where("taxonomy_id", "=", group).executeTakeFirst();
		return Number(result?.count ?? 0);
	}
	/**
	* Resolve a parent reference (a row id or a translation_group) to the value
	* persisted in `parent_id`: the parent's translation_group, which is
	* locale-agnostic so the child stays nested in every locale. A
	* translation_group normally equals its anchor row's id, which satisfies the
	* self-FK on `parent_id`. If that anchor row is missing (a translation whose
	* anchor was deleted), fall back to the id we were given so we never write a
	* dangling FK value.
	*/
	async resolveParentRef(idOrGroup) {
		const group = await this.resolveTranslationGroup(idOrGroup);
		if (!group) return idOrGroup;
		return await this.db.selectFrom("taxonomies").select("id").where("id", "=", group).executeTakeFirst() ? group : idOrGroup;
	}
	async resolveTranslationGroup(idOrGroup) {
		return (await this.db.selectFrom("taxonomies").select(["translation_group"]).where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)])).executeTakeFirst())?.translation_group ?? null;
	}
	/**
	* Batch count entries for multiple taxonomy translation_groups.
	* Chunks the query at SQL_BATCH_SIZE to stay below D1's bind-parameter limit.
	* Returns a Map from translation_group to count.
	*
	* Pass translation_groups (not term ids) — `content_taxonomies.taxonomy_id`
	* stores the translation_group so a single assignment spans every locale.
	*/
	async countEntriesForTerms(translationGroups) {
		if (translationGroups.length === 0) return /* @__PURE__ */ new Map();
		const { chunks, SQL_BATCH_SIZE } = await import("./chunks-B2isWQhr.mjs").then((n) => n.r);
		const counts = /* @__PURE__ */ new Map();
		for (const chunk of chunks(translationGroups, SQL_BATCH_SIZE)) {
			const rows = await this.db.selectFrom("content_taxonomies").select(["taxonomy_id", (eb) => eb.fn.count("entry_id").as("count")]).where("taxonomy_id", "in", chunk).groupBy("taxonomy_id").execute();
			for (const row of rows) counts.set(row.taxonomy_id, Number(row.count || 0));
		}
		return counts;
	}
	rowToTaxonomy(row) {
		return {
			id: row.id,
			name: row.name,
			slug: row.slug,
			label: row.label,
			parentId: row.parent_id,
			data: row.data ? JSON.parse(row.data) : null,
			locale: row.locale,
			translationGroup: row.translation_group
		};
	}
};

//#endregion
export { taxonomy_exports as n, TaxonomyRepository as t };
//# sourceMappingURL=taxonomy-BxGxZZdu.mjs.map