import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { CustomerEntity, CustomerInteraction } from "../data/entities.js";
import { findPeopleByAddresses, normalizeAddresses } from "./findPeopleByAddresses.js";
import { emitCustomersEvent } from "../events.js";
import { INTERACTION_STATUS_COMPLETED } from "./interactionStatus.js";
const POSTGRES_UNIQUE_VIOLATION = "23505";
async function handler(payload, ctx) {
  const linkId = payload?.channelLinkId ?? payload?.messageChannelLinkId;
  if (typeof linkId !== "string" || !linkId) return;
  if (typeof payload.tenantId !== "string" || !payload.tenantId) return;
  const tenantId = payload.tenantId;
  const organizationId = payload.organizationId ?? null;
  if (!organizationId) return;
  const dscope = { tenantId, organizationId };
  const em = ctx.resolve("em").fork();
  const link = await findOneWithDecryption(
    em,
    "MessageChannelLink",
    { id: linkId, tenantId, organizationId },
    void 0,
    dscope
  );
  if (!link) return;
  const metaJson = link.channelMetadata ?? null;
  const payloadJson = link.channelPayload ?? null;
  let channelUserId = null;
  if (typeof payload.channelId === "string" && payload.channelId) {
    const channel = await findOneWithDecryption(
      em,
      "CommunicationChannel",
      { id: payload.channelId, tenantId, organizationId },
      void 0,
      dscope
    );
    channelUserId = channel?.userId ?? null;
  }
  const rawAddresses = [];
  if (payloadJson) {
    collectAddressField(rawAddresses, payloadJson.from);
    collectAddressField(rawAddresses, payloadJson.to);
    collectAddressField(rawAddresses, payloadJson.cc);
    collectAddressField(rawAddresses, payloadJson.bcc);
  }
  if (rawAddresses.length === 0 && metaJson) {
    collectAddressField(rawAddresses, metaJson.from);
    collectAddressField(rawAddresses, metaJson.to);
    collectAddressField(rawAddresses, metaJson.cc);
    collectAddressField(rawAddresses, metaJson.bcc);
  }
  const normalized = normalizeAddresses(rawAddresses);
  const crmPersonIdHint = typeof metaJson?.crmPersonId === "string" ? metaJson.crmPersonId : null;
  let crmPersonId = null;
  if (crmPersonIdHint) {
    const hintedPerson = await findOneWithDecryption(
      em,
      CustomerEntity,
      { id: crmPersonIdHint, kind: "person", tenantId, organizationId, deletedAt: null },
      void 0,
      dscope
    );
    if (hintedPerson) crmPersonId = crmPersonIdHint;
  }
  if (normalized.length === 0 && !crmPersonId) {
    await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson);
    return;
  }
  const matched = await findPeopleByAddresses(em, normalized, tenantId, organizationId);
  const personIdSet = new Set(matched.map((m) => m.id));
  if (crmPersonId) personIdSet.add(crmPersonId);
  if (personIdSet.size === 0) {
    await handleThreadingInheritance(em, link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson);
    return;
  }
  const linkDirection = link.direction === "inbound" || link.direction === "outbound" ? link.direction : null;
  const visibility = resolveVisibility(metaJson, channelUserId, linkDirection);
  const subject = typeof metaJson?.subject === "string" ? metaJson.subject : typeof payloadJson?.subject === "string" ? payloadJson.subject : null;
  const bodyText = typeof metaJson?.bodyText === "string" ? metaJson.bodyText : typeof payloadJson?.text === "string" ? payloadJson.text : null;
  const occurredAt = link.createdAt instanceof Date ? link.createdAt : /* @__PURE__ */ new Date();
  const providerKey = typeof link.providerKey === "string" ? link.providerKey : null;
  await persistInteractions(
    em,
    personIdSet,
    {
      linkId,
      tenantId,
      organizationId,
      interactionType: "email",
      title: subject,
      body: bodyText,
      authorUserId: channelUserId,
      occurredAt,
      visibility,
      channelProviderKey: providerKey
    }
  );
}
async function handleThreadingInheritance(em, _link, linkId, tenantId, organizationId, channelUserId, metaJson, payloadJson) {
  const inboundMessageId = typeof _link.messageId === "string" ? _link.messageId : null;
  if (inboundMessageId && organizationId) {
    const threadPersonRows = await em.getConnection().execute(
      `SELECT DISTINCT ci.entity_id AS entity_id
       FROM messages inbound_m
       JOIN messages thread_m ON thread_m.thread_id = inbound_m.thread_id
       JOIN message_channel_links mcl ON mcl.message_id = thread_m.id
       JOIN customer_interactions ci ON ci.external_message_id = mcl.id
       WHERE inbound_m.id = ?
         AND inbound_m.tenant_id = ?
         AND inbound_m.organization_id = ?
         AND inbound_m.deleted_at IS NULL
         AND inbound_m.thread_id IS NOT NULL
         AND thread_m.tenant_id = ?
         AND thread_m.organization_id = ?
         AND thread_m.deleted_at IS NULL
         AND mcl.tenant_id = ?
         AND mcl.organization_id = ?
         AND ci.tenant_id = ?
         AND ci.organization_id = ?
         AND ci.interaction_type = 'email'
         AND ci.deleted_at IS NULL
         AND ci.entity_id IS NOT NULL
       LIMIT 200`,
      [
        inboundMessageId,
        tenantId,
        organizationId,
        tenantId,
        organizationId,
        tenantId,
        organizationId,
        tenantId,
        organizationId
      ]
    );
    const threadPersonIds = new Set(
      threadPersonRows.map((row) => row.entity_id).filter((id) => !!id)
    );
    if (threadPersonIds.size > 0) {
      const subject2 = typeof metaJson?.subject === "string" ? metaJson.subject : typeof payloadJson?.subject === "string" ? payloadJson.subject : null;
      const bodyText = typeof payloadJson?.text === "string" ? payloadJson.text : typeof metaJson?.bodyText === "string" ? metaJson.bodyText : null;
      const occurredAt2 = _link.createdAt instanceof Date ? _link.createdAt : /* @__PURE__ */ new Date();
      const providerKey2 = typeof _link.providerKey === "string" ? _link.providerKey : null;
      await persistInteractions(em, threadPersonIds, {
        linkId,
        tenantId,
        organizationId,
        interactionType: "email",
        title: subject2,
        body: bodyText,
        authorUserId: channelUserId,
        occurredAt: occurredAt2,
        visibility: channelUserId ? "private" : "shared",
        channelProviderKey: providerKey2
      });
      return;
    }
  }
  const refIds = [];
  const inReplyTo = typeof metaJson?.inReplyTo === "string" ? stripBrackets(metaJson.inReplyTo) : typeof payloadJson?.inReplyTo === "string" ? stripBrackets(payloadJson.inReplyTo) : null;
  if (inReplyTo) refIds.push(inReplyTo);
  const refs = Array.isArray(metaJson?.references) ? metaJson.references : Array.isArray(payloadJson?.references) ? payloadJson.references : [];
  for (const r of refs) {
    if (typeof r === "string") {
      const stripped = stripBrackets(r);
      if (stripped && !refIds.includes(stripped)) refIds.push(stripped);
    }
  }
  if (refIds.length === 0) return;
  const dscope = { tenantId, organizationId };
  const messageIdCandidates = Array.from(
    new Set(refIds.flatMap((ref) => [ref, `<${ref}>`]))
  );
  const placeholders = messageIdCandidates.map(() => "?").join(", ");
  const parentLinkRows = await em.getConnection().execute(
    `SELECT id FROM message_channel_links
     WHERE tenant_id = ?
       AND organization_id = ?
       AND channel_metadata->>'messageId' IN (${placeholders})
     LIMIT 200`,
    [tenantId, organizationId, ...messageIdCandidates]
  );
  const matchedParentIds = parentLinkRows.map((parentLink) => parentLink.id);
  if (matchedParentIds.length === 0) return;
  const parentInteractions = await findWithDecryption(
    em,
    CustomerInteraction,
    {
      externalMessageId: { $in: matchedParentIds },
      tenantId,
      organizationId,
      interactionType: "email",
      deletedAt: null
    },
    void 0,
    dscope
  );
  const inheritedPersonIdSet = new Set(
    parentInteractions.map((pi) => pi.entity?.id).filter(Boolean)
  );
  if (inheritedPersonIdSet.size === 0) return;
  const visibility = channelUserId ? "private" : "shared";
  const link = _link;
  const inheritedMeta = link.channelMetadata ?? null;
  const subject = typeof inheritedMeta?.subject === "string" ? inheritedMeta.subject : null;
  const occurredAt = link.createdAt instanceof Date ? link.createdAt : /* @__PURE__ */ new Date();
  const providerKey = typeof link.providerKey === "string" ? link.providerKey : null;
  await persistInteractions(
    em,
    inheritedPersonIdSet,
    {
      linkId,
      tenantId,
      organizationId,
      interactionType: "email",
      title: subject,
      body: null,
      authorUserId: channelUserId,
      occurredAt,
      visibility,
      channelProviderKey: providerKey
    }
  );
}
async function persistInteractions(em, personIdSet, data) {
  for (const personId of personIdSet) {
    const rowEm = em.fork();
    const entityRef = rowEm.getReference(CustomerEntity, personId);
    const interaction = rowEm.create(CustomerInteraction, {
      tenantId: data.tenantId,
      organizationId: data.organizationId,
      entity: entityRef,
      interactionType: data.interactionType,
      title: data.title,
      body: data.body,
      authorUserId: data.authorUserId,
      occurredAt: data.occurredAt,
      // Emails are logged AFTER they're sent/received — they are not
      // scheduled work. Without an explicit status the entity default
      // ('planned') combined with a past `occurredAt` makes the activity
      // timeline render the email as "overdue", which is the wrong UX.
      // The canonical terminal-success value is `INTERACTION_STATUS_COMPLETED`
      // ('done'); see `lib/interactionStatus.ts` for the open/terminal semantics.
      status: INTERACTION_STATUS_COMPLETED,
      externalMessageId: data.linkId,
      visibility: data.visibility,
      channelProviderKey: data.channelProviderKey
    });
    try {
      await rowEm.flush();
    } catch (err) {
      const code = err.code;
      if (code !== POSTGRES_UNIQUE_VIOLATION) throw err;
      continue;
    }
    try {
      await emitCustomersEvent("customers.email.linked", {
        personId,
        interactionId: interaction.id,
        tenantId: data.tenantId,
        organizationId: data.organizationId
      });
    } catch {
    }
  }
}
function resolveVisibility(metaJson, channelUserId, direction) {
  if (direction === "outbound") {
    if (metaJson?.crmVisibility === "shared") return "shared";
    if (metaJson?.crmVisibility === "private") return "private";
  }
  return channelUserId ? "private" : "shared";
}
function collectAddressField(out, value) {
  if (!value) return;
  if (typeof value === "string") {
    out.push(value);
    return;
  }
  if (Array.isArray(value)) {
    for (const item of value) {
      if (typeof item === "string") {
        out.push(item);
      } else if (item && typeof item === "object" && typeof item.address === "string") {
        out.push(item.address);
      }
    }
    return;
  }
  if (typeof value === "object" && typeof value.address === "string") {
    out.push(value.address);
  }
}
function stripBrackets(value) {
  const trimmed = value.trim();
  if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
    return trimmed.slice(1, -1);
  }
  return trimmed;
}
export {
  handler as default
};
//# sourceMappingURL=link-channel-message-handler.js.map
