import { InjectQueue } from '@nestjs/bullmq';
import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { Queue } from 'bullmq';
import { PrismaService } from '../../prisma/prisma.service';
import { CarteraConfigService } from '../config/cartera-config.service';
import { CrearCampanaDto, FiltrosCampanaDto } from '../dto/campana.dto';
import { CarteraImportadaFuente } from '../fuentes/cartera-importada-fuente.service';
import { ErpNovasisFuente } from '../fuentes/erp-novasis-fuente.service';
import { WebhookService } from '../webhooks/webhook.service';

export const CAMPANAS_QUEUE = 'cartera-campanas';

export interface CampanaJob {
  envio_id: string;
}

interface ClienteContacto {
  cliente_id: string;
  cliente_nombre: string;
  telefono: string | null;
  documento: string;
  saldo: number;
  moneda: string;
  dias_mora: number;
  fecha_vencimiento: Date | null;
}

@Injectable()
export class CampanasService {
  private readonly logger = new Logger(CampanasService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly carteraConfig: CarteraConfigService,
    private readonly erpNovasisFuente: ErpNovasisFuente,
    private readonly carteraImportadaFuente: CarteraImportadaFuente,
    private readonly webhooks: WebhookService,
    @InjectQueue(CAMPANAS_QUEUE) private readonly queue: Queue<CampanaJob>,
  ) {}

  async preview(empresa_id: string, filtros: FiltrosCampanaDto) {
    const contactos = await this.cargarContactos(empresa_id, filtros);
    const con_telefono = contactos.filter((c) => c.telefono);
    return {
      total_elegibles: contactos.length,
      con_telefono: con_telefono.length,
      sin_telefono: contactos.length - con_telefono.length,
      muestra: con_telefono.slice(0, 5).map((c) => ({
        cliente_nombre: c.cliente_nombre,
        telefono: this.maskPhone(c.telefono),
        saldo: c.saldo,
        moneda: c.moneda,
        dias_mora: c.dias_mora,
      })),
    };
  }

  async crear(empresa_id: string, usuario_id: string, dto: CrearCampanaDto) {
    const contactos = await this.cargarContactos(empresa_id, dto.filtros ?? {});
    const elegibles = contactos.filter((c) => c.telefono);
    if (elegibles.length === 0) {
      throw new BadRequestException(
        'Ningún cliente elegible tiene teléfono configurado. Ajustá filtros o cargá teléfonos en los clientes.',
      );
    }

    const campana = await this.prisma.cartera_campanas.create({
      data: {
        empresa_id,
        nombre: dto.nombre,
        canal: dto.canal,
        mensaje_template: dto.mensaje_template,
        filtros: (dto.filtros ?? {}) as unknown as Prisma.InputJsonValue,
        total_clientes: elegibles.length,
        estado: 'enviando',
        creado_por: usuario_id,
      },
    });

    // Crear envíos pendientes en batch + encolar BullMQ jobs.
    for (const c of elegibles) {
      const mensaje = this.renderizar(dto.mensaje_template, c);
      const envio = await this.prisma.cartera_campana_envios.create({
        data: {
          campana_id: campana.id,
          cliente_id: c.cliente_id,
          cliente_nombre: c.cliente_nombre,
          telefono: this.normalizarTelefono(c.telefono!, dto.canal),
          mensaje_renderizado: mensaje,
        },
      });
      try {
        await this.queue.add(
          'enviar',
          { envio_id: envio.id },
          {
            attempts: 3,
            backoff: { type: 'exponential', delay: 30_000 },
            removeOnComplete: 1000,
            removeOnFail: 5000,
          },
        );
      } catch (err: any) {
        this.logger.error(`No se pudo encolar envío ${envio.id}: ${err?.message ?? err}`);
      }
    }

    return campana;
  }

  async listar(empresa_id: string) {
    return this.prisma.cartera_campanas.findMany({
      where: { empresa_id },
      orderBy: { created_at: 'desc' },
      take: 50,
    });
  }

  async detalle(empresa_id: string, id: string) {
    const campana = await this.prisma.cartera_campanas.findFirst({
      where: { id, empresa_id },
    });
    if (!campana) throw new NotFoundException('Campaña no encontrada');
    const envios = await this.prisma.cartera_campana_envios.findMany({
      where: { campana_id: id },
      orderBy: { created_at: 'asc' },
      take: 200,
    });
    return { ...campana, envios };
  }

  async cancelar(empresa_id: string, id: string) {
    const campana = await this.prisma.cartera_campanas.findFirst({
      where: { id, empresa_id },
    });
    if (!campana) throw new NotFoundException('Campaña no encontrada');
    if (campana.estado === 'completada' || campana.estado === 'cancelada') {
      throw new BadRequestException(`Campaña ya está en estado ${campana.estado}`);
    }
    await this.prisma.cartera_campana_envios.updateMany({
      where: { campana_id: id, estado: 'pendiente' },
      data: { estado: 'cancelado' },
    });
    return this.prisma.cartera_campanas.update({
      where: { id },
      data: { estado: 'cancelada', finished_at: new Date() },
    });
  }

