import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { CobResultadoGestion, Prisma } from '@prisma/client';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import {
  AnularGestionDto,
  CreateGestionDto,
  QueryGestionesDto,
  UpdateGestionDto,
} from './dto/gestiones.dto';

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

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

  private async resolverCobradorId(empresaId: string, userId: string, cobradorIdDto?: string) {
    if (cobradorIdDto) return cobradorIdDto;
    const vc = await this.prisma.vendedores_cobradores.findFirst({
      where: { usuario_id: userId, empresa_id: empresaId, active: true },
      select: { id: true },
    });
    return vc?.id ?? null;
  }

  async create(dto: CreateGestionDto, empresaId: string, userId: string) {
    const fecha = dto.fecha ? new Date(dto.fecha) : new Date();
    if (isNaN(fecha.getTime())) throw new BadRequestException('Fecha inválida');

    const cliente = await this.prisma.clientes.findFirst({
      where: { id: dto.cliente_id },
      select: { id: true },
    });
    if (!cliente) throw new NotFoundException('Cliente no encontrado');

    if (dto.factura_cab_id) {
      const factura = await this.prisma.factura_cab.findFirst({
        where: { id: dto.factura_cab_id, empresa_id: empresaId, cliente_id: dto.cliente_id },
        select: { id: true },
      });
      if (!factura) throw new BadRequestException('Factura no pertenece al cliente o empresa');
    }

    const cobradorId = await this.resolverCobradorId(empresaId, userId, dto.cobrador_id);

    const gestion = await this.prisma.$transaction(async (tx) => {
      let promesaCreadaId: string | null = null;

      if (dto.resultado === CobResultadoGestion.PROMESA_PAGO) {
        if (!dto.promesa_fecha) {
          throw new BadRequestException('Resultado PROMESA_PAGO requiere promesa_fecha');
        }
        const fechaProm = new Date(dto.promesa_fecha);
        if (isNaN(fechaProm.getTime())) {
          throw new BadRequestException('promesa_fecha inválida');
        }
        const recordatorio = new Date(fechaProm);
        recordatorio.setDate(recordatorio.getDate() - 1);

        const tipoEvidencia = (dto.promesa_tipo_evidencia ?? 'VERBAL') as any;
        const pagadorTipo = (dto.promesa_pagador_tipo ?? 'CLIENTE') as any;
        if (pagadorTipo !== 'CLIENTE' && !dto.promesa_pagador_cliente_id) {
          throw new BadRequestException(
            'Si el pagador no es CLIENTE, promesa_pagador_cliente_id es requerido',
          );
        }
        const notas = dto.promesa_notas ?? dto.observacion ?? null;

        // ── Modo enriquecido: una promesa por cuota seleccionada, con prorrateo ──
        if (dto.promesa_cuota_ids && dto.promesa_cuota_ids.length > 0) {
          const cuotas = await tx.factura_cuotas.findMany({
            where: {
              id: { in: dto.promesa_cuota_ids },
              factura_cab: { empresa_id: empresaId, cliente_id: dto.cliente_id },
            },
            select: {
              id: true,
              factura_cab_id: true,
              saldo_pendiente: true,
              dmoncuota: true,
            },
          });
          if (cuotas.length !== dto.promesa_cuota_ids.length) {
            throw new BadRequestException(
              'Alguna cuota no existe o no pertenece al cliente/empresa',
            );
          }

          const saldos = cuotas.map((c) =>
            Number(c.saldo_pendiente ?? c.dmoncuota ?? 0),
          );
          const saldoTotal = saldos.reduce((a, b) => a + b, 0);
          const montoUsuario =
            dto.promesa_monto != null && Number(dto.promesa_monto) > 0
              ? Number(dto.promesa_monto)
              : saldoTotal;
          const factor = saldoTotal > 0 ? montoUsuario / saldoTotal : 0;

          const promesasIds: string[] = [];
          for (let i = 0; i < cuotas.length; i++) {
            const c = cuotas[i];
            const montoCuota = Math.round(saldos[i] * factor);
            const p = await tx.promesas_pago.create({
              data: {
                empresa_id: empresaId,
                factura_cab_id: c.factura_cab_id,
                cliente_id: dto.cliente_id,
                cobrador_id: cobradorId ?? null,
                cuota_id: c.id,
                fecha_prometida: fechaProm,
                monto_prometido: new Prisma.Decimal(montoCuota),
                estado: 'pendiente',
                notas,
                fecha_recordatorio: recordatorio,
                creado_por: userId,
                tipo_evidencia: tipoEvidencia,
                pagador_tipo: pagadorTipo,
                pagador_cliente_id:
                  pagadorTipo !== 'CLIENTE' ? dto.promesa_pagador_cliente_id : null,
              },
              select: { id: true },
            });
            promesasIds.push(p.id);
          }
          // Vinculamos la primera promesa al campo `promesa_creada_id` de la gestión
          // para preservar compatibilidad con el listado actual. Las demás quedan
          // accesibles por cliente_id + creado_por + created_at.
          promesaCreadaId = promesasIds[0] ?? null;
        } else {
          // ── Modo simple (legacy): una sola promesa con fecha + monto ─────────
          if (!dto.factura_cab_id) {
            throw new BadRequestException(
              'Resultado PROMESA_PAGO requiere factura_cab_id o promesa_cuota_ids',
            );
          }
          const promesa = await tx.promesas_pago.create({
            data: {
              empresa_id: empresaId,
              factura_cab_id: dto.factura_cab_id,
              cliente_id: dto.cliente_id,
              cobrador_id: cobradorId ?? null,
              fecha_prometida: fechaProm,
              monto_prometido: dto.promesa_monto ?? null,
              estado: 'pendiente',
              notas,
              fecha_recordatorio: recordatorio,
              creado_por: userId,
              tipo_evidencia: tipoEvidencia,
              pagador_tipo: pagadorTipo,
              pagador_cliente_id:
                pagadorTipo !== 'CLIENTE' ? dto.promesa_pagador_cliente_id ?? null : null,
            },
            select: { id: true },
          });
          promesaCreadaId = promesa.id;
        }
      }

      return tx.cob_gestion.create({
        data: {
          empresa_id: empresaId,
          cliente_id: dto.cliente_id,
          factura_cab_id: dto.factura_cab_id ?? null,
          cobrador_id: cobradorId,
          fecha,
          tipo_gestion: dto.tipo_gestion,
          resultado: dto.resultado,
          observacion: dto.observacion ?? null,
          proxima_accion: dto.proxima_accion ?? null,
          proxima_fecha: dto.proxima_fecha ? new Date(dto.proxima_fecha) : null,
          geo_lat: dto.geo_lat != null ? new Prisma.Decimal(dto.geo_lat) : null,
          geo_lng: dto.geo_lng != null ? new Prisma.Decimal(dto.geo_lng) : null,
          duracion_seg: dto.duracion_seg ?? null,
          adjuntos: dto.adjuntos ?? Prisma.JsonNull,
          promesa_creada_id: promesaCreadaId,
          usuario_id: userId,
        },
      });
    });

    try {
      await this.auditService.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'CREATE',
        entity_type: 'cob_gestion',
        entity_id: gestion.id,
        descripcion: `Gestión ${dto.tipo_gestion}/${dto.resultado} | Cliente ${dto.cliente_id}${dto.factura_cab_id ? ' | Factura ' + dto.factura_cab_id : ''}`,
      });
    } catch (e) {
      this.logger.warn(`Audit log falló: ${(e as Error).message}`);
    }

    return gestion;
  }

  async findAll(empresaId: string, query: QueryGestionesDto) {
    const where: Prisma.cob_gestionWhereInput = {
      empresa_id: empresaId,
      ...(query.clienteId ? { cliente_id: query.clienteId } : {}),
      ...(query.cobradorId ? { cobrador_id: query.cobradorId } : {}),
      ...(query.facturaId ? { factura_cab_id: query.facturaId } : {}),
      ...(query.resultado ? { resultado: query.resultado } : {}),
      ...(query.tipo_gestion ? { tipo_gestion: query.tipo_gestion } : {}),
      ...(query.incluir_anuladas ? {} : { anulada: false }),
    };

    if (query.desde || query.hasta) {
      where.fecha = {
        ...(query.desde ? { gte: new Date(query.desde) } : {}),
        ...(query.hasta ? { lte: new Date(query.hasta + 'T23:59:59.999Z') } : {}),
      };
    }

    return this.prisma.cob_gestion.findMany({
      where,
      orderBy: { fecha: 'desc' },
      include: {
        cliente: {
          select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true } } },
        },
        factura_cab: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } },
      },
      take: 500,
    });
  }

  async getTimeline(clienteId: string, empresaId: string) {
    const cliente = await this.prisma.clientes.findFirst({
      where: { id: clienteId },
      select: { id: true },
    });
    if (!cliente) throw new NotFoundException('Cliente no encontrado');

    const gestiones = await this.prisma.cob_gestion.findMany({
      where: { empresa_id: empresaId, cliente_id: clienteId, anulada: false },
      orderBy: { fecha: 'desc' },
      include: {
        factura_cab: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } },
      },
      take: 200,
    });

    // cob_gestion no tiene relación nombrada a usuario en el schema; resolvemos
    // los nombres de quien registró cada gestión con una query separada.
    const usuarioIds = Array.from(
      new Set(gestiones.map((g) => g.usuario_id).filter(Boolean) as string[]),
    );
    const usuarios = usuarioIds.length
      ? await this.prisma.usuario.findMany({
          where: { id: { in: usuarioIds } },
          select: { id: true, nombres: true, apellidos: true, username: true },
        })
      : [];
    const usuarioMap = new Map(usuarios.map((u) => [u.id, u]));

    return gestiones.map((g) => {
      const u = usuarioMap.get(g.usuario_id);
      const usuario_nombre = u
        ? [u.nombres, u.apellidos].filter(Boolean).join(' ').trim() || u.username
        : null;
      return { ...g, usuario_nombre };
    });
  }

  async update(id: string, dto: UpdateGestionDto, empresaId: string, userId: string) {
    const gestion = await this.prisma.cob_gestion.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!gestion) throw new NotFoundException('Gestión no encontrada');
    if (gestion.anulada) throw new BadRequestException('Gestión anulada, no se puede editar');

    // Solo el autor puede editar en el mismo día (después: supervisor)
    const mismoDia =
      gestion.created_at.toISOString().slice(0, 10) === new Date().toISOString().slice(0, 10);
    if (gestion.usuario_id !== userId && mismoDia) {
      throw new ForbiddenException('Solo el autor puede editar dentro del mismo día');
    }

    const updated = await this.prisma.cob_gestion.update({
      where: { id },
      data: {
        observacion: dto.observacion ?? gestion.observacion,
        proxima_accion: dto.proxima_accion ?? gestion.proxima_accion,
        proxima_fecha: dto.proxima_fecha ? new Date(dto.proxima_fecha) : gestion.proxima_fecha,
        resultado: dto.resultado ?? gestion.resultado,
        tipo_gestion: dto.tipo_gestion ?? gestion.tipo_gestion,
      },
    });

    try {
      await this.auditService.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'UPDATE',
        entity_type: 'cob_gestion',
        entity_id: id,
        descripcion: `Gestión editada`,
      });
    } catch (e) {
      this.logger.warn(`Audit log falló: ${(e as Error).message}`);
    }

    return updated;
  }

  /**
   * Descarta el agendamiento de una gestión (saca `proxima_fecha` + `proxima_accion`
   * sin anular la gestión). Sirve para sacar entradas de "Mi agenda" cuando el
   * seguimiento ya no es necesario y no se va a registrar una nueva gestión. La
   * gestión original queda intacta en el timeline del cliente.
   */
  async descartarAgenda(id: string, empresaId: string, userId: string) {
    const gestion = await this.prisma.cob_gestion.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!gestion) throw new NotFoundException('Gestión no encontrada');
    if (gestion.anulada) throw new BadRequestException('Gestión anulada, no se puede modificar');
    if (!gestion.proxima_fecha) {
      throw new BadRequestException('La gestión no tiene agendamiento que descartar');
    }

    const updated = await this.prisma.cob_gestion.update({
      where: { id },
      data: { proxima_fecha: null, proxima_accion: null, updated_at: new Date() },
    });

    try {
      await this.auditService.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'UPDATE',
        entity_type: 'cob_gestion',
        entity_id: id,
        descripcion: 'Descartó agendamiento (próxima acción / fecha)',
      });
    } catch (_) {
      /* audit opcional */
    }

    return updated;
  }

  async anular(id: string, dto: AnularGestionDto, empresaId: string, userId: string) {
    const gestion = await this.prisma.cob_gestion.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!gestion) throw new NotFoundException('Gestión no encontrada');
    if (gestion.anulada) throw new BadRequestException('Gestión ya anulada');
    if (!dto.motivo?.trim()) throw new BadRequestException('Motivo requerido');

    const updated = await this.prisma.cob_gestion.update({
      where: { id },
      data: {
        anulada: true,
        motivo_anulacion: dto.motivo,
        anulada_at: new Date(),
        anulada_por_id: userId,
      },
    });

    try {
      await this.auditService.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'DELETE',
        entity_type: 'cob_gestion',
        entity_id: id,
        descripcion: `Gestión anulada | Motivo: ${dto.motivo}`,
      });
    } catch (e) {
      this.logger.warn(`Audit log falló: ${(e as Error).message}`);
    }

    return updated;
  }

  /**
   * Agenda de la gestora/usuario actual (o de toda la oficina si scope=oficina).
   * Devuelve gestiones con próxima_acción agendada, agrupadas en:
   *   - vencidas: proxima_fecha < hoy
   *   - hoy: proxima_fecha = hoy
   *   - proximas: hoy < proxima_fecha <= hoy + 7 días
   *
   * Por defecto filtra por usuario_id del que registró la gestión (típico de gestoras
   * de oficina que no son cobradoras). Con scope=oficina ignora el filtro de usuario,
   * útil para supervisor.
   *
   * Excluye gestiones cuyo cliente ya tiene una gestión posterior (asume seguimiento
   * cumplido).
   */
  async miAgenda(empresaId: string, userId: string, scope: 'mias' | 'oficina' = 'mias') {
    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);
    const finHoy = new Date(hoy);
    finHoy.setHours(23, 59, 59, 999);
    const fin7d = new Date(hoy);
    fin7d.setDate(fin7d.getDate() + 7);
    fin7d.setHours(23, 59, 59, 999);

    const baseWhere: Prisma.cob_gestionWhereInput = {
      empresa_id: empresaId,
      anulada: false,
      proxima_fecha: { not: null, lte: fin7d },
      ...(scope === 'mias' ? { usuario_id: userId } : {}),
    };

    const gestiones = await this.prisma.cob_gestion.findMany({
      where: baseWhere,
      orderBy: { proxima_fecha: 'asc' },
      include: {
        cliente: {
          select: {
            id: true,
            nombre_fantasia: true,
            personas: { select: { razon_social: true, ruc: true } },
          },
        },
        factura_cab: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } },
      },
      take: 500,
    });

    // cob_gestion no tiene relación nombrada a usuario en el schema, así que resolvemos
    // los nombres con una query separada (típicamente pocos usuarios distintos).
    const usuarioIds = Array.from(
      new Set(gestiones.map((g) => g.usuario_id).filter(Boolean) as string[]),
    );
    const usuarios = usuarioIds.length
      ? await this.prisma.usuario.findMany({
          where: { id: { in: usuarioIds } },
          select: { id: true, nombres: true, apellidos: true, username: true },
        })
      : [];
    const usuarioMap = new Map(usuarios.map((u) => [u.id, u]));

    // Filtro de "ya hay una gestión posterior" — se considera resuelto el seguimiento.
    const clienteIds = Array.from(new Set(gestiones.map((g) => g.cliente_id)));
    const ultimaPorCliente = new Map<string, Date>();
    if (clienteIds.length) {
      const ultimas = await this.prisma.cob_gestion.groupBy({
        by: ['cliente_id'],
        where: { empresa_id: empresaId, cliente_id: { in: clienteIds }, anulada: false },
        _max: { fecha: true },
      });
      for (const u of ultimas) {
        if (u._max.fecha) ultimaPorCliente.set(u.cliente_id, u._max.fecha);
      }
    }

    const vencidas: any[] = [];
    const hoyArr: any[] = [];
    const proximas: any[] = [];

    for (const g of gestiones) {
      if (!g.proxima_fecha) continue;
      // Si hubo una gestión posterior al registro de esta agenda → ya se atendió, salteamos.
      const ultima = ultimaPorCliente.get(g.cliente_id);
      if (ultima && ultima > g.fecha) continue;

      const f = new Date(g.proxima_fecha);
      f.setHours(0, 0, 0, 0);
      const u = usuarioMap.get(g.usuario_id);
      const nombreUsuario = u
        ? [u.nombres, u.apellidos].filter(Boolean).join(' ').trim() || u.username
        : null;
      const item = {
        id: g.id,
        cliente_id: g.cliente_id,
        cliente_razon_social:
          g.cliente?.personas?.razon_social ?? g.cliente?.nombre_fantasia ?? '—',
        cliente_ruc: g.cliente?.personas?.ruc ?? null,
        factura: g.factura_cab
          ? `${g.factura_cab.dest}-${g.factura_cab.dpunexp}-${g.factura_cab.dnumdoc}`
          : null,
        proxima_accion: g.proxima_accion,
        proxima_fecha: g.proxima_fecha,
        observacion: g.observacion,
        fecha_registro: g.fecha,
        usuario_nombre: nombreUsuario,
      };
      if (f < hoy) vencidas.push(item);
      else if (f.getTime() === hoy.getTime()) hoyArr.push(item);
      else proximas.push(item);
    }

    return {
      vencidas,
      hoy: hoyArr,
      proximas,
      totales: {
        vencidas: vencidas.length,
        hoy: hoyArr.length,
        proximas: proximas.length,
        urgentes: vencidas.length + hoyArr.length, // para el badge
      },
    };
  }

  async getProximasAcciones(cobradorId: string, empresaId: string) {
    const hoy = new Date();
    hoy.setHours(0, 0, 0, 0);
    return this.prisma.cob_gestion.findMany({
      where: {
        empresa_id: empresaId,
        cobrador_id: cobradorId,
        anulada: false,
        proxima_fecha: { gte: hoy },
      },
      orderBy: { proxima_fecha: 'asc' },
      include: {
        cliente: {
          select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true } } },
        },
        factura_cab: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } },
      },
      take: 100,
    });
  }

  /**
   * Reporte de productividad por cobrador en un rango de fechas.
   * Cuenta gestiones por tipo y resultado, agrupado por cobrador.
   */
  async getReporteProductividad(empresaId: string, desde: string, hasta: string) {
    if (!desde || !hasta) throw new BadRequestException('desde y hasta son requeridos');
    const fechaDesde = new Date(desde);
    const fechaHasta = new Date(hasta + 'T23:59:59.999Z');

    const gestiones = await this.prisma.cob_gestion.findMany({
      where: {
        empresa_id: empresaId,
        anulada: false,
        fecha: { gte: fechaDesde, lte: fechaHasta },
        cobrador_id: { not: null },
      },
      select: {
        cobrador_id: true,
        tipo_gestion: true,
        resultado: true,
      },
    });

    const cobradorIds = Array.from(new Set(gestiones.map((g) => g.cobrador_id!).filter(Boolean)));
    const cobradores = await this.prisma.vendedores_cobradores.findMany({
      where: { id: { in: cobradorIds } },
      select: { id: true, nombre: true, apellido: true },
    });
    const nombreMap = new Map(
      cobradores.map((c) => [c.id, `${c.nombre ?? ''} ${c.apellido ?? ''}`.trim()]),
    );

    type Row = {
      cobrador_id: string;
      cobrador: string;
      total: number;
      por_tipo: Record<string, number>;
      por_resultado: Record<string, number>;
      efectivas: number;
      tasa_efectividad: number;
    };
    const efectivos: CobResultadoGestion[] = [
      CobResultadoGestion.COBRADO_TOTAL,
      CobResultadoGestion.COBRADO_PARCIAL,
      CobResultadoGestion.PROMESA_PAGO,
    ];

    const map = new Map<string, Row>();
    for (const g of gestiones) {
      const key = g.cobrador_id!;
      if (!map.has(key)) {
        map.set(key, {
          cobrador_id: key,
          cobrador: nombreMap.get(key) ?? 'Sin nombre',
          total: 0,
          por_tipo: {},
          por_resultado: {},
          efectivas: 0,
          tasa_efectividad: 0,
        });
      }
      const row = map.get(key)!;
      row.total += 1;
      row.por_tipo[g.tipo_gestion] = (row.por_tipo[g.tipo_gestion] ?? 0) + 1;
      row.por_resultado[g.resultado] = (row.por_resultado[g.resultado] ?? 0) + 1;
      if (efectivos.includes(g.resultado)) row.efectivas += 1;
    }
    for (const row of map.values()) {
      row.tasa_efectividad = row.total > 0 ? +((row.efectivas * 100) / row.total).toFixed(2) : 0;
    }

    return Array.from(map.values()).sort((a, b) => b.total - a.total);
  }
}
