import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { createHmac } from 'crypto';
import { firmaCoincide } from 'src/common/utils/cifrado.util';
import {
  type CanalWhatsApp, type EnvioResult, type IWhatsAppProvider, type WebhookEvento,
} from './whatsapp-provider.interface';

/** Versión de la Graph API. Subirla es un cambio consciente, no un default flotante. */
export const VERSION_GRAPH = 'v21.0';
const BASE_GRAPH = 'https://graph.facebook.com';

/**
 * Conector oficial: Meta Cloud API. Es el default de producción (spec §6.6.1).
 *
 * No hay SDK: son dos endpoints REST y un webhook firmado. Meter una dependencia
 * para esto sería cargar el árbol entero de `facebook-nodejs-business-sdk` por
 * dos llamadas `fetch`.
 */
@Injectable()
export class MetaCloudProvider implements IWhatsAppProvider {
  readonly tipo = 'meta_cloud' as const;
  private readonly logger = new Logger(MetaCloudProvider.name);

  enviarTexto(canal: CanalWhatsApp, para: string, texto: string): Promise<EnvioResult> {
    return this.postMensaje(canal, {
      messaging_product: 'whatsapp',
      to: para,
      type: 'text',
      text: { body: texto, preview_url: true },
    });
  }

  enviarPlantilla(
    canal: CanalWhatsApp,
    para: string,
    plantilla: string,
    idioma: string,
    variables: string[],
  ): Promise<EnvioResult> {
    return this.postMensaje(canal, {
      messaging_product: 'whatsapp',
      to: para,
      type: 'template',
      template: {
        name: plantilla,
        language: { code: idioma },
        // Meta exige el componente incluso vacío cuando la plantilla no tiene variables.
        ...(variables.length
          ? { components: [{ type: 'body', parameters: variables.map((v) => ({ type: 'text', text: v })) }] }
          : {}),
      },
    });
  }

  enviarAdjunto(
    canal: CanalWhatsApp,
    para: string,
    url: string,
    mime: string,
    descripcion?: string,
  ): Promise<EnvioResult> {
    // Meta discrimina por tipo, no por mime: una imagen y un PDF van en campos distintos.
    const tipo = mime.startsWith('image/') ? 'image' : mime.startsWith('audio/') ? 'audio' : 'document';
    return this.postMensaje(canal, {
      messaging_product: 'whatsapp',
      to: para,
      type: tipo,
      [tipo]: { link: url, ...(descripcion ? { caption: descripcion } : {}) },
    });
  }

  /**
   * Valida `X-Hub-Signature-256` y traduce el payload.
   *
   * La firma se calcula sobre el cuerpo **crudo**: si se recalculara sobre el
   * JSON ya parseado y re-serializado, cualquier diferencia de orden de claves o
   * de escape haría fallar firmas válidas. Por eso llega `cuerpoCrudo`.
   */
  parsearWebhook(
    headers: Record<string, string | undefined>,
    cuerpoCrudo: string,
    canal: CanalWhatsApp,
  ): WebhookEvento[] {
    const secreto = canal.credenciales.app_secret;
    if (!secreto) {
      throw new UnauthorizedException('El canal no tiene app_secret configurado: no se puede validar la firma');
    }

    const recibida = headers['x-hub-signature-256'] ?? headers['X-Hub-Signature-256'];
    if (!recibida) throw new UnauthorizedException('Falta la cabecera X-Hub-Signature-256');

    const esperada = `sha256=${createHmac('sha256', secreto).update(cuerpoCrudo, 'utf8').digest('hex')}`;
    if (!firmaCoincide(recibida, esperada)) {
      throw new UnauthorizedException('Firma del webhook inválida');
    }

    return this.traducir(cuerpoCrudo);
  }

  /** El GET de suscripción: Meta espera el `hub.challenge` en texto plano. */
  static verificarSuscripcion(
    modo: string | undefined,
    token: string | undefined,
    challenge: string | undefined,
    tokenEsperado: string | null | undefined,
  ): string {
    if (modo !== 'subscribe' || !token || !tokenEsperado || !firmaCoincide(token, tokenEsperado)) {
      throw new UnauthorizedException('Verificación de webhook rechazada');
    }
    return challenge ?? '';
  }

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

