import { registerCommand } from "@open-mercato/shared/lib/commands";
import { buildChanges } from "@open-mercato/shared/lib/commands/helpers";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { invalidateDictionaryCache } from "../api/dictionaries/cache.js";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { loadRoleTypeUsage } from "../lib/roleTypeUsage.js";
import {
  customerDictionaryEntryCreateSchema,
  customerDictionaryEntryDeleteSchema,
  customerDictionaryEntryUpdateSchema
} from "../data/validators.js";
import { CustomerDictionaryEntry } from "../data/entities.js";
import {
  ensureOrganizationScope,
  ensureTenantScope,
  extractUndoPayload,
  normalizeDictionaryColor,
  normalizeDictionaryIcon
} from "./shared.js";
import { resolveRedoSnapshot } from "@open-mercato/shared/lib/commands/redo";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("customers");
function buildRoleTypeInUseError(usageCount, ownerAssignments, relationshipAssignments) {
  return new CrudHttpError(409, {
    code: "role_type_in_use",
    error: "Role type is in use",
    usageCount,
    ownerAssignments,
    relationshipAssignments
  });
}
function normalizeValue(input) {
  const trimmed = input.trim();
  if (!trimmed) {
    throw new CrudHttpError(400, { error: "Dictionary entry value is required" });
  }
  return { value: trimmed, normalized: trimmed.toLowerCase() };
}
function sanitizeColor(input) {
  if (input === void 0) return void 0;
  if (input === null) return null;
  return normalizeDictionaryColor(input);
}
function sanitizeIcon(input) {
  if (input === void 0) return void 0;
  if (input === null) return null;
  return normalizeDictionaryIcon(input);
}
function toSnapshot(entry) {
  return {
    id: entry.id,
    tenantId: entry.tenantId,
    organizationId: entry.organizationId,
    kind: entry.kind,
    value: entry.value,
    normalizedValue: entry.normalizedValue,
    label: entry.label,
    color: entry.color ?? null,
    icon: entry.icon ?? null
  };
}
async function loadSnapshot(em, id, scope) {
  const entry = await findOneWithDecryption(em, CustomerDictionaryEntry, { id }, {}, scope);
  return entry ? toSnapshot(entry) : null;
}
function applySnapshot(entry, snapshot) {
  entry.tenantId = snapshot.tenantId;
  entry.organizationId = snapshot.organizationId;
  entry.kind = snapshot.kind;
  entry.value = snapshot.value;
  entry.normalizedValue = snapshot.normalizedValue;
  entry.label = snapshot.label;
  entry.color = snapshot.color;
  entry.icon = snapshot.icon;
}
async function invalidateCache(ctx, snapshot) {
  if (!snapshot) return;
  let cache;
  try {
    cache = ctx.container.resolve("cache");
  } catch (err) {
    logger.warn("cache resolve failed; skipping invalidation", { component: "commands.dictionaries", err });
    cache = void 0;
  }
  if (!cache) return;
  await invalidateDictionaryCache(cache, {
    tenantId: snapshot.tenantId,
    mappedKind: snapshot.kind,
    organizationIds: [snapshot.organizationId]
  });
}
const createDictionaryEntryCommand = {
  id: "customers.dictionaryEntries.create",
  async execute(rawInput, ctx) {
    const parsed = customerDictionaryEntryCreateSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    const value = normalizeValue(parsed.value);
    const label = parsed.label?.trim() || value.value;
    const color = sanitizeColor(parsed.color);
    const icon = sanitizeIcon(parsed.icon);
    const existing = await findOneWithDecryption(
      em,
      CustomerDictionaryEntry,
      {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId,
        kind: parsed.kind,
        normalizedValue: value.normalized
      },
      {},
      {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId
      }
    );
    if (existing) {
      const before = toSnapshot(existing);
      let changed = false;
      if (parsed.label !== void 0 && existing.label !== label) {
        existing.label = label;
        changed = true;
      }
      if (color !== void 0 && existing.color !== color) {
        existing.color = color ?? null;
        changed = true;
      }
      if (icon !== void 0 && existing.icon !== icon) {
        existing.icon = icon ?? null;
        changed = true;
      }
      if (changed) {
        existing.updatedAt = /* @__PURE__ */ new Date();
        await em.flush();
        return {
          entryId: existing.id,
          tenantId: parsed.tenantId,
          organizationId: parsed.organizationId,
          mode: "updated",
          before
        };
      }
      return {
        entryId: existing.id,
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId,
        mode: "unchanged",
        before
      };
    }
    const entry = em.create(CustomerDictionaryEntry, {
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      kind: parsed.kind,
      value: value.value,
      normalizedValue: value.normalized,
      label,
      color: color ?? null,
      icon: icon ?? null
    });
    em.persist(entry);
    await em.flush();
    return {
      entryId: entry.id,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      mode: "created"
    };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    return loadSnapshot(em, result.entryId, {
      tenantId: result.tenantId,
      organizationId: result.organizationId
    });
  },
  buildLog: async ({ result, snapshots }) => {
    const after = snapshots.after;
    if (!after) return null;
    const { translate } = await resolveTranslations();
    if (result.mode === "created") {
      return {
        actionLabel: translate("customers.audit.dictionaryEntries.create", "Add dictionary entry"),
        resourceKind: "customers.dictionary_entry",
        resourceId: after.id,
        tenantId: after.tenantId,
        organizationId: after.organizationId,
        snapshotAfter: after,
        payload: {
          undo: { after }
        }
      };
    }
    if (result.mode === "updated") {
      const before = result.before ?? null;
      if (!before) return null;
      const changes = buildChanges(
        before,
        after,
        ["label", "color", "icon"]
      );
      if (!changes || Object.keys(changes).length === 0) return null;
      return {
        actionLabel: translate("customers.audit.dictionaryEntries.update", "Update dictionary entry"),
        resourceKind: "customers.dictionary_entry",
        resourceId: after.id,
        tenantId: after.tenantId,
        organizationId: after.organizationId,
        snapshotBefore: before,
        snapshotAfter: after,
        changes,
        payload: {
          undo: { before, after }
        }
      };
    }
    return null;
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    if (!undo) return;
    const em = ctx.container.resolve("em").fork();
    const after = undo.after ?? logEntry?.snapshotAfter ?? null;
    if (after && !undo.before) {
      ensureTenantScope(ctx, after.tenantId);
      ensureOrganizationScope(ctx, after.organizationId);
      const entry = await findOneWithDecryption(
        em,
        CustomerDictionaryEntry,
        { id: after.id },
        {},
        { tenantId: after.tenantId, organizationId: after.organizationId }
      );
      if (entry) {
        await em.remove(entry).flush();
        await invalidateCache(ctx, after);
        return;
      }
      await em.nativeDelete(CustomerDictionaryEntry, { id: after.id });
      await invalidateCache(ctx, after);
      return;
    }
    const before = undo.before ?? logEntry?.snapshotBefore ?? null;
    if (before) {
      ensureTenantScope(ctx, before.tenantId);
      ensureOrganizationScope(ctx, before.organizationId);
      let entry = await findOneWithDecryption(
        em,
        CustomerDictionaryEntry,
        { id: before.id },
        {},
        { tenantId: before.tenantId, organizationId: before.organizationId }
      );
      if (!entry) {
        entry = em.create(CustomerDictionaryEntry, {
          id: before.id,
          tenantId: before.tenantId,
          organizationId: before.organizationId,
          kind: before.kind,
          value: before.value,
          normalizedValue: before.normalizedValue,
          label: before.label,
          color: before.color,
          icon: before.icon
        });
        em.persist(entry);
      } else {
        applySnapshot(entry, before);
      }
      await em.flush();
      await invalidateCache(ctx, before);
    }
  },
  redo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const after = resolveRedoSnapshot(logEntry);
    if (!after) {
      throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for dictionary entry create" });
    }
    ensureTenantScope(ctx, after.tenantId);
    ensureOrganizationScope(ctx, after.organizationId);
    const em = ctx.container.resolve("em").fork();
    let entry = await findOneWithDecryption(
      em,
      CustomerDictionaryEntry,
      { id: after.id },
      {},
      { tenantId: after.tenantId, organizationId: after.organizationId }
    );
    if (!entry) {
      entry = em.create(CustomerDictionaryEntry, {
        id: after.id,
        tenantId: after.tenantId,
        organizationId: after.organizationId,
        kind: after.kind,
        value: after.value,
        normalizedValue: after.normalizedValue,
        label: after.label,
        color: after.color,
        icon: after.icon
      });
      em.persist(entry);
    } else {
      applySnapshot(entry, after);
      entry.updatedAt = /* @__PURE__ */ new Date();
    }
    await em.flush();
    await invalidateCache(ctx, after);
    return {
      entryId: after.id,
      tenantId: after.tenantId,
      organizationId: after.organizationId,
      mode: undo?.before ? "updated" : "created",
      ...undo?.before ? { before: undo.before } : {}
    };
  }
};
const updateDictionaryEntryCommand = {
  id: "customers.dictionaryEntries.update",
  async prepare(rawInput, ctx) {
    const parsed = customerDictionaryEntryUpdateSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadSnapshot(em, parsed.id, {
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId
    });
    return snapshot ? { before: snapshot } : {};
  },
  async execute(rawInput, ctx) {
    const parsed = customerDictionaryEntryUpdateSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    const entry = await findOneWithDecryption(
      em,
      CustomerDictionaryEntry,
      { id: parsed.id },
      {},
      {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId
      }
    );
    if (!entry || entry.organizationId !== parsed.organizationId || entry.tenantId !== parsed.tenantId || entry.kind !== parsed.kind) {
      throw new CrudHttpError(404, { error: "Dictionary entry not found" });
    }
    let changed = false;
    if (parsed.value !== void 0) {
      const value = normalizeValue(parsed.value);
      if (value.normalized !== entry.normalizedValue) {
        if (entry.kind === "person_company_role") {
          const usage = await loadRoleTypeUsage(em, {
            tenantId: parsed.tenantId,
            organizationId: entry.organizationId,
            value: entry.value
          });
          if (usage.total > 0) {
            throw buildRoleTypeInUseError(
              usage.total,
              usage.ownerAssignments,
              usage.relationshipAssignments
            );
          }
        }
        const duplicate = await findOneWithDecryption(
          em,
          CustomerDictionaryEntry,
          {
            id: { $ne: entry.id },
            tenantId: parsed.tenantId,
            organizationId: parsed.organizationId,
            kind: parsed.kind,
            normalizedValue: value.normalized
          },
          {},
          {
            tenantId: parsed.tenantId,
            organizationId: parsed.organizationId
          }
        );
        if (duplicate) {
          throw new CrudHttpError(409, { error: "An entry with this value already exists" });
        }
        entry.value = value.value;
        entry.normalizedValue = value.normalized;
        if (parsed.label === void 0) {
          entry.label = value.value;
        }
        changed = true;
      }
    }
    if (parsed.label !== void 0) {
      const label = parsed.label.trim() || entry.value;
      if (entry.label !== label) {
        entry.label = label;
        changed = true;
      }
    }
    if (parsed.color !== void 0) {
      const color = sanitizeColor(parsed.color);
      if (entry.color !== (color ?? null)) {
        entry.color = color ?? null;
        changed = true;
      }
    }
    if (parsed.icon !== void 0) {
      const icon = sanitizeIcon(parsed.icon);
      if (entry.icon !== (icon ?? null)) {
        entry.icon = icon ?? null;
        changed = true;
      }
    }
    if (changed) {
      entry.updatedAt = /* @__PURE__ */ new Date();
      await em.flush();
    }
    return {
      entryId: entry.id,
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId,
      changed
    };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    return loadSnapshot(em, result.entryId, {
      tenantId: result.tenantId,
      organizationId: result.organizationId
    });
  },
  buildLog: async ({ snapshots, result }) => {
    const before = snapshots.before;
    const after = snapshots.after;
    if (!before || !after || !result.changed) return null;
    const changes = buildChanges(
      before,
      after,
      ["value", "label", "color", "icon"]
    );
    if (!changes || Object.keys(changes).length === 0) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("customers.audit.dictionaryEntries.update", "Update dictionary entry"),
      resourceKind: "customers.dictionary_entry",
      resourceId: after.id,
      tenantId: after.tenantId,
      organizationId: after.organizationId,
      snapshotBefore: before,
      snapshotAfter: after,
      changes,
      payload: {
        undo: { before, after }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const before = undo?.before ?? logEntry?.snapshotBefore ?? null;
    if (!before) return;
    ensureTenantScope(ctx, before.tenantId);
    ensureOrganizationScope(ctx, before.organizationId);
    const em = ctx.container.resolve("em").fork();
    let entry = await findOneWithDecryption(
      em,
      CustomerDictionaryEntry,
      { id: before.id },
      {},
      { tenantId: before.tenantId, organizationId: before.organizationId }
    );
    if (!entry) {
      entry = em.create(CustomerDictionaryEntry, {
        id: before.id,
        tenantId: before.tenantId,
        organizationId: before.organizationId,
        kind: before.kind,
        value: before.value,
        normalizedValue: before.normalizedValue,
        label: before.label,
        color: before.color,
        icon: before.icon
      });
      em.persist(entry);
    } else {
      applySnapshot(entry, before);
    }
    await em.flush();
    await invalidateCache(ctx, before);
  }
};
const deleteDictionaryEntryCommand = {
  id: "customers.dictionaryEntries.delete",
  async prepare(rawInput, ctx) {
    const parsed = customerDictionaryEntryDeleteSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    const snapshot = await loadSnapshot(em, parsed.id, {
      tenantId: parsed.tenantId,
      organizationId: parsed.organizationId
    });
    return snapshot ? { before: snapshot } : {};
  },
  async execute(rawInput, ctx) {
    const parsed = customerDictionaryEntryDeleteSchema.parse(rawInput);
    ensureTenantScope(ctx, parsed.tenantId);
    ensureOrganizationScope(ctx, parsed.organizationId);
    const em = ctx.container.resolve("em").fork();
    const entry = await findOneWithDecryption(
      em,
      CustomerDictionaryEntry,
      { id: parsed.id },
      {},
      {
        tenantId: parsed.tenantId,
        organizationId: parsed.organizationId
      }
    );
    if (!entry || entry.organizationId !== parsed.organizationId || entry.tenantId !== parsed.tenantId || entry.kind !== parsed.kind) {
      throw new CrudHttpError(404, { error: "Dictionary entry not found" });
    }
    if (entry.kind === "person_company_role") {
      const usage = await loadRoleTypeUsage(em, {
        tenantId: entry.tenantId,
        organizationId: entry.organizationId,
        value: entry.value
      });
      if (usage.total > 0) {
        throw buildRoleTypeInUseError(
          usage.total,
          usage.ownerAssignments,
          usage.relationshipAssignments
        );
      }
    }
    em.remove(entry);
    await em.flush();
    return { entryId: entry.id };
  },
  buildLog: async ({ snapshots }) => {
    const before = snapshots.before;
    if (!before) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("customers.audit.dictionaryEntries.delete", "Delete dictionary entry"),
      resourceKind: "customers.dictionary_entry",
      resourceId: before.id,
      tenantId: before.tenantId,
      organizationId: before.organizationId,
      snapshotBefore: before,
      payload: {
        undo: { before }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const undo = extractUndoPayload(logEntry);
    const before = undo?.before ?? logEntry?.snapshotBefore ?? null;
    if (!before) return;
    ensureTenantScope(ctx, before.tenantId);
    ensureOrganizationScope(ctx, before.organizationId);
    const em = ctx.container.resolve("em").fork();
    let entry = await findOneWithDecryption(
      em,
      CustomerDictionaryEntry,
      { id: before.id },
      {},
      { tenantId: before.tenantId, organizationId: before.organizationId }
    );
    if (!entry) {
      entry = em.create(CustomerDictionaryEntry, {
        id: before.id,
        tenantId: before.tenantId,
        organizationId: before.organizationId,
        kind: before.kind,
        value: before.value,
        normalizedValue: before.normalizedValue,
        label: before.label,
        color: before.color,
        icon: before.icon
      });
      em.persist(entry);
    } else {
      applySnapshot(entry, before);
    }
    await em.flush();
    await invalidateCache(ctx, before);
  }
};
registerCommand(createDictionaryEntryCommand);
registerCommand(updateDictionaryEntryCommand);
registerCommand(deleteDictionaryEntryCommand);
//# sourceMappingURL=dictionaries.js.map
