import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { ClientesService } from 'src/clientes/clientes.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { aE164 } from '../common/telefono.util';
import { ConvertirLeadDto } from './dto/convertir-lead.dto';
import type { CreateClienteDto } from 'src/clientes/dto/create-cliente.dto';
import { CreateLeadDto } from './dto/create-lead.dto';
import { DescartarLeadDto } from './dto/descartar-lead.dto';
import { UpdateLeadDto } from './dto/update-lead.dto';

/** Lo que devuelve `create` cuando el contacto ya existía. */
export type ResultadoDedupe =
  | { duplicado: 'cliente'; cliente_id: string; razon_social: string }
  | { duplicado: 'lead'; lead: unknown };

/**
 * Leads y captura multicanal (M91). Ver docs/spec-crm-360.md §6.2.
 *
 * El lead es el contacto que todavía no compró. Vive en su propia tabla y no en
 * `personas` porque la mayoría no va a comprar nunca y ensuciarían el padrón que
 * usa facturación. Al convertirlo recién ahí nace el cliente.
 */
@Injectable()
export class LeadsService {
  private readonly logger = new Logger(LeadsService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
    private readonly clientesService: ClientesService,
  ) {}

  private normalizarEmail(email?: string | null) {
    return email?.trim().toLowerCase() || null;
  }

  private normalizarRuc(ruc?: string | null) {
    return ruc?.replace(/\D/g, '').trim() || null;
  }

  /**
   * ¿Este contacto ya es cliente? Se mira por RUC y por teléfono normalizado.
   *
   * Si ya es cliente NO se crea lead: la conversación se cuelga del cliente. Un
   * lead paralelo haría que el vendedor le ofrezca crédito a alguien que ya
   * tiene saldo vencido, sin verlo.
   */
  private async clienteExistente(empresa_id: string, ruc: string | null, telefono: string | null) {
    const condiciones: any[] = [];
    if (ruc) condiciones.push({ ruc });
    if (telefono) {
      // El padrón guarda el teléfono sin normalizar, así que se comparan los
      // últimos 8 dígitos, que es la parte estable del número paraguayo.
      const cola = telefono.slice(-8);
      condiciones.push({ celular: { endsWith: cola } }, { telefono: { endsWith: cola } });
    }
    if (!condiciones.length) return null;

    return this.prisma.clientes.findFirst({
      where: { deleted: false, personas: { empresa_id, deleted: false, OR: condiciones } },
      select: { id: true, personas: { select: { razon_social: true } } },
    });
  }

  /**
   * Reparto automático. La spec pide round-robin con un índice rotativo en
   * Redis; se usa "el vendedor con menos leads abiertos", que da el mismo
   * reparto parejo pero sin estado externo: si Redis se reinicia, un índice
   * rotativo vuelve a cero y le carga todo al primero de la lista.
   */
  private async vendedorMenosCargado(empresa_id: string): Promise<string | null> {
    const vendedores = await this.prisma.vendedores_cobradores.findMany({
      where: { empresa_id, active: true, tipo: { in: ['vendedor', 'ambos'] } },
      select: { id: true },
      orderBy: { nombre: 'asc' },
    });
    if (!vendedores.length) return null;

    const cargas = await this.prisma.crm_leads.groupBy({
      by: ['asignado_a'],
      where: { empresa_id, estado: { in: ['nuevo', 'contactado', 'calificado'] }, asignado_a: { not: null } },
      _count: { _all: true },
    });
    const porVendedor = new Map(cargas.map((c) => [c.asignado_a, c._count._all]));

    // Empate: gana el primero alfabéticamente, para que el reparto sea
    // reproducible y los tests no dependan del orden que devuelva la base.
    return vendedores.reduce((mejor, v) =>
      (porVendedor.get(v.id) ?? 0) < (porVendedor.get(mejor.id) ?? 0) ? v : mejor,
    ).id;
  }

  async findAll(
    empresa_id: string,
    filtros: { estado?: string; asignado_a?: string; texto?: string } = {},
  ) {
    const texto = filtros.texto?.trim();
    return this.prisma.crm_leads.findMany({
      where: {
        empresa_id,
        ...(filtros.estado ? { estado: filtros.estado } : {}),
        ...(filtros.asignado_a ? { asignado_a: filtros.asignado_a } : {}),
        ...(texto
          ? {
              OR: [
                { nombre: { contains: texto, mode: 'insensitive' } },
                { empresa_nombre: { contains: texto, mode: 'insensitive' } },
                { email: { contains: texto, mode: 'insensitive' } },
                { telefono_e164: { contains: texto } },
                { ruc: { contains: texto } },
              ],
            }
          : {}),
      },
      include: { crm_origenes: { select: { codigo: true, descripcion: true } } },
      orderBy: { created_at: 'desc' },
    });
  }

