import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { ChannelThreadMapping, ChannelThreadToken } from "../data/entities.js";
import { extractTokenFromBody, extractTokenFromHeaders } from "./thread-token.js";
const SUBJECT_PREFIX_PATTERN = /^\s*((?:re|fwd|fw|aw|wg|sv|tr|antw)\s*[:\-]\s*|\[[^\]]+\]\s*)+/i;
const PARTICIPANT_LOOKBACK_DAYS = 30;
const MAX_REFERENCES_TO_SCAN = 40;
function normalizeSubject(subject) {
  if (typeof subject !== "string") return "";
  let current = subject.trim();
  let safety = 16;
  while (safety > 0 && SUBJECT_PREFIX_PATTERN.test(current)) {
    current = current.replace(SUBJECT_PREFIX_PATTERN, "").trim();
    safety -= 1;
  }
  return current.toLowerCase();
}
async function matchThread(input, deps) {
  const { em, now } = deps;
  const tenantId = input.tenantId;
  const dscope = { tenantId, organizationId: input.organizationId };
  const headerToken = extractTokenFromHeaders(input.inReplyTo, input.references);
  if (headerToken) {
    const threadId = await resolveTokenThread(em, dscope, {
      tenantId,
      channelId: input.channelId,
      token: headerToken,
      now
    });
    if (threadId) {
      return {
        messageThreadId: threadId,
        matchedBy: "token-references",
        confidence: "high"
      };
    }
  }
  const bodyToken = extractTokenFromBody(input.bodyHtml, input.bodyPlain);
  if (bodyToken) {
    const threadId = await resolveTokenThread(em, dscope, {
      tenantId,
      channelId: input.channelId,
      token: bodyToken,
      now
    });
    if (threadId) {
      return {
        messageThreadId: threadId,
        matchedBy: "token-body",
        confidence: "high"
      };
    }
  }
  const candidates = collectReferenceCandidates(input.inReplyTo, input.references);
  if (candidates.length > 0) {
    const jwzMatch = await findThreadByMessageIds(em, {
      tenantId,
      organizationId: input.organizationId,
      channelId: input.channelId,
      messageIds: candidates
    });
    if (jwzMatch) {
      return {
        messageThreadId: jwzMatch,
        matchedBy: "jwz-headers",
        confidence: "medium"
      };
    }
  }
  const normalizedSubject = normalizeSubject(input.subject);
  if (normalizedSubject.length > 0) {
    const cutoff = subtractDays((now ?? (() => /* @__PURE__ */ new Date()))(), PARTICIPANT_LOOKBACK_DAYS);
    const participants = collectParticipants(input);
    if (participants.length > 0) {
      const subjectMatch = await findThreadBySubjectParticipants(em, {
        tenantId,
        organizationId: input.organizationId,
        channelId: input.channelId,
        normalizedSubject,
        participants,
        cutoff
      });
      if (subjectMatch) {
        return {
          messageThreadId: subjectMatch,
          matchedBy: "subject-participants",
          confidence: "low"
        };
      }
    }
  }
  return null;
}
async function resolveTokenThread(em, dscope, args) {
  const row = await findOneWithDecryption(
    em,
    ChannelThreadToken,
    {
      tenantId: args.tenantId,
      organizationId: dscope.organizationId,
      token: args.token
    },
    void 0,
    dscope
  );
  if (!row) return null;
  const mapping = await findOneWithDecryption(
    em,
    ChannelThreadMapping,
    {
      tenantId: args.tenantId,
      organizationId: dscope.organizationId,
      messageThreadId: row.messageThreadId,
      channelId: args.channelId
    },
    void 0,
    dscope
  );
  if (!mapping) return null;
  await em.getConnection().execute(
    `UPDATE channel_thread_tokens
        SET last_seen_at = ?
      WHERE id = ?
        AND tenant_id = ?
        AND ((?::uuid IS NULL AND organization_id IS NULL) OR organization_id = ?::uuid)`,
    [
      (args.now ?? (() => /* @__PURE__ */ new Date()))(),
      row.id,
      args.tenantId,
      dscope.organizationId,
      dscope.organizationId
    ]
  );
  return row.messageThreadId;
}
function collectReferenceCandidates(inReplyTo, references) {
  const out = [];
  const push = (value) => {
    if (typeof value !== "string") return;
    const stripped = value.replace(/^<|>$/g, "").trim();
    if (stripped.length > 0) out.push(stripped);
  };
  push(inReplyTo);
  if (Array.isArray(references)) {
    for (const ref of references) push(ref);
  }
  return Array.from(new Set(out)).slice(0, MAX_REFERENCES_TO_SCAN);
}
async function findThreadByMessageIds(em, args) {
  if (args.messageIds.length === 0) return null;
  const idArray = `{${args.messageIds.map((id) => `"${id.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",")}}`;
  const rows = await em.getConnection().execute(
    `SELECT link.message_id FROM message_channel_links AS link
       INNER JOIN external_conversations AS conv
         ON conv.id = link.external_conversation_id
      WHERE link.tenant_id = ?
        AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)
        AND conv.tenant_id = ?
        AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)
        AND conv.channel_id = ?
        AND link.channel_metadata->>'messageId' = ANY(?::text[])
      LIMIT 1`,
    [
      args.tenantId,
      args.organizationId,
      args.organizationId,
      args.tenantId,
      args.organizationId,
      args.organizationId,
      args.channelId,
      idArray
    ]
  );
  if (!rows || rows.length === 0) return null;
  const messageId = rows[0].message_id;
  const threadRows = await em.getConnection().execute(
    `SELECT thread_id FROM messages
      WHERE id = ?
        AND tenant_id = ?
        AND ((?::uuid IS NULL AND organization_id IS NULL) OR organization_id = ?::uuid)
        AND deleted_at IS NULL
      LIMIT 1`,
    [messageId, args.tenantId, args.organizationId, args.organizationId]
  );
  if (!threadRows || threadRows.length === 0) return null;
  return threadRows[0].thread_id;
}
function collectParticipants(input) {
  const set = /* @__PURE__ */ new Set();
  const push = (value) => {
    if (typeof value !== "string") return;
    const cleaned = value.trim().toLowerCase();
    if (cleaned.length > 0) set.add(cleaned);
  };
  push(input.fromAddress);
  for (const addr of input.toAddresses) push(addr);
  for (const addr of input.ccAddresses) push(addr);
  return Array.from(set);
}
async function findThreadBySubjectParticipants(em, args) {
  const subjectLowerLike = args.normalizedSubject;
  const participantList = `{${args.participants.map((p) => `"${p.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",")}}`;
  const rows = await em.getConnection().execute(
    `SELECT messages.thread_id
       FROM message_channel_links AS link
       INNER JOIN external_conversations AS conv
         ON conv.id = link.external_conversation_id
       INNER JOIN messages
         ON messages.id = link.message_id
         AND messages.tenant_id = link.tenant_id
         AND ((link.organization_id IS NULL AND messages.organization_id IS NULL) OR messages.organization_id = link.organization_id)
         AND messages.deleted_at IS NULL
      WHERE link.tenant_id = ?
        AND ((?::uuid IS NULL AND link.organization_id IS NULL) OR link.organization_id = ?::uuid)
        AND conv.tenant_id = ?
        AND ((?::uuid IS NULL AND conv.organization_id IS NULL) OR conv.organization_id = ?::uuid)
        AND conv.channel_id = ?
        AND link.created_at >= ?
        AND lower(regexp_replace(coalesce(link.channel_metadata->>'subject', ''),
              '^\\s*((re|fwd|fw|aw|wg|sv|tr|antw)\\s*[:\\-]\\s*|\\[[^\\]]+\\]\\s*)+',
              '',
              'i'
            )) = ?
        AND (
          lower(coalesce(link.channel_metadata->>'from', '')) = ANY(?::text[])
          OR EXISTS (
            SELECT 1 FROM jsonb_array_elements_text(coalesce(link.channel_metadata->'to', '[]'::jsonb)) AS t(addr)
              WHERE lower(t.addr) = ANY(?::text[])
          )
          OR EXISTS (
            SELECT 1 FROM jsonb_array_elements_text(coalesce(link.channel_metadata->'cc', '[]'::jsonb)) AS t(addr)
              WHERE lower(t.addr) = ANY(?::text[])
          )
        )
      ORDER BY link.created_at DESC
      LIMIT 1`,
    [
      args.tenantId,
      args.organizationId,
      args.organizationId,
      args.tenantId,
      args.organizationId,
      args.organizationId,
      args.channelId,
      args.cutoff,
      subjectLowerLike,
      participantList,
      participantList,
      participantList
    ]
  );
  if (!rows || rows.length === 0) return null;
  return rows[0].thread_id;
}
function subtractDays(from, days) {
  const next = new Date(from);
  next.setUTCDate(next.getUTCDate() - days);
  return next;
}
export {
  matchThread,
  normalizeSubject
};
//# sourceMappingURL=thread-matcher.js.map
