import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { CanalesService } from './canales.service';
import { WhatsAppProviderFactory } from './whatsapp/whatsapp-provider.factory';
import {
  BaileysAutomaticoBloqueadoError, type CategoriaPlantilla, type EnvioResult,
  permiteAutomaticos,
} from './whatsapp/whatsapp-provider.interface';

/**
 * Salida de mensajes (ticket CRM-22). Todo lo que el CRM envía pasa por acá.
 *
 * La distinción que importa es **manual vs automático**, no quién llama:
 *
 * - **Manual**: una persona escribiendo en la bandeja. Se permite en cualquier
 *   proveedor, incluido Baileys.
 * - **Automático**: KuDE recurrente, dunning, campañas, agente IA. Se **bloquea**
 *   en Baileys (regla D4, spec §6.6.1): es protocolo no oficial y un envío masivo
 *   es la forma más rápida de que Meta bloquee el número del cliente para siempre.
 *
 * El bloqueo vive acá y no en el conector porque el conector no sabe si del otro
 * lado hay una persona tipeando o un cron.
 */
@Injectable()
export class MensajeriaService {
  private readonly logger = new Logger(MensajeriaService.name);

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

  /** Envío de una persona. Permitido en cualquier proveedor. */
  async enviarManual(canal_id: string, para: string, texto: string): Promise<EnvioResult> {
    const canal = await this.canales.paraProveedor(canal_id);
    const r = await this.factory.para(canal.proveedor).enviarTexto(canal, para, texto);
    await this.registrarResultado(canal_id, r);
    return r;
  }

  /**
   * Envío disparado por el sistema. Lanza `BaileysAutomaticoBloqueadoError` si el
   * canal es Baileys — a propósito como excepción propia y no un
   * `BadRequestException` genérico, para que quien lo llame desde un job pueda
   * distinguir "no se puede por el proveedor" de "falló el envío".
   */
  async enviarAutomatico(canal_id: string, para: string, texto: string): Promise<EnvioResult> {
    const canal = await this.canales.paraProveedor(canal_id);
    if (!permiteAutomaticos(canal.proveedor)) {
      throw new BaileysAutomaticoBloqueadoError(canal.identificador ?? canal_id);
    }
    const r = await this.factory.para(canal.proveedor).enviarTexto(canal, para, texto);
    await this.registrarResultado(canal_id, r);
    return r;
  }

  /**
   * Plantilla aprobada. Las plantillas sólo existen en los conectores oficiales,
   * así que esto es siempre automático por definición.
   */
  async enviarPlantilla(
    canal_id: string,
    para: string,
    plantilla: string,
    idioma: string,
    variables: string[] = [],
  ): Promise<EnvioResult> {
    const canal = await this.canales.paraProveedor(canal_id);
    if (!permiteAutomaticos(canal.proveedor)) {
      throw new BaileysAutomaticoBloqueadoError(canal.identificador ?? canal_id);
    }
    const r = await this.factory
      .para(canal.proveedor)
      .enviarPlantilla(canal, para, plantilla, idioma, variables);
    await this.registrarResultado(canal_id, r);
    return r;
  }

  async enviarAdjunto(
    canal_id: string,
    para: string,
    url: string,
    mime: string,
    descripcion?: string,
    automatico = false,
  ): Promise<EnvioResult> {
    const canal = await this.canales.paraProveedor(canal_id);
    if (automatico && !permiteAutomaticos(canal.proveedor)) {
      throw new BaileysAutomaticoBloqueadoError(canal.identificador ?? canal_id);
    }
    const r = await this.factory
      .para(canal.proveedor)
      .enviarAdjunto(canal, para, url, mime, descripcion);
    await this.registrarResultado(canal_id, r);
    return r;
  }

  /** Sólo Baileys: la sesión por QR se cae y hay que volver a vincular. */
  async estadoSesion(canal_id: string) {
    const canal = await this.canales.paraProveedor(canal_id);
    const proveedor = this.factory.para(canal.proveedor);
    if (!proveedor.estadoSesion) {
      return { soportado: false as const, estado: null };
    }
    return { soportado: true as const, estado: await proveedor.estadoSesion(canal) };
  }

  /**
   * Costo estimado de un mensaje según la tarifa vigente de Meta.
   *
   * Devuelve `null` cuando no hay tarifa cargada para ese país y categoría, en
   * vez de asumir cero: un costo de 0 en pantalla es peor que "no sé", porque
   * invita a mandar campañas creyendo que son gratis. Las tarifas las publica
   * Meta y se cargan por `PUT /crm/canales/tarifas`.
   *
   * Desde el 01/10/2026 Meta también cobra las respuestas dentro de la ventana
   * de 24 h, así que la categoría `service` ya no es gratuita.
   */
  async estimarCosto(
    pais: string,
    categoria: CategoriaPlantilla,
    fecha: Date = new Date(),
  ): Promise<{ precio_usd: number; vigente_desde: Date } | null> {
    const tarifa = await this.prisma.crm_tarifas_whatsapp.findFirst({
      where: { pais: pais.toUpperCase(), categoria, vigente_desde: { lte: fecha } },
      orderBy: { vigente_desde: 'desc' },
    });
    if (!tarifa) return null;
    return { precio_usd: Number(tarifa.precio_usd), vigente_desde: tarifa.vigente_desde };
  }

  /** Carga o actualiza una tarifa publicada por Meta. */
  async upsertTarifa(pais: string, categoria: CategoriaPlantilla, precio_usd: number, vigente_desde: string) {
    if (precio_usd < 0) throw new BadRequestException('El precio no puede ser negativo');
    const clave = {
      pais: pais.toUpperCase(),
      categoria,
      vigente_desde: new Date(vigente_desde),
    };
    return this.prisma.crm_tarifas_whatsapp.upsert({
      where: { pais_categoria_vigente_desde: clave },
      create: { ...clave, precio_usd },
      update: { precio_usd },
    });
  }

  listarTarifas(pais?: string) {
    return this.prisma.crm_tarifas_whatsapp.findMany({
      where: pais ? { pais: pais.toUpperCase() } : {},
      orderBy: [{ pais: 'asc' }, { categoria: 'asc' }, { vigente_desde: 'desc' }],
    });
  }

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

  /**
   * Un rechazo del proveedor no es una excepción: el mensaje no salió pero la
   * conversación sigue viva y el agente tiene que poder reintentar. Queda en el
   * canal para que la pantalla muestre por qué.
   */
  private async registrarResultado(canal_id: string, r: EnvioResult) {
    if (r.estado === 'rechazado') {
      this.logger.warn(`Envío rechazado en el canal ${canal_id}: ${r.error ?? 'sin detalle'}`);
      await this.canales.marcarEstado(canal_id, false, r.error);
    } else {
      await this.canales.marcarEstado(canal_id, true);
    }
  }
}