  async findOne(id: string, empresa_id: string) {
    const lead = await this.prisma.crm_leads.findFirst({
      where: { id, empresa_id },
      include: { crm_origenes: true, crm_motivos: true },
    });
    if (!lead) throw new NotFoundException('Lead no encontrado');
    return lead;
  }

  /**
   * Alta con deduplicación. Nunca lanza por duplicado: devuelve qué encontró,
   * porque el llamador suele ser un webhook de WhatsApp y un 409 ahí sólo
   * genera reintentos.
   */
  async create(dto: CreateLeadDto, empresa_id: string, user_id?: string) {
    const telefono_e164 = aE164(dto.telefono);
    const email = this.normalizarEmail(dto.email);
    const ruc = this.normalizarRuc(dto.ruc);

    if (dto.telefono && !telefono_e164) {
      throw new BadRequestException(`No se pudo interpretar el teléfono "${dto.telefono}"`);
    }

    const cliente = await this.clienteExistente(empresa_id, ruc, telefono_e164);
    if (cliente) {
      return {
        duplicado: 'cliente' as const,
        cliente_id: cliente.id,
        razon_social: cliente.personas?.razon_social ?? '',
      };
    }

    const abierto = await this.prisma.crm_leads.findFirst({
      where: {
        empresa_id,
        estado: { not: 'descartado' },
        OR: [
          ...(telefono_e164 ? [{ telefono_e164 }] : []),
          ...(ruc ? [{ ruc }] : []),
          ...(email ? [{ email }] : []),
        ],
      },
    });
    if (abierto) return { duplicado: 'lead' as const, lead: abierto };

    const asignado_a = dto.asignado_a ?? (await this.vendedorMenosCargado(empresa_id));

    const lead = await this.prisma.crm_leads.create({
      data: {
        empresa_id,
        sucursal_id: dto.sucursal_id ?? null,
        nombre: dto.nombre.trim(),
        empresa_nombre: dto.empresa_nombre?.trim() || null,
        ruc,
        dv: dto.dv?.trim() || null,
        telefono_e164,
        email,
        origen_id: dto.origen_id ?? null,
        campana: dto.campana?.trim() || null,
        utm: (dto.utm ?? {}) as Prisma.InputJsonValue,
        estado: dto.estado ?? 'nuevo',
        asignado_a,
        datos_extra: (dto.datos_extra ?? {}) as Prisma.InputJsonValue,
        created_by: user_id ?? null,
      },
    });

    await this.auditService.log({
      empresa_id, user_id, action: 'CREAR', entity_type: 'crm_leads', entity_id: lead.id, new_value: lead,
    });
    this.logger.log(`Lead creado: ${lead.nombre} (${lead.id}) asignado=${asignado_a ?? 'sin vendedor'}`);
    return lead;
  }

