import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { CommunicationChannel } from "../data/entities.js";
import {
  COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID
} from "../commands/ingest-inbound-message.js";
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from "../lib/queue.js";
import { preservePushState } from "../lib/push-state.js";
import { writeIngestDeadLetter } from "../lib/dead-letter.js";
import { classifyOutboundError, computeBackoffMs, isReauthError } from "../lib/error-classification.js";
import { refreshCredentialsIfNeeded } from "../lib/credential-refresh.js";
import { emitCommunicationChannelsEvent } from "../events.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("communication_channels").child({ component: "poll-channel" });
const POLL_CHANNEL_MAX_ATTEMPTS = 3;
const MAX_DRAIN_PAGES = 100;
const metadata = {
  queue: COMMUNICATION_CHANNELS_QUEUES.poll,
  id: "communication_channels:poll-channel",
  concurrency: 10
};
async function handle(job, ctx) {
  const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload;
  const em = ctx.resolve("em").fork();
  const adapterRegistry = ctx.resolve("channelAdapterRegistry");
  const channel = await findOneWithDecryption(
    em,
    CommunicationChannel,
    {
      id: channelId,
      tenantId: scope.tenantId,
      organizationId: scope.organizationId ?? null,
      deletedAt: null
    },
    void 0,
    scope
  );
  if (!channel) {
    logger.warn("channel not found (skipping)", { channelId });
    return;
  }
  if (!channel.isActive) return;
  if (channel.status !== "connected" && channel.status !== "error") return;
  const adapter = adapterRegistry?.get(channel.providerKey);
  if (!adapter) {
    logger.warn("no adapter for provider", { providerKey: channel.providerKey, channelId });
    return;
  }
  const capabilities = channel.capabilities ?? null;
  if (capabilities?.realtimePush !== false) {
    return;
  }
  if (typeof adapter.fetchHistory !== "function") {
    return;
  }
  let credentialsService = null;
  try {
    credentialsService = ctx.resolve("integrationCredentialsService");
  } catch {
    credentialsService = null;
  }
  const credentialsScope = {
    tenantId: scope.tenantId,
    organizationId: scope.organizationId ?? scope.tenantId,
    userId: channel.userId ?? null
  };
  let credentials = {};
  if (channel.credentialsRef && credentialsService) {
    try {
      credentials = await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope) ?? {};
    } catch {
      credentials = {};
    }
  }
  const refreshed = await refreshCredentialsIfNeeded(
    {
      adapter,
      channelId: channel.id,
      credentials,
      scope: credentialsScope
    },
    { credentialsService }
  );
  credentials = refreshed.credentials;
  let normalized = [];
  let nextCursor;
  let hasMore = false;
  try {
    const result = await adapter.fetchHistory({
      conversationId: channel.externalIdentifier ?? channel.id,
      credentials,
      cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : void 0,
      channelState: channel.channelState ?? void 0,
      scope: {
        tenantId: scope.tenantId,
        organizationId: scope.organizationId ?? scope.tenantId
      },
      // `contactFilter.sinceDays` is a hint to provider adapters about how far
      // back to look on first-poll bootstrap. For UID-incremental polls (every
      // tick after the first), adapters ignore this and just fetch new mail
      // since the persisted cursor.
      contactFilter: { addresses: [], sinceDays: 7 }
    });
    normalized = Array.isArray(result?.messages) ? result.messages : [];
    nextCursor = result?.nextCursor;
    hasMore = result?.hasMore === true;
  } catch (err) {
    await handlePollError(err, em, channel, scope, attempt, job.payload);
    return;
  }
  const commandBus = ctx.resolve("commandBus");
  const containerProxy = { resolve: ctx.resolve.bind(ctx) };
  const commandCtx = {
    container: containerProxy,
    auth: null,
    organizationScope: null,
    selectedOrganizationId: scope.organizationId ?? null,
    organizationIds: scope.organizationId ? [scope.organizationId] : null
  };
  let transientIngestAbort = false;
  for (const message of normalized) {
    try {
      const input = {
        channelId: channel.id,
        providerKey: channel.providerKey,
        channelType: channel.channelType,
        scope: {
          tenantId: scope.tenantId,
          organizationId: scope.organizationId ?? null
        },
        message
      };
      await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {
        input,
        ctx: commandCtx
      });
    } catch (err) {
      const classification = classifyOutboundError(err);
      if (classification.transient) {
        logger.warn("transient ingest failure; aborting page so cursor is not advanced", { channelId: channel.id, reason: classification.message });
        transientIngestAbort = true;
        break;
      }
      await writeIngestDeadLetter({
        em,
        scope,
        channel,
        message,
        err,
        errorMessage: classification.message
      });
      logger.warn("permanent ingest failure; recorded in dead-letter and advancing cursor", { channelId: channel.id, externalMessageId: message.externalMessageId, reason: classification.message });
    }
  }
  if (transientIngestAbort) {
    channel.lastError = "transient_ingest_failure";
    await em.flush();
    return;
  }
  channel.lastPolledAt = /* @__PURE__ */ new Date();
  if (channel.lastError) channel.lastError = null;
  if (channel.status === "error") {
    channel.status = "connected";
  }
  if (typeof nextCursor === "string" && nextCursor.length > 0) {
    const decoded = decodeChannelStateCursor(nextCursor);
    if (decoded) {
      channel.channelState = preservePushState(channel.channelState, decoded);
    }
  }
  await em.flush();
  if (hasMore) {
    if (drainPage < MAX_DRAIN_PAGES) {
      const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll);
      await queue.enqueue(
        { channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 },
        { delayMs: 250 }
      );
    } else {
      logger.warn("drain page cap reached; stopping re-enqueue until the next scheduled tick", { maxDrainPages: MAX_DRAIN_PAGES, channelId: channel.id });
    }
  }
}
function decodeChannelStateCursor(cursor) {
  try {
    const decoded = Buffer.from(cursor, "base64").toString("utf8");
    const parsed = JSON.parse(decoded);
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
      return parsed;
    }
    return null;
  } catch {
    return null;
  }
}
async function handlePollError(err, em, channel, scope, attempt, payload) {
  const classification = classifyOutboundError(err);
  channel.lastError = classification.message;
  if (isReauthError(classification)) {
    channel.status = "requires_reauth";
    await em.flush();
    await emitCommunicationChannelsEvent(
      "communication_channels.channel.requires_reauth",
      {
        channelId: channel.id,
        providerKey: channel.providerKey,
        channelType: channel.channelType,
        reason: classification.message,
        tenantId: scope.tenantId,
        organizationId: scope.organizationId ?? null
      },
      { persistent: true }
    );
    return;
  }
  if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {
    await em.flush();
    const next = { ...payload, attempt: attempt + 1 };
    const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll);
    await queue.enqueue(next, {
      delayMs: computeBackoffMs(attempt)
    });
    return;
  }
  channel.status = "error";
  await em.flush();
}
export {
  POLL_CHANNEL_MAX_ATTEMPTS,
  handle as default,
  metadata
};
//# sourceMappingURL=poll-channel.js.map
