import { z } from "zod";
import {
  createAiApiOperationRunner
} from "@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner";
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { TERMINAL_INTERACTION_STATUS_LIST } from "../lib/interactionStatus.js";
import {
  CustomerActivity,
  CustomerComment,
  CustomerDeal,
  CustomerDealCompanyLink,
  CustomerDealPersonLink,
  CustomerInteraction,
  CustomerTodoLink
} from "../data/entities.js";
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();
}
const blankToUndefined = (value) => {
  if (typeof value !== "string") return value;
  const trimmed = value.trim();
  return trimmed.length === 0 ? void 0 : trimmed;
};
async function loadDealForScope(em, ctx, tenantId, dealId) {
  const where = { id: dealId, tenantId, deletedAt: null };
  if (ctx.organizationId) where.organizationId = ctx.organizationId;
  const deal = await findOneWithDecryption(
    em,
    CustomerDeal,
    where,
    void 0,
    buildScope(ctx, tenantId)
  );
  if (!deal || deal.tenantId !== tenantId) return null;
  if (ctx.organizationId && deal.organizationId !== ctx.organizationId) return null;
  return deal;
}
async function resolveDealCommentEntityId(em, ctx, tenantId, dealId) {
  const personLink = await em.findOne(
    CustomerDealPersonLink,
    { deal: dealId, tenantId },
    { populate: ["personEntity"] }
  );
  if (personLink) {
    const linked = personLink.personEntity;
    if (linked && typeof linked === "object" && typeof linked.id === "string") return linked.id;
    const raw = personLink.personEntity;
    if (typeof raw === "string") return raw;
  }
  const companyLink = await em.findOne(
    CustomerDealCompanyLink,
    { deal: dealId, tenantId },
    { populate: ["companyEntity"] }
  );
  if (companyLink) {
    const linked = companyLink.companyEntity;
    if (linked && typeof linked === "object" && typeof linked.id === "string") return linked.id;
    const raw = companyLink.companyEntity;
    if (typeof raw === "string") return raw;
  }
  return null;
}
async function loadCommentForScope(em, ctx, tenantId, commentId) {
  const where = { id: commentId, tenantId };
  if (ctx.organizationId) where.organizationId = ctx.organizationId;
  const row = await findOneWithDecryption(
    em,
    CustomerComment,
    where,
    void 0,
    buildScope(ctx, tenantId)
  );
  if (!row || row.tenantId !== tenantId) return null;
  if (ctx.organizationId && row.organizationId !== ctx.organizationId) return null;
  return row;
}
async function loadActivityForScope(em, ctx, tenantId, activityId) {
  const where = { id: activityId, tenantId };
  if (ctx.organizationId) where.organizationId = ctx.organizationId;
  const row = await findOneWithDecryption(
    em,
    CustomerActivity,
    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 commentEntityIdOf(row) {
  const ent = row.entity;
  if (!ent) return null;
  if (typeof ent === "string") return ent;
  if (typeof ent === "object" && typeof ent.id === "string") return ent.id;
  return null;
}
function activityEntityIdOf(row) {
  const ent = row.entity;
  if (!ent) return null;
  if (typeof ent === "string") return ent;
  if (typeof ent === "object" && typeof ent.id === "string") return ent.id;
  return null;
}
const listActivitiesInput = z.object({
  personId: z.string().uuid().optional().describe("Restrict to activities attached to this person entity id."),
  companyId: z.string().uuid().optional().describe("Restrict to activities attached to this company entity id."),
  dealId: z.string().uuid().optional().describe("Restrict to activities attached to this deal id."),
  activityType: z.string().optional().describe('Filter by activity type (e.g. "call", "email").'),
  limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
  offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
}).passthrough();
const listActivitiesTool = {
  name: "customers.list_activities",
  displayName: "List activities",
  description: "List logged customer activities (calls, emails, meetings, notes, etc.) scoped to tenant + organization. Supply `personId` / `companyId` / `dealId` to narrow; otherwise returns the most recent activities across the tenant.",
  inputSchema: listActivitiesInput,
  requiredFeatures: ["customers.activities.view"],
  tags: ["read", "customers"],
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = listActivitiesInput.parse(rawInput);
    const em = resolveEm(ctx);
    const limit = input.limit ?? 50;
    const offset = input.offset ?? 0;
    const where = { tenantId };
    if (ctx.organizationId) where.organizationId = ctx.organizationId;
    const entityId = input.personId ?? input.companyId ?? null;
    if (entityId) where.entity = entityId;
    if (input.dealId) where.deal = input.dealId;
    if (input.activityType) where.activityType = input.activityType;
    const [rows, total] = await Promise.all([
      findWithDecryption(
        em,
        CustomerActivity,
        where,
        { limit, offset, orderBy: { occurredAt: "desc", createdAt: "desc" } },
        buildScope(ctx, tenantId)
      ),
      em.count(CustomerActivity, where)
    ]);
    const filtered = rows.filter((row) => row.tenantId === tenantId);
    return {
      items: filtered.map((row) => ({
        id: row.id,
        activityType: row.activityType,
        subject: row.subject ?? null,
        body: row.body ?? null,
        occurredAt: row.occurredAt ? new Date(row.occurredAt).toISOString() : null,
        authorUserId: row.authorUserId ?? null,
        entityId: activityEntityIdOf(row),
        dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
        organizationId: row.organizationId ?? null,
        tenantId: row.tenantId ?? null,
        createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
      })),
      total,
      limit,
      offset
    };
  }
};
const listTasksInput = z.object({
  personId: z.string().uuid().optional().describe("Restrict to tasks linked to this person entity id."),
  companyId: z.string().uuid().optional().describe("Restrict to tasks linked to this company entity id."),
  dealId: z.string().uuid().optional().describe("Restrict to tasks connected to this deal id."),
  status: z.enum(["open", "in_progress", "waiting", "done", "canceled", "cancelled"]).optional().describe('Filter canonical interaction tasks by status. "open" matches every non-terminal status (planned/in_progress/waiting and custom statuses). "cancelled" is accepted as an alias for "canceled". Ignored when listing legacy todo links.'),
  limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
  offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
}).passthrough();
const listTasksTool = {
  name: "customers.list_tasks",
  displayName: "List tasks",
  description: 'List customer tasks scoped to tenant + organization. Returns canonical interaction tasks (interactionType="task") merged with legacy todo links for compatibility.',
  inputSchema: listTasksInput,
  requiredFeatures: ["customers.activities.view"],
  tags: ["read", "customers"],
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = listTasksInput.parse(rawInput);
    const em = resolveEm(ctx);
    const limit = input.limit ?? 50;
    const offset = input.offset ?? 0;
    const entityId = input.personId ?? input.companyId ?? null;
    const interactionWhere = {
      tenantId,
      interactionType: "task",
      deletedAt: null
    };
    if (ctx.organizationId) interactionWhere.organizationId = ctx.organizationId;
    if (entityId) interactionWhere.entity = entityId;
    if (input.dealId) interactionWhere.dealId = input.dealId;
    if (input.status) {
      if (input.status === "open") {
        interactionWhere.status = { $nin: [...TERMINAL_INTERACTION_STATUS_LIST] };
      } else if (input.status === "cancelled") {
        interactionWhere.status = "canceled";
      } else {
        interactionWhere.status = input.status;
      }
    }
    const interactionRows = await findWithDecryption(
      em,
      CustomerInteraction,
      interactionWhere,
      { limit, offset, orderBy: { scheduledAt: "desc", createdAt: "desc" } },
      buildScope(ctx, tenantId)
    );
    const legacyWhere = { tenantId };
    if (ctx.organizationId) legacyWhere.organizationId = ctx.organizationId;
    if (entityId) legacyWhere.entity = entityId;
    const legacyRows = input.status || input.dealId ? [] : await findWithDecryption(
      em,
      CustomerTodoLink,
      legacyWhere,
      { limit, offset, orderBy: { createdAt: "desc" } },
      buildScope(ctx, tenantId)
    );
    const filteredInteractions = interactionRows.filter((row) => row.tenantId === tenantId);
    const filteredLegacy = legacyRows.filter((row) => row.tenantId === tenantId);
    const items = [
      ...filteredInteractions.map((row) => ({
        kind: "interaction",
        id: row.id,
        title: row.title ?? null,
        body: row.body ?? null,
        status: row.status,
        scheduledAt: row.scheduledAt ? new Date(row.scheduledAt).toISOString() : null,
        occurredAt: row.occurredAt ? new Date(row.occurredAt).toISOString() : null,
        dealId: row.dealId ?? null,
        ownerUserId: row.ownerUserId ?? null,
        priority: row.priority ?? null,
        organizationId: row.organizationId ?? null,
        tenantId: row.tenantId ?? null,
        createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
      })),
      ...filteredLegacy.map((row) => ({
        kind: "todo_link",
        id: row.id,
        todoId: row.todoId,
        todoSource: row.todoSource,
        entityId: row.entity && typeof row.entity === "object" ? row.entity.id : row.entity ?? null,
        organizationId: row.organizationId ?? null,
        tenantId: row.tenantId ?? null,
        createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
      }))
    ];
    return {
      items,
      total: items.length,
      limit,
      offset
    };
  }
};
const listDealCommentsInput = z.object({
  dealId: z.string().uuid().describe("Deal id whose comments should be listed."),
  limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
  offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
}).passthrough();
const listDealCommentsTool = {
  name: "customers.list_deal_comments",
  displayName: "List deal comments",
  description: "List comments left on a specific deal, ordered by most recent first. Read-only; tenant + organization scoped.",
  inputSchema: listDealCommentsInput,
  requiredFeatures: ["customers.activities.view"],
  tags: ["read", "customers"],
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = listDealCommentsInput.parse(rawInput);
    const em = resolveEm(ctx);
    const limit = input.limit ?? 50;
    const offset = input.offset ?? 0;
    const where = { tenantId, deal: input.dealId };
    if (ctx.organizationId) where.organizationId = ctx.organizationId;
    const [rows, total] = await Promise.all([
      findWithDecryption(
        em,
        CustomerComment,
        where,
        { limit, offset, orderBy: { createdAt: "desc" } },
        buildScope(ctx, tenantId)
      ),
      em.count(CustomerComment, where)
    ]);
    const filtered = rows.filter((row) => row.tenantId === tenantId);
    return {
      items: filtered.map((row) => ({
        id: row.id,
        body: row.body ?? null,
        entityId: commentEntityIdOf(row),
        dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
        authorUserId: row.authorUserId ?? null,
        organizationId: row.organizationId ?? null,
        tenantId: row.tenantId ?? null,
        createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
        updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null
      })),
      total,
      limit,
      offset
    };
  }
};
const manageDealCommentInput = z.object({
  operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new comment, update an existing one, or delete it."),
  dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` \u2014 the deal the comment is attached to."),
  commentId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete` \u2014 id of the existing comment row."),
  body: z.preprocess(blankToUndefined, z.string().min(1).max(8e3).optional()).describe("Comment text. Required for `create`; optional on `update`.")
}).superRefine((value, ctx) => {
  if (value.operation === "create") {
    if (!value.dealId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "dealId is required for create.", path: ["dealId"] });
    if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for create.", path: ["body"] });
  }
  if (value.operation === "update") {
    if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for update.", path: ["commentId"] });
    if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for update.", path: ["body"] });
  }
  if (value.operation === "delete") {
    if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for delete.", path: ["commentId"] });
  }
});
const manageDealCommentTool = {
  name: "customers.manage_deal_comment",
  displayName: "Manage deal comment",
  description: "Create, update, or delete a comment on a deal. Use `operation` to pick the action. Under `destructive-confirm-required` policy, only the `delete` branch routes through the approval card; `create` and `update` execute directly.",
  inputSchema: manageDealCommentInput,
  requiredFeatures: ["customers.activities.manage"],
  tags: ["write", "customers"],
  isMutation: true,
  // Predicate `isDestructive`: only the `delete` branch counts as
  // destructive. Under `confirm-required` policy every branch still
  // gates (the framework ignores this flag); under
  // `destructive-confirm-required` only deletes go through the approval
  // card while creates/updates run directly.
  isDestructive: (input) => {
    if (!input || typeof input !== "object") return false;
    return input.operation === "delete";
  },
  loadBeforeRecord: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageDealCommentInput.parse(rawInput);
    const em = resolveEm(ctx);
    if (input.operation === "create") {
      const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
      if (!deal) return null;
      return {
        recordId: deal.id,
        entityType: "customers.deal",
        recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
        before: { commentId: null, body: null, dealId: deal.id }
      };
    }
    const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
    if (!existing) return null;
    return {
      recordId: existing.id,
      entityType: "customers.customer_comment",
      recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
      before: {
        body: existing.body ?? null,
        dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null,
        entityId: commentEntityIdOf(existing),
        authorUserId: existing.authorUserId ?? null
      }
    };
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageDealCommentInput.parse(rawInput);
    const em = resolveEm(ctx);
    const runner = createAiApiOperationRunner(ctx);
    if (input.operation === "create") {
      const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
      if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
      const organizationId = deal.organizationId;
      if (!organizationId) throw new Error(`Deal "${deal.id}" has no organization scope.`);
      const dealEntityId = await resolveDealCommentEntityId(em, ctx, tenantId, deal.id);
      if (!dealEntityId) {
        throw new Error(
          `Deal "${deal.id}" has no linked person or company. Link a contact to the deal in the backoffice before adding a comment, or post the comment directly on the person/company record instead.`
        );
      }
      const body2 = {
        tenantId,
        organizationId,
        dealId: deal.id,
        // Comments require an `entityId` (the person/company on the timeline).
        entityId: dealEntityId,
        body: input.body
      };
      const response2 = await runner.run({ method: "POST", path: "/customers/comments", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? "Failed to create comment");
      }
      const result = response2.data ?? {};
      return {
        operation: "create",
        commentId: result.id ?? null,
        dealId: deal.id,
        commandName: "customers.comments.create",
        before: null,
        after: { body: input.body ?? null, dealId: deal.id }
      };
    }
    if (input.operation === "update") {
      const existing2 = await loadCommentForScope(em, ctx, tenantId, input.commentId);
      if (!existing2) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
      const organizationId = existing2.organizationId;
      if (!organizationId) throw new Error(`Comment "${existing2.id}" has no organization scope.`);
      const body2 = {
        id: existing2.id,
        tenantId,
        organizationId,
        body: input.body
      };
      const response2 = await runner.run({ method: "PUT", path: "/customers/comments", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? `Failed to update comment "${existing2.id}"`);
      }
      const after = await loadCommentForScope(em, ctx, tenantId, existing2.id);
      return {
        operation: "update",
        commentId: existing2.id,
        commandName: "customers.comments.update",
        before: { body: existing2.body ?? null },
        after: after ? { body: after.body ?? null } : null
      };
    }
    const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
    if (!existing) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
    const body = { id: existing.id };
    const response = await runner.run({ method: "DELETE", path: "/customers/comments", body });
    if (!response.success) {
      throw new Error(response.error ?? `Failed to delete comment "${existing.id}"`);
    }
    return {
      operation: "delete",
      commentId: existing.id,
      commandName: "customers.comments.delete",
      before: { body: existing.body ?? null },
      after: null
    };
  }
};
const manageDealActivityInput = z.object({
  operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new activity, update an existing one, or delete it."),
  activityId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete`."),
  dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` \u2014 the deal the activity is logged against."),
  activityType: z.preprocess(blankToUndefined, z.string().min(1).max(100).optional()).describe('Required for `create` \u2014 e.g. "call", "email", "meeting", "note".'),
  subject: z.preprocess(blankToUndefined, z.string().max(200).optional()).describe("Optional short subject line."),
  body: z.preprocess(blankToUndefined, z.string().max(8e3).optional()).describe("Optional free-text body."),
  occurredAt: z.preprocess(blankToUndefined, z.string().datetime().optional()).describe('ISO-8601 timestamp when the activity occurred. Omit for "now" (server-side default applies).')
}).superRefine((value, ctx) => {
  if (value.operation === "create") {
    if (!value.dealId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "dealId is required for create.", path: ["dealId"] });
    if (!value.activityType) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityType is required for create.", path: ["activityType"] });
  }
  if (value.operation === "update" || value.operation === "delete") {
    if (!value.activityId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityId is required.", path: ["activityId"] });
  }
});
const manageDealActivityTool = {
  name: "customers.manage_deal_activity",
  displayName: "Manage deal activity",
  description: "Create, update, or delete a deal activity (call, email, meeting, note, etc.). Mutation tool \u2014 every call routes through the AI pending-action approval gate. Use `operation` to pick the action.",
  inputSchema: manageDealActivityInput,
  requiredFeatures: ["customers.activities.manage"],
  tags: ["write", "customers"],
  isMutation: true,
  loadBeforeRecord: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageDealActivityInput.parse(rawInput);
    const em = resolveEm(ctx);
    if (input.operation === "create") {
      const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
      if (!deal) return null;
      return {
        recordId: deal.id,
        entityType: "customers.deal",
        recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
        before: { activityId: null, dealId: deal.id }
      };
    }
    const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
    if (!existing) return null;
    return {
      recordId: existing.id,
      entityType: "customers.customer_activity",
      recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
      before: {
        activityType: existing.activityType,
        subject: existing.subject ?? null,
        body: existing.body ?? null,
        occurredAt: existing.occurredAt ? new Date(existing.occurredAt).toISOString() : null,
        dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null
      }
    };
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageDealActivityInput.parse(rawInput);
    const em = resolveEm(ctx);
    const runner = createAiApiOperationRunner(ctx);
    if (input.operation === "create") {
      const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
      if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
      const organizationId = deal.organizationId;
      if (!organizationId) throw new Error(`Deal "${deal.id}" has no organization scope.`);
      const dealEntity = deal.entity;
      const entityId = dealEntity && typeof dealEntity === "object" ? dealEntity.id ?? null : typeof dealEntity === "string" ? dealEntity : null;
      if (!entityId) {
        throw new Error(`Deal "${deal.id}" has no associated person/company; cannot attach an activity.`);
      }
      const body2 = {
        tenantId,
        organizationId,
        entityId,
        dealId: deal.id,
        activityType: input.activityType
      };
      if (input.subject) body2.subject = input.subject;
      if (input.body) body2.body = input.body;
      if (input.occurredAt) body2.occurredAt = input.occurredAt;
      const response2 = await runner.run({ method: "POST", path: "/customers/activities", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? "Failed to create activity");
      }
      const result = response2.data ?? {};
      return {
        operation: "create",
        activityId: result.id ?? null,
        dealId: deal.id,
        commandName: "customers.activities.create",
        before: null,
        after: {
          activityType: input.activityType ?? null,
          subject: input.subject ?? null,
          body: input.body ?? null,
          occurredAt: input.occurredAt ?? null
        }
      };
    }
    if (input.operation === "update") {
      const existing2 = await loadActivityForScope(em, ctx, tenantId, input.activityId);
      if (!existing2) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
      const organizationId = existing2.organizationId;
      if (!organizationId) throw new Error(`Activity "${existing2.id}" has no organization scope.`);
      const body2 = { id: existing2.id, tenantId, organizationId };
      if (input.activityType) body2.activityType = input.activityType;
      if (input.subject !== void 0) body2.subject = input.subject;
      if (input.body !== void 0) body2.body = input.body;
      if (input.occurredAt !== void 0) body2.occurredAt = input.occurredAt;
      const response2 = await runner.run({ method: "PUT", path: "/customers/activities", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? `Failed to update activity "${existing2.id}"`);
      }
      const after = await loadActivityForScope(em, ctx, tenantId, existing2.id);
      return {
        operation: "update",
        activityId: existing2.id,
        commandName: "customers.activities.update",
        before: {
          activityType: existing2.activityType,
          subject: existing2.subject ?? null,
          body: existing2.body ?? null,
          occurredAt: existing2.occurredAt ? new Date(existing2.occurredAt).toISOString() : null
        },
        after: after ? {
          activityType: after.activityType,
          subject: after.subject ?? null,
          body: after.body ?? null,
          occurredAt: after.occurredAt ? new Date(after.occurredAt).toISOString() : null
        } : null
      };
    }
    const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
    if (!existing) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
    const body = { id: existing.id };
    const response = await runner.run({ method: "DELETE", path: "/customers/activities", body });
    if (!response.success) {
      throw new Error(response.error ?? `Failed to delete activity "${existing.id}"`);
    }
    return {
      operation: "delete",
      activityId: existing.id,
      commandName: "customers.activities.delete",
      before: {
        activityType: existing.activityType,
        subject: existing.subject ?? null,
        body: existing.body ?? null
      },
      after: null
    };
  }
};
const listRecordCommentsInput = z.object({
  personId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Restrict to comments on this person entity id."),
  companyId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Restrict to comments on this company entity id."),
  dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Restrict to comments attached to this deal id."),
  limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
  offset: z.number().int().min(0).optional().describe("Rows to skip (default 0).")
}).superRefine((value, ctx) => {
  if (!value.personId && !value.companyId && !value.dealId) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Provide at least one of personId, companyId, or dealId.",
      path: ["personId"]
    });
  }
});
const listRecordCommentsTool = {
  name: "customers.list_record_comments",
  displayName: "List record comments",
  description: "List comments left on a person, company, or deal record. Read-only; tenant + organization scoped. Provide at least one of `personId`, `companyId`, or `dealId`.",
  inputSchema: listRecordCommentsInput,
  requiredFeatures: ["customers.activities.view"],
  tags: ["read", "customers"],
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = listRecordCommentsInput.parse(rawInput);
    const em = resolveEm(ctx);
    const limit = input.limit ?? 50;
    const offset = input.offset ?? 0;
    const where = { tenantId };
    if (ctx.organizationId) where.organizationId = ctx.organizationId;
    const entityId = input.personId ?? input.companyId ?? null;
    if (entityId) where.entity = entityId;
    if (input.dealId) where.deal = input.dealId;
    const [rows, total] = await Promise.all([
      findWithDecryption(
        em,
        CustomerComment,
        where,
        { limit, offset, orderBy: { createdAt: "desc" } },
        buildScope(ctx, tenantId)
      ),
      em.count(CustomerComment, where)
    ]);
    const filtered = rows.filter((row) => row.tenantId === tenantId);
    return {
      items: filtered.map((row) => ({
        id: row.id,
        body: row.body ?? null,
        entityId: commentEntityIdOf(row),
        dealId: row.deal && typeof row.deal === "object" ? row.deal.id : row.deal ?? null,
        authorUserId: row.authorUserId ?? null,
        organizationId: row.organizationId ?? null,
        tenantId: row.tenantId ?? null,
        createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null,
        updatedAt: row.updatedAt ? new Date(row.updatedAt).toISOString() : null
      })),
      total,
      limit,
      offset
    };
  }
};
const manageRecordCommentInput = z.object({
  operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new comment, update an existing one, or delete it."),
  personId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `companyId`) \u2014 the person entity the comment is attached to."),
  companyId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `personId`) \u2014 the company entity the comment is attached to."),
  dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Optional on `create` \u2014 when set, the comment also shows up under that deal."),
  commentId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete` \u2014 id of the existing comment row."),
  body: z.preprocess(blankToUndefined, z.string().min(1).max(8e3).optional()).describe("Comment text. Required for `create`; optional on `update`.")
}).superRefine((value, ctx) => {
  if (value.operation === "create") {
    if (!value.personId && !value.companyId) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Provide personId or companyId for create.",
        path: ["personId"]
      });
    }
    if (value.personId && value.companyId) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Provide only one of personId or companyId.",
        path: ["personId"]
      });
    }
    if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for create.", path: ["body"] });
  }
  if (value.operation === "update") {
    if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for update.", path: ["commentId"] });
    if (!value.body) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "body is required for update.", path: ["body"] });
  }
  if (value.operation === "delete") {
    if (!value.commentId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "commentId is required for delete.", path: ["commentId"] });
  }
});
const manageRecordCommentTool = {
  name: "customers.manage_record_comment",
  displayName: "Manage record comment",
  description: "Create, update, or delete a comment on a person, company, or deal record. Mutation tool \u2014 every call routes through the AI pending-action approval gate. Use `operation` to pick the action; for `create` provide `personId` OR `companyId` (and optionally `dealId`).",
  inputSchema: manageRecordCommentInput,
  requiredFeatures: ["customers.activities.manage"],
  tags: ["write", "customers"],
  isMutation: true,
  loadBeforeRecord: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageRecordCommentInput.parse(rawInput);
    const em = resolveEm(ctx);
    if (input.operation === "create") {
      const entityId = input.personId ?? input.companyId;
      if (input.dealId) {
        const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
        if (!deal) return null;
        return {
          recordId: deal.id,
          entityType: "customers.deal",
          recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
          before: { commentId: null, body: null, entityId, dealId: deal.id }
        };
      }
      return {
        recordId: entityId,
        entityType: input.personId ? "customers.person" : "customers.company",
        recordVersion: null,
        before: { commentId: null, body: null, entityId, dealId: null }
      };
    }
    const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
    if (!existing) return null;
    return {
      recordId: existing.id,
      entityType: "customers.customer_comment",
      recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
      before: {
        body: existing.body ?? null,
        dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null,
        entityId: commentEntityIdOf(existing),
        authorUserId: existing.authorUserId ?? null
      }
    };
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageRecordCommentInput.parse(rawInput);
    const em = resolveEm(ctx);
    const runner = createAiApiOperationRunner(ctx);
    if (input.operation === "create") {
      const entityId = input.personId ?? input.companyId;
      let organizationId = ctx.organizationId;
      let dealId = null;
      if (input.dealId) {
        const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
        if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
        organizationId = deal.organizationId ?? organizationId;
        dealId = deal.id;
      }
      if (!organizationId) {
        throw new Error("Organization scope is required to create a comment.");
      }
      const body2 = {
        tenantId,
        organizationId,
        entityId,
        body: input.body
      };
      if (dealId) body2.dealId = dealId;
      const response2 = await runner.run({ method: "POST", path: "/customers/comments", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? "Failed to create comment");
      }
      const result = response2.data ?? {};
      return {
        operation: "create",
        commentId: result.id ?? null,
        entityId,
        dealId,
        commandName: "customers.comments.create",
        before: null,
        after: { body: input.body ?? null, entityId, dealId }
      };
    }
    if (input.operation === "update") {
      const existing2 = await loadCommentForScope(em, ctx, tenantId, input.commentId);
      if (!existing2) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
      const organizationId = existing2.organizationId;
      if (!organizationId) throw new Error(`Comment "${existing2.id}" has no organization scope.`);
      const body2 = {
        id: existing2.id,
        tenantId,
        organizationId,
        body: input.body
      };
      const response2 = await runner.run({ method: "PUT", path: "/customers/comments", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? `Failed to update comment "${existing2.id}"`);
      }
      const after = await loadCommentForScope(em, ctx, tenantId, existing2.id);
      return {
        operation: "update",
        commentId: existing2.id,
        commandName: "customers.comments.update",
        before: { body: existing2.body ?? null },
        after: after ? { body: after.body ?? null } : null
      };
    }
    const existing = await loadCommentForScope(em, ctx, tenantId, input.commentId);
    if (!existing) throw new Error(`Comment "${input.commentId}" is not accessible to the caller.`);
    const body = { id: existing.id };
    const response = await runner.run({ method: "DELETE", path: "/customers/comments", body });
    if (!response.success) {
      throw new Error(response.error ?? `Failed to delete comment "${existing.id}"`);
    }
    return {
      operation: "delete",
      commentId: existing.id,
      commandName: "customers.comments.delete",
      before: { body: existing.body ?? null },
      after: null
    };
  }
};
const manageRecordActivityInput = z.object({
  operation: z.enum(["create", "update", "delete"]).describe("Which write to perform: create a new activity, update an existing one, or delete it."),
  activityId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `update` and `delete`."),
  personId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `companyId`) \u2014 the person entity the activity is logged on."),
  companyId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Required for `create` (or supply `personId`) \u2014 the company entity the activity is logged on."),
  dealId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Optional on `create` \u2014 when set, the activity is also linked to that deal."),
  activityType: z.preprocess(blankToUndefined, z.string().min(1).max(100).optional()).describe('Required for `create` \u2014 e.g. "call", "email", "meeting", "note".'),
  subject: z.preprocess(blankToUndefined, z.string().max(200).optional()).describe("Optional short subject line."),
  body: z.preprocess(blankToUndefined, z.string().max(8e3).optional()).describe("Optional free-text body."),
  occurredAt: z.preprocess(blankToUndefined, z.string().datetime().optional()).describe('ISO-8601 timestamp when the activity occurred. Omit for "now" (server-side default applies).')
}).superRefine((value, ctx) => {
  if (value.operation === "create") {
    if (!value.personId && !value.companyId) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Provide personId or companyId for create.",
        path: ["personId"]
      });
    }
    if (value.personId && value.companyId) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Provide only one of personId or companyId.",
        path: ["personId"]
      });
    }
    if (!value.activityType) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityType is required for create.", path: ["activityType"] });
  }
  if (value.operation === "update" || value.operation === "delete") {
    if (!value.activityId) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "activityId is required.", path: ["activityId"] });
  }
});
const manageRecordActivityTool = {
  name: "customers.manage_record_activity",
  displayName: "Manage record activity",
  description: "Create, update, or delete an activity (call, email, meeting, note) on a person, company, or deal record. Mutation tool \u2014 every call routes through the AI pending-action approval gate. For `create` provide `personId` OR `companyId` (and optionally `dealId`).",
  inputSchema: manageRecordActivityInput,
  requiredFeatures: ["customers.activities.manage"],
  tags: ["write", "customers"],
  isMutation: true,
  loadBeforeRecord: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageRecordActivityInput.parse(rawInput);
    const em = resolveEm(ctx);
    if (input.operation === "create") {
      const entityId = input.personId ?? input.companyId;
      if (input.dealId) {
        const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
        if (!deal) return null;
        return {
          recordId: deal.id,
          entityType: "customers.deal",
          recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
          before: { activityId: null, dealId: deal.id, entityId }
        };
      }
      return {
        recordId: entityId,
        entityType: input.personId ? "customers.person" : "customers.company",
        recordVersion: null,
        before: { activityId: null, entityId, dealId: null }
      };
    }
    const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
    if (!existing) return null;
    return {
      recordId: existing.id,
      entityType: "customers.customer_activity",
      recordVersion: recordVersionFromUpdatedAt(existing.updatedAt),
      before: {
        activityType: existing.activityType,
        subject: existing.subject ?? null,
        body: existing.body ?? null,
        occurredAt: existing.occurredAt ? new Date(existing.occurredAt).toISOString() : null,
        entityId: activityEntityIdOf(existing),
        dealId: existing.deal && typeof existing.deal === "object" ? existing.deal.id : existing.deal ?? null
      }
    };
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = manageRecordActivityInput.parse(rawInput);
    const em = resolveEm(ctx);
    const runner = createAiApiOperationRunner(ctx);
    if (input.operation === "create") {
      const entityId = input.personId ?? input.companyId;
      let organizationId = ctx.organizationId;
      let dealId = null;
      if (input.dealId) {
        const deal = await loadDealForScope(em, ctx, tenantId, input.dealId);
        if (!deal) throw new Error(`Deal "${input.dealId}" is not accessible to the caller.`);
        organizationId = deal.organizationId ?? organizationId;
        dealId = deal.id;
      }
      if (!organizationId) {
        throw new Error("Organization scope is required to create an activity.");
      }
      const body2 = {
        tenantId,
        organizationId,
        entityId,
        activityType: input.activityType
      };
      if (dealId) body2.dealId = dealId;
      if (input.subject) body2.subject = input.subject;
      if (input.body) body2.body = input.body;
      if (input.occurredAt) body2.occurredAt = input.occurredAt;
      const response2 = await runner.run({ method: "POST", path: "/customers/activities", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? "Failed to create activity");
      }
      const result = response2.data ?? {};
      return {
        operation: "create",
        activityId: result.id ?? null,
        entityId,
        dealId,
        commandName: "customers.activities.create",
        before: null,
        after: {
          activityType: input.activityType ?? null,
          subject: input.subject ?? null,
          body: input.body ?? null,
          occurredAt: input.occurredAt ?? null
        }
      };
    }
    if (input.operation === "update") {
      const existing2 = await loadActivityForScope(em, ctx, tenantId, input.activityId);
      if (!existing2) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
      const organizationId = existing2.organizationId;
      if (!organizationId) throw new Error(`Activity "${existing2.id}" has no organization scope.`);
      const body2 = { id: existing2.id, tenantId, organizationId };
      if (input.activityType) body2.activityType = input.activityType;
      if (input.subject !== void 0) body2.subject = input.subject;
      if (input.body !== void 0) body2.body = input.body;
      if (input.occurredAt !== void 0) body2.occurredAt = input.occurredAt;
      const response2 = await runner.run({ method: "PUT", path: "/customers/activities", body: body2 });
      if (!response2.success) {
        throw new Error(response2.error ?? `Failed to update activity "${existing2.id}"`);
      }
      const after = await loadActivityForScope(em, ctx, tenantId, existing2.id);
      return {
        operation: "update",
        activityId: existing2.id,
        commandName: "customers.activities.update",
        before: {
          activityType: existing2.activityType,
          subject: existing2.subject ?? null,
          body: existing2.body ?? null,
          occurredAt: existing2.occurredAt ? new Date(existing2.occurredAt).toISOString() : null
        },
        after: after ? {
          activityType: after.activityType,
          subject: after.subject ?? null,
          body: after.body ?? null,
          occurredAt: after.occurredAt ? new Date(after.occurredAt).toISOString() : null
        } : null
      };
    }
    const existing = await loadActivityForScope(em, ctx, tenantId, input.activityId);
    if (!existing) throw new Error(`Activity "${input.activityId}" is not accessible to the caller.`);
    const body = { id: existing.id };
    const response = await runner.run({ method: "DELETE", path: "/customers/activities", body });
    if (!response.success) {
      throw new Error(response.error ?? `Failed to delete activity "${existing.id}"`);
    }
    return {
      operation: "delete",
      activityId: existing.id,
      commandName: "customers.activities.delete",
      before: {
        activityType: existing.activityType,
        subject: existing.subject ?? null,
        body: existing.body ?? null
      },
      after: null
    };
  }
};
const activitiesTasksAiTools = [
  listActivitiesTool,
  listTasksTool,
  listDealCommentsTool,
  manageDealCommentTool,
  manageDealActivityTool,
  listRecordCommentsTool,
  manageRecordCommentTool,
  manageRecordActivityTool
];
var activities_tasks_pack_default = activitiesTasksAiTools;
export {
  activitiesTasksAiTools,
  activities_tasks_pack_default as default
};
//# sourceMappingURL=activities-tasks-pack.js.map