  async update(id: string, dto: UpdateLeadDto, empresa_id: string, user_id?: string) {
    const anterior = await this.findOne(id, empresa_id);
    if (anterior.estado === 'convertido' || anterior.estado === 'descartado') {
      throw new ConflictException(`El lead está ${anterior.estado} y ya no se edita`);
    }

    const telefono_e164 = dto.telefono !== undefined ? aE164(dto.telefono) : undefined;
    if (dto.telefono && !telefono_e164) {
      throw new BadRequestException(`No se pudo interpretar el teléfono "${dto.telefono}"`);
    }

    const lead = await this.prisma.crm_leads.update({
      where: { id },
      data: {
        ...(dto.nombre !== undefined ? { nombre: dto.nombre.trim() } : {}),
        ...(dto.empresa_nombre !== undefined ? { empresa_nombre: dto.empresa_nombre?.trim() || null } : {}),
        ...(dto.ruc !== undefined ? { ruc: this.normalizarRuc(dto.ruc) } : {}),
        ...(dto.dv !== undefined ? { dv: dto.dv?.trim() || null } : {}),
        ...(telefono_e164 !== undefined ? { telefono_e164 } : {}),
        ...(dto.email !== undefined ? { email: this.normalizarEmail(dto.email) } : {}),
        ...(dto.origen_id !== undefined ? { origen_id: dto.origen_id } : {}),
        ...(dto.campana !== undefined ? { campana: dto.campana?.trim() || null } : {}),
        ...(dto.utm !== undefined ? { utm: dto.utm as Prisma.InputJsonValue } : {}),
        ...(dto.sucursal_id !== undefined ? { sucursal_id: dto.sucursal_id } : {}),
        ...(dto.asignado_a !== undefined ? { asignado_a: dto.asignado_a } : {}),
        ...(dto.datos_extra !== undefined ? { datos_extra: dto.datos_extra as Prisma.InputJsonValue } : {}),
        // El primer paso a "contactado" marca el reloj del SLA de primer contacto.
        ...(dto.estado !== undefined ? { estado: dto.estado } : {}),
        ...(dto.estado === 'contactado' && !anterior.primer_contacto_at
          ? { primer_contacto_at: new Date() }
          : {}),
        updated_at: new Date(),
      },
    });

    await this.auditService.log({
      empresa_id, user_id, action: 'EDITAR', entity_type: 'crm_leads', entity_id: id,
      old_value: anterior, new_value: lead,
    });
    return lead;
  }

  /**
   * Convierte el lead en cliente. El cliente se crea SIEMPRE vía
   * ClientesService: insertarlo directo saltearía los límites del plan, la
   * validación de RUC duplicado y la auditoría.
   *
   * La oportunidad la abre CRM-12; acá sólo queda el cliente vinculado.
   */
  /**
   * SIFEN sólo acepta ciertas combinaciones: un contribuyente opera B2B o B2G, y
   * un no contribuyente, B2C o B2F. Validarlo acá evita crear un cliente que
   * después no se puede facturar — que es como se descubría antes, recién al
   * cerrar la venta, con el cliente ya creado y la oportunidad ya cargada.
   *
   * Si no se manda, se deriva el más común de cada caso.
   */
  private async tipoOperacionValido(ruc: string | null, pedido?: string): Promise<string> {
    const esContribuyente = Boolean(ruc);
    const permitidos = esContribuyente ? [1, 3] : [2, 4];

    if (pedido) {
      const tipo = await this.prisma.tipo_operacion.findUnique({
        where: { id: pedido },
        select: { codigo: true, descripcion: true },
      });
      if (!tipo) throw new BadRequestException('El tipo de operación no existe');
      if (!permitidos.includes(tipo.codigo ?? 0)) {
        throw new BadRequestException(
          `${tipo.descripcion} no sirve para un ${esContribuyente ? 'contribuyente' : 'no contribuyente'}: ` +
            `usá ${esContribuyente ? 'B2B o B2G' : 'B2C o B2F'}`,
        );
      }
      return pedido;
    }

    const codigo = esContribuyente ? 1 : 2; // B2B para empresas, B2C para consumidor final
    const tipo = await this.prisma.tipo_operacion.findFirst({ where: { codigo }, select: { id: true } });
    if (!tipo) {
      throw new BadRequestException(
        'No hay un tipo de operación por defecto en el catálogo: mandá tipo_operacion_id',
      );
    }
    return tipo.id;
  }

  /**
   * Un cliente fiscal necesita datos distintos según sea contribuyente o no, y
   * `PersonasService` los exige uno por uno con un 409 desde el fondo de la
   * llamada. Validar acá permite devolver `campos_faltantes` de una, igual que
   * hace el resto del CRM, para que la pantalla sepa qué pedir.
   */
  private validarDatosFiscales(ruc: string | null, dto: ConvertirLeadDto) {
    const faltantes: string[] = [];
    if (ruc) {
      if (!dto.tipo_contribuyente_id) faltantes.push('tipo_contribuyente_id');
    } else {
      if (!dto.tipo_documento_id) faltantes.push('tipo_documento_id');
      if (!dto.nro_documento) faltantes.push('nro_documento');
    }
    if (faltantes.length) {
      throw new BadRequestException({
        message: ruc
          ? `El lead tiene RUC, así que el cliente es contribuyente y falta: ${faltantes.join(', ')}`
          : `El lead no tiene RUC, así que el cliente no es contribuyente y falta: ${faltantes.join(', ')}`,
        campos_faltantes: faltantes,
      });
    }
  }