  private traducir(cuerpoCrudo: string): WebhookEvento[] {
    let cuerpo: MetaWebhook;
    try {
      cuerpo = JSON.parse(cuerpoCrudo) as MetaWebhook;
    } catch {
      // La firma ya cerró, así que esto es un payload mal formado de Meta, no un
      // ataque. Se registra y se descarta: reventar haría que Meta reintente.
      this.logger.warn('Webhook con firma válida pero cuerpo no parseable');
      return [];
    }

    const eventos: WebhookEvento[] = [];
    for (const entrada of cuerpo.entry ?? []) {
      for (const cambio of entrada.changes ?? []) {
        const valor = cambio.value ?? {};
        const externo = valor.metadata?.phone_number_id ?? null;
        const nombrePorTelefono = new Map(
          (valor.contacts ?? []).map((c) => [c.wa_id, c.profile?.name ?? null]),
        );

        for (const m of valor.messages ?? []) {
          const adjunto = m.image ?? m.document ?? m.audio ?? m.video ?? null;
          eventos.push({
            clase: 'mensaje',
            identificador_externo: externo,
            telefono: m.from,
            nombre_contacto: nombrePorTelefono.get(m.from) ?? null,
            id_externo: m.id,
            tipo: m.type === 'text' ? 'texto' : adjunto ? 'adjunto' : 'otro',
            texto: m.text?.body ?? adjunto?.caption ?? null,
            // Meta entrega un media id, no una URL: la descarga es un paso aparte
            // que necesita el token, y la hace la ingesta (CRM-23).
            adjunto_url: adjunto?.id ?? null,
            adjunto_mime: adjunto?.mime_type ?? null,
            fecha: this.aFecha(m.timestamp),
          });
        }

        for (const s of valor.statuses ?? []) {
          eventos.push({
            clase: 'estado',
            identificador_externo: externo,
            id_externo: s.id,
            estado: MAPA_ESTADO[s.status] ?? 'enviado',
            error: s.errors?.[0]?.title ?? null,
            fecha: this.aFecha(s.timestamp),
          });
        }
      }
    }
    return eventos;
  }

  /** Meta manda epoch en segundos, como string. */
  private aFecha(ts: string | undefined): Date {
    const n = Number(ts);
    return Number.isFinite(n) && n > 0 ? new Date(n * 1000) : new Date();
  }

  private async postMensaje(canal: CanalWhatsApp, cuerpo: Record<string, unknown>): Promise<EnvioResult> {
    const { access_token } = canal.credenciales;
    if (!access_token || !canal.identificador_externo) {
      return { id_externo: null, estado: 'rechazado', error: 'El canal no tiene access_token o phone_number_id' };
    }

    const url = `${BASE_GRAPH}/${VERSION_GRAPH}/${canal.identificador_externo}/messages`;
    try {
      const r = await fetch(url, {
        method: 'POST',
        headers: { Authorization: `Bearer ${access_token}`, 'Content-Type': 'application/json' },
        body: JSON.stringify(cuerpo),
      });
      const data = (await r.json()) as MetaRespuestaEnvio;

      if (!r.ok) {
        const error = data?.error?.message ?? `HTTP ${r.status}`;
        this.logger.warn(`Meta rechazó el envío del canal ${canal.id}: ${error}`);
        return { id_externo: null, estado: 'rechazado', error };
      }

      return {
        id_externo: data.messages?.[0]?.id ?? null,
        // Meta acusa recibo, no entrega: el estado real llega por webhook.
        estado: 'encolado',
      };
    } catch (e) {
      const error = e instanceof Error ? e.message : String(e);
      this.logger.error(`No se pudo contactar a Meta para el canal ${canal.id}: ${error}`);
      return { id_externo: null, estado: 'rechazado', error };
    }
  }
}

const MAPA_ESTADO: Record<string, 'enviado' | 'entregado' | 'leido' | 'fallido'> = {
  sent: 'enviado',
  delivered: 'entregado',
  read: 'leido',
  failed: 'fallido',
};

// ── Forma del payload de Meta, sólo lo que se usa ────────────────────────────

interface MetaAdjunto {
  id?: string;
  mime_type?: string;
  caption?: string;
}

interface MetaWebhook {
  entry?: {
    changes?: {
      value?: {
        metadata?: { phone_number_id?: string };
        contacts?: { wa_id: string; profile?: { name?: string } }[];
        messages?: {
          from: string;
          id: string;
          timestamp?: string;
          type?: string;
          text?: { body?: string };
          image?: MetaAdjunto;
          document?: MetaAdjunto;
          audio?: MetaAdjunto;
          video?: MetaAdjunto;
        }[];
        statuses?: {
          id: string;
          status: string;
          timestamp?: string;
          errors?: { title?: string }[];
        }[];
      };
    }[];
  }[];
}

interface MetaRespuestaEnvio {
  messages?: { id?: string }[];
  error?: { message?: string };
}