  /**
   * Cuando un worker termina su job, decrementa contadores y, si era el último,
   * marca la campaña como completada + emite webhook.
   */
  async tickAvance(envio_id: string, exito: boolean): Promise<void> {
    const envio = await this.prisma.cartera_campana_envios.findUnique({
      where: { id: envio_id },
      select: { campana_id: true },
    });
    if (!envio) return;
    await this.prisma.cartera_campanas.update({
      where: { id: envio.campana_id },
      data: {
        enviados: exito ? { increment: 1 } : undefined,
        fallidos: exito ? undefined : { increment: 1 },
      },
    });
    const restantes = await this.prisma.cartera_campana_envios.count({
      where: { campana_id: envio.campana_id, estado: 'pendiente' },
    });
    if (restantes === 0) {
      const campana = await this.prisma.cartera_campanas.update({
        where: { id: envio.campana_id },
        data: { estado: 'completada', finished_at: new Date() },
      });
      void this.webhooks
        .publicar(campana.empresa_id, 'campana.enviada', {
          campana_id: campana.id,
          nombre: campana.nombre,
          canal: campana.canal,
          total_clientes: campana.total_clientes,
          enviados: campana.enviados,
          fallidos: campana.fallidos,
          finished_at: campana.finished_at,
        })
        .catch(() => {});
    }
  }

  // ─── Helpers ─────────────────────────────────────────────────────────────

  private async cargarContactos(
    empresa_id: string,
    filtros: FiltrosCampanaDto,
  ): Promise<ClienteContacto[]> {
    const config = await this.carteraConfig.getOrCreate(empresa_id);
    const fuente =
      config.fuente_default === 'cartera-importada'
        ? this.carteraImportadaFuente
        : this.erpNovasisFuente;

    const cuentas = await fuente.listarCuentasPendientes({
      empresa_id,
      cobrador_id: filtros.cobrador_id,
      page: 1,
      page_size: 1000,
    });

    let elegibles = cuentas;
    if (filtros.min_dias_mora !== undefined) {
      elegibles = elegibles.filter((c) => c.dias_mora >= filtros.min_dias_mora!);
    }
    if (filtros.max_dias_mora !== undefined) {
      elegibles = elegibles.filter((c) => c.dias_mora <= filtros.max_dias_mora!);
    }
    if (filtros.search) {
      const s = filtros.search.toLowerCase().trim();
      elegibles = elegibles.filter((c) =>
        c.cliente_nombre.toLowerCase().includes(s),
      );
    }

    // Una entrada por cliente (consolidamos su mejor cuenta).
    const porCliente = new Map<string, ClienteContacto>();
    for (const c of elegibles) {
      const existing = porCliente.get(c.cliente_id);
      if (!existing || c.saldo_pendiente > existing.saldo) {
        porCliente.set(c.cliente_id, {
          cliente_id: c.cliente_id,
          cliente_nombre: c.cliente_nombre,
          telefono: null,
          documento: c.documento_numero,
          saldo: Number(c.saldo_pendiente),
          moneda: c.moneda_codigo,
          dias_mora: c.dias_mora,
          fecha_vencimiento: c.fecha_vencimiento,
        });
      }
    }

    // Hidratar teléfonos según la fuente.
    if (config.fuente_default === 'cartera-importada') {
      const ids = [...porCliente.keys()];
      const docs = await this.prisma.cartera_importada_documentos.findMany({
        where: { empresa_id, cliente_externo_id: { in: ids } },
        select: { cliente_externo_id: true, cliente_telefono: true },
        distinct: ['cliente_externo_id'],
      });
      for (const d of docs) {
        const c = porCliente.get(d.cliente_externo_id);
        if (c) c.telefono = d.cliente_telefono ?? null;
      }
    } else {
      const ids = [...porCliente.keys()];
      const clientes = await this.prisma.clientes.findMany({
        where: { id: { in: ids } },
        select: {
          id: true,
          personas: { select: { telefono: true, celular: true } },
        },
      });
      for (const cl of clientes) {
        const c = porCliente.get(cl.id);
        if (c) c.telefono = cl.personas?.celular ?? cl.personas?.telefono ?? null;
      }
    }

    return [...porCliente.values()];
  }

  private renderizar(template: string, c: ClienteContacto): string {
    const monto = `${c.moneda} ${Math.round(c.saldo).toLocaleString('es-PY')}`;
    return template
      .replace(/\{\{\s*nombre\s*\}\}/gi, c.cliente_nombre)
      .replace(/\{\{\s*saldo\s*\}\}/gi, monto)
      .replace(/\{\{\s*documento\s*\}\}/gi, c.documento)
      .replace(/\{\{\s*dias_mora\s*\}\}/gi, String(c.dias_mora))
      .replace(
        /\{\{\s*vencimiento\s*\}\}/gi,
        c.fecha_vencimiento ? new Date(c.fecha_vencimiento).toLocaleDateString('es-PY') : '—',
      )
      .slice(0, 1500);
  }

  private normalizarTelefono(raw: string, canal: 'SMS' | 'WHATSAPP'): string {
    let limpio = raw.replace(/[^\d+]/g, '');
    if (!limpio.startsWith('+')) {
      // Heurística simple: Paraguay = +595 si arranca con 0 o 9.
      if (limpio.startsWith('0')) limpio = '+595' + limpio.slice(1);
      else if (limpio.startsWith('595')) limpio = '+' + limpio;
      else if (limpio.startsWith('9')) limpio = '+595' + limpio;
      else limpio = '+' + limpio;
    }
    return canal === 'WHATSAPP' ? `whatsapp:${limpio}` : limpio;
  }

  private maskPhone(t: string | null): string {
    if (!t) return '—';
    if (t.length < 4) return t;
    return t.slice(0, -4).replace(/\d/g, '•') + t.slice(-4);
  }
}
