import { resolveKyselyClient } from "../lib/kysely.js";
import { fetchStuckThresholdDays } from "../lib/stuckDeals.js";
import { TERMINAL_INTERACTION_STATUS_LIST } from "../lib/interactionStatus.js";
const ENRICHER_TIMEOUT_MS = 2e3;
const DAY_MS = 24 * 60 * 60 * 1e3;
function parseDate(value) {
  if (!value) return null;
  if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
  if (typeof value === "string") {
    const date = new Date(value);
    return Number.isNaN(date.getTime()) ? null : date;
  }
  return null;
}
function diffDays(from, to) {
  return Math.floor((to.getTime() - from.getTime()) / DAY_MS);
}
function stringArrayOrNull(value) {
  if (!Array.isArray(value)) return null;
  const strings = value.filter((entry) => typeof entry === "string");
  return strings.length > 0 ? strings : null;
}
async function fetchOpenInteractionCounts(db, dealIds, organizationId, tenantId) {
  const map = /* @__PURE__ */ new Map();
  if (dealIds.size === 0) return map;
  const rows = await db.selectFrom("customer_interactions").select(["deal_id"]).select((eb) => eb.fn.countAll().as("count")).where("deal_id", "in", [...dealIds]).where("organization_id", "=", organizationId).where("tenant_id", "=", tenantId).where("deleted_at", "is", null).where("status", "not in", [...TERMINAL_INTERACTION_STATUS_LIST]).groupBy("deal_id").execute();
  for (const row of rows) {
    if (row.deal_id == null) continue;
    const count = typeof row.count === "number" ? row.count : Number(row.count);
    if (Number.isFinite(count)) map.set(row.deal_id, count);
  }
  return map;
}
async function fetchLatestStageTransitions(db, dealIds, organizationId, tenantId) {
  const map = /* @__PURE__ */ new Map();
  if (dealIds.size === 0) return map;
  const rows = await db.selectFrom("customer_deal_stage_transitions").select(["deal_id"]).select((eb) => eb.fn.max("transitioned_at").as("last_transitioned_at")).where("deal_id", "in", [...dealIds]).where("organization_id", "=", organizationId).where("tenant_id", "=", tenantId).where("deleted_at", "is", null).groupBy("deal_id").execute();
  for (const row of rows) {
    const parsed = parseDate(row.last_transitioned_at);
    if (parsed) map.set(row.deal_id, parsed);
  }
  return map;
}
function buildPipelineState(record, openInteractionCounts, latestTransitions, threshold, now, today) {
  const openActivitiesCount = openInteractionCounts.get(record.id) ?? 0;
  const transitionAt = latestTransitions.get(record.id) ?? parseDate(record.created_at) ?? now;
  const daysInCurrentStage = Math.max(0, diffDays(transitionAt, now));
  const expectedClose = parseDate(record.expected_close_at);
  const status = typeof record.status === "string" ? record.status : null;
  const isOverdue = status === "open" && !!expectedClose && expectedClose < today;
  const isStuck = daysInCurrentStage > threshold;
  return { openActivitiesCount, daysInCurrentStage, isStuck, isOverdue };
}
const dealPipelineEnricher = {
  id: "customers.deal-pipeline-state",
  targetEntity: "customers.deal",
  // No `features` gate: the deals list route already enforces `customers.deals.view`
  // at the route-metadata level (see api/deals/route.ts). Declaring `features` here
  // would silently disable the enricher in environments where `rbacService` resolves
  // to undefined or `getGrantedFeatures` throws — `hasRequiredFeatures` treats a
  // missing `userFeatures` as "no access", which made `_pipeline` disappear from
  // CI responses (TC-CRM-066) while keeping the local kanban working.
  priority: 10,
  timeout: ENRICHER_TIMEOUT_MS,
  critical: false,
  fallback: {
    _pipeline: {
      openActivitiesCount: 0,
      daysInCurrentStage: 0,
      isStuck: false,
      isOverdue: false
    }
  },
  async enrichOne(record, context) {
    const enriched = await this.enrichMany([record], context);
    return enriched[0];
  },
  async enrichMany(records, context) {
    if (records.length === 0) return records;
    const now = /* @__PURE__ */ new Date();
    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const emptyCounts = /* @__PURE__ */ new Map();
    const emptyTransitions = /* @__PURE__ */ new Map();
    const FALLBACK_THRESHOLD = 14;
    const db = resolveKyselyClient(context.em);
    if (!db) {
      return records.map((record) => ({
        ...record,
        _pipeline: buildPipelineState(
          record,
          emptyCounts,
          emptyTransitions,
          FALLBACK_THRESHOLD,
          now,
          today
        )
      }));
    }
    const dealIds = /* @__PURE__ */ new Set();
    for (const record of records) {
      if (typeof record.id === "string") dealIds.add(record.id);
    }
    if (dealIds.size === 0) {
      return records.map((record) => ({
        ...record,
        _pipeline: buildPipelineState(
          record,
          emptyCounts,
          emptyTransitions,
          FALLBACK_THRESHOLD,
          now,
          today
        )
      }));
    }
    const [openInteractionCounts, latestTransitions, threshold] = await Promise.all([
      fetchOpenInteractionCounts(db, dealIds, context.organizationId, context.tenantId),
      fetchLatestStageTransitions(db, dealIds, context.organizationId, context.tenantId),
      fetchStuckThresholdDays(db, context.organizationId, context.tenantId)
    ]);
    return records.map((record) => ({
      ...record,
      _pipeline: buildPipelineState(
        record,
        openInteractionCounts,
        latestTransitions,
        threshold,
        now,
        today
      )
    }));
  }
};
const privateEmailCountEnricher = {
  id: "customers.private-email-count",
  targetEntity: "customers.person",
  features: ["customers.people.view"],
  priority: 30,
  timeout: 1500,
  fallback: { _privateEmailCount: 0 },
  critical: false,
  async enrichOne(record, context) {
    const enriched = await this.enrichMany([record], context);
    return enriched[0];
  },
  async enrichMany(records, context) {
    if (records.length === 0) return records;
    const db = resolveKyselyClient(context.em);
    if (!db) {
      return records.map((record) => ({ ...record, _privateEmailCount: 0 }));
    }
    const userId = context.userId;
    if (!userId || userId.startsWith("api_key:")) {
      return records.map((record) => ({ ...record, _privateEmailCount: 0 }));
    }
    const personIds = records.map((r) => r.id).filter((id) => typeof id === "string" && id.length > 0);
    if (personIds.length === 0) {
      return records.map((record) => ({ ...record, _privateEmailCount: 0 }));
    }
    const rows = await db.selectFrom("customer_interactions").select(["entity_id"]).select((eb) => eb.fn.countAll().as("count")).where("tenant_id", "=", context.tenantId).where("organization_id", "=", context.organizationId).where("interaction_type", "=", "email").where("visibility", "=", "private").where("deleted_at", "is", null).where("entity_id", "in", personIds).where("author_user_id", "!=", userId).groupBy("entity_id").execute();
    const countMap = /* @__PURE__ */ new Map();
    for (const row of rows) {
      const personId = typeof row.entity_id === "string" ? row.entity_id : null;
      if (!personId) continue;
      const count = typeof row.count === "number" ? row.count : Number(row.count);
      if (Number.isFinite(count)) countMap.set(personId, count);
    }
    return records.map((record) => ({
      ...record,
      _privateEmailCount: countMap.get(record.id) ?? 0
    }));
  }
};
const interactionEmailCardEnricher = {
  id: "customers.interaction-email-card",
  // Must match the entity ID passed to applyResponseEnrichers in the interactions
  // GET route (api/interactions/route.ts line 539): 'customers.interaction'.
  targetEntity: "customers.interaction",
  features: ["customers.interactions.view"],
  priority: 25,
  timeout: 1500,
  fallback: {},
  critical: false,
  async enrichOne(record, ctx) {
    const results = await this.enrichMany([record], ctx);
    return results[0];
  },
  async enrichMany(records, ctx) {
    if (records.length === 0) return records;
    const emailRecords = records.filter(
      (r) => r.interactionType === "email" && typeof r.externalMessageId === "string" && r.externalMessageId.length > 0
    );
    if (emailRecords.length === 0) return records;
    const linkIds = Array.from(
      new Set(emailRecords.map((r) => r.externalMessageId))
    );
    const kysely = resolveKyselyClient(ctx.em);
    if (!kysely) {
      return records;
    }
    let linkRows;
    try {
      linkRows = await kysely.selectFrom("message_channel_links").select(["id", "channel_metadata"]).where("tenant_id", "=", ctx.tenantId).where(
        (eb) => eb.or([
          eb("organization_id", "=", ctx.organizationId),
          eb("organization_id", "is", null)
        ])
      ).where("id", "in", linkIds).execute();
    } catch {
      return records;
    }
    const byLinkId = /* @__PURE__ */ new Map();
    for (const row of linkRows) {
      const meta = row.channel_metadata ?? {};
      const fields = {
        externalMessageId: row.id,
        rfcMessageId: typeof meta.messageId === "string" ? meta.messageId : null,
        fromAddress: typeof meta.from === "string" ? meta.from : null,
        toAddresses: stringArrayOrNull(meta.to),
        ccAddresses: stringArrayOrNull(meta.cc),
        // bcc is intentionally NOT surfaced: BCC recipients are blind by design,
        // so exposing them to every teammate who can view a shared email would
        // leak the blind-copy list. Keep it out of the enriched response.
        subject: typeof meta.subject === "string" ? meta.subject : null,
        inReplyTo: typeof meta.inReplyTo === "string" ? meta.inReplyTo : null,
        references: stringArrayOrNull(meta.references)
      };
      byLinkId.set(row.id, fields);
    }
    const currentUserId = ctx.userId;
    return records.map((r) => {
      if (r.interactionType !== "email" || typeof r.externalMessageId !== "string" || r.externalMessageId.length === 0) {
        return r;
      }
      const fields = byLinkId.get(r.externalMessageId);
      if (!fields) return r;
      const visibility = r.visibility === "private" || r.visibility === "shared" ? r.visibility : null;
      const authorUserId = typeof r.authorUserId === "string" ? r.authorUserId : null;
      const isAuthor = Boolean(currentUserId && authorUserId && authorUserId === currentUserId);
      if (visibility === "private" && !isAuthor) {
        return r;
      }
      const existingIntegrations = r._integrations ?? {};
      return {
        ...r,
        _integrations: {
          ...existingIntegrations,
          email: {
            ...fields,
            currentVisibility: visibility,
            isAuthor
          }
        }
      };
    });
  }
};
const enrichers = [dealPipelineEnricher, privateEmailCountEnricher, interactionEmailCardEnricher];
export {
  buildPipelineState,
  enrichers,
  interactionEmailCardEnricher,
  privateEmailCountEnricher
};
//# sourceMappingURL=enrichers.js.map
