import { sql } from "kysely";
import { setRecordCustomFields } from "@open-mercato/core/modules/entities/lib/helpers";
import { validateCustomFieldValuesServer } from "@open-mercato/core/modules/entities/lib/validation";
import { sanitizeCustomFieldHtmlRichTextValuesServer } from "@open-mercato/core/modules/entities/lib/htmlRichTextSanitizer";
import {
  CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY
} from "../crud/types.js";
import { CrudHttpError } from "../crud/errors.js";
import { resolveRegisteredEntityTableName } from "../query/engine.js";
import { getEntityIds } from "../encryption/entityIds.js";
import { normalizeCustomFieldValues } from "../custom-fields/normalize.js";
import { parseBooleanToken } from "../boolean.js";
import { isReadProjectionAlwaysConsistent } from "./consistency.js";
import { isEventDeclared } from "../../modules/events/index.js";
import { createLogger } from "../logger/index.js";
const logger = createLogger("shared").child({ component: "data-engine" });
const undeclaredEventWarned = /* @__PURE__ */ new Set();
function warnIfUndeclaredEvent(eventName, context) {
  if (isEventDeclared(eventName)) return;
  if (undeclaredEventWarned.has(eventName)) return;
  undeclaredEventWarned.add(eventName);
  logger.warn("Emitting undeclared event \u2014 declare it in the owning module events.ts (createModuleEvents) so the event registry stays authoritative", { context, eventName });
}
function __resetUndeclaredEventWarningsForTests() {
  undeclaredEventWarned.clear();
}
const COVERAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1e3;
const coverageRefreshTracker = /* @__PURE__ */ new Map();
function shouldTriggerCoverageRefresh(entityType, tenantId) {
  if (!entityType) return false;
  const key = `${entityType}|${tenantId ?? "__null__"}`;
  const now = Date.now();
  const last = coverageRefreshTracker.get(key) ?? 0;
  if (now - last < COVERAGE_REFRESH_INTERVAL_MS) return false;
  coverageRefreshTracker.set(key, now);
  return true;
}
const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = "system_entity_records_blocked";
function isOrmBackedSystemEntityId(em, entityId) {
  const registry = getEntityIds(false);
  const moduleIds = Object.values(registry).flatMap((moduleEntities) => Object.values(moduleEntities ?? {}));
  if (moduleIds.length > 0 && !moduleIds.includes(entityId)) return false;
  return resolveRegisteredEntityTableName(em, entityId) !== null;
}
function assertCustomEntityStorageEntityId(em, entityId) {
  if (isOrmBackedSystemEntityId(em, entityId)) {
    throw new CrudHttpError(400, {
      error: "Records are available for custom entities only",
      code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,
      entityId
    });
  }
}
class DefaultDataEngine {
  constructor(em, container) {
    this.em = em;
    this.container = container;
    this.pendingSideEffects = /* @__PURE__ */ new Map();
  }
  async setCustomFields(opts) {
    const { entityId, recordId, organizationId = null, tenantId = null, values } = opts;
    const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
      entityId,
      organizationId,
      tenantId,
      values
    });
    await this.validateCustomFieldValues(entityId, organizationId, tenantId, sanitizedValues);
    let encryptionService = null;
    try {
      encryptionService = this.container.resolve("tenantEncryptionService");
    } catch {
      encryptionService = null;
    }
    await setRecordCustomFields(this.em, {
      entityId,
      recordId,
      organizationId,
      tenantId,
      values: sanitizedValues,
      encryptionService
    });
    if (opts.notify !== false) {
      let bus = null;
      try {
        bus = this.container.resolve("eventBus");
      } catch {
        bus = null;
      }
      if (bus) {
        const [mod, ent] = (entityId || "").split(":");
        if (mod && ent) {
          const eventName = `${mod}.${ent}.updated`;
          warnIfUndeclaredEvent(eventName, "setCustomFields");
          try {
            await bus.emitEvent(eventName, { id: recordId, organizationId, tenantId }, {
              persistent: true,
              tenantId,
              organizationId,
              emitterModuleId: mod
            });
          } catch {
          }
        }
      }
    }
  }
  normalizeDocValues(values) {
    const out = {};
    for (const [k, v] of Object.entries(values || {})) {
      if (k === "id" || k === "entity_id" || k === "entityId") continue;
      if (k.startsWith("cf_")) out[`cf:${k.slice(3)}`] = v;
      else out[k] = v;
    }
    return out;
  }
  backcompatEavEnabled() {
    try {
      return parseBooleanToken(process.env.ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM ?? "") === true;
    } catch {
      return false;
    }
  }
  getKysely() {
    return this.em.getKysely();
  }
  async ensureStorageTableExists() {
    const db = this.getKysely();
    const exists = await db.selectFrom("information_schema.tables").select(sql`1`.as("present")).where("table_name", "=", "custom_entities_storage").executeTakeFirst();
    if (!exists) {
      throw new Error("custom_entities_storage table is missing. Run migrations (yarn db:migrate).");
    }
  }
  normalizeValuesForValidation(values) {
    if (!values) return {};
    const out = {};
    for (const [key, value] of Object.entries(values)) {
      if (value === void 0) continue;
      if (key.startsWith("cf_") || key.startsWith("cf:")) {
        const normalized = key.slice(3);
        if (normalized) out[normalized] = value;
        continue;
      }
      out[key] = value;
    }
    return out;
  }
  async validateCustomFieldValues(entityId, organizationId, tenantId, values) {
    const prepared = this.normalizeValuesForValidation(values);
    if (!entityId || Object.keys(prepared).length === 0) return;
    const result = await validateCustomFieldValuesServer(this.em, {
      entityId,
      organizationId,
      tenantId,
      values: prepared
    });
    if (!result.ok) {
      throw new CrudHttpError(400, { error: "Validation failed", fields: result.fieldErrors });
    }
  }
  async createCustomEntityRecord(opts) {
    assertCustomEntityStorageEntityId(this.em, opts.entityId);
    const db = this.getKysely();
    await this.ensureStorageTableExists();
    const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
      entityId: opts.entityId,
      organizationId: opts.organizationId ?? null,
      tenantId: opts.tenantId ?? null,
      values: opts.values || {}
    });
    await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues);
    const rawId = String(opts.recordId ?? "").trim();
    const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(rawId);
    const sentinel = rawId.toLowerCase();
    const shouldGenerate = !rawId || !isUuid || sentinel === "create" || sentinel === "new" || sentinel === "null" || sentinel === "undefined";
    const id = shouldGenerate ? (() => {
      const g = globalThis;
      if (g.crypto?.randomUUID) return g.crypto.randomUUID();
      return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
        const r = Math.random() * 16 | 0;
        const v = c === "x" ? r : r & 3 | 8;
        return v.toString(16);
      });
    })() : rawId;
    const orgId = opts.organizationId ?? null;
    const tenantId = opts.tenantId ?? null;
    const doc = { id, ...this.normalizeDocValues(sanitizedValues || {}) };
    const now = sql`now()`;
    const payload = {
      entity_type: opts.entityId,
      entity_id: id,
      organization_id: orgId,
      tenant_id: tenantId,
      doc: sql`${JSON.stringify(doc)}::jsonb`,
      updated_at: now,
      created_at: now,
      deleted_at: null
    };
    try {
      await db.insertInto("custom_entities_storage").values(payload).onConflict((oc) => oc.columns(["entity_type", "entity_id", "organization_id"]).doUpdateSet({
        doc: sql`${JSON.stringify(doc)}::jsonb`,
        updated_at: sql`now()`,
        deleted_at: null
      })).execute();
    } catch {
      try {
        const updated = await db.updateTable("custom_entities_storage").set({
          doc: sql`${JSON.stringify(doc)}::jsonb`,
          updated_at: sql`now()`,
          deleted_at: null
        }).where("entity_type", "=", opts.entityId).where("entity_id", "=", id).where("organization_id", orgId === null ? "is" : "=", orgId).where("tenant_id", tenantId === null ? "is" : "=", tenantId).executeTakeFirst();
        if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {
          await db.insertInto("custom_entities_storage").values(payload).execute();
        }
      } catch (err) {
        throw err;
      }
    }
    if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {
      await this.setCustomFields({
        entityId: opts.entityId,
        recordId: id,
        organizationId: orgId,
        tenantId,
        values: normalizeCustomFieldValues(sanitizedValues),
        notify: opts.notify
        // defaults to true
      });
    }
    return { id };
  }
  async updateCustomEntityRecord(opts) {
    assertCustomEntityStorageEntityId(this.em, opts.entityId);
    const db = this.getKysely();
    const sanitizedValues = await sanitizeCustomFieldHtmlRichTextValuesServer(this.em, {
      entityId: opts.entityId,
      organizationId: opts.organizationId ?? null,
      tenantId: opts.tenantId ?? null,
      values: opts.values || {}
    });
    await this.validateCustomFieldValues(opts.entityId, opts.organizationId ?? null, opts.tenantId ?? null, sanitizedValues);
    const id = String(opts.recordId);
    const orgId = opts.organizationId ?? null;
    const tenantId = opts.tenantId ?? null;
    await this.ensureStorageTableExists();
    const applyScope = (q) => {
      let chain = q.where("entity_type", "=", opts.entityId);
      chain = chain.where("entity_id", "=", id);
      chain = orgId === null ? chain.where("organization_id", "is", null) : chain.where("organization_id", "=", orgId);
      chain = tenantId === null ? chain.where("tenant_id", "is", null) : chain.where("tenant_id", "=", tenantId);
      return chain;
    };
    const row = await applyScope(
      db.selectFrom("custom_entities_storage").select(["doc"])
    ).executeTakeFirst();
    const prevDoc = row?.doc || { id };
    const nextDoc = { ...prevDoc, ...this.normalizeDocValues(sanitizedValues || {}), id };
    try {
      const updated = await applyScope(
        db.updateTable("custom_entities_storage").set({
          doc: sql`${JSON.stringify(nextDoc)}::jsonb`,
          updated_at: sql`now()`,
          deleted_at: null
        })
      ).executeTakeFirst();
      if (!updated || Number(updated.numUpdatedRows ?? 0) === 0) {
        await db.insertInto("custom_entities_storage").values({
          entity_type: opts.entityId,
          entity_id: id,
          organization_id: orgId,
          tenant_id: tenantId,
          doc: sql`${JSON.stringify(nextDoc)}::jsonb`,
          created_at: sql`now()`,
          updated_at: sql`now()`,
          deleted_at: null
        }).execute();
      }
    } catch (err) {
      throw err;
    }
    if (this.backcompatEavEnabled() && sanitizedValues && Object.keys(sanitizedValues).length > 0) {
      await this.setCustomFields({
        entityId: opts.entityId,
        recordId: id,
        organizationId: orgId,
        tenantId,
        values: normalizeCustomFieldValues(sanitizedValues),
        notify: opts.notify
        // defaults to true
      });
    }
  }
  async deleteCustomEntityRecord(opts) {
    assertCustomEntityStorageEntityId(this.em, opts.entityId);
    const db = this.getKysely();
    const id = String(opts.recordId);
    const orgId = opts.organizationId ?? null;
    const tenantId = opts.tenantId ?? null;
    const soft = opts.soft !== false;
    const applyScope = (q) => {
      let chain = q.where("entity_type", "=", opts.entityId);
      chain = chain.where("entity_id", "=", id);
      chain = orgId === null ? chain.where("organization_id", "is", null) : chain.where("organization_id", "=", orgId);
      chain = tenantId === null ? chain.where("tenant_id", "is", null) : chain.where("tenant_id", "=", tenantId);
      return chain;
    };
    if (soft) {
      await applyScope(
        db.updateTable("custom_entities_storage").set({
          deleted_at: sql`now()`,
          updated_at: sql`now()`
        })
      ).execute();
    } else {
      await applyScope(db.deleteFrom("custom_entities_storage")).execute();
    }
    try {
      const { CustomFieldValue } = await import("@open-mercato/core/modules/entities/data/entities");
      const values = await this.em.find(CustomFieldValue, {
        entityId: opts.entityId,
        recordId: id,
        organizationId: orgId,
        tenantId: opts.tenantId ?? null
      });
      const now = /* @__PURE__ */ new Date();
      const mutated = values.filter((record) => {
        if (record.deletedAt) return false;
        record.deletedAt = now;
        return true;
      });
      if (mutated.length) {
        for (const record of values) this.em.persist(record);
        await this.em.flush();
      }
    } catch {
    }
  }
  async createOrmEntity(opts) {
    const entity = this.em.create(
      opts.entity,
      opts.data
    );
    await this.em.persist(entity).flush();
    return entity;
  }
  async updateOrmEntity(opts) {
    const current = await this.em.findOne(opts.entity, opts.where);
    if (!current) return null;
    await opts.apply(current);
    await this.em.persist(current).flush();
    return current;
  }
  async deleteOrmEntity(opts) {
    const current = await this.em.findOne(opts.entity, opts.where);
    if (!current) return null;
    if (opts.soft !== false) {
      const field = opts.softDeleteField || "deletedAt";
      if (typeof current === "object" && current !== null) {
        ;
        current[field] = /* @__PURE__ */ new Date();
        await this.em.persist(current).flush();
      }
    } else {
      await this.em.remove(current).flush();
    }
    return current;
  }
  async emitOrmEntityEvent(opts) {
    const { action, entity, events, indexer, identifiers, syncOrigin, suppress } = opts;
    const emitEvents = !!events && !suppress?.skipEvents;
    const runIndexer = !!indexer && !suppress?.skipReindex;
    if (!emitEvents && !runIndexer) return;
    if (!identifiers?.id) return;
    let bus = null;
    try {
      bus = this.container.resolve("eventBus");
    } catch {
      bus = null;
    }
    if (!bus) return;
    const ctx = {
      action,
      entity,
      identifiers: {
        id: identifiers.id,
        organizationId: identifiers.organizationId ?? null,
        tenantId: identifiers.tenantId ?? null
      },
      syncOrigin: syncOrigin ?? null,
      actorUserId: opts.actorUserId ?? null
    };
    if (events && !suppress?.skipEvents) {
      const eventName = `${events.module}.${events.entity}.${action}`;
      warnIfUndeclaredEvent(eventName, "emitOrmEntityEvent");
      const builtPayload = events.buildPayload ? events.buildPayload(ctx) : {
        id: ctx.identifiers.id,
        organizationId: ctx.identifiers.organizationId,
        tenantId: ctx.identifiers.tenantId,
        ...ctx.syncOrigin ? { syncOrigin: ctx.syncOrigin } : {}
      };
      const payload = indexer && builtPayload && typeof builtPayload === "object" && !Array.isArray(builtPayload) ? Object.defineProperty(
        { ...builtPayload },
        CRUD_QUERY_INDEX_MANAGED_PAYLOAD_KEY,
        { value: true, enumerable: false }
      ) : builtPayload;
      try {
        await bus.emitEvent(eventName, payload, {
          persistent: !!events.persistent,
          tenantId: ctx.identifiers.tenantId ?? null,
          organizationId: ctx.identifiers.organizationId ?? null,
          emitterModuleId: events.module
        });
      } catch {
      }
    }
    if (indexer && !suppress?.skipReindex) {
      const alwaysConsistent = isReadProjectionAlwaysConsistent();
      const resolveCoverageBaseDelta = () => {
        if (action === "created") return 1;
        if (action === "deleted") return -1;
        return void 0;
      };
      const coverageBaseDelta = resolveCoverageBaseDelta();
      if (action === "deleted") {
        const payload = indexer.buildDeletePayload ? indexer.buildDeletePayload(ctx) : {
          entityType: indexer.entityType,
          recordId: ctx.identifiers.id,
          organizationId: ctx.identifiers.organizationId,
          tenantId: ctx.identifiers.tenantId
        };
        const enrichedPayload = payload;
        enrichedPayload.crudAction = action;
        if (coverageBaseDelta !== void 0) enrichedPayload.coverageBaseDelta = coverageBaseDelta;
        if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin;
        if (alwaysConsistent) {
          await bus.emitEvent("query_index.delete_one", enrichedPayload, { rethrowHandlerErrors: true });
        } else {
          await bus.emitEvent("query_index.delete_one", enrichedPayload).catch((err) => {
            logger.error("query_index.delete_one emit failed", { err });
          });
        }
      } else {
        const payload = indexer.buildUpsertPayload ? indexer.buildUpsertPayload(ctx) : {
          entityType: indexer.entityType,
          recordId: ctx.identifiers.id,
          organizationId: ctx.identifiers.organizationId,
          tenantId: ctx.identifiers.tenantId
        };
        const enrichedPayload = payload;
        enrichedPayload.crudAction = action;
        if (coverageBaseDelta !== void 0) enrichedPayload.coverageBaseDelta = coverageBaseDelta;
        if (ctx.syncOrigin) enrichedPayload.syncOrigin = ctx.syncOrigin;
        if (alwaysConsistent) {
          await bus.emitEvent("query_index.upsert_one", enrichedPayload, { rethrowHandlerErrors: true });
        } else {
          await bus.emitEvent("query_index.upsert_one", enrichedPayload).catch((err) => {
            logger.error("query_index.upsert_one emit failed", { err });
          });
        }
      }
      if (alwaysConsistent || shouldTriggerCoverageRefresh(indexer.entityType, ctx.identifiers.tenantId ?? null)) {
        const coveragePayload = {
          entityType: indexer.entityType,
          tenantId: ctx.identifiers.tenantId ?? null,
          organizationId: null,
          delayMs: 0
        };
        if (alwaysConsistent) {
          await bus.emitEvent("query_index.coverage.refresh", coveragePayload, { rethrowHandlerErrors: true });
        } else {
          void bus.emitEvent("query_index.coverage.refresh", coveragePayload).catch(() => void 0);
        }
      }
    }
  }
  markOrmEntityChange(opts) {
    const { entity, identifiers } = opts;
    if (!entity) return;
    if (!identifiers?.id) return;
    const key = this.buildSideEffectKey(opts.action, identifiers);
    const existing = this.pendingSideEffects.get(key);
    if (existing) {
      existing.entity = entity;
      existing.identifiers = {
        id: identifiers.id,
        organizationId: identifiers.organizationId ?? null,
        tenantId: identifiers.tenantId ?? null
      };
      existing.syncOrigin = opts.syncOrigin ?? null;
      existing.actorUserId = opts.actorUserId ?? null;
      if (opts.events) existing.events = opts.events;
      if (opts.indexer) existing.indexer = opts.indexer;
      this.pendingSideEffects.set(key, existing);
      return;
    }
    const entry = {
      action: opts.action,
      entity,
      identifiers: {
        id: identifiers.id,
        organizationId: identifiers.organizationId ?? null,
        tenantId: identifiers.tenantId ?? null
      },
      syncOrigin: opts.syncOrigin ?? null,
      actorUserId: opts.actorUserId ?? null
    };
    if (opts.events) entry.events = opts.events;
    if (opts.indexer) entry.indexer = opts.indexer;
    this.pendingSideEffects.set(key, entry);
  }
  async flushOrmEntityChanges(suppress) {
    if (!this.pendingSideEffects.size) return;
    const entries = Array.from(this.pendingSideEffects.values());
    this.pendingSideEffects.clear();
    for (const entry of entries) {
      try {
        await this.emitOrmEntityEvent({
          action: entry.action,
          entity: entry.entity,
          identifiers: entry.identifiers,
          syncOrigin: entry.syncOrigin ?? null,
          actorUserId: entry.actorUserId ?? null,
          events: entry.events,
          indexer: entry.indexer,
          suppress
        });
      } catch (error) {
        if (isReadProjectionAlwaysConsistent()) {
          throw error;
        }
      }
    }
  }
  buildSideEffectKey(action, identifiers) {
    const id = identifiers.id ?? "";
    const org = identifiers.organizationId ?? "";
    const tenant = identifiers.tenantId ?? "";
    return [action, id, org, tenant].join("|");
  }
}
export {
  DefaultDataEngine,
  SYSTEM_ENTITY_RECORDS_BLOCKED_CODE,
  __resetUndeclaredEventWarningsForTests,
  assertCustomEntityStorageEntityId,
  isOrmBackedSystemEntityId
};
//# sourceMappingURL=engine.js.map
