/**
 * Public WhatsApp provider callbacks (auth = channel token in the URL).
 * Rate limited, body-capped and schema-checked; payloads are never logged.
 */
import { createHash } from 'node:crypto';
import type { FastifyInstance, FastifyRequest } from 'fastify';
import { channelService } from '../services/channelService.js';
import { messagingService } from '../services/messagingService.js';
import {
  InboundChannelNotFoundError,
  processWhatsAppInbound,
} from '../services/whatsappInboundService.js';
import {
  downloadWebhookMedia,
  handleAfricasTalkingWebhook,
  webhookRetryAfterSeconds,
} from '../services/africasTalkingWebhook.js';

const str = (maxLength: number) => ({ type: 'string', maxLength });

export const WEBHOOK_BODY_LIMIT_BYTES = 1 * 1024 * 1024;

/**
 * Africa's Talking does not guarantee JSON: the same callback can arrive as an HTML form post.
 * Without a parser for it Fastify answers 415, which the provider reads as a broken endpoint.
 *
 * Registered on the caller's scope only, so the rest of the API keeps rejecting form bodies, and
 * built on `URLSearchParams` rather than a plugin — a flat key/value decode needs no dependency.
 */
export function registerWebhookFormBodyParser(scope: FastifyInstance): void {
  scope.addContentTypeParser(
    'application/x-www-form-urlencoded',
    { parseAs: 'string', bodyLimit: WEBHOOK_BODY_LIMIT_BYTES },
    (_req, body, done) => {
      try {
        done(null, Object.fromEntries(new URLSearchParams(body as string)));
      } catch (err) {
        done(err as Error, undefined);
      }
    },
  );
}

/**
 * One budget per channel rather than per caller IP.
 *
 * The provider calls from a small pool of egress addresses shared by every tenant, so an IP key
 * lets one busy channel throttle everyone else's inbound. 600/min is roughly ten messages a second
 * on a single channel — comfortably above real traffic, low enough to bound a runaway loop.
 *
 * The key is a digest, not the token: rate-limit keys end up in store dumps and metrics labels, and
 * the token is the only credential this endpoint has. Hashing keeps the one-key-per-channel
 * behaviour without putting the credential anywhere it could be read back.
 */
export const inboundWhatsappRateLimit = {
  max: 600,
  timeWindow: '1 minute',
  keyGenerator: (req: FastifyRequest): string => {
    const { token } = (req.params ?? {}) as { token?: string };
    return `wa-cb:${createHash('sha256').update(token ?? '').digest('hex')}`;
  },
};

/** Known Africa's Talking fields are type/length checked; unknown extras are left to the mapper. */
const callbackBodySchema = {
  type: 'object',
  properties: {
    waNumber: str(32),
    phoneNumber: str(32),
    from: str(32),
    mobile: str(32),
    messageId: str(256),
    id: str(256),
    type: str(64),
    messageType: str(64),
    mediaType: str(64),
    text: str(16_000),
    message: str(16_000),
    caption: str(16_000),
    mediaUrl: str(2048),
    url: str(2048),
    fileName: str(256),
    filename: str(256),
    body: {
      type: 'object',
      properties: {
        messageId: str(256),
        type: str(64),
        mediaType: str(64),
        message: str(16_000),
        text: str(16_000),
        caption: str(16_000),
        url: str(2048),
        mediaUrl: str(2048),
        fileName: str(256),
      },
    },
  },
};

export async function inboundWhatsappRoutes(fastify: FastifyInstance) {
  registerWebhookFormBodyParser(fastify);

  fastify.post(
    '/inbound/whatsapp/africas-talking/:token',
    {
      config: { rateLimit: inboundWhatsappRateLimit },
      bodyLimit: WEBHOOK_BODY_LIMIT_BYTES,
      schema: {
        params: { type: 'object', required: ['token'], properties: { token: str(64) } },
        body: callbackBodySchema,
      },
    },
    async (req, reply) => {
      const { token } = req.params as { token: string };

      let result: { status: number; body: unknown };
      try {
        result = await handleAfricasTalkingWebhook(token, req.body, {
          findChannel: t => channelService.getWhatsAppByInboundToken(t, 'africas_talking'),
          isDuplicate: waId => messagingService.isDuplicate(waId),
          processInbound: input => processWhatsAppInbound(input, { io: fastify.io, log: fastify.log }),
          downloadMedia: (url, kind) => downloadWebhookMedia(url, kind),
        });
      } catch (err) {
        if (err instanceof InboundChannelNotFoundError) {
          return reply.status(404).send({ error: 'Unknown token' });
        }
        throw err;
      }

      if (result.status >= 400) {
        const { reason } = result.body as { reason?: string };
        fastify.log.warn({ status: result.status, reason }, '[inboundWhatsapp] callback rejected');
      }

      const retryAfter = webhookRetryAfterSeconds(result.body);
      if (retryAfter !== null) reply.header('Retry-After', String(retryAfter));
      return reply.status(result.status).send(result.body);
    },
  );
}
