import { Controller, Get, Headers, HttpCode, Logger, Param, Post, Query, Req } from '@nestjs/common';
import { ApiExcludeController } from '@nestjs/swagger';
import type { Request } from 'express';
import { CanalesService } from './canales.service';
import { MetaCloudProvider } from './whatsapp/meta-cloud.provider';
import { WhatsAppProviderFactory } from './whatsapp/whatsapp-provider.factory';
import type { WebhookEvento } from './whatsapp/whatsapp-provider.interface';

/**
 * Webhooks entrantes de WhatsApp (ticket CRM-22).
 *
 * Controller aparte y **sin guards a propósito**: lo llama Meta, que no tiene
 * token JWT ni app key. El origen se valida con la firma HMAC del payload, igual
 * que hace el webhook de Novasis Pay. Separarlo del controller de canales es lo
 * que evita perforar los guards del módulo.
 *
 * **Siempre responde 200**, incluso cuando algo falla aguas abajo. Meta reintenta
 * ante cualquier otra respuesta y, con varios reintentos, un error nuestro se
 * convierte en mensajes duplicados. Lo que no se pudo procesar queda en el log.
 * La excepción es la firma inválida: ahí sí corta antes de tocar nada.
 */
@ApiExcludeController()
@Controller({ path: 'crm/webhooks/whatsapp', version: '1' })
export class CrmWebhooksController {
  private readonly logger = new Logger(CrmWebhooksController.name);

  constructor(
    private readonly canales: CanalesService,
    private readonly factory: WhatsAppProviderFactory,
  ) {}

  /**
   * Handshake de suscripción de Meta. Espera el `hub.challenge` **en texto
   * plano**: si se devolviera un JSON, Meta rechaza la suscripción.
   */
  @Get('meta')
  async verificarMeta(
    @Query('hub.mode') modo: string,
    @Query('hub.verify_token') token: string,
    @Query('hub.challenge') challenge: string,
  ): Promise<string> {
    const canal = token ? await this.canales.porVerifyToken(token) : null;
    const resultado = MetaCloudProvider.verificarSuscripcion(
      modo,
      token,
      challenge,
      canal?.webhook_verify_token,
    );
    this.logger.log(`Suscripción de webhook verificada para el canal ${canal?.id}`);
    return resultado;
  }

  @Post('meta')
  @HttpCode(200)
  async recibirMeta(@Req() req: Request, @Headers() headers: Record<string, string>) {
    const crudo = (req as unknown as { rawBody?: string }).rawBody ?? '';
    // Meta manda un POST por app, no por número: hay que mirar el payload para
    // saber a qué canal pertenece. Se lee sin confiar —sólo para rutear— y la
    // firma se valida después, con el app_secret de ESE canal.
    const canalId = this.canalDelPayloadMeta(crudo);
    if (!canalId) {
      this.logger.warn('Webhook de Meta sin phone_number_id reconocible: se descarta');
      return { recibido: true };
    }

    const canal = await this.canales.porIdentificadorExterno(canalId);
    if (!canal) {
      this.logger.warn(`Webhook de Meta para un phone_number_id sin canal activo: ${canalId}`);
      return { recibido: true };
    }

    // Si la firma no cierra, esto lanza y responde 401: es lo único que no se
    // perdona, porque procesar un payload no firmado es inyectar mensajes.
    const eventos = this.factory.para(canal.proveedor).parsearWebhook(headers, crudo, canal);
    return this.procesar(canal.id, eventos);
  }

  /**
   * Evolution no firma: el secreto es la URL, que lleva el id del canal. No es
   * equivalente a la firma de Meta y por eso este modo es sólo para pruebas.
   */
  @Post('evolution/:canalId')
  @HttpCode(200)
  async recibirEvolution(
    @Param('canalId') canalId: string,
    @Req() req: Request,
    @Headers() headers: Record<string, string>,
  ) {
    const crudo = (req as unknown as { rawBody?: string }).rawBody ?? '';
    let canal;
    try {
      canal = await this.canales.paraProveedor(canalId);
    } catch {
      this.logger.warn(`Webhook de Evolution para un canal inexistente: ${canalId}`);
      return { recibido: true };
    }

    const eventos = this.factory.para(canal.proveedor).parsearWebhook(headers, crudo, canal);
    return this.procesar(canal.id, eventos);
  }

  // ---------------------------------------------------------------------------

  /**
   * Por ahora sólo cuenta y registra: la ingesta —crear lead, abrir conversación,
   * guardar el mensaje, avisar a la bandeja— es CRM-23, que es el ticket
   * siguiente. Dejarlo así es deliberado: el webhook ya se puede suscribir y
   * verificar de punta a punta antes de que exista la bandeja.
   */
  private procesar(canal_id: string, eventos: WebhookEvento[]) {
    const mensajes = eventos.filter((e) => e.clase === 'mensaje').length;
    const estados = eventos.filter((e) => e.clase === 'estado').length;
    if (eventos.length) {
      this.logger.log(`Webhook del canal ${canal_id}: ${mensajes} mensaje(s), ${estados} acuse(s)`);
    }
    return { recibido: true, mensajes, estados };
  }

  /** Lectura mínima y defensiva: sólo para saber a qué canal rutear. */
  private canalDelPayloadMeta(crudo: string): string | null {
    try {
      const cuerpo = JSON.parse(crudo) as {
        entry?: { changes?: { value?: { metadata?: { phone_number_id?: string } } }[] }[];
      };
      for (const entrada of cuerpo.entry ?? []) {
        for (const cambio of entrada.changes ?? []) {
          const id = cambio.value?.metadata?.phone_number_id;
          if (id) return id;
        }
      }
    } catch {
      return null;
    }
    return null;
  }
}
