import { registerCommand } from "@open-mercato/shared/lib/commands";
import { buildChanges, requireId, parseWithCustomFields, setCustomFieldsIfAny, emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
import { UniqueConstraintViolationException } from "@mikro-orm/core";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
import { resolveRedoSnapshot, restoreCreatedRow } from "@open-mercato/shared/lib/commands/redo";
import { E } from "../../../generated/entities.ids.generated.js";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import {
  CatalogProductVariant,
  CatalogProductPrice,
  CatalogProduct,
  CatalogOffer,
  CatalogPriceKind
} from "../data/entities.js";
import { Attachment } from "@open-mercato/core/modules/attachments/data/entities";
import { resolveAttachmentScopePair } from "@open-mercato/core/modules/attachments/lib/scope";
import {
  variantCreateSchema,
  variantUpdateSchema
} from "../data/validators.js";
import { isValidGtin, normalizeGtinValue } from "../lib/gtin.js";
import {
  cloneJson,
  commandActorScope,
  ensureOrganizationScope,
  ensureTenantScope,
  emitCatalogQueryIndexEvent,
  extractUndoPayload,
  requireProduct,
  toNumericString,
  getErrorConstraint,
  getErrorMessage
} from "./shared.js";
import { SalesTaxRate } from "@open-mercato/core/modules/sales/data/entities";
const variantCrudEvents = {
  module: "catalog",
  entity: "variant",
  persistent: true,
  buildPayload: (ctx) => ({
    id: ctx.identifiers.id,
    organizationId: ctx.identifiers.organizationId,
    tenantId: ctx.identifiers.tenantId
  })
};
const VARIANT_CHANGE_KEYS = [
  "name",
  "sku",
  "barcode",
  "gtinType",
  "hsCode",
  "statusEntryId",
  "isDefault",
  "isActive",
  "weightValue",
  "weightUnit",
  "taxRateId",
  "taxRate",
  "dimensions",
  "optionValues",
  "customFieldsetCode",
  "metadata"
];
async function loadVariantSnapshot(em, id, options = {}) {
  const record = await em.findOne(CatalogProductVariant, { id, deletedAt: null });
  if (!record) return null;
  const prices = options.includePrices ? await loadVariantPriceSnapshots(em, record.id, {
    tenantId: record.tenantId,
    organizationId: record.organizationId
  }) : null;
  const custom = await loadCustomFieldSnapshot(em, {
    entityId: E.catalog.catalog_product_variant,
    recordId: record.id,
    tenantId: record.tenantId,
    organizationId: record.organizationId
  });
  const productId = typeof record.product === "string" ? record.product : record.product.id;
  return {
    id: record.id,
    productId,
    organizationId: record.organizationId,
    tenantId: record.tenantId,
    name: record.name ?? null,
    sku: record.sku ?? null,
    barcode: record.barcode ?? null,
    gtinType: record.gtinType ?? null,
    hsCode: record.hsCode ?? null,
    statusEntryId: record.statusEntryId ?? null,
    isDefault: record.isDefault,
    isActive: record.isActive,
    weightValue: record.weightValue ?? null,
    weightUnit: record.weightUnit ?? null,
    taxRateId: record.taxRateId ?? null,
    taxRate: record.taxRate ?? null,
    dimensions: record.dimensions ? cloneJson(record.dimensions) : null,
    metadata: record.metadata ? cloneJson(record.metadata) : null,
    optionValues: record.optionValues ? cloneJson(record.optionValues) : null,
    customFieldsetCode: record.customFieldsetCode ?? null,
    createdAt: record.createdAt.toISOString(),
    updatedAt: record.updatedAt.toISOString(),
    custom: Object.keys(custom).length ? custom : null,
    prices: prices && prices.length ? prices : null
  };
}
function variantSeedFromSnapshot(snapshot) {
  return {
    id: snapshot.id,
    product: snapshot.productId,
    organizationId: snapshot.organizationId,
    tenantId: snapshot.tenantId,
    name: snapshot.name ?? null,
    sku: snapshot.sku ?? null,
    barcode: snapshot.barcode ?? null,
    gtinType: snapshot.gtinType ?? null,
    hsCode: snapshot.hsCode ?? null,
    statusEntryId: snapshot.statusEntryId ?? null,
    isDefault: snapshot.isDefault,
    isActive: snapshot.isActive,
    weightValue: snapshot.weightValue ?? null,
    weightUnit: snapshot.weightUnit ?? null,
    taxRateId: snapshot.taxRateId ?? null,
    taxRate: snapshot.taxRate ?? null,
    dimensions: snapshot.dimensions ? cloneJson(snapshot.dimensions) : null,
    metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : null,
    optionValues: snapshot.optionValues ? cloneJson(snapshot.optionValues) : null,
    customFieldsetCode: snapshot.customFieldsetCode ?? null,
    createdAt: new Date(snapshot.createdAt),
    updatedAt: new Date(snapshot.updatedAt)
  };
}
function applyVariantSnapshot(record, snapshot) {
  record.organizationId = snapshot.organizationId;
  record.tenantId = snapshot.tenantId;
  record.name = snapshot.name ?? null;
  record.sku = snapshot.sku ?? null;
  record.barcode = snapshot.barcode ?? null;
  record.gtinType = snapshot.gtinType ?? null;
  record.hsCode = snapshot.hsCode ?? null;
  record.statusEntryId = snapshot.statusEntryId ?? null;
  record.isDefault = snapshot.isDefault;
  record.isActive = snapshot.isActive;
  record.weightValue = snapshot.weightValue ?? null;
  record.weightUnit = snapshot.weightUnit ?? null;
  record.taxRateId = snapshot.taxRateId ?? null;
  record.taxRate = snapshot.taxRate ?? null;
  record.dimensions = snapshot.dimensions ? cloneJson(snapshot.dimensions) : null;
  record.metadata = snapshot.metadata ? cloneJson(snapshot.metadata) : null;
  record.optionValues = snapshot.optionValues ? cloneJson(snapshot.optionValues) : null;
  record.customFieldsetCode = snapshot.customFieldsetCode ?? null;
  record.createdAt = new Date(snapshot.createdAt);
  record.updatedAt = new Date(snapshot.updatedAt);
}
async function resolveVariantTaxRate(em, product, taxRateIdInput, taxRateInput) {
  const organizationId = product.organizationId;
  const tenantId = product.tenantId;
  const normalizedRate = taxRateInput === null || taxRateInput === void 0 ? null : (() => {
    const numeric = typeof taxRateInput === "string" ? Number(taxRateInput) : taxRateInput;
    return Number.isFinite(numeric) ? toNumericString(numeric) : null;
  })();
  if (taxRateIdInput === null) {
    return { taxRateId: product.taxRateId ?? null, taxRate: product.taxRate ?? null };
  }
  if (!taxRateIdInput) {
    return { taxRateId: product.taxRateId ?? null, taxRate: product.taxRate ?? normalizedRate };
  }
  const record = await em.findOne(SalesTaxRate, {
    id: taxRateIdInput,
    organizationId,
    tenantId,
    deletedAt: null
  });
  if (!record) {
    throw new CrudHttpError(400, { error: "Tax class not found" });
  }
  return { taxRateId: taxRateIdInput, taxRate: record.rate ?? normalizedRate };
}
async function loadVariantPriceSnapshots(em, variantId, scope) {
  const prices = await findWithDecryption(
    em,
    CatalogProductPrice,
    { variant: variantId, tenantId: scope.tenantId, organizationId: scope.organizationId },
    { populate: ["priceKind", "product", "offer"] },
    { tenantId: scope.tenantId, organizationId: scope.organizationId }
  );
  const snapshots = [];
  for (const price of prices) {
    const variantRef = price.variant;
    const variantIdValue = typeof variantRef === "string" ? variantRef : variantRef ? variantRef.id : null;
    const productRef = price.product ? price.product : typeof price.variant === "object" && price.variant ? price.variant.product : null;
    const productId = typeof productRef === "string" ? productRef : productRef ? productRef.id : null;
    const priceKindRef = price.priceKind;
    const priceKindId = typeof priceKindRef === "string" ? priceKindRef : priceKindRef ? priceKindRef.id : null;
    if (!priceKindId) {
      throw new CrudHttpError(400, { error: "Price is missing price kind metadata." });
    }
    const priceKindCode = typeof priceKindRef === "object" && priceKindRef ? priceKindRef.code : price.kind;
    const custom = await loadCustomFieldSnapshot(em, {
      entityId: E.catalog.catalog_product_price,
      recordId: price.id,
      tenantId: price.tenantId,
      organizationId: price.organizationId
    });
    snapshots.push({
      id: price.id,
      variantId: variantIdValue,
      productId,
      offerId: typeof price.offer === "string" ? price.offer : price.offer ? price.offer.id : null,
      organizationId: price.organizationId,
      tenantId: price.tenantId,
      priceKindId,
      priceKindCode,
      currencyCode: price.currencyCode,
      kind: price.kind,
      minQuantity: price.minQuantity,
      maxQuantity: price.maxQuantity ?? null,
      unitPriceNet: price.unitPriceNet ?? null,
      unitPriceGross: price.unitPriceGross ?? null,
      taxRate: price.taxRate ?? null,
      taxAmount: price.taxAmount ?? null,
      channelId: price.channelId ?? null,
      userId: price.userId ?? null,
      userGroupId: price.userGroupId ?? null,
      customerId: price.customerId ?? null,
      customerGroupId: price.customerGroupId ?? null,
      metadata: price.metadata ? cloneJson(price.metadata) : null,
      startsAt: price.startsAt ? price.startsAt.toISOString() : null,
      endsAt: price.endsAt ? price.endsAt.toISOString() : null,
      createdAt: price.createdAt.toISOString(),
      updatedAt: price.updatedAt.toISOString(),
      custom: Object.keys(custom).length ? custom : null
    });
  }
  return snapshots;
}
async function restoreVariantPricesFromSnapshots(em, variant, snapshots, dataEngine) {
  if (!snapshots.length) return;
  const productRef = typeof variant.product === "string" ? await requireProduct(em, variant.product, {
    tenantId: variant.tenantId,
    organizationId: variant.organizationId
  }) : variant.product;
  for (const snapshot of snapshots) {
    const product = snapshot.productId && snapshot.productId !== productRef.id ? em.getReference(CatalogProduct, snapshot.productId) : productRef;
    const offer = snapshot.offerId ? em.getReference(CatalogOffer, snapshot.offerId) : null;
    const priceKind = em.getReference(CatalogPriceKind, snapshot.priceKindId);
    let record = await em.findOne(CatalogProductPrice, { id: snapshot.id });
    if (!record) {
      record = em.create(CatalogProductPrice, {
        id: snapshot.id,
        variant,
        product,
        offer,
        organizationId: snapshot.organizationId,
        tenantId: snapshot.tenantId,
        currencyCode: snapshot.currencyCode,
        priceKind,
        kind: snapshot.priceKindCode || snapshot.kind,
        minQuantity: snapshot.minQuantity,
        maxQuantity: snapshot.maxQuantity ?? null,
        unitPriceNet: snapshot.unitPriceNet ?? null,
        unitPriceGross: snapshot.unitPriceGross ?? null,
        taxRate: snapshot.taxRate ?? null,
        taxAmount: snapshot.taxAmount ?? null,
        channelId: snapshot.channelId ?? null,
        userId: snapshot.userId ?? null,
        userGroupId: snapshot.userGroupId ?? null,
        customerId: snapshot.customerId ?? null,
        customerGroupId: snapshot.customerGroupId ?? null,
        metadata: snapshot.metadata ? cloneJson(snapshot.metadata) : null,
        startsAt: snapshot.startsAt ? new Date(snapshot.startsAt) : null,
        endsAt: snapshot.endsAt ? new Date(snapshot.endsAt) : null,
        createdAt: new Date(snapshot.createdAt),
        updatedAt: new Date(snapshot.updatedAt)
      });
      em.persist(record);
    } else {
      record.variant = variant;
      record.product = product;
      record.offer = offer;
      record.priceKind = priceKind;
      record.organizationId = snapshot.organizationId;
      record.tenantId = snapshot.tenantId;
      record.currencyCode = snapshot.currencyCode;
      record.kind = snapshot.priceKindCode || snapshot.kind;
      record.minQuantity = snapshot.minQuantity;
      record.maxQuantity = snapshot.maxQuantity ?? null;
      record.unitPriceNet = snapshot.unitPriceNet ?? null;
      record.unitPriceGross = snapshot.unitPriceGross ?? null;
      record.taxRate = snapshot.taxRate ?? null;
      record.taxAmount = snapshot.taxAmount ?? null;
      record.channelId = snapshot.channelId ?? null;
      record.userId = snapshot.userId ?? null;
      record.userGroupId = snapshot.userGroupId ?? null;
      record.customerId = snapshot.customerId ?? null;
      record.customerGroupId = snapshot.customerGroupId ?? null;
      record.metadata = snapshot.metadata ? cloneJson(snapshot.metadata) : null;
      record.startsAt = snapshot.startsAt ? new Date(snapshot.startsAt) : null;
      record.endsAt = snapshot.endsAt ? new Date(snapshot.endsAt) : null;
      record.createdAt = new Date(snapshot.createdAt);
      record.updatedAt = new Date(snapshot.updatedAt);
    }
  }
  await em.flush();
  for (const snapshot of snapshots) {
    if (!snapshot.custom || !Object.keys(snapshot.custom).length) continue;
    await setCustomFieldsIfAny({
      dataEngine,
      entityId: E.catalog.catalog_product_price,
      recordId: snapshot.id,
      organizationId: snapshot.organizationId,
      tenantId: snapshot.tenantId,
      values: snapshot.custom
    });
  }
}
function splitOptionValuesFromMetadata(metadata) {
  if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
    return {
      metadata: metadata ? cloneJson(metadata) : null,
      optionValues: null,
      hadOptionValues: false
    };
  }
  const { optionValues, ...rest } = metadata;
  const normalizedMetadata = Object.keys(rest).length ? cloneJson(rest) : null;
  return {
    metadata: normalizedMetadata,
    optionValues: normalizeOptionValues(optionValues),
    hadOptionValues: optionValues !== void 0
  };
}
function normalizeOptionValues(input) {
  if (!input || typeof input !== "object" || Array.isArray(input)) return null;
  const normalized = {};
  for (const [rawKey, rawValue] of Object.entries(input)) {
    if (typeof rawValue !== "string") continue;
    const key = rawKey.trim();
    const value = rawValue.trim();
    if (!key || !value) continue;
    normalized[key] = value;
  }
  return Object.keys(normalized).length ? normalized : null;
}
function resolveProductId(record) {
  return typeof record.product === "string" ? record.product : record.product.id;
}
async function enforceSingleDefaultVariant(em, variant) {
  if (!variant.isDefault) return null;
  const productId = resolveProductId(variant);
  const existingDefault = await em.findOne(
    CatalogProductVariant,
    { product: productId, isDefault: true, deletedAt: null, id: { $ne: variant.id } },
    { fields: ["id", "isDefault"] }
  );
  if (existingDefault) {
    existingDefault.isDefault = false;
    return existingDefault.id;
  }
  return null;
}
async function aggregateVariantMediaToProduct(em, variant) {
  const productId = resolveProductId(variant);
  const buildKey = (attachment) => [
    attachment.fileName?.trim() ?? "",
    attachment.fileSize ?? "",
    attachment.storageDriver ?? "",
    attachment.partitionCode ?? ""
  ].join("|");
  const attachments = await em.find(
    Attachment,
    {
      entityId: E.catalog.catalog_product_variant,
      recordId: variant.id,
      organizationId: variant.organizationId ?? void 0,
      tenantId: variant.tenantId ?? void 0
    },
    {
      fields: [
        "id",
        "partitionCode",
        "fileName",
        "mimeType",
        "fileSize",
        "storageDriver",
        "storagePath",
        "storageMetadata",
        "url",
        "organizationId",
        "tenantId",
        "fileSize",
        "storageDriver",
        "partitionCode"
      ]
    }
  );
  if (!attachments.length) return;
  const existing = await em.find(
    Attachment,
    {
      entityId: E.catalog.catalog_product,
      recordId: productId,
      organizationId: variant.organizationId ?? void 0,
      tenantId: variant.tenantId ?? void 0
    },
    {
      fields: ["storagePath", "fileName", "fileSize", "storageDriver", "partitionCode"]
    }
  );
  const existingKeys = new Set(existing.map((item) => buildKey(item)));
  let created = 0;
  for (const source of attachments) {
    const key = buildKey(source);
    if (existingKeys.has(key)) continue;
    const scope = resolveAttachmentScopePair(source, variant) ?? { organizationId: null, tenantId: null };
    const clone = em.create(Attachment, {
      entityId: E.catalog.catalog_product,
      recordId: productId,
      organizationId: scope.organizationId,
      tenantId: scope.tenantId,
      partitionCode: source.partitionCode,
      fileName: source.fileName,
      mimeType: source.mimeType,
      fileSize: source.fileSize,
      storageDriver: source.storageDriver,
      storagePath: source.storagePath,
      storageMetadata: source.storageMetadata ? cloneJson(source.storageMetadata) : null,
      url: source.url
    });
    em.persist(clone);
    existingKeys.add(key);
    created += 1;
  }
  if (created > 0) {
    await em.flush();
  }
}
const createVariantCommand = {
  id: "catalog.variants.create",
  async execute(rawInput, ctx) {
    const { parsed, custom } = parseWithCustomFields(variantCreateSchema, rawInput);
    const em = ctx.container.resolve("em").fork();
    const product = await requireProduct(em, parsed.productId, commandActorScope(ctx));
    ensureTenantScope(ctx, product.tenantId);
    ensureOrganizationScope(ctx, product.organizationId);
    const { taxRateId, taxRate } = await resolveVariantTaxRate(
      em,
      product,
      parsed.taxRateId ?? null,
      parsed.taxRate
    );
    const metadataSplit = splitOptionValuesFromMetadata(parsed.metadata);
    const resolvedOptionValues = parsed.optionValues ?? (metadataSplit.hadOptionValues ? metadataSplit.optionValues : null);
    const gtinType = parsed.gtinType ?? null;
    const barcode = gtinType && parsed.barcode ? normalizeGtinValue(gtinType, parsed.barcode) : parsed.barcode ?? null;
    const now = /* @__PURE__ */ new Date();
    const record = em.create(CatalogProductVariant, {
      organizationId: product.organizationId,
      tenantId: product.tenantId,
      product,
      name: parsed.name ?? null,
      sku: parsed.sku ?? null,
      barcode,
      gtinType,
      hsCode: parsed.hsCode ?? null,
      statusEntryId: parsed.statusEntryId ?? null,
      isDefault: parsed.isDefault ?? false,
      isActive: parsed.isActive ?? true,
      weightValue: toNumericString(parsed.weightValue),
      weightUnit: parsed.weightUnit ?? null,
      taxRateId,
      taxRate,
      dimensions: parsed.dimensions ? cloneJson(parsed.dimensions) : null,
      metadata: metadataSplit.metadata,
      optionValues: resolvedOptionValues ? cloneJson(resolvedOptionValues) : null,
      customFieldsetCode: parsed.customFieldsetCode ?? null,
      createdAt: now,
      updatedAt: now
    });
    em.persist(record);
    let previousDefaultVariantId = null;
    try {
      await withAtomicFlush(
        em,
        [
          () => em.flush(),
          async () => {
            if (record.isDefault) {
              previousDefaultVariantId = await enforceSingleDefaultVariant(em, record);
              await em.flush();
            }
          },
          () => aggregateVariantMediaToProduct(em, record)
        ],
        { transaction: true }
      );
    } catch (error) {
      await rethrowVariantUniqueConstraint(error);
    }
    await setCustomFieldsIfAny({
      dataEngine: ctx.container.resolve("dataEngine"),
      entityId: E.catalog.catalog_product_variant,
      recordId: record.id,
      organizationId: record.organizationId,
      tenantId: record.tenantId,
      values: custom
    });
    await emitCatalogQueryIndexEvent(ctx, {
      entityType: E.catalog.catalog_product_variant,
      recordId: record.id,
      organizationId: record.organizationId,
      tenantId: record.tenantId,
      action: "created"
    });
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "created",
      entity: record,
      identifiers: {
        id: record.id,
        organizationId: record.organizationId,
        tenantId: record.tenantId
      },
      events: variantCrudEvents
    });
    return { variantId: record.id, previousDefaultVariantId };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    return loadVariantSnapshot(em, result.variantId);
  },
  buildLog: async ({ result, snapshots }) => {
    const after = snapshots.after;
    if (!after) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("catalog.audit.variants.create", "Create product variant"),
      resourceKind: "catalog.variant",
      resourceId: result.variantId,
      parentResourceKind: "catalog.product",
      parentResourceId: after.productId ?? null,
      tenantId: after.tenantId,
      organizationId: after.organizationId,
      snapshotAfter: after,
      payload: {
        undo: {
          after,
          previousDefaultVariantId: result?.previousDefaultVariantId ?? null
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const after = payload?.after;
    if (!after) return;
    const em = ctx.container.resolve("em").fork();
    const record = await em.findOne(CatalogProductVariant, { id: after.id });
    if (!record) return;
    ensureTenantScope(ctx, record.tenantId);
    ensureOrganizationScope(ctx, record.organizationId);
    em.remove(record);
    await em.flush();
    if (payload?.previousDefaultVariantId) {
      const previousDefault = await em.findOne(CatalogProductVariant, { id: payload.previousDefaultVariantId });
      if (previousDefault) {
        ensureTenantScope(ctx, previousDefault.tenantId);
        ensureOrganizationScope(ctx, previousDefault.organizationId);
        previousDefault.isDefault = true;
        await em.flush();
      }
    }
    const resetValues = buildCustomFieldResetMap(void 0, after.custom ?? void 0);
    if (Object.keys(resetValues).length) {
      await setCustomFieldsIfAny({
        dataEngine: ctx.container.resolve("dataEngine"),
        entityId: E.catalog.catalog_product_variant,
        recordId: after.id,
        organizationId: after.organizationId,
        tenantId: after.tenantId,
        values: resetValues
      });
    }
  },
  redo: async ({ ctx, logEntry }) => {
    const after = resolveRedoSnapshot(logEntry);
    if (!after) throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for variant create" });
    const em = ctx.container.resolve("em").fork();
    const record = await restoreCreatedRow(
      em,
      CatalogProductVariant,
      after.id,
      () => variantSeedFromSnapshot(after)
    );
    applyVariantSnapshot(record, after);
    let previousDefaultVariantId = null;
    try {
      await withAtomicFlush(
        em,
        [
          () => em.flush(),
          async () => {
            if (record.isDefault) {
              previousDefaultVariantId = await enforceSingleDefaultVariant(em, record);
              await em.flush();
            }
          },
          () => aggregateVariantMediaToProduct(em, record)
        ],
        { transaction: true }
      );
    } catch (error) {
      await rethrowVariantUniqueConstraint(error);
    }
    if (after.custom && Object.keys(after.custom).length) {
      await setCustomFieldsIfAny({
        dataEngine: ctx.container.resolve("dataEngine"),
        entityId: E.catalog.catalog_product_variant,
        recordId: record.id,
        organizationId: record.organizationId,
        tenantId: record.tenantId,
        values: after.custom
      });
    }
    await emitCatalogQueryIndexEvent(ctx, {
      entityType: E.catalog.catalog_product_variant,
      recordId: record.id,
      organizationId: record.organizationId,
      tenantId: record.tenantId,
      action: "created"
    });
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "created",
      entity: record,
      identifiers: {
        id: record.id,
        organizationId: record.organizationId,
        tenantId: record.tenantId
      },
      events: variantCrudEvents
    });
    return { variantId: record.id, previousDefaultVariantId };
  }
};
const updateVariantCommand = {
  id: "catalog.variants.update",
  async prepare(input, ctx) {
    const id = requireId(input, "Variant id is required");
    const em = ctx.container.resolve("em");
    const snapshot = await loadVariantSnapshot(em, id);
    if (snapshot) {
      ensureTenantScope(ctx, snapshot.tenantId);
      ensureOrganizationScope(ctx, snapshot.organizationId);
    }
    return snapshot ? { before: snapshot } : {};
  },
  async execute(rawInput, ctx) {
    const { parsed, custom } = parseWithCustomFields(variantUpdateSchema, rawInput);
    const em = ctx.container.resolve("em").fork();
    const record = await em.findOne(CatalogProductVariant, { id: parsed.id, deletedAt: null });
    if (!record) throw new CrudHttpError(404, { error: "Catalog variant not found" });
    ensureTenantScope(ctx, record.tenantId);
    ensureOrganizationScope(ctx, record.organizationId);
    const product = await requireProduct(em, record.product.id, {
      tenantId: record.tenantId,
      organizationId: record.organizationId
    });
    if (!product) throw new CrudHttpError(400, { error: "Variant product missing" });
    const taxRateProvided = parsed.taxRateId !== void 0 || parsed.taxRate !== void 0;
    const resolvedTaxRate = taxRateProvided ? await resolveVariantTaxRate(em, product, parsed.taxRateId ?? null, parsed.taxRate) : null;
    let normalizedGtinBarcode = null;
    if (parsed.gtinType !== void 0 || parsed.barcode !== void 0) {
      const effectiveGtinType = parsed.gtinType !== void 0 ? parsed.gtinType ?? null : record.gtinType ?? null;
      if (effectiveGtinType) {
        const effectiveBarcode = parsed.barcode !== void 0 ? parsed.barcode ?? null : record.barcode ?? null;
        if (!effectiveBarcode || !effectiveBarcode.trim().length) {
          await throwGtinBarcodeRequiredError();
        } else {
          const normalized = normalizeGtinValue(effectiveGtinType, effectiveBarcode);
          if (!isValidGtin(effectiveGtinType, normalized)) {
            await throwInvalidGtinError();
          }
          normalizedGtinBarcode = normalized;
        }
      }
    }
    let previousDefaultVariantId = null;
    try {
      await withAtomicFlush(
        em,
        [
          () => {
            if (parsed.name !== void 0) record.name = parsed.name ?? null;
            if (parsed.sku !== void 0) record.sku = parsed.sku ?? null;
            if (parsed.barcode !== void 0) record.barcode = parsed.barcode ?? null;
            if (parsed.gtinType !== void 0) record.gtinType = parsed.gtinType ?? null;
            if (parsed.hsCode !== void 0) record.hsCode = parsed.hsCode ?? null;
            if (normalizedGtinBarcode !== null) record.barcode = normalizedGtinBarcode;
            if (parsed.statusEntryId !== void 0) record.statusEntryId = parsed.statusEntryId ?? null;
            if (parsed.isDefault !== void 0) record.isDefault = parsed.isDefault;
            if (parsed.isActive !== void 0) record.isActive = parsed.isActive;
            if (Object.prototype.hasOwnProperty.call(parsed, "weightValue")) {
              record.weightValue = toNumericString(parsed.weightValue);
            }
            if (parsed.weightUnit !== void 0) record.weightUnit = parsed.weightUnit ?? null;
            if (parsed.dimensions !== void 0) {
              record.dimensions = parsed.dimensions ? cloneJson(parsed.dimensions) : null;
            }
            let metadataSplit = null;
            if (parsed.metadata !== void 0) {
              metadataSplit = splitOptionValuesFromMetadata(parsed.metadata);
              record.metadata = metadataSplit.metadata;
            }
            if (parsed.optionValues !== void 0) {
              record.optionValues = parsed.optionValues ? cloneJson(parsed.optionValues) : null;
            } else if (metadataSplit?.hadOptionValues) {
              record.optionValues = metadataSplit.optionValues ? cloneJson(metadataSplit.optionValues) : null;
            }
            if (taxRateProvided) {
              record.taxRateId = resolvedTaxRate?.taxRateId ?? null;
              record.taxRate = resolvedTaxRate?.taxRate ?? null;
            }
            if (parsed.customFieldsetCode !== void 0) {
              record.customFieldsetCode = parsed.customFieldsetCode ?? null;
            }
          },
          async () => {
            if (parsed.isDefault === true) {
              previousDefaultVariantId = await enforceSingleDefaultVariant(em, record);
            }
          },
          () => aggregateVariantMediaToProduct(em, record)
        ],
        { transaction: true }
      );
    } catch (error) {
      await rethrowVariantUniqueConstraint(error);
    }
    if (custom && Object.keys(custom).length) {
      await setCustomFieldsIfAny({
        dataEngine: ctx.container.resolve("dataEngine"),
        entityId: E.catalog.catalog_product_variant,
        recordId: record.id,
        organizationId: record.organizationId,
        tenantId: record.tenantId,
        values: custom
      });
    }
    await emitCatalogQueryIndexEvent(ctx, {
      entityType: E.catalog.catalog_product_variant,
      recordId: record.id,
      organizationId: record.organizationId,
      tenantId: record.tenantId,
      action: "updated"
    });
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "updated",
      entity: record,
      identifiers: {
        id: record.id,
        organizationId: record.organizationId,
        tenantId: record.tenantId
      },
      events: variantCrudEvents
    });
    return { variantId: record.id, previousDefaultVariantId };
  },
  captureAfter: async (_input, result, ctx) => {
    const em = ctx.container.resolve("em").fork();
    return loadVariantSnapshot(em, result.variantId);
  },
  buildLog: async ({ result, snapshots }) => {
    const before = snapshots.before;
    const after = snapshots.after;
    if (!before || !after) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("catalog.audit.variants.update", "Update product variant"),
      resourceKind: "catalog.variant",
      resourceId: before.id,
      parentResourceKind: "catalog.product",
      parentResourceId: before.productId ?? null,
      tenantId: before.tenantId,
      organizationId: before.organizationId,
      snapshotBefore: before,
      snapshotAfter: after,
      changes: buildChanges(
        before,
        after,
        VARIANT_CHANGE_KEYS
      ),
      payload: {
        undo: {
          before,
          after,
          previousDefaultVariantId: result?.previousDefaultVariantId ?? null
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    if (!before) return;
    const after = payload?.after;
    const em = ctx.container.resolve("em").fork();
    let record = await em.findOne(CatalogProductVariant, { id: before.id });
    if (!record) {
      const product = await requireProduct(em, before.productId, {
        tenantId: before.tenantId,
        organizationId: before.organizationId
      });
      record = em.create(CatalogProductVariant, {
        id: before.id,
        product,
        organizationId: before.organizationId,
        tenantId: before.tenantId,
        name: before.name ?? null,
        sku: before.sku ?? null,
        barcode: before.barcode ?? null,
        statusEntryId: before.statusEntryId ?? null,
        isDefault: before.isDefault,
        isActive: before.isActive,
        weightValue: before.weightValue ?? null,
        weightUnit: before.weightUnit ?? null,
        dimensions: before.dimensions ? cloneJson(before.dimensions) : null,
        metadata: before.metadata ? cloneJson(before.metadata) : null,
        optionValues: before.optionValues ? cloneJson(before.optionValues) : null,
        customFieldsetCode: before.customFieldsetCode ?? null,
        createdAt: new Date(before.createdAt),
        updatedAt: new Date(before.updatedAt)
      });
      em.persist(record);
    }
    ensureTenantScope(ctx, before.tenantId);
    ensureOrganizationScope(ctx, before.organizationId);
    applyVariantSnapshot(record, before);
    await em.flush();
    const previousDefaultId = payload?.previousDefaultVariantId;
    if (previousDefaultId) {
      const previousDefault = await em.findOne(CatalogProductVariant, { id: previousDefaultId });
      if (previousDefault) {
        ensureTenantScope(ctx, previousDefault.tenantId);
        ensureOrganizationScope(ctx, previousDefault.organizationId);
        previousDefault.isDefault = true;
        await em.flush();
      }
    }
    const resetValues = buildCustomFieldResetMap(
      before.custom ?? void 0,
      after?.custom ?? void 0
    );
    if (Object.keys(resetValues).length) {
      await setCustomFieldsIfAny({
        dataEngine: ctx.container.resolve("dataEngine"),
        entityId: E.catalog.catalog_product_variant,
        recordId: before.id,
        organizationId: before.organizationId,
        tenantId: before.tenantId,
        values: resetValues
      });
    }
  }
};
const deleteVariantCommand = {
  id: "catalog.variants.delete",
  async prepare(input, ctx) {
    const id = requireId(input, "Variant id is required");
    const em = ctx.container.resolve("em");
    const snapshot = await loadVariantSnapshot(em, id, { includePrices: true });
    if (snapshot) {
      ensureTenantScope(ctx, snapshot.tenantId);
      ensureOrganizationScope(ctx, snapshot.organizationId);
    }
    return snapshot ? { before: snapshot } : {};
  },
  async execute(input, ctx) {
    const id = requireId(input, "Variant id is required");
    const em = ctx.container.resolve("em").fork();
    const record = await em.findOne(CatalogProductVariant, { id });
    if (!record) throw new CrudHttpError(404, { error: "Catalog variant not found" });
    ensureTenantScope(ctx, record.tenantId);
    ensureOrganizationScope(ctx, record.organizationId);
    const baseEm = ctx.container.resolve("em");
    const snapshot = await loadVariantSnapshot(baseEm, id, { includePrices: true });
    const dataEngine = ctx.container.resolve("dataEngine");
    const priceSnapshots = snapshot?.prices && snapshot.prices.length ? snapshot.prices : await loadVariantPriceSnapshots(baseEm, id, {
      tenantId: record.tenantId,
      organizationId: record.organizationId
    });
    if (priceSnapshots.length) {
      await em.nativeDelete(CatalogProductPrice, { id: { $in: priceSnapshots.map((price) => price.id) } });
    } else {
      await em.nativeDelete(CatalogProductPrice, { variant: record });
    }
    em.remove(record);
    await em.flush();
    for (const priceSnapshot of priceSnapshots) {
      const resetValues = buildCustomFieldResetMap(priceSnapshot.custom ?? void 0, void 0);
      if (!Object.keys(resetValues).length) continue;
      await setCustomFieldsIfAny({
        dataEngine,
        entityId: E.catalog.catalog_product_price,
        recordId: priceSnapshot.id,
        organizationId: priceSnapshot.organizationId,
        tenantId: priceSnapshot.tenantId,
        values: resetValues
      });
    }
    if (snapshot?.custom && Object.keys(snapshot.custom).length) {
      const resetValues = buildCustomFieldResetMap(snapshot.custom, void 0);
      if (Object.keys(resetValues).length) {
        await setCustomFieldsIfAny({
          dataEngine,
          entityId: E.catalog.catalog_product_variant,
          recordId: id,
          organizationId: snapshot.organizationId,
          tenantId: snapshot.tenantId,
          values: resetValues
        });
      }
    }
    await emitCatalogQueryIndexEvent(ctx, {
      entityType: E.catalog.catalog_product_variant,
      recordId: id,
      organizationId: snapshot?.organizationId ?? record.organizationId,
      tenantId: snapshot?.tenantId ?? record.tenantId,
      action: "deleted"
    });
    await emitCrudSideEffects({
      dataEngine: ctx.container.resolve("dataEngine"),
      action: "deleted",
      entity: record,
      identifiers: {
        id,
        organizationId: snapshot?.organizationId ?? record.organizationId,
        tenantId: snapshot?.tenantId ?? record.tenantId
      },
      events: variantCrudEvents
    });
    return { variantId: id };
  },
  buildLog: async ({ snapshots }) => {
    const before = snapshots.before;
    if (!before) return null;
    const { translate } = await resolveTranslations();
    return {
      actionLabel: translate("catalog.audit.variants.delete", "Delete product variant"),
      resourceKind: "catalog.variant",
      resourceId: before.id,
      parentResourceKind: "catalog.product",
      parentResourceId: before.productId ?? null,
      tenantId: before.tenantId,
      organizationId: before.organizationId,
      snapshotBefore: before,
      payload: {
        undo: {
          before
        }
      }
    };
  },
  undo: async ({ logEntry, ctx }) => {
    const payload = extractUndoPayload(logEntry);
    const before = payload?.before;
    if (!before) return;
    const em = ctx.container.resolve("em").fork();
    let record = await em.findOne(CatalogProductVariant, { id: before.id });
    if (!record) {
      const product = await requireProduct(em, before.productId, {
        tenantId: before.tenantId,
        organizationId: before.organizationId
      });
      record = em.create(CatalogProductVariant, {
        id: before.id,
        product,
        organizationId: before.organizationId,
        tenantId: before.tenantId,
        name: before.name ?? null,
        sku: before.sku ?? null,
        barcode: before.barcode ?? null,
        statusEntryId: before.statusEntryId ?? null,
        isDefault: before.isDefault,
        isActive: before.isActive,
        weightValue: before.weightValue ?? null,
        weightUnit: before.weightUnit ?? null,
        dimensions: before.dimensions ? cloneJson(before.dimensions) : null,
        metadata: before.metadata ? cloneJson(before.metadata) : null,
        customFieldsetCode: before.customFieldsetCode ?? null,
        createdAt: new Date(before.createdAt),
        updatedAt: new Date(before.updatedAt)
      });
      em.persist(record);
    }
    ensureTenantScope(ctx, before.tenantId);
    ensureOrganizationScope(ctx, before.organizationId);
    applyVariantSnapshot(record, before);
    if (before.prices?.length) {
      const dataEngine = ctx.container.resolve("dataEngine");
      await restoreVariantPricesFromSnapshots(em, record, before.prices, dataEngine);
    }
    await em.flush();
    if (before.custom && Object.keys(before.custom).length) {
      await setCustomFieldsIfAny({
        dataEngine: ctx.container.resolve("dataEngine"),
        entityId: E.catalog.catalog_product_variant,
        recordId: before.id,
        organizationId: before.organizationId,
        tenantId: before.tenantId,
        values: before.custom
      });
    }
  }
};
async function throwDuplicateVariantSkuError() {
  const { translate } = await resolveTranslations();
  const message = translate("catalog.variants.errors.skuExists", "SKU already in use.");
  throw new CrudHttpError(400, {
    error: message,
    fieldErrors: { sku: message },
    details: [{ path: ["sku"], message, code: "duplicate", origin: "validation" }]
  });
}
async function throwDuplicateVariantGtinError() {
  const { translate } = await resolveTranslations();
  const message = translate(
    "catalog.variants.errors.gtinExists",
    "Another variant already uses this identifier."
  );
  throw new CrudHttpError(400, {
    error: message,
    fieldErrors: { barcode: message },
    details: [{ path: ["barcode"], message, code: "duplicate", origin: "validation" }]
  });
}
async function throwGtinBarcodeRequiredError() {
  const { translate } = await resolveTranslations();
  const message = translate(
    "catalog.variants.validation.gtinBarcodeRequired",
    "A barcode value is required when an identifier type is set."
  );
  throw new CrudHttpError(400, {
    error: message,
    fieldErrors: { barcode: message },
    details: [{ path: ["barcode"], message, code: "invalid", origin: "validation" }]
  });
}
async function throwInvalidGtinError() {
  const { translate } = await resolveTranslations();
  const message = translate(
    "catalog.variants.validation.gtinChecksum",
    "The barcode does not match the selected identifier type."
  );
  throw new CrudHttpError(400, {
    error: message,
    fieldErrors: { barcode: message },
    details: [{ path: ["barcode"], message, code: "invalid", origin: "validation" }]
  });
}
async function rethrowVariantUniqueConstraint(error) {
  if (error instanceof UniqueConstraintViolationException) {
    const constraint = getErrorConstraint(error);
    const message = getErrorMessage(error).toLowerCase();
    if (constraint === "catalog_product_variants_sku_unique" || message.includes("catalog_product_variants_sku_unique")) {
      await throwDuplicateVariantSkuError();
    }
    if (constraint === "catalog_product_variants_gtin_scope_unique" || message.includes("catalog_product_variants_gtin_scope_unique")) {
      await throwDuplicateVariantGtinError();
    }
  }
  throw error;
}
registerCommand(createVariantCommand);
registerCommand(updateVariantCommand);
registerCommand(deleteVariantCommand);
//# sourceMappingURL=variants.js.map
