import { z } from "zod";
import { defineApiBackedAiTool } from "@open-mercato/ai-assistant/modules/ai_assistant/lib/api-backed-tool";
import {
  createAiApiOperationRunner
} from "@open-mercato/ai-assistant/modules/ai_assistant/lib/ai-api-operation-runner";
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import {
  CustomerDeal,
  CustomerPipelineStage
} 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 };
}
const listDealsInput = z.object({
  q: z.string().trim().optional().describe("Search text matched against deal title / description. Omit or leave empty to list all."),
  limit: z.number().int().min(1).max(100).optional().describe("Maximum rows to return (default 50, max 100)."),
  offset: z.number().int().min(0).optional().describe("Number of rows to skip (default 0)."),
  personId: z.string().uuid().optional().describe("Return only deals linked to this person entity id."),
  companyId: z.string().uuid().optional().describe("Return only deals linked to this company entity id."),
  pipelineStageId: z.string().uuid().optional().describe("Return only deals at this pipeline stage."),
  status: z.string().optional().describe('Filter by deal status (e.g. "open", "won", "lost").')
}).passthrough();
const listDealsTool = defineApiBackedAiTool({
  name: "customers.list_deals",
  displayName: "List deals",
  description: "Search / list deals for the caller tenant + organization. Optional filters include linked person / company / pipeline stage.",
  inputSchema: listDealsInput,
  requiredFeatures: ["customers.deals.view"],
  toOperation: (input, ctx) => {
    assertTenantScope(ctx);
    const limit = input.limit ?? 50;
    const offset = input.offset ?? 0;
    const page = Math.floor(offset / limit) + 1;
    const query = {
      page,
      pageSize: limit
    };
    if (input.q?.trim()) query.search = input.q.trim();
    if (input.personId) query.personId = input.personId;
    if (input.companyId) query.companyId = input.companyId;
    if (input.pipelineStageId) query.pipelineStageId = input.pipelineStageId;
    if (input.status) query.status = input.status;
    const operation = {
      method: "GET",
      path: "/customers/deals",
      query
    };
    return operation;
  },
  mapResponse: (response, input) => {
    const limit = input.limit ?? 50;
    const offset = input.offset ?? 0;
    const data = response.data ?? {};
    const rawItems = Array.isArray(data.items) ? data.items : [];
    return {
      items: rawItems.map((row) => {
        const expectedCloseRaw = row.expected_close_at ?? row.expectedCloseAt ?? null;
        const expectedCloseAt = expectedCloseRaw ? new Date(String(expectedCloseRaw)).toISOString() : null;
        const createdAtRaw = row.created_at ?? row.createdAt ?? null;
        const createdAt = createdAtRaw ? new Date(String(createdAtRaw)).toISOString() : null;
        return {
          id: row.id,
          title: row.title ?? null,
          description: row.description ?? null,
          status: row.status ?? null,
          pipelineId: row.pipeline_id ?? row.pipelineId ?? null,
          pipelineStageId: row.pipeline_stage_id ?? row.pipelineStageId ?? null,
          valueAmount: row.value_amount ?? row.valueAmount ?? null,
          valueCurrency: row.value_currency ?? row.valueCurrency ?? null,
          probability: row.probability ?? null,
          ownerUserId: row.owner_user_id ?? row.ownerUserId ?? null,
          expectedCloseAt,
          source: row.source ?? null,
          organizationId: row.organization_id ?? row.organizationId ?? null,
          tenantId: row.tenant_id ?? row.tenantId ?? null,
          createdAt
        };
      }),
      total: typeof data.total === "number" ? data.total : 0,
      limit,
      offset
    };
  }
});
const getDealInput = z.object({
  dealId: z.string().uuid().describe("Deal id (UUID)."),
  includeRelated: z.boolean().optional().describe("When true, include notes, activities, linked people and companies (each capped at 100).")
});
function toIsoDeal(value) {
  if (!value) return null;
  const dt = value instanceof Date ? value : new Date(String(value));
  if (Number.isNaN(dt.getTime())) return null;
  return dt.toISOString();
}
const getDealTool = {
  name: "customers.get_deal",
  displayName: "Get deal",
  description: "Fetch a deal by id with fields and (optionally) notes, activities, linked people, and linked companies. Returns { found: false } when outside tenant/org scope.",
  inputSchema: getDealInput,
  requiredFeatures: ["customers.deals.view"],
  tags: ["read", "customers"],
  handler: async (rawInput, ctx) => {
    const { tenantId: _tenantId } = assertTenantScope(ctx);
    void _tenantId;
    const input = getDealInput.parse(rawInput);
    const includeRelated = !!input.includeRelated;
    const runner = createAiApiOperationRunner(ctx);
    const detailResponse = await runner.run({
      method: "GET",
      path: `/customers/deals/${input.dealId}`
    });
    if (!detailResponse.success) {
      if (detailResponse.statusCode === 404 || detailResponse.statusCode === 403) {
        return { found: false, dealId: input.dealId };
      }
      throw new Error(detailResponse.error ?? `Failed to fetch deal ${input.dealId}`);
    }
    const detail = detailResponse.data ?? {};
    const dealRow = detail.deal ?? null;
    if (!dealRow) {
      return { found: false, dealId: input.dealId };
    }
    const customFields = detail.customFields ?? {};
    const peopleRows = Array.isArray(detail.people) ? detail.people : [];
    const companiesRows = Array.isArray(detail.companies) ? detail.companies : [];
    let related = null;
    if (includeRelated) {
      const [activitiesResponse, commentsResponse] = await Promise.all([
        runner.run({
          method: "GET",
          path: "/customers/activities",
          query: { dealId: input.dealId, page: 1, pageSize: 100, sortField: "occurredAt", sortDir: "desc" }
        }),
        runner.run({
          method: "GET",
          path: "/customers/comments",
          query: { dealId: input.dealId, page: 1, pageSize: 100 }
        })
      ]);
      const activities = activitiesResponse.success && Array.isArray(activitiesResponse.data?.items) ? activitiesResponse.data.items : [];
      const comments = commentsResponse.success && Array.isArray(commentsResponse.data?.items) ? commentsResponse.data.items : [];
      related = {
        activities: activities.map((activity) => ({
          id: activity.id,
          activityType: activity.activityType ?? activity.activity_type ?? null,
          subject: activity.subject ?? null,
          body: activity.body ?? null,
          occurredAt: toIsoDeal(activity.occurredAt ?? activity.occurred_at),
          createdAt: toIsoDeal(activity.createdAt ?? activity.created_at)
        })),
        notes: comments.map((comment) => ({
          id: comment.id,
          body: comment.body,
          authorUserId: comment.authorUserId ?? comment.author_user_id ?? null,
          createdAt: toIsoDeal(comment.createdAt ?? comment.created_at)
        })),
        people: peopleRows.map((person) => {
          if (!person || typeof person !== "object") return null;
          const id = typeof person.id === "string" ? person.id : null;
          if (!id) return null;
          const subtitle = typeof person.subtitle === "string" ? person.subtitle : null;
          const label = typeof person.label === "string" ? person.label : "";
          const entry = {
            id,
            displayName: label,
            primaryEmail: subtitle && subtitle.includes("@") ? subtitle : null,
            primaryPhone: subtitle && !subtitle.includes("@") ? subtitle : null,
            participantRole: null
          };
          return entry;
        }).filter(
          (value) => value !== null
        ),
        companies: companiesRows.map((company) => {
          if (!company || typeof company !== "object") return null;
          const id = typeof company.id === "string" ? company.id : null;
          if (!id) return null;
          const label = typeof company.label === "string" ? company.label : "";
          const entry = {
            id,
            displayName: label,
            primaryEmail: null,
            primaryPhone: null
          };
          return entry;
        }).filter(
          (value) => value !== null
        )
      };
    }
    return {
      found: true,
      deal: {
        id: dealRow.id,
        title: typeof dealRow.title === "string" ? dealRow.title : "",
        description: dealRow.description ?? null,
        status: dealRow.status ?? null,
        pipelineId: dealRow.pipelineId ?? null,
        pipelineStageId: dealRow.pipelineStageId ?? null,
        valueAmount: dealRow.valueAmount ?? null,
        valueCurrency: dealRow.valueCurrency ?? null,
        probability: dealRow.probability ?? null,
        ownerUserId: dealRow.ownerUserId ?? null,
        expectedCloseAt: toIsoDeal(dealRow.expectedCloseAt),
        source: dealRow.source ?? null,
        organizationId: dealRow.organizationId ?? null,
        tenantId: dealRow.tenantId ?? null,
        createdAt: toIsoDeal(dealRow.createdAt),
        updatedAt: toIsoDeal(dealRow.updatedAt)
      },
      customFields,
      related
    };
  }
};
const blankToUndefined = (value) => {
  if (typeof value !== "string") return value;
  const trimmed = value.trim();
  return trimmed.length === 0 ? void 0 : trimmed;
};
const updateDealStageInput = z.object({
  dealId: z.string().uuid().describe("Deal id (UUID) to update."),
  toPipelineStageId: z.preprocess(blankToUndefined, z.string().uuid().optional()).describe("Target pipeline stage id (UUID). Preferred \u2014 tenant-scoped stage record."),
  toStage: z.preprocess(blankToUndefined, z.string().min(1).max(50).optional()).describe(
    'Target status slug (e.g. "open", "won", "lost"). Used when the deal does not belong to a managed pipeline.'
  )
}).refine(
  (value) => Boolean(value.toPipelineStageId) !== Boolean(value.toStage),
  {
    message: "Provide exactly one of toPipelineStageId or toStage.",
    path: ["toPipelineStageId"]
  }
);
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();
}
function titleStatus(value) {
  if (!value) return void 0;
  return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
}
async function loadDealWithStage(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 loadPipelineStage(em, ctx, tenantId, stageId, organizationId) {
  return findOneWithDecryption(
    em,
    CustomerPipelineStage,
    {
      id: stageId,
      tenantId,
      organizationId
    },
    void 0,
    buildScope(ctx, tenantId)
  );
}
const updateDealStageTool = {
  name: "customers.update_deal_stage",
  displayName: "Update deal stage",
  description: 'Move a deal to a different pipeline stage (by stage id) or change its top-level status (e.g. "open", "won", "lost"). Mutation tool \u2014 flows through the AI pending-action approval gate.',
  inputSchema: updateDealStageInput,
  requiredFeatures: ["customers.deals.manage"],
  tags: ["write", "customers"],
  isMutation: true,
  loadBeforeRecord: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = updateDealStageInput.parse(rawInput);
    const em = resolveEm(ctx);
    const deal = await loadDealWithStage(em, ctx, tenantId, input.dealId);
    if (!deal) return null;
    let afterStatus = deal.status ?? null;
    let afterPipelineStageId = deal.pipelineStageId ?? null;
    let afterPipelineStageLabel = deal.pipelineStage ?? null;
    if (input.toPipelineStageId) {
      const organizationId = deal.organizationId ?? ctx.organizationId ?? null;
      const stage = organizationId ? await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId) : null;
      afterPipelineStageId = input.toPipelineStageId;
      afterPipelineStageLabel = stage?.label ?? input.toPipelineStageId;
    } else if (input.toStage) {
      afterStatus = input.toStage;
    }
    const beforeStatus = deal.status ?? null;
    const beforePipelineStageId = deal.pipelineStageId ?? null;
    const beforePipelineStageLabel = deal.pipelineStage ?? beforePipelineStageId;
    return {
      recordId: deal.id,
      entityType: "customers.deal",
      recordVersion: recordVersionFromUpdatedAt(deal.updatedAt),
      before: {
        status: beforeStatus,
        pipelineStageId: beforePipelineStageId
      },
      after: {
        status: afterStatus,
        pipelineStageId: afterPipelineStageId
      },
      display: {
        fieldLabels: {
          status: "Status",
          pipelineStageId: "Pipeline stage"
        },
        before: {
          ...beforeStatus ? { status: titleStatus(beforeStatus) } : {},
          ...beforePipelineStageLabel ? { pipelineStageId: beforePipelineStageLabel } : {}
        },
        after: {
          ...afterStatus ? { status: titleStatus(afterStatus) } : {},
          ...afterPipelineStageLabel ? { pipelineStageId: afterPipelineStageLabel } : {}
        }
      }
    };
  },
  handler: async (rawInput, ctx) => {
    const { tenantId } = assertTenantScope(ctx);
    const input = updateDealStageInput.parse(rawInput);
    const em = resolveEm(ctx);
    const deal = await loadDealWithStage(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 "${input.dealId}" has no organization scope.`);
    }
    const before = {
      status: deal.status ?? null,
      pipelineStage: deal.pipelineStage ?? null,
      pipelineStageId: deal.pipelineStageId ?? null
    };
    const body = {
      id: deal.id,
      tenantId,
      organizationId
    };
    if (input.toPipelineStageId) {
      const stage = await loadPipelineStage(em, ctx, tenantId, input.toPipelineStageId, organizationId);
      if (!stage) {
        throw new Error("Pipeline stage not found.");
      }
      body.pipelineStageId = input.toPipelineStageId;
    } else if (input.toStage) {
      body.status = input.toStage;
    }
    const runner = createAiApiOperationRunner(ctx);
    const response = await runner.run({
      method: "PUT",
      path: "/customers/deals",
      body
    });
    if (!response.success) {
      throw new Error(response.error ?? `Failed to update deal "${deal.id}"`);
    }
    const after = await loadDealWithStage(em, ctx, tenantId, deal.id);
    return {
      recordId: deal.id,
      commandName: "customers.deals.update",
      before,
      after: after ? {
        status: after.status ?? null,
        pipelineStage: after.pipelineStage ?? null,
        pipelineStageId: after.pipelineStageId ?? null
      } : null
    };
  }
};
const dealsAiTools = [listDealsTool, getDealTool, updateDealStageTool];
var deals_pack_default = dealsAiTools;
export {
  dealsAiTools,
  deals_pack_default as default
};
//# sourceMappingURL=deals-pack.js.map
