import { and, eq } from "drizzle-orm";
import { isLocaleActive, listLanguages, markFailed, setTranslation, translationValue } from "@platform-modules/i18n-content";
import { executeWithFallback, getAdapter } from "@platform-modules/ai";
//#region src/errors.ts
var I18nTranslatorError = class extends Error {
	code;
	name = "I18nTranslatorError";
	_tag = "I18nTranslatorError";
	constructor(code, message) {
		super(message);
		this.code = code;
	}
};
var UnknownLocaleError = class extends I18nTranslatorError {
	locale;
	code = "UNKNOWN_LOCALE";
	name = "UnknownLocaleError";
	constructor(locale) {
		super("UNKNOWN_LOCALE", `locale '${locale}' is not active or does not exist`);
		this.locale = locale;
	}
};
var SourceUnavailableError = class extends I18nTranslatorError {
	entityType;
	entityId;
	code = "SOURCE_UNAVAILABLE";
	name = "SourceUnavailableError";
	constructor(entityType, entityId) {
		super("SOURCE_UNAVAILABLE", `source text unavailable for ${entityType}/${entityId}`);
		this.entityType = entityType;
		this.entityId = entityId;
	}
};
var NoAdaptersConfiguredError = class extends I18nTranslatorError {
	code = "NO_ADAPTERS_CONFIGURED";
	name = "NoAdaptersConfiguredError";
	constructor() {
		super("NO_ADAPTERS_CONFIGURED", "no AI adapters are configured; register at least one adapter via setAdapterFactories");
	}
};
function isI18nTranslatorError(e) {
	return e instanceof Error && e._tag === "I18nTranslatorError";
}
function isUnknownLocaleError(e) {
	return isI18nTranslatorError(e) && e.code === "UNKNOWN_LOCALE" && typeof e.locale === "string";
}
function isSourceUnavailableError(e) {
	return isI18nTranslatorError(e) && e.code === "SOURCE_UNAVAILABLE" && typeof e.entityType === "string" && typeof e.entityId === "string";
}
function isNoAdaptersConfiguredError(e) {
	return isI18nTranslatorError(e) && e.code === "NO_ADAPTERS_CONFIGURED";
}
//#endregion
//#region src/hash.ts
/**
* Returns a SHA-256 hex digest of the normalised source text.
* NFC normalisation and trim are applied before hashing, so purely whitespace-only
* source edits (leading/trailing spaces, equivalent Unicode sequences) produce
* the same hash and are intentionally treated as unchanged.
*/
const encoder = new TextEncoder();
async function hashSource(text) {
	const normalized = text.normalize("NFC").trim();
	const data = encoder.encode(normalized);
	const digest = await crypto.subtle.digest("SHA-256", data);
	return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
//#endregion
//#region src/translate.ts
const PROVIDER_TRY_ORDER = [
	"openai-compat",
	"anthropic",
	"google",
	"workers-ai"
];
const PROVIDER_DEFAULT_MODEL = {
	"openai-compat": "gpt-4o-mini",
	anthropic: "claude-haiku-4-5",
	google: "gemini-1.5-flash",
	"workers-ai": "@cf/meta/llama-3.1-8b-instruct"
};
function resolveFallbackChain(modelId) {
	const resolvedModelId = modelId || void 0;
	const chain = [];
	for (const provider of PROVIDER_TRY_ORDER) try {
		chain.push({
			adapter: getAdapter(provider, {}),
			model: PROVIDER_DEFAULT_MODEL[provider]
		});
	} catch {
		continue;
	}
	if (chain.length === 0) throw new NoAdaptersConfiguredError();
	if (resolvedModelId) chain[0].model = resolvedModelId;
	return chain;
}
function buildTranslationPrompt(sourceLocale, targetLocale, fields) {
	const fieldLines = fields.map((f) => `- ${f.fieldKey}: ${f.value}`).join("\n");
	return [
		`Translate the following fields from ${sourceLocale} to ${targetLocale}.`,
		"Return ONLY valid JSON with the same field keys and translated string values.",
		"Fields:",
		fieldLines
	].join("\n");
}
/**
* Strip markdown code fences and/or prose preamble before JSON.parse.
* Models (gpt-4o-mini, gemini-1.5-flash, llama) routinely emit ```json
* wrappers or leading prose — raw JSON.parse throws on those, marking the
* entire field-batch FAILED. This extracts the JSON object robustly.
*/
function extractJson(content) {
	const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/);
	if (fenced) return fenced[1].trim();
	const start = content.indexOf("{");
	const end = content.lastIndexOf("}");
	if (start !== -1 && end !== -1 && end > start) return content.slice(start, end + 1);
	return content.trim();
}
/** Maximum allowed length (chars) for a single translated field value. */
const MAX_TRANSLATION_FIELD_LENGTH = 5e4;
function parseTranslationResponse(content, fieldKeys) {
	const parsed = JSON.parse(extractJson(content));
	if (typeof parsed !== "object" || parsed === null) throw new Error("translation response is not a JSON object");
	const record = parsed;
	const out = {};
	for (const fieldKey of fieldKeys) {
		const value = record[fieldKey];
		if (typeof value !== "string") throw new Error(`missing or invalid translation for field '${fieldKey}'`);
		if (value.length === 0) throw new Error(`empty translation returned for field '${fieldKey}'`);
		if (value.length > MAX_TRANSLATION_FIELD_LENGTH) throw new Error(`translation for field '${fieldKey}' exceeds maximum length (${value.length} chars)`);
		out[fieldKey] = value;
	}
	return out;
}
async function translateEntity(q, args) {
	for (const locale of args.targetLocales) if (!await isLocaleActive(q, locale)) throw new UnknownLocaleError(locale);
	let sourceLocale;
	let sourceFields;
	try {
		const sourceResult = await args.source.getSourceText({
			entityType: args.entityType,
			entityId: args.entityId,
			fields: args.fields
		});
		sourceLocale = sourceResult.locale;
		sourceFields = sourceResult.fields;
	} catch {
		throw new SourceUnavailableError(args.entityType, args.entityId);
	}
	const fieldHashes = /* @__PURE__ */ new Map();
	for (const fieldKey of args.fields) if (fieldKey in sourceFields) fieldHashes.set(fieldKey, await hashSource(sourceFields[fieldKey]));
	const skipUnchanged = args.skipUnchanged !== false;
	const normalizedModelId = args.modelId || null;
	const results = [];
	for (const targetLocale of args.targetLocales) {
		const fieldOutcomes = [];
		const toTranslate = [];
		if (targetLocale === sourceLocale) {
			for (const fieldKey of args.fields) fieldOutcomes.push({
				fieldKey,
				outcome: "SKIPPED"
			});
			results.push({
				locale: targetLocale,
				fields: fieldOutcomes
			});
			continue;
		}
		for (const fieldKey of args.fields) {
			if (!(fieldKey in sourceFields)) {
				fieldOutcomes.push({
					fieldKey,
					outcome: "SKIPPED"
				});
				continue;
			}
			const sourceHash = fieldHashes.get(fieldKey);
			const sourceValue = sourceFields[fieldKey];
			if (sourceValue === "") {
				fieldOutcomes.push({
					fieldKey,
					outcome: "SKIPPED"
				});
				continue;
			}
			if (skipUnchanged) {
				const [existing] = await q.select({
					sourceHash: translationValue.sourceHash,
					status: translationValue.status
				}).from(translationValue).where(and(eq(translationValue.entityType, args.entityType), eq(translationValue.entityId, args.entityId), eq(translationValue.fieldKey, fieldKey), eq(translationValue.locale, targetLocale))).limit(1);
				if (existing?.sourceHash === sourceHash && existing.status === "OK") {
					fieldOutcomes.push({
						fieldKey,
						outcome: "SKIPPED"
					});
					continue;
				}
			}
			toTranslate.push({
				fieldKey,
				value: sourceValue,
				sourceHash
			});
		}
		if (toTranslate.length === 0) {
			results.push({
				locale: targetLocale,
				fields: fieldOutcomes
			});
			continue;
		}
		const chain = resolveFallbackChain(args.modelId);
		const reqModel = (args.modelId || void 0) ?? chain[0].model;
		const prompt = buildTranslationPrompt(sourceLocale, targetLocale, toTranslate.map((f) => ({
			fieldKey: f.fieldKey,
			value: f.value
		})));
		let translated;
		try {
			translated = parseTranslationResponse((await executeWithFallback({
				model: reqModel,
				messages: [{
					role: "user",
					content: prompt
				}],
				maxTokens: 8192,
				temperature: 0
			}, [...chain], { continueOnNonRetryable: true })).content, toTranslate.map((f) => f.fieldKey));
		} catch {
			for (const item of toTranslate) {
				await markFailed(q, {
					entityType: args.entityType,
					entityId: args.entityId,
					fieldKey: item.fieldKey,
					locale: targetLocale,
					sourceHash: item.sourceHash,
					modelId: normalizedModelId
				});
				fieldOutcomes.push({
					fieldKey: item.fieldKey,
					outcome: "FAILED"
				});
			}
			results.push({
				locale: targetLocale,
				fields: fieldOutcomes
			});
			continue;
		}
		for (const item of toTranslate) try {
			await setTranslation(q, {
				entityType: args.entityType,
				entityId: args.entityId,
				fieldKey: item.fieldKey,
				locale: targetLocale,
				value: translated[item.fieldKey],
				sourceHash: item.sourceHash,
				modelId: normalizedModelId,
				manualOverride: false
			});
			fieldOutcomes.push({
				fieldKey: item.fieldKey,
				outcome: "OK"
			});
		} catch {
			await markFailed(q, {
				entityType: args.entityType,
				entityId: args.entityId,
				fieldKey: item.fieldKey,
				locale: targetLocale,
				sourceHash: item.sourceHash,
				modelId: normalizedModelId
			});
			fieldOutcomes.push({
				fieldKey: item.fieldKey,
				outcome: "FAILED"
			});
		}
		results.push({
			locale: targetLocale,
			fields: fieldOutcomes
		});
	}
	return { results };
}
//#endregion
//#region src/worker.ts
const TRANSLATION_SWEEP_JOB = "i18n.translation.sweep";
function passthroughSchema() {
	return { "~standard": {
		version: 1,
		vendor: "i18n-translator",
		validate: (value) => ({ value })
	} };
}
function idempotencyKey(unit) {
	return `${unit.entityType}:${unit.entityId}:${unit.fieldKey}:${unit.locale}:${unit.sourceHash}`;
}
function entityBatchKey(unit) {
	return `${unit.entityType}:${unit.entityId}:${unit.locale}`;
}
async function runSweepInternal(env, limitPerLocale) {
	const languages = await listLanguages(env.q, { activeOnly: true });
	for (const language of languages) {
		const staleUnits = await env.selectStale(env.q, {
			locale: language.code,
			limit: limitPerLocale
		});
		const pendingByKey = /* @__PURE__ */ new Map();
		for (const unit of staleUnits) {
			const key = idempotencyKey(unit);
			if (await env.idempotencyStore.seen(key)) continue;
			pendingByKey.set(key, unit);
		}
		const batches = /* @__PURE__ */ new Map();
		for (const unit of pendingByKey.values()) {
			const batchKey = entityBatchKey(unit);
			const group = batches.get(batchKey) ?? [];
			group.push(unit);
			batches.set(batchKey, group);
		}
		for (const units of batches.values()) {
			const first = units[0];
			const unitByField = new Map(units.map((unit) => [unit.fieldKey, unit]));
			try {
				const localeResult = (await translateEntity(env.q, {
					entityType: first.entityType,
					entityId: first.entityId,
					fields: units.map((unit) => unit.fieldKey),
					targetLocales: [first.locale],
					source: env.source
				})).results.find((entry) => entry.locale === first.locale);
				if (!localeResult) continue;
				for (const field of localeResult.fields) {
					const unit = unitByField.get(field.fieldKey);
					if (!unit) continue;
					if (field.outcome === "OK" || field.outcome === "SKIPPED") await env.idempotencyStore.mark(idempotencyKey(unit));
				}
			} catch {
				continue;
			}
		}
	}
}
function createTranslationWorker(deps) {
	const env = {
		q: deps.q,
		source: deps.source,
		idempotencyStore: deps.idempotencyStore,
		selectStale: deps.selectStale
	};
	deps.registry.register(TRANSLATION_SWEEP_JOB, passthroughSchema(), async (handlerEnv) => {
		await runSweepInternal(handlerEnv, 100);
	});
	return { async runSweep(args = {}) {
		await runSweepInternal(env, args.limitPerLocale ?? 100);
	} };
}
//#endregion
export { hashSource as a, SourceUnavailableError as c, isNoAdaptersConfiguredError as d, isSourceUnavailableError as f, translateEntity as i, UnknownLocaleError as l, createTranslationWorker as n, I18nTranslatorError as o, isUnknownLocaleError as p, idempotencyKey as r, NoAdaptersConfiguredError as s, TRANSLATION_SWEEP_JOB as t, isI18nTranslatorError as u };
