import { z } from "zod";
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import {
  buildCustomFieldFiltersFromQuery,
  extractAllCustomFieldEntries
} from "@open-mercato/shared/lib/crud/custom-fields";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import {
  CatalogOffer,
  CatalogProduct,
  CatalogProductCategory,
  CatalogProductCategoryAssignment,
  CatalogProductPrice,
  CatalogProductUnitConversion,
  CatalogProductVariant,
  CatalogProductTagAssignment
} from "../../data/entities.js";
import { CATALOG_PRODUCT_TYPES } from "../../data/types.js";
import {
  productCreateSchema,
  productUpdateSchema
} from "../../data/validators.js";
import { parseScopedCommandInput, resolveCrudRecordId } from "../utils.js";
import { splitCustomFieldPayload } from "@open-mercato/shared/lib/crud/custom-fields";
import { E } from "../../../../generated/entities.ids.generated.js";
import * as F from "../../../../generated/entities/catalog_product/index.js";
import { parseBooleanFlag, sanitizeSearchTerm } from "../helpers.js";
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
import { buildScopedWhere } from "@open-mercato/shared/lib/api/crud";
import {
  resolvePriceChannelId,
  resolvePriceOfferId,
  resolvePriceVariantId,
  resolvePriceKindCode
} from "../../lib/pricing.js";
import { fieldsetCodeRegex } from "@open-mercato/core/modules/entities/data/validators";
import { SalesChannel } from "@open-mercato/core/modules/sales/data/entities";
import {
  createCatalogCrudOpenApi,
  createPagedListResponseSchema,
  defaultOkResponseSchema
} from "../openapi.js";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { canonicalizeUnitCode, toUnitLookupKey } from "../../lib/unitCodes.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("catalog");
const rawBodySchema = z.object({}).passthrough();
const UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/;
const listSchema = z.object({
  page: z.coerce.number().min(1).default(1),
  pageSize: z.coerce.number().min(1).max(100).default(50),
  id: z.string().uuid().optional(),
  search: z.string().optional(),
  status: z.string().optional(),
  isActive: z.string().optional(),
  configurable: z.string().optional(),
  productType: z.enum(CATALOG_PRODUCT_TYPES).optional(),
  channelIds: z.string().optional(),
  channelId: z.string().uuid().optional(),
  categoryIds: z.string().optional(),
  tagIds: z.string().optional(),
  offerId: z.string().uuid().optional(),
  userId: z.string().uuid().optional(),
  userGroupId: z.string().uuid().optional(),
  customerId: z.string().uuid().optional(),
  customerGroupId: z.string().uuid().optional(),
  quantity: z.coerce.number().min(1).max(1e5).optional(),
  quantityUnit: z.string().trim().max(50).optional(),
  priceDate: z.string().optional(),
  sortField: z.string().optional(),
  sortDir: z.enum(["asc", "desc"]).optional(),
  withDeleted: z.coerce.boolean().optional(),
  customFieldset: z.string().regex(fieldsetCodeRegex).optional()
}).passthrough();
const routeMetadata = {
  GET: { requireAuth: true, requireFeatures: ["catalog.products.view"] },
  POST: { requireAuth: true, requireFeatures: ["catalog.products.manage"] },
  PUT: { requireAuth: true, requireFeatures: ["catalog.products.manage"] },
  DELETE: { requireAuth: true, requireFeatures: ["catalog.products.manage"] }
};
const metadata = routeMetadata;
function parseIdList(raw) {
  if (!raw) return [];
  return raw.split(",").map((value) => value.trim()).filter((value) => UUID_REGEX.test(value));
}
async function buildProductFilters(query, ctx) {
  const filters = {};
  const em = ctx.container.resolve("em").fork();
  const restrictedProductIds = { value: null };
  const intersectProductIds = (ids) => {
    const normalized = ids.filter(
      (id) => typeof id === "string" && id.trim().length > 0
    );
    const current = new Set(normalized);
    if (!current.size) {
      restrictedProductIds.value = /* @__PURE__ */ new Set();
      return;
    }
    if (!restrictedProductIds.value) {
      restrictedProductIds.value = current;
      return;
    }
    restrictedProductIds.value = new Set(
      Array.from(restrictedProductIds.value).filter((id) => current.has(id))
    );
  };
  const applyRestrictedProducts = () => {
    if (!restrictedProductIds.value) return;
    if (restrictedProductIds.value.size === 0) {
      filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
      return;
    }
    const ids = Array.from(restrictedProductIds.value);
    const existing = filters.id;
    if (existing && typeof existing === "object") {
      if ("$eq" in existing && typeof existing.$eq === "string") {
        const target = existing.$eq;
        if (!restrictedProductIds.value.has(target)) {
          filters.id = { $eq: "00000000-0000-0000-0000-000000000000" };
        }
        return;
      }
      if ("$in" in existing && Array.isArray(existing.$in)) {
        const subset = existing.$in.filter(
          (id) => restrictedProductIds.value.has(id)
        );
        filters.id = subset.length ? { $in: subset } : { $eq: "00000000-0000-0000-0000-000000000000" };
        return;
      }
    }
    filters.id = ids.length === 1 ? { $eq: ids[0] } : { $in: ids };
  };
  if (query.id) {
    filters.id = { $eq: query.id };
  }
  if (query.status && query.status.trim()) {
    filters.status_entry_id = { $eq: query.status.trim() };
  }
  const active = parseBooleanFlag(query.isActive);
  if (active !== void 0) {
    filters.is_active = active;
  }
  const configurable = parseBooleanFlag(query.configurable);
  if (configurable !== void 0) {
    filters.is_configurable = configurable;
  }
  if (query.productType) {
    filters.product_type = { $eq: query.productType };
  }
  const scope = {
    organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
    tenantId: ctx.auth?.tenantId ?? null
  };
  const term = sanitizeSearchTerm(query.search);
  const channelFilterIds = parseIdList(query.channelIds);
  const categoryFilterIds = parseIdList(query.categoryIds);
  const tagFilterIds = parseIdList(query.tagIds);
  const customFieldset = typeof query.customFieldset === "string" && query.customFieldset.trim().length ? query.customFieldset.trim() : null;
  const tenantId = ctx.auth?.tenantId ?? null;
  const searchTask = async () => {
    if (!term) return null;
    const like = `%${escapeLikePattern(term)}%`;
    const searchMatches = await findWithDecryption(
      em,
      CatalogProduct,
      {
        ...scope,
        ...query.withDeleted ? {} : { deletedAt: null },
        $or: [
          { title: { $ilike: like } },
          { subtitle: { $ilike: like } },
          { description: { $ilike: like } },
          { sku: { $ilike: like } },
          { handle: { $ilike: like } }
        ]
      },
      { fields: ["id"] },
      scope
    );
    return searchMatches.map((product) => product.id).filter((id) => typeof id === "string" && id.length > 0);
  };
  const channelTask = async () => {
    if (!channelFilterIds.length) return null;
    const offerRows = await findWithDecryption(
      em,
      CatalogOffer,
      {
        channelId: { $in: channelFilterIds },
        deletedAt: null,
        ...scope
      },
      { fields: ["id", "product"] },
      scope
    );
    return offerRows.map(
      (offer) => typeof offer.product === "string" ? offer.product : offer.product?.id ?? null
    ).filter((id) => !!id);
  };
  const categoryTask = async () => {
    if (!categoryFilterIds.length) return null;
    const assignments = await findWithDecryption(
      em,
      CatalogProductCategoryAssignment,
      { category: { $in: categoryFilterIds }, ...scope },
      { fields: ["id", "product"] },
      scope
    );
    return assignments.map(
      (assignment) => typeof assignment.product === "string" ? assignment.product : assignment.product?.id ?? null
    ).filter((id) => !!id);
  };
  const tagTask = async () => {
    if (!tagFilterIds.length) return null;
    const assignments = await findWithDecryption(
      em,
      CatalogProductTagAssignment,
      { tag: { $in: tagFilterIds }, ...scope },
      { fields: ["id", "product"] },
      scope
    );
    return assignments.map(
      (assignment) => typeof assignment.product === "string" ? assignment.product : assignment.product?.id ?? null
    ).filter((id) => !!id);
  };
  const customFieldTask = async () => {
    try {
      const scopedEm = ctx.container.resolve("em");
      return await buildCustomFieldFiltersFromQuery({
        entityIds: [E.catalog.catalog_product],
        query,
        em: scopedEm,
        tenantId,
        fieldset: customFieldset ?? void 0
      });
    } catch (err) {
      logger.debug("catalog.products custom field filter error", { err });
      return {};
    }
  };
  const [searchIds, channelIds, categoryIds, tagIds, cfFilters] = await Promise.all([
    searchTask(),
    channelTask(),
    categoryTask(),
    tagTask(),
    customFieldTask()
  ]);
  for (const productIds of [searchIds, channelIds, categoryIds, tagIds]) {
    if (productIds) intersectProductIds(productIds);
  }
  Object.assign(filters, cfFilters);
  applyRestrictedProducts();
  return filters;
}
function buildPricingContext(query, channelFallback) {
  const quantity = Number.isFinite(Number(query.quantity)) ? Number(query.quantity) : 1;
  const parsedDate = query.priceDate ? new Date(query.priceDate) : /* @__PURE__ */ new Date();
  const channelId = query.channelId ?? channelFallback ?? null;
  return {
    channelId,
    offerId: query.offerId ?? null,
    userId: query.userId ?? null,
    userGroupId: query.userGroupId ?? null,
    customerId: query.customerId ?? null,
    customerGroupId: query.customerGroupId ?? null,
    quantity: Number.isFinite(quantity) && quantity > 0 ? quantity : 1,
    date: Number.isNaN(parsedDate.getTime()) ? /* @__PURE__ */ new Date() : parsedDate
  };
}
async function decorateProductsAfterList(payload, ctx) {
  const items = Array.isArray(payload?.items) ? payload.items : [];
  if (!items.length) return;
  const productIds = items.map((item) => typeof item.id === "string" ? item.id : null).filter((id) => !!id);
  if (!productIds.length) return;
  try {
    const em = ctx.container.resolve("em").fork();
    const scope = {
      organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
      tenantId: ctx.auth?.tenantId ?? null
    };
    const offers = await findWithDecryption(
      em,
      CatalogOffer,
      { product: { $in: productIds }, deletedAt: null, ...scope },
      { orderBy: { createdAt: "asc" } },
      scope
    );
    const channelIds = Array.from(
      new Set(
        offers.map((offer) => offer.channelId).filter(
          (id) => typeof id === "string" && id.length > 0
        )
      )
    );
    const channelLookup = /* @__PURE__ */ new Map();
    if (channelIds.length) {
      const scopedChannelsWhere = buildScopedWhere(
        { id: { $in: channelIds } },
        {
          organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
          organizationIds: Array.isArray(ctx.organizationIds) ? ctx.organizationIds : void 0,
          tenantId: ctx.auth?.tenantId ?? null
        }
      );
      const channels = await findWithDecryption(em, SalesChannel, scopedChannelsWhere, {
        fields: ["id", "name", "code"]
      });
      for (const channel of channels) {
        channelLookup.set(channel.id, {
          name: channel.name,
          code: channel.code ?? null
        });
      }
    }
    const offersByProduct = /* @__PURE__ */ new Map();
    for (const offer of offers) {
      const productId = typeof offer.product === "string" ? offer.product : offer.product?.id ?? null;
      if (!productId) continue;
      const channelInfo = channelLookup.get(offer.channelId);
      const entry = offersByProduct.get(productId) ?? [];
      entry.push({
        id: offer.id,
        channelId: offer.channelId,
        channelName: channelInfo?.name ?? null,
        channelCode: channelInfo?.code ?? null,
        title: offer.title,
        description: offer.description ?? null,
        isActive: offer.isActive,
        defaultMediaId: offer.defaultMediaId ?? null,
        defaultMediaUrl: offer.defaultMediaUrl ?? null,
        metadata: offer.metadata ?? null,
        updatedAt: offer.updatedAt instanceof Date ? offer.updatedAt.toISOString() : typeof offer.updatedAt === "string" ? offer.updatedAt : null
      });
      offersByProduct.set(productId, entry);
    }
    const categoryAssignments = await findWithDecryption(
      em,
      CatalogProductCategoryAssignment,
      { product: { $in: productIds }, ...scope },
      { populate: ["category"], orderBy: { position: "asc" } },
      scope
    );
    const parentIds = /* @__PURE__ */ new Set();
    for (const assignment of categoryAssignments) {
      const category = typeof assignment.category === "string" ? null : assignment.category ?? null;
      if (!category) continue;
      const parentId = category.parentId ?? null;
      if (parentId) parentIds.add(parentId);
    }
    const parentCategories = parentIds.size ? await findWithDecryption(
      em,
      CatalogProductCategory,
      { id: { $in: Array.from(parentIds) }, ...scope },
      { fields: ["id", "name"] },
      scope
    ) : [];
    const parentNameById = /* @__PURE__ */ new Map();
    for (const parent of parentCategories) {
      parentNameById.set(parent.id, parent.name ?? null);
    }
    const categoriesByProduct = /* @__PURE__ */ new Map();
    for (const assignment of categoryAssignments) {
      const productId = typeof assignment.product === "string" ? assignment.product : assignment.product?.id ?? null;
      if (!productId) continue;
      const category = typeof assignment.category === "string" ? null : assignment.category ?? null;
      if (!category) continue;
      const parentId = category.parentId ?? null;
      const parentName = parentId ? parentNameById.get(parentId) ?? null : null;
      const bucket = categoriesByProduct.get(productId) ?? [];
      bucket.push({
        id: category.id,
        name: category.name ?? null,
        treePath: category.treePath ?? null,
        parentId,
        parentName
      });
      categoriesByProduct.set(productId, bucket);
    }
    const tagAssignments = await findWithDecryption(
      em,
      CatalogProductTagAssignment,
      { product: { $in: productIds } },
      { populate: ["tag"] },
      {
        tenantId: ctx.auth?.tenantId ?? null,
        organizationId: ctx.auth?.orgId ?? null
      }
    );
    const tagsByProduct = /* @__PURE__ */ new Map();
    for (const assignment of tagAssignments) {
      const productId = typeof assignment.product === "string" ? assignment.product : assignment.product?.id ?? null;
      if (!productId) continue;
      const tag = typeof assignment.tag === "string" ? null : assignment.tag ?? null;
      if (!tag) continue;
      const label = typeof tag.label === "string" && tag.label.trim().length ? tag.label : null;
      if (!label) continue;
      const bucket = tagsByProduct.get(productId) ?? [];
      bucket.push(label);
      tagsByProduct.set(productId, bucket);
    }
    const variants = await findWithDecryption(
      em,
      CatalogProductVariant,
      { product: { $in: productIds }, deletedAt: null, ...scope },
      { fields: ["id", "product"] },
      scope
    );
    const variantToProduct = /* @__PURE__ */ new Map();
    for (const variant of variants) {
      const productId = typeof variant.product === "string" ? variant.product : variant.product?.id ?? null;
      if (!productId) continue;
      variantToProduct.set(variant.id, productId);
    }
    const variantIds = Array.from(variantToProduct.keys());
    const priceWhere = variantIds.length > 0 ? {
      $or: [
        { product: { $in: productIds } },
        { variant: { $in: variantIds } }
      ]
    } : { product: { $in: productIds } };
    const priceRows = await findWithDecryption(
      em,
      CatalogProductPrice,
      { ...priceWhere, ...scope },
      { populate: ["offer", "variant", "product", "priceKind"] },
      scope
    );
    const pricesByProduct = /* @__PURE__ */ new Map();
    for (const price of priceRows) {
      let productId = null;
      if (price.product) {
        productId = typeof price.product === "string" ? price.product : price.product?.id ?? null;
      } else if (price.variant) {
        const variantId = typeof price.variant === "string" ? price.variant : price.variant.id;
        productId = variantToProduct.get(variantId) ?? null;
      }
      if (!productId) continue;
      const entry = pricesByProduct.get(productId) ?? [];
      entry.push(price);
      pricesByProduct.set(productId, entry);
    }
    const requestQuantityUnitKey = toUnitLookupKey(
      ctx.query.quantityUnit
    );
    const conversionsByProduct = /* @__PURE__ */ new Map();
    const conversionOrganizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null;
    const conversionTenantId = ctx.auth?.tenantId ?? null;
    if (requestQuantityUnitKey && productIds.length && conversionOrganizationId && conversionTenantId) {
      const conversionRows = await findWithDecryption(
        em,
        CatalogProductUnitConversion,
        {
          product: { $in: productIds },
          organizationId: conversionOrganizationId,
          tenantId: conversionTenantId,
          deletedAt: null,
          isActive: true
        },
        { fields: ["id", "product", "unitCode", "toBaseFactor"] },
        { organizationId: conversionOrganizationId, tenantId: conversionTenantId }
      );
      for (const row of conversionRows) {
        const productId = typeof row.product === "string" ? row.product : row.product?.id ?? null;
        const unitKey = toUnitLookupKey(row.unitCode);
        const factor = Number(row.toBaseFactor);
        if (!productId || !unitKey || !Number.isFinite(factor) || factor <= 0)
          continue;
        const bucket = conversionsByProduct.get(productId) ?? /* @__PURE__ */ new Map();
        bucket.set(unitKey, factor);
        conversionsByProduct.set(productId, bucket);
      }
    }
    const channelFilterIds = parseIdList(ctx.query.channelIds);
    const channelContext = ctx.query.channelId ?? (channelFilterIds.length === 1 ? channelFilterIds[0] : null);
    const pricingContext = buildPricingContext(ctx.query, channelContext);
    const pricingService = ctx.container.resolve(
      "catalogPricingService"
    );
    const pricingEntries = [];
    for (const item of items) {
      const id = typeof item.id === "string" ? item.id : null;
      if (!id) {
        pricingEntries.push(null);
        continue;
      }
      const offerEntries = offersByProduct.get(id) ?? [];
      item.offers = offerEntries;
      const channelIds2 = Array.from(
        new Set(
          offerEntries.map(
            (offer) => typeof offer.channelId === "string" ? offer.channelId : null
          ).filter((channelId) => !!channelId)
        )
      );
      item.channelIds = channelIds2;
      const categories = categoriesByProduct.get(id) ?? [];
      item.categories = categories;
      item.categoryIds = categories.map((category) => category.id);
      item.tags = tagsByProduct.get(id) ?? [];
      if (item.is_quote_only === true) {
        item.pricing = null;
        pricingEntries.push(null);
        continue;
      }
      const priceCandidates = pricesByProduct.get(id) ?? [];
      const normalizedQuantityForPricing = (() => {
        if (!requestQuantityUnitKey) return pricingContext.quantity;
        const baseUnit = toUnitLookupKey(item.default_unit);
        if (!baseUnit || requestQuantityUnitKey === baseUnit)
          return pricingContext.quantity;
        const productConversions = conversionsByProduct.get(id);
        const factor = productConversions?.get(requestQuantityUnitKey) ?? null;
        if (!factor || !Number.isFinite(factor) || factor <= 0) {
          logger.debug("catalog.products invalid conversion factor", { productId: id, unit: requestQuantityUnitKey, factor });
          return pricingContext.quantity;
        }
        const normalized = pricingContext.quantity * factor;
        return Number.isFinite(normalized) && normalized > 0 ? normalized : pricingContext.quantity;
      })();
      const channelScopedContext = pricingContext.channelId || channelIds2.length !== 1 ? pricingContext : { ...pricingContext, channelId: channelIds2[0] };
      pricingEntries.push({
        rows: priceCandidates,
        context: { ...channelScopedContext, quantity: normalizedQuantityForPricing }
      });
    }
    const resolveInputs = [];
    const resolveIndices = [];
    for (let i = 0; i < pricingEntries.length; i++) {
      if (pricingEntries[i] !== null) {
        resolveInputs.push(pricingEntries[i]);
        resolveIndices.push(i);
      }
    }
    const priceResults = await pricingService.resolvePriceMany(resolveInputs);
    for (let i = 0; i < resolveIndices.length; i++) {
      const item = items[resolveIndices[i]];
      const best = priceResults[i];
      if (best) {
        item.pricing = {
          kind: resolvePriceKindCode(best),
          price_kind_id: typeof best.priceKind === "string" ? best.priceKind : best.priceKind?.id ?? null,
          price_kind_code: resolvePriceKindCode(best),
          currency_code: best.currencyCode,
          unit_price_net: best.unitPriceNet,
          unit_price_gross: best.unitPriceGross,
          min_quantity: best.minQuantity,
          max_quantity: best.maxQuantity ?? null,
          tax_rate: best.taxRate ?? null,
          tax_amount: best.taxAmount ?? null,
          scope: {
            variant_id: resolvePriceVariantId(best),
            offer_id: resolvePriceOfferId(best),
            channel_id: resolvePriceChannelId(best),
            user_id: best.userId ?? null,
            user_group_id: best.userGroupId ?? null,
            customer_id: best.customerId ?? null,
            customer_group_id: best.customerGroupId ?? null
          }
        };
      } else {
        item.pricing = null;
      }
    }
  } catch (error) {
    logger.error("decorateProductsAfterList Failed to load unit conversions", { err: error });
  }
  const searchTerm = ctx.query.search ? sanitizeSearchTerm(ctx.query.search) : null;
  if (searchTerm && !ctx.query.sortField && Array.isArray(payload.items)) {
    const needle = searchTerm.toLowerCase();
    payload.items.sort((a, b) => {
      const scoreA = scoreProductSearchRelevance(needle, a.title, a.sku);
      const scoreB = scoreProductSearchRelevance(needle, b.title, b.sku);
      if (scoreA !== scoreB) return scoreA - scoreB;
      return (a.title ?? "").localeCompare(b.title ?? "");
    });
  }
}
function scoreProductSearchRelevance(needle, title, sku) {
  const t = (title ?? "").toLowerCase();
  const s = (sku ?? "").toLowerCase();
  if (t === needle) return 0;
  if (s === needle) return 1;
  if (t.startsWith(needle)) return 2;
  if (s.startsWith(needle)) return 3;
  if (t.includes(needle)) return 4;
  if (s.includes(needle)) return 5;
  return 6;
}
const crud = makeCrudRoute({
  metadata: routeMetadata,
  orm: {
    entity: CatalogProduct,
    idField: "id",
    orgField: "organizationId",
    tenantField: "tenantId",
    softDeleteField: "deletedAt"
  },
  indexer: {
    entityType: E.catalog.catalog_product
  },
  list: {
    schema: listSchema,
    entityId: E.catalog.catalog_product,
    fields: [
      F.id,
      F.title,
      F.subtitle,
      F.description,
      F.sku,
      F.handle,
      "tax_rate_id",
      "tax_rate",
      F.product_type,
      F.status_entry_id,
      F.primary_currency_code,
      F.default_unit,
      "default_sales_unit",
      "default_sales_unit_quantity",
      "uom_rounding_scale",
      "uom_rounding_mode",
      "unit_price_enabled",
      "unit_price_reference_unit",
      "unit_price_base_quantity",
      F.default_media_id,
      F.default_media_url,
      F.weight_value,
      F.weight_unit,
      F.dimensions,
      F.is_configurable,
      F.is_active,
      "country_of_origin_code",
      "pkwiu_code",
      "cn_code",
      "hs_code",
      "tax_classification_code",
      "gtu_codes",
      "age_min",
      "is_excise_good",
      "excise_category",
      "requires_prescription",
      "hazmat_class",
      "un_number",
      "hazmat_packing_group",
      "contains_lithium_battery",
      "launch_at",
      "end_of_life_at",
      "available_from",
      "available_until",
      "min_order_qty",
      "max_order_qty",
      "order_qty_increment",
      "requires_shipping",
      "is_quote_only",
      "seo_title",
      "seo_description",
      "canonical_url",
      F.metadata,
      "custom_fieldset_code",
      "option_schema_id",
      F.created_at,
      F.updated_at
    ],
    decorateCustomFields: { entityIds: [E.catalog.catalog_product] },
    sortFieldMap: {
      title: F.title,
      sku: F.sku,
      createdAt: F.created_at,
      updatedAt: F.updated_at
    },
    buildFilters: buildProductFilters,
    transformItem: (item) => {
      if (!item) return item;
      const normalized = { ...item };
      const cfEntries = extractAllCustomFieldEntries(item);
      for (const key of Object.keys(normalized)) {
        if (key.startsWith("cf:")) {
          delete normalized[key];
        }
      }
      const defaultUnit = canonicalizeUnitCode(normalized.default_unit) ?? null;
      const defaultSalesUnit = canonicalizeUnitCode(normalized.default_sales_unit) ?? null;
      const unitPriceReferenceUnit = canonicalizeUnitCode(normalized.unit_price_reference_unit) ?? null;
      return {
        ...normalized,
        default_unit: defaultUnit,
        default_sales_unit: defaultSalesUnit,
        unit_price_reference_unit: unitPriceReferenceUnit,
        ...cfEntries,
        unit_price: {
          enabled: Boolean(normalized.unit_price_enabled),
          reference_unit: unitPriceReferenceUnit,
          base_quantity: normalized.unit_price_base_quantity ?? null
        }
      };
    }
  },
  hooks: {
    afterList: decorateProductsAfterList
  },
  actions: {
    create: {
      commandId: "catalog.products.create",
      schema: rawBodySchema,
      mapInput: async ({ raw, ctx }) => {
        const { translate } = await resolveTranslations();
        const parsed = parseScopedCommandInput(
          productCreateSchema,
          raw ?? {},
          ctx,
          translate
        );
        const { base, custom } = splitCustomFieldPayload(parsed);
        return Object.keys(custom).length ? { ...base, customFields: custom } : base;
      },
      response: ({ result }) => ({
        id: result?.productId ?? result?.id ?? null
      }),
      status: 201
    },
    update: {
      commandId: "catalog.products.update",
      schema: rawBodySchema,
      mapInput: async ({ raw, ctx }) => {
        const { translate } = await resolveTranslations();
        const parsed = parseScopedCommandInput(
          productUpdateSchema,
          raw ?? {},
          ctx,
          translate
        );
        const { base, custom } = splitCustomFieldPayload(parsed);
        return Object.keys(custom).length ? { ...base, customFields: custom } : base;
      },
      response: () => ({ ok: true })
    },
    delete: {
      commandId: "catalog.products.delete",
      schema: rawBodySchema,
      mapInput: async ({ parsed, ctx }) => {
        const { translate } = await resolveTranslations();
        const id = resolveCrudRecordId(parsed, ctx, translate);
        if (!id)
          throw new CrudHttpError(400, {
            error: translate(
              "catalog.errors.id_required",
              "Product id is required."
            )
          });
        return { id };
      },
      response: () => ({ ok: true })
    }
  }
});
const GET = crud.GET;
const POST = crud.POST;
const PUT = crud.PUT;
const DELETE = crud.DELETE;
const productListItemSchema = z.object({
  id: z.string().uuid(),
  title: z.string().nullable().optional(),
  subtitle: z.string().nullable().optional(),
  description: z.string().nullable().optional(),
  sku: z.string().nullable().optional(),
  handle: z.string().nullable().optional(),
  product_type: z.string().nullable().optional(),
  status_entry_id: z.string().uuid().nullable().optional(),
  primary_currency_code: z.string().nullable().optional(),
  default_unit: z.string().nullable().optional(),
  default_sales_unit: z.string().nullable().optional(),
  default_sales_unit_quantity: z.number().nullable().optional(),
  uom_rounding_scale: z.number().nullable().optional(),
  uom_rounding_mode: z.enum(["half_up", "down", "up"]).nullable().optional(),
  unit_price_enabled: z.boolean().nullable().optional(),
  unit_price_reference_unit: z.enum(["kg", "l", "m2", "m3", "pc"]).nullable().optional(),
  unit_price_base_quantity: z.number().nullable().optional(),
  unit_price: z.object({
    enabled: z.boolean(),
    reference_unit: z.enum(["kg", "l", "m2", "m3", "pc"]).nullable(),
    base_quantity: z.number().nullable()
  }).optional(),
  default_media_id: z.string().uuid().nullable().optional(),
  default_media_url: z.string().nullable().optional(),
  weight_value: z.number().nullable().optional(),
  weight_unit: z.string().nullable().optional(),
  dimensions: z.record(z.string(), z.unknown()).nullable().optional(),
  is_configurable: z.boolean().nullable().optional(),
  is_active: z.boolean().nullable().optional(),
  country_of_origin_code: z.string().nullable().optional(),
  pkwiu_code: z.string().nullable().optional(),
  cn_code: z.string().nullable().optional(),
  hs_code: z.string().nullable().optional(),
  tax_classification_code: z.string().nullable().optional(),
  gtu_codes: z.array(z.string()).nullable().optional(),
  age_min: z.number().nullable().optional(),
  is_excise_good: z.boolean().nullable().optional(),
  excise_category: z.string().nullable().optional(),
  requires_prescription: z.boolean().nullable().optional(),
  hazmat_class: z.string().nullable().optional(),
  un_number: z.string().nullable().optional(),
  hazmat_packing_group: z.string().nullable().optional(),
  contains_lithium_battery: z.boolean().nullable().optional(),
  launch_at: z.string().nullable().optional(),
  end_of_life_at: z.string().nullable().optional(),
  available_from: z.string().nullable().optional(),
  available_until: z.string().nullable().optional(),
  min_order_qty: z.number().nullable().optional(),
  max_order_qty: z.number().nullable().optional(),
  order_qty_increment: z.number().nullable().optional(),
  requires_shipping: z.boolean().nullable().optional(),
  is_quote_only: z.boolean().nullable().optional(),
  seo_title: z.string().nullable().optional(),
  seo_description: z.string().nullable().optional(),
  canonical_url: z.string().nullable().optional(),
  metadata: z.record(z.string(), z.unknown()).nullable().optional(),
  custom_fieldset_code: z.string().nullable().optional(),
  option_schema_id: z.string().uuid().nullable().optional(),
  created_at: z.string().nullable().optional(),
  updated_at: z.string().nullable().optional(),
  offers: z.array(z.record(z.string(), z.unknown())).optional(),
  channelIds: z.array(z.string()).optional(),
  categories: z.array(z.record(z.string(), z.unknown())).optional(),
  categoryIds: z.array(z.string()).optional(),
  tags: z.array(z.string()).optional(),
  pricing: z.record(z.string(), z.unknown()).nullable().optional()
});
const openApi = createCatalogCrudOpenApi({
  resourceName: "Product",
  pluralName: "Products",
  querySchema: listSchema,
  listResponseSchema: createPagedListResponseSchema(productListItemSchema),
  create: {
    schema: productCreateSchema,
    description: "Creates a new product in the catalog."
  },
  update: {
    schema: productUpdateSchema,
    responseSchema: defaultOkResponseSchema,
    description: "Updates an existing product by id."
  },
  del: {
    schema: z.object({ id: z.string().uuid() }),
    responseSchema: defaultOkResponseSchema,
    description: "Deletes a product by id."
  }
});
export {
  DELETE,
  GET,
  POST,
  PUT,
  buildPricingContext,
  buildProductFilters,
  metadata,
  openApi,
  parseIdList,
  scoreProductSearchRelevance
};
//# sourceMappingURL=route.js.map
