import { createHash } from "node:crypto";
import { resolveEffectiveMutationPolicy } from "./agent-policy.js";
import { AiPendingActionRepository } from "../data/repositories/AiPendingActionRepository.js";
class AiMutationPreparationError extends Error {
  constructor(code, message) {
    super(message);
    this.code = code;
    this.name = "AiMutationPreparationError";
  }
}
const MUTATION_PREVIEW_CARD_COMPONENT_ID = "mutation-preview-card";
const NO_RESOLVER_SIDE_EFFECTS_MESSAGE = "Tool did not declare a field-diff resolver; action will proceed without a preview.";
function assertTenantScope(ctx) {
  if (!ctx.tenantId) {
    throw new AiMutationPreparationError(
      "tenant_scope_missing",
      "prepareMutation requires a tenant-scoped request context."
    );
  }
  return ctx.tenantId;
}
function resolveEm(container) {
  if (!container) {
    throw new AiMutationPreparationError(
      "container_missing",
      "prepareMutation requires an Awilix container to resolve the EntityManager."
    );
  }
  let em = null;
  try {
    em = container.resolve("em");
  } catch {
    em = null;
  }
  if (!em) {
    throw new AiMutationPreparationError(
      "em_missing",
      'prepareMutation could not resolve "em" from the container.'
    );
  }
  return em;
}
function toolHandlerContext(ctx) {
  return {
    tenantId: ctx.tenantId,
    organizationId: ctx.organizationId,
    userId: ctx.userId,
    container: ctx.container,
    userFeatures: ctx.features,
    isSuperAdmin: ctx.isSuperAdmin
  };
}
function safeStringify(value) {
  const seen = /* @__PURE__ */ new WeakSet();
  return JSON.stringify(value, (_key, raw) => {
    if (raw && typeof raw === "object") {
      if (seen.has(raw)) return "[Circular]";
      seen.add(raw);
      const entries = Object.entries(raw);
      entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
      return entries.reduce((acc, [k, v]) => {
        acc[k] = v;
        return acc;
      }, {});
    }
    return raw;
  });
}
function computeMutationIdempotencyKey(input) {
  const canonical = safeStringify({
    tenant: input.tenantId,
    org: input.organizationId ?? null,
    agent: input.agentId,
    conversation: input.conversationId ?? null,
    tool: input.toolName,
    input: input.normalizedInput ?? {}
  });
  return createHash("sha256").update(canonical).digest("hex");
}
function computeFieldDiff(before, after, display) {
  const diff = [];
  const keys = /* @__PURE__ */ new Set([
    ...Object.keys(before ?? {}),
    ...Object.keys(after ?? {})
  ]);
  for (const field of keys) {
    const beforeValue = before ? before[field] : void 0;
    const afterValue = after ? after[field] : void 0;
    if (!Object.is(beforeValue, afterValue) && safeStringify(beforeValue) !== safeStringify(afterValue)) {
      const fieldLabel = display?.fieldLabels?.[field];
      const beforeDisplay = display?.before?.[field];
      const afterDisplay = display?.after?.[field];
      diff.push({
        field,
        ...fieldLabel !== void 0 ? { fieldLabel } : {},
        before: beforeValue,
        after: afterValue,
        ...beforeDisplay !== void 0 ? { beforeDisplay } : {},
        ...afterDisplay !== void 0 ? { afterDisplay } : {}
      });
    }
  }
  return diff;
}
function extractPatchFromArgs(args) {
  const raw = args?.patch;
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
    return raw;
  }
  const envelope = /* @__PURE__ */ new Set([
    "id",
    "recordId",
    "records",
    "attachmentIds",
    "_sessionToken"
  ]);
  const reduced = {};
  for (const [key, value] of Object.entries(args ?? {})) {
    if (envelope.has(key)) continue;
    reduced[key] = value;
  }
  return reduced;
}
function matchBatchPatch(args, recordId) {
  const rawList = args?.records;
  if (Array.isArray(rawList)) {
    const match = rawList.find((entry) => {
      if (!entry || typeof entry !== "object") return false;
      const candidate = entry;
      return candidate.recordId === recordId || candidate.id === recordId;
    });
    if (match && typeof match === "object") {
      const patch = match.patch;
      if (patch && typeof patch === "object" && !Array.isArray(patch)) {
        return patch;
      }
      const envelope = /* @__PURE__ */ new Set(["recordId", "id"]);
      const reduced = {};
      for (const [key, value] of Object.entries(match)) {
        if (envelope.has(key)) continue;
        reduced[key] = value;
      }
      return reduced;
    }
  }
  return {};
}
function normalizeAttachmentIds(args) {
  const raw = args?.attachmentIds;
  if (!Array.isArray(raw)) return [];
  return raw.filter((value) => typeof value === "string" && value.length > 0);
}
async function buildSingleRecordDiff(tool, input, ctx) {
  const resolver = tool.loadBeforeRecord;
  if (!resolver) {
    console.warn(
      `[AI Agents] prepareMutation: tool "${tool.name}" declared isMutation=true but no loadBeforeRecord resolver; shipping empty fieldDiff.`
    );
    return {
      fieldDiff: [],
      targetEntityType: null,
      targetRecordId: null,
      recordVersion: null,
      sideEffectsSummary: NO_RESOLVER_SIDE_EFFECTS_MESSAGE
    };
  }
  const handlerContext = toolHandlerContext(ctx);
  const before = await resolver(
    input.toolCallArgs,
    handlerContext
  );
  if (!before) {
    return {
      fieldDiff: [],
      targetEntityType: null,
      targetRecordId: null,
      recordVersion: null,
      sideEffectsSummary: null
    };
  }
  const patch = before.after ?? extractPatchFromArgs(input.toolCallArgs);
  const fieldDiff = computeFieldDiff(before.before, patch, before.display);
  return {
    fieldDiff,
    targetEntityType: before.entityType,
    targetRecordId: before.recordId,
    recordVersion: before.recordVersion,
    sideEffectsSummary: null
  };
}
async function buildBatchRecords(tool, input, ctx) {
  const resolver = tool.loadBeforeRecords;
  if (!resolver) {
    console.warn(
      `[AI Agents] prepareMutation: bulk tool "${tool.name}" declared isMutation=true but no loadBeforeRecords resolver; shipping empty records[].`
    );
    return {
      records: null,
      targetEntityType: null,
      sideEffectsSummary: NO_RESOLVER_SIDE_EFFECTS_MESSAGE
    };
  }
  const handlerContext = toolHandlerContext(ctx);
  const rows = await resolver(
    input.toolCallArgs,
    handlerContext
  );
  if (!Array.isArray(rows) || rows.length === 0) {
    return {
      records: null,
      targetEntityType: null,
      sideEffectsSummary: null
    };
  }
  const diffs = rows.map((row) => {
    const patch = row.after ?? matchBatchPatch(input.toolCallArgs, row.recordId);
    return {
      recordId: row.recordId,
      entityType: row.entityType,
      label: row.label,
      fieldDiff: computeFieldDiff(row.before, patch, row.display),
      recordVersion: row.recordVersion ?? null
    };
  });
  const [firstEntity] = rows;
  return {
    records: diffs,
    targetEntityType: firstEntity ? firstEntity.entityType : null,
    sideEffectsSummary: null
  };
}
async function prepareMutation(input, ctx) {
  const { agent, tool } = input;
  if (tool.isMutation !== true) {
    throw new AiMutationPreparationError(
      "not_a_mutation_tool",
      `Tool "${tool.name}" is not a mutation tool; prepareMutation should not be invoked.`
    );
  }
  const effectivePolicy = resolveEffectiveMutationPolicy(
    agent.mutationPolicy,
    input.mutationPolicyOverride ?? null,
    agent.id
  );
  if (effectivePolicy === "read-only") {
    throw new AiMutationPreparationError(
      "read_only_agent",
      `Agent "${agent.id}" has effective mutationPolicy=read-only; mutation tool "${tool.name}" cannot be prepared.`
    );
  }
  const tenantId = assertTenantScope(ctx);
  const em = resolveEm(ctx.container);
  const repo = new AiPendingActionRepository(em);
  const isBulk = tool.isBulk === true;
  let fieldDiff = [];
  let records = null;
  let targetEntityType = null;
  let targetRecordId = null;
  let recordVersion = null;
  let sideEffectsSummary = null;
  if (isBulk) {
    const batch = await buildBatchRecords(tool, input, ctx);
    records = batch.records;
    targetEntityType = batch.targetEntityType;
    sideEffectsSummary = batch.sideEffectsSummary;
  } else {
    const single = await buildSingleRecordDiff(tool, input, ctx);
    fieldDiff = single.fieldDiff;
    targetEntityType = single.targetEntityType;
    targetRecordId = single.targetRecordId;
    recordVersion = single.recordVersion;
    sideEffectsSummary = single.sideEffectsSummary;
  }
  const normalizedInput = input.toolCallArgs ?? {};
  const conversationId = input.conversationId ?? null;
  const idempotencyKey = computeMutationIdempotencyKey({
    tenantId,
    organizationId: ctx.organizationId ?? null,
    agentId: agent.id,
    conversationId,
    toolName: tool.name,
    normalizedInput
  });
  const pendingAction = await repo.create(
    {
      agentId: agent.id,
      toolName: tool.name,
      idempotencyKey,
      createdByUserId: ctx.userId,
      normalizedInput,
      conversationId,
      targetEntityType,
      targetRecordId,
      fieldDiff,
      records,
      sideEffectsSummary,
      recordVersion,
      attachmentIds: normalizeAttachmentIds(normalizedInput),
      now: input.now
    },
    {
      tenantId,
      organizationId: ctx.organizationId ?? null,
      userId: ctx.userId
    }
  );
  const uiPart = {
    componentId: MUTATION_PREVIEW_CARD_COMPONENT_ID,
    props: {
      pendingActionId: pendingAction.id,
      expiresAt: pendingAction.expiresAt.toISOString(),
      ...records ? { records } : { fieldDiff },
      ...sideEffectsSummary ? { sideEffectsSummary } : {}
    }
  };
  return { uiPart, pendingAction };
}
const MUTATION_PREVIEW_CARD_COMPONENT = MUTATION_PREVIEW_CARD_COMPONENT_ID;
export {
  AiMutationPreparationError,
  MUTATION_PREVIEW_CARD_COMPONENT,
  computeMutationIdempotencyKey,
  prepareMutation
};
//# sourceMappingURL=prepare-mutation.js.map