  /**
   * Un contribuyente es, por definición, el que tiene RUC. Así que la naturaleza
   * se deriva del RUC y no se le pregunta al usuario algo que ya está implícito
   * en lo que cargó.
   */
  private async naturalezaPorRuc(ruc: string | null): Promise<string> {
    const descripcion = ruc ? 'Contribuyente' : 'no contribuyente';
    const fila = await this.prisma.naturaleza_receptor.findFirst({
      where: { descripcion: { equals: descripcion, mode: 'insensitive' } },
      select: { id: true },
    });
    if (!fila) {
      throw new BadRequestException(
        `No hay una naturaleza de receptor "${descripcion}" en el catálogo: mandá naturaleza_id explícito`,
      );
    }
    return fila.id;
  }

  async convertir(id: string, dto: ConvertirLeadDto, empresa_id: string, user_id?: string) {
    const lead = await this.findOne(id, empresa_id);
    if (lead.estado === 'convertido') throw new ConflictException('El lead ya fue convertido');
    if (lead.estado === 'descartado') throw new ConflictException('El lead está descartado');

    let cliente_id = dto.cliente_id ?? null;

    if (!cliente_id) {
      // `CreateClienteDto` extiende `CreatePersonaDto`, que exige `naturaleza_id`
      // y, según la naturaleza, datos fiscales distintos.
      const naturaleza_id = dto.naturaleza_id ?? (await this.naturalezaPorRuc(lead.ruc));
      this.validarDatosFiscales(lead.ruc, dto);
      const tipo_operacion_id = await this.tipoOperacionValido(lead.ruc, dto.tipo_operacion_id);

      const creado = await this.clientesService.create(
        {
          tipo_operacion_id,
          naturaleza_id,
          razon_social: dto.razon_social?.trim() || lead.empresa_nombre || lead.nombre,
          ruc: lead.ruc ?? undefined,
          // El DV viaja como texto en el lead y como número en la persona.
          dv: lead.dv ? Number(lead.dv) : undefined,
          tipo_contribuyente_id: dto.tipo_contribuyente_id,
          tipo_documento_id: dto.tipo_documento_id,
          nro_documento: dto.nro_documento,
          email: lead.email ?? undefined,
          celular: lead.telefono_e164 ?? undefined,
        } as CreateClienteDto,
        empresa_id,
        user_id,
      );
      cliente_id = (creado as any)?.data?.cliente?.id ?? null;
      if (!cliente_id) throw new BadRequestException('No se pudo crear el cliente a partir del lead');
    } else {
      const existe = await this.prisma.clientes.findFirst({
        where: { id: cliente_id, deleted: false, personas: { empresa_id } },
        select: { id: true },
      });
      if (!existe) throw new NotFoundException('El cliente indicado no existe en esta empresa');
    }

    const actualizado = await this.prisma.crm_leads.update({
      where: { id },
      data: { estado: 'convertido', cliente_id, convertido_at: new Date(), updated_at: new Date() },
    });

    await this.auditService.log({
      empresa_id, user_id, action: 'PROCESAR', entity_type: 'crm_leads', entity_id: id,
      old_value: lead, new_value: actualizado,
    });
    this.logger.log(`Lead convertido: ${lead.nombre} (${id}) → cliente ${cliente_id}`);
    return actualizado;
  }

  /** Descarte con motivo obligatorio: sin el motivo no se aprende por qué se pierden. */
  async descartar(id: string, dto: DescartarLeadDto, empresa_id: string, user_id?: string) {
    const lead = await this.findOne(id, empresa_id);
    if (lead.estado === 'convertido') throw new ConflictException('Un lead convertido no se descarta');

    const motivo = await this.prisma.crm_motivos.findFirst({
      where: { id: dto.motivo_descarte_id, empresa_id, tipo: 'descarte' },
      select: { id: true },
    });
    if (!motivo) throw new BadRequestException('El motivo no existe o no es de tipo "descarte"');

    const actualizado = await this.prisma.crm_leads.update({
      where: { id },
      data: { estado: 'descartado', motivo_descarte_id: dto.motivo_descarte_id, updated_at: new Date() },
    });

    await this.auditService.log({
      empresa_id, user_id, action: 'ELIMINAR', entity_type: 'crm_leads', entity_id: id,
      old_value: lead, new_value: actualizado,
    });
    return actualizado;
  }
}
