import { NextResponse } from "next/server";
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
import { CommunicationChannel } from "../../../../data/entities.js";
import { getChannelAdapterRegistry } from "../../../../lib/adapter-registry-singleton.js";
import {
  COMMUNICATION_CHANNELS_QUEUES,
  getCommunicationChannelsQueue
} from "../../../../lib/queue.js";
import { createLogger } from "@open-mercato/shared/lib/logger";
const logger = createLogger("communication_channels").child({ component: "inbound-webhook" });
const metadata = {
  path: "/communication_channels/webhook/[provider]",
  POST: {
    requireAuth: false,
    // Unauthenticated by design (signature verification IS the auth), but the
    // handler fans out an O(N) cross-tenant candidate scan, so bound per-IP
    // request volume to limit abuse. Generous enough for real provider traffic;
    // the dedicated gmail route carries its own matching limits.
    rateLimit: { points: 120, duration: 60, keyPrefix: "cc_webhook_inbound" }
  }
};
async function POST(req, { params }) {
  const resolvedParams = await params;
  const providerKey = resolvedParams.provider;
  const registry = getChannelAdapterRegistry();
  const adapter = registry?.get(providerKey);
  if (!adapter) {
    return NextResponse.json(
      { error: `No ChannelAdapter for provider: ${providerKey}` },
      { status: 404 }
    );
  }
  const rawBody = await req.text();
  const headers = {};
  req.headers.forEach((value, key) => {
    headers[key] = value;
  });
  const container = await createRequestContainer();
  const em = container.resolve("em").fork();
  let credentialsService = null;
  try {
    credentialsService = container.resolve("integrationCredentialsService");
  } catch {
    credentialsService = null;
  }
  try {
    const candidates = await findWithDecryption(
      em,
      CommunicationChannel,
      {
        providerKey,
        isActive: true,
        deletedAt: null
      },
      { orderBy: { createdAt: "desc" } }
    );
    let matchedChannel = null;
    let matchedScope = null;
    let event = null;
    let lastVerificationError = null;
    for (const candidate of candidates) {
      const candidateScope = {
        tenantId: candidate.tenantId,
        organizationId: candidate.organizationId ?? candidate.tenantId
      };
      const credentialsLookupScope = {
        ...candidateScope,
        userId: candidate.userId ?? null
      };
      let credentials = {};
      if (candidate.credentialsRef && credentialsService) {
        try {
          credentials = await credentialsService.resolve(`channel_${providerKey}`, credentialsLookupScope) ?? {};
        } catch {
          credentials = {};
        }
      }
      try {
        event = await adapter.verifyWebhook({
          rawBody,
          headers,
          credentials,
          scope: candidateScope
        });
        matchedChannel = candidate;
        matchedScope = candidateScope;
        break;
      } catch (error) {
        lastVerificationError = error;
      }
    }
    if (!event || !matchedChannel || !matchedScope) {
      throw lastVerificationError ?? new Error("Webhook verification failed: no matching channel");
    }
    if (event.eventType === "reaction") {
      if (typeof adapter.normalizeInboundReaction !== "function") {
        return NextResponse.json(
          {
            received: true,
            queued: false,
            reason: `adapter '${providerKey}' does not implement normalizeInboundReaction`
          },
          { status: 202 }
        );
      }
      const reactionEvent = await adapter.normalizeInboundReaction(event);
      const reactionJob = {
        kind: "inbound",
        providerKey,
        channelId: matchedChannel.id,
        channelType: matchedChannel.channelType,
        event: reactionEvent,
        // Use the channel's REAL org (null when null), matching the poll and
        // dedicated gmail webhook path — `candidateScope` falls org
        // back to tenantId for credential/verify lookups, which must not leak
        // into the ingest scope (it would diverge dedup for null-org channels).
        scope: { tenantId: matchedScope.tenantId, organizationId: matchedChannel.organizationId ?? null },
        attempt: 1
      };
      const reactionsQueue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.reactions);
      await reactionsQueue.enqueue(reactionJob);
      return NextResponse.json({ received: true, queued: true, kind: "reaction" }, { status: 202 });
    }
    if (event.eventType && event.eventType !== "message") {
      return NextResponse.json(
        {
          received: true,
          queued: false,
          reason: `event ${event.eventType} not yet handled`
        },
        { status: 202 }
      );
    }
    const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.inbound);
    const jobPayload = {
      providerKey,
      channelId: matchedChannel.id,
      channelType: matchedChannel.channelType,
      raw: event,
      // Ingest scope uses the channel's REAL org (null when null) so dedup matches
      // the poll + dedicated webhook paths; see the reaction branch above.
      scope: { tenantId: matchedScope.tenantId, organizationId: matchedChannel.organizationId ?? null }
    };
    await queue.enqueue(jobPayload);
    return NextResponse.json({ received: true, queued: true, kind: "message" }, { status: 202 });
  } catch (error) {
    logger.warn(
      "inbound webhook verification failed",
      { err: error }
    );
    return NextResponse.json({ error: "verification_failed" }, { status: 401 });
  }
}
const openApi = {
  tags: ["CommunicationChannels"],
  summary: "Receive a communication channel webhook",
  methods: {
    POST: {
      summary: "Process an inbound channel webhook (Slack, WhatsApp, Email, ...)",
      tags: ["CommunicationChannels"],
      responses: [
        { status: 202, description: "Webhook accepted for async processing" },
        { status: 401, description: "Signature verification failed against every candidate channel" },
        { status: 404, description: "Unknown provider" }
      ]
    }
  }
};
var route_default = POST;
export {
  POST,
  route_default as default,
  metadata,
  openApi
};
//# sourceMappingURL=route.js.map
