import { z } from "zod";
import {
  findOneWithDecryption,
  findWithDecryption
} from "@open-mercato/shared/lib/encryption/find";
import { loadCustomFieldDefinitionIndex } from "@open-mercato/shared/lib/crud/custom-fields";
import { E } from "../../../generated/entities.ids.generated.js";
import {
  createAiApiOperationRunner
} from "@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner";
import { CatalogProduct, CatalogProductPrice } from "../data/entities.js";
import { Attachment } from "@open-mercato/core/modules/attachments/data/entities";
import {
  assertTenantScope
} from "./types.js";
function resolveEm(ctx) {
  return ctx.container.resolve("em");
}
function buildScope(ctx, tenantId) {
  return { tenantId, organizationId: ctx.organizationId };
}
function recordVersionFromUpdatedAt(updatedAt) {
  if (!updatedAt) return null;
  const value = updatedAt instanceof Date ? updatedAt : new Date(updatedAt);
  if (Number.isNaN(value.getTime())) return null;
  return value.toISOString();
}
async function loadProductForScope(em, ctx, tenantId, productId) {
  const where = { id: productId, tenantId, deletedAt: null };
  if (ctx.organizationId) where.organizationId = ctx.organizationId;
  const row = await findOneWithDecryption(
    em,
    CatalogProduct,
    where,
    void 0,
    buildScope(ctx, tenantId)
  );
  if (!row || row.tenantId !== tenantId) return null;
  if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
  return row;
}
function productSnapshot(row) {
  return {
    title: row.title ?? null,
    subtitle: row.subtitle ?? null,
    description: row.description ?? null,
    sku: row.sku ?? null,
    handle: row.handle ?? null,
    isActive: row.isActive ?? null,
    primaryCurrencyCode: row.primaryCurrencyCode ?? null
  };
}
function productLabel(row) {
  if (row.title && row.title.trim().length) return row.title;
  if (row.sku && row.sku.trim().length) return row.sku;
  return row.id;
}
const pricePatchSchema = z.object({
  priceKindId: z.string().uuid().optional(),
  currencyCode: z.string().trim().min(3).max(8).optional(),
  amount: z.number().nonnegative()
}).strict();
async function validateProductPriceScope(em, tenantId, organizationId, productId, priceInput) {
  if (priceInput.currencyCode) {
    const candidates = await findWithDecryption(
      em,
      CatalogProductPrice,
      { tenantId, product: productId },
      void 0,
      { tenantId, organizationId }
    );
    const seen = new Set(candidates.map((entry) => (entry.currencyCode ?? "").toUpperCase()));
    if (seen.size > 0 && !seen.has(priceInput.currencyCode.toUpperCase())) {
      return {
        ok: false,
        code: "currency_out_of_scope",
        message: `Currency "${priceInput.currencyCode}" is not configured for product ${productId}.`
      };
    }
  }
  return { ok: true };
}
const updateProductInput = z.object({
  productId: z.string().uuid().describe("Catalog product id (UUID)."),
  title: z.string().trim().min(1).max(255).optional(),
  subtitle: z.string().trim().max(255).nullable().optional(),
  description: z.string().trim().max(4e3).nullable().optional(),
  isActive: z.boolean().optional(),
  price: pricePatchSchema.optional().describe("Optional price adjustment; must match an existing currency for the product.")
}).strict();
const updateProductTool = {
  name: "catalog.update_product",
  displayName: "Update product",
  description: "Update a single catalog product (title, subtitle, description, isActive, optional price). Routes through the AI pending-action approval gate.",
  inputSchema: updateProductInput,
  requiredFeatures: ["catalog.products.manage"],
  tags: ["write", "catalog", "merchandising"],
  isMutation: true,
  isBulk: false,
  loadBeforeRecord: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = updateProductInput.parse(rawInput);
    const em = resolveEm(ctx);
    const row = await loadProductForScope(em, ctx, tenantId, input.productId);
    if (!row) return null;
    return {
      recordId: row.id,
      entityType: "catalog.product",
      recordVersion: recordVersionFromUpdatedAt(row.updatedAt),
      before: productSnapshot(row)
    };
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = updateProductInput.parse(rawInput);
    const em = resolveEm(ctx);
    const row = await loadProductForScope(em, ctx, tenantId, input.productId);
    if (!row) {
      throw new Error(`Product "${input.productId}" is not accessible to the caller.`);
    }
    const organizationId = row.organizationId;
    const before = productSnapshot(row);
    if (input.price) {
      const check = await validateProductPriceScope(
        em,
        tenantId,
        organizationId,
        row.id,
        input.price
      );
      if (!check.ok) {
        const error = new Error(check.message);
        error.code = check.code;
        throw error;
      }
    }
    const body = {
      id: row.id,
      tenantId,
      organizationId
    };
    if (input.title !== void 0) body.title = input.title;
    if (input.subtitle !== void 0) body.subtitle = input.subtitle;
    if (input.description !== void 0) body.description = input.description;
    if (input.isActive !== void 0) body.isActive = input.isActive;
    const runner = createAiApiOperationRunner(ctx);
    const response = await runner.run({
      method: "PUT",
      path: "/catalog/products",
      body
    });
    if (!response.success) {
      throw new Error(response.error ?? `Failed to update product "${row.id}"`);
    }
    const after = await loadProductForScope(em, ctx, tenantId, row.id);
    return {
      recordId: row.id,
      commandName: "catalog.products.update",
      before,
      after: after ? productSnapshot(after) : null
    };
  }
};
const bulkUpdateProductRecordSchema = z.object({
  recordId: z.string().uuid(),
  title: z.string().trim().min(1).max(255).optional(),
  subtitle: z.string().trim().max(255).nullable().optional(),
  description: z.string().trim().max(4e3).nullable().optional(),
  isActive: z.boolean().optional(),
  price: pricePatchSchema.optional()
}).strict();
const bulkUpdateProductsInput = z.object({
  records: z.array(bulkUpdateProductRecordSchema).min(1).max(50).describe("One entry per product to update. Max 50 rows per batch.")
}).strict();
const bulkUpdateProductsTool = {
  name: "catalog.bulk_update_products",
  displayName: "Bulk update products",
  description: "Update several catalog products in a single approval. Emits ONE pending action whose records[] contains one diff per product. Stale-version or cross-tenant rows are collected in failedRecords[] without aborting the remaining writes.",
  inputSchema: bulkUpdateProductsInput,
  requiredFeatures: ["catalog.products.manage"],
  tags: ["write", "catalog", "merchandising", "bulk"],
  isMutation: true,
  isBulk: true,
  loadBeforeRecords: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = bulkUpdateProductsInput.parse(rawInput);
    const em = resolveEm(ctx);
    const rows = [];
    for (const entry of input.records) {
      const product = await loadProductForScope(em, ctx, tenantId, entry.recordId);
      if (!product) continue;
      rows.push({
        recordId: product.id,
        entityType: "catalog.product",
        label: productLabel(product),
        recordVersion: recordVersionFromUpdatedAt(product.updatedAt),
        before: productSnapshot(product)
      });
    }
    return rows;
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = bulkUpdateProductsInput.parse(rawInput);
    const em = resolveEm(ctx);
    const runner = createAiApiOperationRunner(ctx);
    const results = [];
    const failedRecordIds = [];
    for (const entry of input.records) {
      const product = await loadProductForScope(em, ctx, tenantId, entry.recordId);
      if (!product) {
        failedRecordIds.push(entry.recordId);
        results.push({
          recordId: entry.recordId,
          status: "skipped",
          before: null,
          after: null,
          error: { code: "record_not_found", message: "Product is not accessible to the caller." }
        });
        continue;
      }
      const organizationId = product.organizationId;
      const before = productSnapshot(product);
      if (entry.price) {
        const check = await validateProductPriceScope(em, tenantId, organizationId, product.id, entry.price);
        if (!check.ok) {
          failedRecordIds.push(product.id);
          results.push({
            recordId: product.id,
            status: "failed",
            before,
            after: null,
            error: { code: check.code, message: check.message }
          });
          continue;
        }
      }
      const body = {
        id: product.id,
        tenantId,
        organizationId
      };
      if (entry.title !== void 0) body.title = entry.title;
      if (entry.subtitle !== void 0) body.subtitle = entry.subtitle;
      if (entry.description !== void 0) body.description = entry.description;
      if (entry.isActive !== void 0) body.isActive = entry.isActive;
      try {
        const response = await runner.run({
          method: "PUT",
          path: "/catalog/products",
          body
        });
        if (!response.success) {
          const code = typeof response.details?.code === "string" ? response.details.code : "command_failed";
          failedRecordIds.push(product.id);
          results.push({
            recordId: product.id,
            status: "failed",
            before,
            after: null,
            error: { code, message: response.error ?? "API operation failed" }
          });
          continue;
        }
        const after = await loadProductForScope(em, ctx, tenantId, product.id);
        results.push({
          recordId: product.id,
          status: "updated",
          before,
          after: after ? productSnapshot(after) : null
        });
      } catch (error) {
        failedRecordIds.push(product.id);
        const message = error instanceof Error ? error.message : String(error);
        const code = error?.code ?? "command_failed";
        results.push({
          recordId: product.id,
          status: "failed",
          before,
          after: null,
          error: { code, message }
        });
      }
    }
    const everyFailed = results.length > 0 && results.every((entry) => entry.status !== "updated");
    return {
      commandName: "catalog.products.update",
      records: results,
      failedRecordIds,
      error: everyFailed ? { code: "all_records_failed", message: "No records were updated." } : void 0
    };
  }
};
const attributeExtractionRecordSchema = z.object({
  recordId: z.string().uuid(),
  attributes: z.record(z.string(), z.unknown()).describe("Attribute key \u2192 value map produced by catalog.extract_attributes_from_description.")
}).strict();
const applyAttributeExtractionInput = z.object({
  records: z.array(attributeExtractionRecordSchema).min(1).max(50).describe("One extraction payload per product. Max 50 rows per batch.")
}).strict();
async function resolveAttributeKeyIndex(ctx, tenantId) {
  try {
    const em = resolveEm(ctx);
    const index = await loadCustomFieldDefinitionIndex({
      em,
      entityIds: E.catalog.catalog_product,
      tenantId,
      organizationIds: ctx.organizationId ? [ctx.organizationId] : null
    });
    const keys = /* @__PURE__ */ new Set();
    if (index && typeof index.keys === "function") {
      for (const key of index.keys()) {
        keys.add(String(key));
      }
    }
    return keys;
  } catch {
    return /* @__PURE__ */ new Set();
  }
}
const applyAttributeExtractionTool = {
  name: "catalog.apply_attribute_extraction",
  displayName: "Apply attribute extraction",
  description: "Persist the structured attribute output of catalog.extract_attributes_from_description. Re-validates every attribute key against the CURRENT catalog.product custom-field schema before the pending action is created \u2014 schema drift trips attribute_not_in_schema.",
  inputSchema: applyAttributeExtractionInput,
  requiredFeatures: ["catalog.products.manage"],
  tags: ["write", "catalog", "merchandising", "attributes"],
  isMutation: true,
  isBulk: true,
  loadBeforeRecords: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = applyAttributeExtractionInput.parse(rawInput);
    const em = resolveEm(ctx);
    const rows = [];
    for (const entry of input.records) {
      const product = await loadProductForScope(em, ctx, tenantId, entry.recordId);
      if (!product) continue;
      rows.push({
        recordId: product.id,
        entityType: "catalog.product",
        label: productLabel(product),
        recordVersion: recordVersionFromUpdatedAt(product.updatedAt),
        before: { attributes: {} }
      });
    }
    return rows;
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = applyAttributeExtractionInput.parse(rawInput);
    const em = resolveEm(ctx);
    const runner = createAiApiOperationRunner(ctx);
    const knownKeys = await resolveAttributeKeyIndex(ctx, tenantId);
    const results = [];
    const failedRecordIds = [];
    for (const entry of input.records) {
      const product = await loadProductForScope(em, ctx, tenantId, entry.recordId);
      if (!product) {
        failedRecordIds.push(entry.recordId);
        results.push({
          recordId: entry.recordId,
          status: "skipped",
          before: null,
          after: null,
          error: { code: "record_not_found", message: "Product is not accessible to the caller." }
        });
        continue;
      }
      const attributeKeys = Object.keys(entry.attributes ?? {});
      const unknownKey = knownKeys.size > 0 ? attributeKeys.find((key) => !knownKeys.has(key)) : void 0;
      if (unknownKey) {
        failedRecordIds.push(product.id);
        results.push({
          recordId: product.id,
          status: "failed",
          before: { attributes: {} },
          after: null,
          error: {
            code: "attribute_not_in_schema",
            message: `Attribute "${unknownKey}" is not part of the current catalog.product schema.`
          }
        });
        continue;
      }
      const organizationId = product.organizationId;
      const customFields = {};
      for (const [key, value] of Object.entries(entry.attributes ?? {})) {
        customFields[key] = value;
      }
      const body = {
        id: product.id,
        tenantId,
        organizationId,
        customFields
      };
      try {
        const response = await runner.run({
          method: "PUT",
          path: "/catalog/products",
          body
        });
        if (!response.success) {
          const code = typeof response.details?.code === "string" ? response.details.code : "command_failed";
          failedRecordIds.push(product.id);
          results.push({
            recordId: product.id,
            status: "failed",
            before: { attributes: {} },
            after: null,
            error: { code, message: response.error ?? "API operation failed" }
          });
          continue;
        }
        results.push({
          recordId: product.id,
          status: "updated",
          before: { attributes: {} },
          after: { attributes: entry.attributes }
        });
      } catch (error) {
        failedRecordIds.push(product.id);
        const message = error instanceof Error ? error.message : String(error);
        const code = error?.code ?? "command_failed";
        results.push({
          recordId: product.id,
          status: "failed",
          before: { attributes: {} },
          after: null,
          error: { code, message }
        });
      }
    }
    const everyFailed = results.length > 0 && results.every((entry) => entry.status !== "updated");
    return {
      commandName: "catalog.products.update",
      records: results,
      failedRecordIds,
      error: everyFailed ? { code: "all_records_failed", message: "No records were updated." } : void 0
    };
  }
};
const mediaUpdateRecordSchema = z.object({
  mediaId: z.string().uuid(),
  altText: z.string().trim().max(500).optional(),
  caption: z.string().trim().max(1e3).optional()
}).strict();
const updateProductMediaDescriptionsInput = z.object({
  mediaUpdates: z.array(mediaUpdateRecordSchema).min(1).max(100).describe("One media record descriptor per attachment id. Exactly one entry is allowed for single-record writes; many entries for bulk writes.")
}).strict();
async function loadProductMediaForScope(em, ctx, tenantId, mediaId) {
  const where = {
    id: mediaId,
    tenantId,
    entityId: E.catalog.catalog_product
  };
  if (ctx.organizationId) where.organizationId = ctx.organizationId;
  const row = await findOneWithDecryption(
    em,
    Attachment,
    where,
    void 0,
    buildScope(ctx, tenantId)
  );
  if (!row || row.tenantId !== tenantId) return null;
  if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
  return row;
}
function mediaSnapshot(row) {
  const metadata = row.storageMetadata ?? {};
  return {
    altText: typeof metadata?.altText === "string" ? metadata.altText : null,
    caption: typeof metadata?.caption === "string" ? metadata.caption : null
  };
}
function mediaLabel(row) {
  if (row.fileName && row.fileName.trim().length) return row.fileName;
  return row.id;
}
const updateProductMediaDescriptionsTool = {
  name: "catalog.update_product_media_descriptions",
  displayName: "Update product media descriptions",
  description: "Update altText / caption on one or many product media attachments in a single approval. Emits one pending action with records[] carrying per-media diffs. Accepts one entry for single-record writes or many for bulk writes \u2014 always routes through loadBeforeRecords to guarantee schema-consistent preview.",
  inputSchema: updateProductMediaDescriptionsInput,
  requiredFeatures: ["catalog.products.manage"],
  tags: ["write", "catalog", "merchandising", "media"],
  isMutation: true,
  isBulk: true,
  loadBeforeRecords: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = updateProductMediaDescriptionsInput.parse(rawInput);
    const em = resolveEm(ctx);
    const rows = [];
    for (const entry of input.mediaUpdates) {
      const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId);
      if (!media) continue;
      rows.push({
        recordId: media.id,
        entityType: "catalog.product_media",
        label: mediaLabel(media),
        recordVersion: recordVersionFromUpdatedAt(media.createdAt ?? null),
        before: mediaSnapshot(media)
      });
    }
    return rows;
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = updateProductMediaDescriptionsInput.parse(rawInput);
    const em = resolveEm(ctx);
    const results = [];
    const failedRecordIds = [];
    const touched = [];
    for (const entry of input.mediaUpdates) {
      const media = await loadProductMediaForScope(em, ctx, tenantId, entry.mediaId);
      if (!media) {
        failedRecordIds.push(entry.mediaId);
        results.push({
          recordId: entry.mediaId,
          status: "skipped",
          before: null,
          after: null,
          error: { code: "record_not_found", message: "Media is not accessible to the caller." }
        });
        continue;
      }
      const before = mediaSnapshot(media);
      touched.push({
        row: media,
        patch: {
          ...entry.altText !== void 0 ? { altText: entry.altText } : {},
          ...entry.caption !== void 0 ? { caption: entry.caption } : {}
        },
        before
      });
    }
    if (touched.length > 0) {
      for (const { row, patch } of touched) {
        const next = { ...row.storageMetadata ?? {} };
        if (patch.altText !== void 0) next.altText = patch.altText;
        if (patch.caption !== void 0) next.caption = patch.caption;
        row.storageMetadata = next;
      }
      await em.flush();
      for (const { row, patch, before } of touched) {
        results.push({
          recordId: row.id,
          status: "updated",
          before,
          after: {
            altText: patch.altText ?? before.altText ?? null,
            caption: patch.caption ?? before.caption ?? null
          }
        });
      }
    }
    const everyFailed = results.length > 0 && results.every((entry) => entry.status !== "updated");
    return {
      commandName: "catalog.products.media.update_descriptions",
      records: results,
      failedRecordIds,
      error: everyFailed ? { code: "all_records_failed", message: "No media records were updated." } : void 0
    };
  }
};
const mutationAiTools = [
  updateProductTool,
  bulkUpdateProductsTool,
  applyAttributeExtractionTool,
  updateProductMediaDescriptionsTool
];
var mutation_pack_default = mutationAiTools;
export {
  mutation_pack_default as default,
  mutationAiTools
};
//# sourceMappingURL=mutation-pack.js.map
