import { z } from "zod";
import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
import { CatalogOffer, CatalogProduct, CatalogProductPrice, CatalogProductVariant } from "../../data/entities.js";
import { offerCreateSchema, offerUpdateSchema } from "../../data/validators.js";
import { parseScopedCommandInput, resolveCrudRecordId } from "../utils.js";
import { E } from "../../../../generated/entities.ids.generated.js";
import * as F from "../../../../generated/entities/catalog_offer/index.js";
import { parseIdList } from "../products/route.js";
import { extractAllCustomFieldEntries } from "@open-mercato/shared/lib/crud/custom-fields";
import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
import {
  createCatalogCrudOpenApi,
  createPagedListResponseSchema,
  defaultOkResponseSchema
} from "../openapi.js";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
const rawBodySchema = z.object({}).passthrough();
const listSchema = z.object({
  page: z.coerce.number().min(1).default(1),
  pageSize: z.coerce.number().min(1).max(100).default(50),
  productId: z.string().uuid().optional(),
  channelId: z.string().uuid().optional(),
  channelIds: z.string().optional(),
  id: z.string().uuid().optional(),
  search: z.string().optional(),
  isActive: z.string().optional(),
  withDeleted: z.coerce.boolean().optional(),
  sortField: z.string().optional(),
  sortDir: z.enum(["asc", "desc"]).optional()
}).passthrough();
const routeMetadata = {
  GET: { requireAuth: true, requireFeatures: ["sales.channels.manage"] },
  POST: { requireAuth: true, requireFeatures: ["sales.channels.manage"] },
  PUT: { requireAuth: true, requireFeatures: ["sales.channels.manage"] },
  DELETE: { requireAuth: true, requireFeatures: ["sales.channels.manage"] }
};
const metadata = routeMetadata;
function normalizeSearch(term) {
  if (!term) return null;
  const trimmed = term.trim();
  if (!trimmed.length) return null;
  return trimmed;
}
function buildOfferFilters(query) {
  const filters = {};
  const searchTerm = normalizeSearch(query.search);
  if (query.id) {
    filters.id = { $eq: query.id };
  }
  if (query.productId) {
    filters.product_id = { $eq: query.productId };
  }
  if (query.channelId) {
    filters.channel_id = { $eq: query.channelId };
  } else {
    const channelIds = parseIdList(query.channelIds);
    if (channelIds.length) {
      filters.channel_id = { $in: channelIds };
    }
  }
  if (searchTerm) {
    const like = `%${escapeLikePattern(searchTerm)}%`;
    filters.$or = [{ [F.title]: { $ilike: like } }, { [F.description]: { $ilike: like } }];
  }
  const isActive = parseBooleanToken(query.isActive);
  if (isActive !== null) filters[F.is_active] = isActive;
  return filters;
}
async function decorateOffersWithDetails(items, ctx) {
  if (!items.length) return;
  const offerIds = items.map((item) => item?.id ? String(item.id) : null).filter((value) => !!value);
  const productIds = items.map((item) => item?.productId ? String(item.productId) : null).filter((value) => !!value);
  if (!offerIds.length && !productIds.length) return;
  const em = ctx.container.resolve("em");
  const scopeTenantId = ctx.auth?.tenantId ?? null;
  if (!scopeTenantId) {
    throw new CrudHttpError(403, "[internal] Missing tenant scope for offer decoration");
  }
  const scopeOrgIds = Array.isArray(ctx.organizationIds) && ctx.organizationIds.length ? Array.from(new Set(ctx.organizationIds)) : ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null ? [ctx.selectedOrganizationId ?? ctx.auth?.orgId] : [];
  const scopeWhere = { tenantId: scopeTenantId };
  if (scopeOrgIds.length === 1) scopeWhere.organizationId = scopeOrgIds[0];
  else if (scopeOrgIds.length > 1) scopeWhere.organizationId = { $in: scopeOrgIds };
  const scope = { tenantId: scopeTenantId, organizationId: scopeOrgIds.length === 1 ? scopeOrgIds[0] : null };
  const [products, prices, defaultVariants] = await Promise.all([
    productIds.length ? em.find(
      CatalogProduct,
      { id: { $in: productIds }, ...scopeWhere },
      {
        fields: ["id", "title", "description", "defaultMediaId", "defaultMediaUrl", "sku"]
      }
    ) : [],
    offerIds.length ? findWithDecryption(
      em,
      CatalogProductPrice,
      { offer: { $in: offerIds }, ...scopeWhere },
      { populate: ["priceKind"] },
      scope
    ) : [],
    productIds.length ? em.find(
      CatalogProductVariant,
      { product: { $in: productIds }, isDefault: true, ...scopeWhere },
      { fields: ["id", "product"] }
    ) : []
  ]);
  const productMap = new Map(
    products.map((product) => [
      product.id,
      {
        id: product.id,
        title: product.title,
        defaultMediaId: product.defaultMediaId ?? null,
        defaultMediaUrl: product.defaultMediaUrl ?? null,
        sku: product.sku ?? null
      }
    ])
  );
  const priceMap = /* @__PURE__ */ new Map();
  prices.forEach((price) => {
    const offerRef = price.offer;
    const offerId = typeof offerRef === "string" ? offerRef : offerRef && typeof offerRef === "object" && "id" in offerRef ? offerRef.id ?? null : null;
    if (!offerId) return;
    const priceKind = price.priceKind;
    const priceKindId = typeof priceKind === "string" ? priceKind : priceKind && typeof priceKind === "object" ? priceKind.id ?? null : null;
    const priceKindCode = priceKind && typeof priceKind === "object" && "code" in priceKind ? priceKind.code ?? null : null;
    const priceKindTitle = priceKind && typeof priceKind === "object" && "title" in priceKind ? priceKind.title ?? null : null;
    const displayMode = priceKind && typeof priceKind === "object" && "displayMode" in priceKind ? priceKind.displayMode ?? "excluding-tax" : "excluding-tax";
    const bucket = priceMap.get(offerId) ?? [];
    bucket.push({
      id: price.id,
      priceKindId,
      priceKindCode,
      priceKindTitle,
      currencyCode: price.currencyCode ?? null,
      unitPriceNet: price.unitPriceNet ?? null,
      unitPriceGross: price.unitPriceGross ?? null,
      displayMode,
      minQuantity: price.minQuantity ?? null,
      maxQuantity: price.maxQuantity ?? null
    });
    priceMap.set(offerId, bucket);
  });
  const variantToProductMap = /* @__PURE__ */ new Map();
  defaultVariants.forEach((variant) => {
    const variantId = typeof variant.id === "string" ? variant.id : null;
    const productRef = typeof variant.product === "string" ? variant.product : variant.product && typeof variant.product === "object" && "id" in variant.product ? variant.product.id ?? null : null;
    if (variantId && productRef) {
      variantToProductMap.set(variantId, productRef);
    }
  });
  const DEFAULT_CHANNEL_KEY = "__default__";
  const productChannelPriceMap = /* @__PURE__ */ new Map();
  const assignFallbackPrice = (productRef, channelRef, payload, priority) => {
    if (!productRef) return;
    const bucket = productChannelPriceMap.get(productRef) ?? /* @__PURE__ */ new Map();
    const effectiveChannel = channelRef ?? DEFAULT_CHANNEL_KEY;
    const existing = bucket.get(effectiveChannel);
    if (existing && existing.priority > priority) return;
    if (existing && existing.priority === priority) {
      bucket.set(effectiveChannel, { prices: [...existing.prices, payload], priority });
      productChannelPriceMap.set(productRef, bucket);
      return;
    }
    bucket.set(effectiveChannel, { prices: [payload], priority });
    productChannelPriceMap.set(productRef, bucket);
  };
  const channelIds = Array.from(new Set(
    items.map((item) => {
      if (typeof item?.channelId === "string") return item.channelId;
      if (typeof item?.channel_id === "string") return item.channel_id;
      return null;
    }).filter((value) => !!value)
  ));
  const channelFilterValues = channelIds.length ? [...channelIds, null] : [null];
  const fallbackTargets = [];
  if (productIds.length) fallbackTargets.push({ product: { $in: productIds } });
  const defaultVariantIds = Array.from(variantToProductMap.keys());
  if (defaultVariantIds.length) fallbackTargets.push({ variant: { $in: defaultVariantIds } });
  const fallbackEntries = fallbackTargets.length ? await findWithDecryption(
    em,
    CatalogProductPrice,
    {
      offer: null,
      ...scopeWhere,
      $and: [
        { $or: fallbackTargets },
        channelFilterValues.includes(null) ? {
          $or: [
            { channelId: { $in: channelFilterValues.filter((id) => typeof id === "string") } },
            { channelId: null }
          ]
        } : { channelId: { $in: channelFilterValues } }
      ]
    },
    { populate: ["priceKind"] },
    scope
  ) : [];
  fallbackEntries.forEach((entry) => {
    const entryChannelId = typeof entry.channelId === "string" && entry.channelId.length ? entry.channelId : null;
    const priceKind = entry.priceKind ?? null;
    const priceKindId = typeof priceKind?.id === "string" ? priceKind.id : null;
    const priceKindCode = typeof priceKind?.code === "string" ? priceKind.code : null;
    const priceKindTitle = typeof priceKind?.title === "string" ? priceKind.title : null;
    const displayMode = typeof priceKind?.displayMode === "string" ? priceKind.displayMode : typeof priceKind?.display_mode === "string" ? priceKind.display_mode : "excluding-tax";
    const payload = {
      priceKindId,
      priceKindCode,
      priceKindTitle,
      currencyCode: entry.currencyCode ?? null,
      unitPriceNet: entry.unitPriceNet ?? null,
      unitPriceGross: entry.unitPriceGross ?? null,
      displayMode
    };
    const variantRef = typeof entry.variant === "string" ? entry.variant : entry.variant && typeof entry.variant === "object" && "id" in entry.variant ? entry.variant.id ?? null : null;
    if (variantRef) {
      const variantProduct = variantToProductMap.get(variantRef);
      const productRef2 = variantProduct ?? (typeof entry.product === "string" ? entry.product : entry.product && typeof entry.product === "object" && "id" in entry.product ? entry.product.id ?? null : null);
      const priority2 = entryChannelId ? 4 : 3;
      assignFallbackPrice(productRef2, entryChannelId, payload, priority2);
      return;
    }
    const productRef = typeof entry.product === "string" ? entry.product : entry.product && typeof entry.product === "object" && "id" in entry.product ? entry.product.id ?? null : null;
    const priority = entryChannelId ? 2 : 1;
    assignFallbackPrice(productRef, entryChannelId, payload, priority);
  });
  items.forEach((item) => {
    const productId = String(item?.productId ?? "");
    item.product = productId ? productMap.get(productId) ?? null : null;
    item.prices = priceMap.get(String(item?.id ?? "")) ?? [];
    const rowChannelId = typeof item?.channelId === "string" ? item.channelId : typeof item?.channel_id === "string" ? item.channel_id : null;
    const bucket = productChannelPriceMap.get(productId);
    const channelKey = rowChannelId ?? DEFAULT_CHANNEL_KEY;
    const channelPrice = bucket?.get(channelKey) ?? null;
    const defaultPrice = bucket?.get(DEFAULT_CHANNEL_KEY) ?? null;
    const effectivePrice = channelPrice ?? defaultPrice ?? null;
    item.productChannelPrice = effectivePrice?.prices?.[0] ?? null;
    item.productDefaultPrices = effectivePrice?.prices ?? [];
  });
}
const crud = makeCrudRoute({
  metadata: routeMetadata,
  orm: {
    entity: CatalogOffer,
    idField: "id",
    orgField: "organizationId",
    tenantField: "tenantId",
    softDeleteField: "deletedAt"
  },
  indexer: { entityType: E.catalog.catalog_offer },
  list: {
    schema: listSchema,
    entityId: E.catalog.catalog_offer,
    fields: [
      F.id,
      "product_id",
      F.organization_id,
      F.tenant_id,
      F.channel_id,
      F.title,
      F.description,
      "default_media_id",
      "default_media_url",
      F.metadata,
      F.is_active,
      F.created_at,
      F.updated_at
    ],
    sortFieldMap: {
      title: F.title,
      createdAt: F.created_at,
      updatedAt: F.updated_at
    },
    buildFilters: async (query) => buildOfferFilters(query),
    transformItem: (item) => {
      if (!item) return item;
      const cfEntries = extractAllCustomFieldEntries(item);
      const base = {
        id: item.id,
        productId: item.product_id ?? null,
        organizationId: item.organization_id ?? null,
        tenantId: item.tenant_id ?? null,
        channelId: item.channel_id ?? null,
        title: item.title ?? "",
        description: item.description ?? null,
        defaultMediaId: item.default_media_id ?? null,
        defaultMediaUrl: item.default_media_url ?? null,
        metadata: item.metadata ?? null,
        isActive: item.is_active ?? false,
        createdAt: item.created_at,
        updatedAt: item.updated_at
      };
      return Object.keys(cfEntries).length ? { ...base, ...cfEntries } : base;
    }
  },
  actions: {
    create: {
      commandId: "catalog.offers.create",
      schema: rawBodySchema,
      mapInput: async ({ raw, ctx }) => {
        const { translate } = await resolveTranslations();
        return parseScopedCommandInput(offerCreateSchema, raw ?? {}, ctx, translate);
      },
      response: ({ result }) => ({ id: result?.offerId ?? null }),
      status: 201
    },
    update: {
      commandId: "catalog.offers.update",
      schema: rawBodySchema,
      mapInput: async ({ raw, ctx }) => {
        const { translate } = await resolveTranslations();
        return parseScopedCommandInput(offerUpdateSchema, raw ?? {}, ctx, translate);
      },
      response: () => ({ ok: true })
    },
    delete: {
      commandId: "catalog.offers.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", "Offer id is required.") });
        }
        return { id };
      },
      response: () => ({ ok: true })
    }
  },
  hooks: {
    afterList: async (payload, ctx) => {
      const items = Array.isArray(payload.items) ? payload.items : [];
      if (!items.length) return;
      await decorateOffersWithDetails(items, ctx);
    }
  }
});
const GET = crud.GET;
const POST = crud.POST;
const PUT = crud.PUT;
const DELETE = crud.DELETE;
const offerListItemSchema = z.object({
  id: z.string().uuid(),
  productId: z.string().uuid().nullable().optional(),
  organizationId: z.string().uuid().nullable().optional(),
  tenantId: z.string().uuid().nullable().optional(),
  channelId: z.string().uuid().nullable().optional(),
  title: z.string(),
  description: z.string().nullable().optional(),
  defaultMediaId: z.string().uuid().nullable().optional(),
  defaultMediaUrl: z.string().nullable().optional(),
  metadata: z.record(z.string(), z.unknown()).nullable().optional(),
  isActive: z.boolean().nullable().optional(),
  createdAt: z.string().nullable().optional(),
  updatedAt: z.string().nullable().optional(),
  product: z.record(z.string(), z.unknown()).nullable().optional(),
  prices: z.array(z.record(z.string(), z.unknown())).optional(),
  productChannelPrice: z.record(z.string(), z.unknown()).nullable().optional(),
  productDefaultPrices: z.array(z.record(z.string(), z.unknown())).optional()
});
const openApi = createCatalogCrudOpenApi({
  resourceName: "Offer",
  pluralName: "Offers",
  querySchema: listSchema,
  listResponseSchema: createPagedListResponseSchema(offerListItemSchema),
  create: {
    schema: offerCreateSchema,
    description: "Creates a new offer linking a product to a sales channel."
  },
  update: {
    schema: offerUpdateSchema,
    responseSchema: defaultOkResponseSchema,
    description: "Updates an existing offer by id."
  },
  del: {
    schema: z.object({ id: z.string().uuid() }),
    responseSchema: defaultOkResponseSchema,
    description: "Deletes an offer by id."
  }
});
export {
  DELETE,
  GET,
  POST,
  PUT,
  buildOfferFilters,
  decorateOffersWithDetails,
  metadata,
  normalizeSearch,
  openApi
};
//# sourceMappingURL=route.js.map
