import { BadRequestException, Injectable, NotFoundException, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { envs } from 'src/config/envs';
import { PrismaService } from 'src/prisma/prisma.service';
import { formatDateResponse } from 'src/utils/utilidades';
import { CreateSolicitudCreditoDto, CreateSolicitudCreditoItemDto } from './dto/create-solicitud-credito.dto';
import { QuerySolicitudesCreditoDto } from './dto/query-solicitudes-credito.dto';
import { UpdateSolicitudCreditoDto } from './dto/update-solicitud-credito.dto';
import { logoEmisor, nombreFantasiaEmisor } from 'src/utils/emisor-comprobante';

export enum EstadoSolicitud {
  BORRADOR = 'borrador',
  PENDIENTE_APROBACION = 'pendiente_aprobacion',
  APROBADA = 'aprobada',
  RECHAZADA = 'rechazada',
  FACTURADA = 'facturada',
  CANCELADA = 'cancelada',
}

@Injectable()
export class SolicitudesCreditoService {
  private readonly logger = new Logger(SolicitudesCreditoService.name);
  constructor(
    private readonly prisma: PrismaService,
  ) {}

  // ═══════════ CRUD ═══════════

  async create(dto: CreateSolicitudCreditoDto, empresa_id: string, user_id: string) {
    const cliente = await this.prisma.clientes.findFirst({
      where: { id: dto.cliente_id, deleted: false, personas: { empresa_id } },
      include: { personas: { select: { razon_social: true } } },
    });
    if (!cliente) throw new NotFoundException('Cliente no encontrado o no pertenece a su empresa');
    const clienteNombre = cliente.personas?.razon_social || cliente.nombre_fantasia || 'N/A';

    // Validación de política quincenal: si vienen los dos días, deben ser distintos
    // y con separación razonable (>=5 días considerando roll-over de mes).
    if (dto.intervalo_dias === 15 && dto.dia_fijo_pago_1 != null && dto.dia_fijo_pago_2 != null) {
      if (dto.dia_fijo_pago_1 === dto.dia_fijo_pago_2) {
        throw new BadRequestException(
          'Los dos días de pago quincenal no pueden ser iguales.',
        );
      }
      const a = Math.min(dto.dia_fijo_pago_1, dto.dia_fijo_pago_2);
      const b = Math.max(dto.dia_fijo_pago_1, dto.dia_fijo_pago_2);
      const sep = Math.min(b - a, 30 - b + a);
      if (sep < 5) {
        throw new BadRequestException(
          `Los días de pago quincenal están muy cerca (${sep} días). Quincenal requiere ~15 días entre pagos.`,
        );
      }
    }

    const solicitud = await this.prisma.$transaction(async (tx) => {
      const created = await tx.solicitud_credito.create({
        data: {
          empresa_id,
          cliente_id: dto.cliente_id,
          sucursal_id: dto.sucursal_id ?? null,
          vendedor_id: dto.vendedor_id ?? null,
          cobrador_id: dto.cobrador_id ?? null,
          condicion_pago_id: dto.condicion_pago_id ?? null,
          plan_cuota_id: dto.plan_cuota_id ?? null,
          punto_expedicion_id: dto.punto_expedicion_id ?? null,
          numeracion_id: dto.numeracion_id ?? null,
          moneda: dto.moneda || 'PYG',
          intervalo_dias: dto.intervalo_dias ?? 30,
          cuota_inicial_total: dto.cuota_inicial_total ?? 0,
          cant_cuotas_total: dto.cant_cuotas ?? null,
          monto_total: dto.monto_total ?? 0,
          saldo_financiar: dto.saldo_financiar ?? 0,
          fecha_primera_cuota: dto.fecha_primera_cuota ? new Date(dto.fecha_primera_cuota) : null,
          dia_fijo_pago_1: dto.dia_fijo_pago_1 ?? null,
          dia_fijo_pago_2: dto.dia_fijo_pago_2 ?? null,
          dia_cobro_semana: dto.dia_cobro_semana ?? null,
          dias_gracia_override: dto.dias_gracia_override ?? null,
          cronograma: dto.cronograma ? JSON.parse(JSON.stringify(dto.cronograma)) : null,
          observaciones: dto.observaciones ?? null,
          estado: dto.estado === 'borrador' ? EstadoSolicitud.BORRADOR : EstadoSolicitud.PENDIENTE_APROBACION,
          creado_por: user_id,
        },
      });

      // Asignar número secuencial si se crea como pendiente de aprobación
      const esConfirmada = created.estado === EstadoSolicitud.PENDIENTE_APROBACION;
      let numeroSolicitud: string | null = null;
      if (esConfirmada) {
        numeroSolicitud = await this.reservarNumeroSolicitud(tx, empresa_id, created.numeracion_id);
        await tx.solicitud_credito.update({
          where: { id: created.id },
          data: { numero_solicitud: numeroSolicitud },
        });
      }

      await this.logHistorial(
        tx, created.id,
        esConfirmada ? 'enviado_aprobacion' : 'creado',
        user_id,
        esConfirmada
          ? `Solicitud ${numeroSolicitud} creada y enviada a aprobación para ${clienteNombre}`
          : `Solicitud creada como borrador para ${clienteNombre}`,
        { cliente_id: dto.cliente_id, numero_solicitud: numeroSolicitud },
      );

      if (dto.items?.length) {
        for (const item of dto.items) {
          await this.addItemInternal(tx, created.id, item, empresa_id);
        }
        await this.recalcularTotales(tx, created.id);
      }

      return created;
    });

    this.logger.log(`Solicitud crédito creada: ${solicitud.id}`, 'SolicitudesCreditoService');
    return {
      message: 'Solicitud de crédito creada exitosamente',
      data: await this.findOneInternal(solicitud.id),
    };
  }

  async findAll(query: QuerySolicitudesCreditoDto, empresa_id: string) {
    const page = parseInt(query.page || '1', 10);
    const limit = parseInt(query.limit || '20', 10);
    const where: Prisma.solicitud_creditoWhereInput = { empresa_id, active: true };

    if (query.estado) where.estado = query.estado;
    if (query.cliente_id) where.cliente_id = query.cliente_id;
    if (query.vendedor_id) where.vendedor_id = query.vendedor_id;
    if (query.sucursal_id) where.sucursal_id = query.sucursal_id;

    if (query.search) {
      where.OR = [
        { numero_solicitud: { contains: query.search, mode: 'insensitive' } },
        { cliente: { personas: { razon_social: { contains: query.search, mode: 'insensitive' } } } },
        { cliente: { personas: { ruc: { contains: query.search, mode: 'insensitive' } } } },
      ];
    }

    if (query.fecha_desde || query.fecha_hasta) {
      where.fecha_solicitud = {};
      if (query.fecha_desde) where.fecha_solicitud.gte = new Date(query.fecha_desde + 'T00:00:00');
      if (query.fecha_hasta) {
        // Sumar 1 día para cubrir timezone (la fecha se envía en hora local)
        const hasta = new Date(query.fecha_hasta + 'T00:00:00');
        hasta.setDate(hasta.getDate() + 1);
        where.fecha_solicitud.lt = hasta;
      }
    }

    const [data, total] = await Promise.all([
      this.prisma.solicitud_credito.findMany({
        where,
        include: {
          cliente: {
            select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true, ruc: true } } },
          },
          vendedor: { select: { id: true, nombre: true, apellido: true } },
          condicion_pago: { select: { id: true, descripcion: true } },
          creador: { select: { id: true, username: true, nombres: true, apellidos: true } },
          _count: { select: { detalle: true } },
        },
        orderBy: { created_at: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.solicitud_credito.count({ where }),
    ]);

    const dataFormat = data.map((item) => ({
      ...item,
      fecha_solicitud: formatDateResponse(item.fecha_solicitud),
      fecha_aprobacion: formatDateResponse(item.fecha_aprobacion),
    }));

    return { dataFormat, pagination: { total, page, limit, totalPages: Math.ceil(total / limit) } };
  }

  async findOne(id: string, empresa_id: string) {
    const solicitud = await this.findOneInternal(id);
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('La solicitud no pertenece a su empresa');
    return solicitud;
  }

  async update(id: string, dto: UpdateSolicitudCreditoDto, empresa_id: string, user_id: string) {
    await this.assertEditable(id, empresa_id);

    if (dto.cliente_id) {
      const cl = await this.prisma.clientes.findFirst({
        where: { id: dto.cliente_id, deleted: false, personas: { empresa_id } },
      });
      if (!cl) throw new NotFoundException('Cliente no encontrado');
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.solicitud_credito.update({
        where: { id },
        data: {
          ...(dto.cliente_id && { cliente_id: dto.cliente_id }),
          ...(dto.sucursal_id !== undefined && { sucursal_id: dto.sucursal_id || null }),
          ...(dto.vendedor_id !== undefined && { vendedor_id: dto.vendedor_id || null }),
          ...(dto.cobrador_id !== undefined && { cobrador_id: dto.cobrador_id || null }),
          ...(dto.condicion_pago_id !== undefined && { condicion_pago_id: dto.condicion_pago_id || null }),
          ...(dto.plan_cuota_id !== undefined && { plan_cuota_id: dto.plan_cuota_id || null }),
          ...(dto.moneda !== undefined && { moneda: dto.moneda }),
          ...(dto.observaciones !== undefined && { observaciones: dto.observaciones }),
          ...(dto.intervalo_dias !== undefined && { intervalo_dias: dto.intervalo_dias }),
          ...(dto.dia_fijo_pago_1 !== undefined && { dia_fijo_pago_1: dto.dia_fijo_pago_1 ?? null }),
          ...(dto.dia_fijo_pago_2 !== undefined && { dia_fijo_pago_2: dto.dia_fijo_pago_2 ?? null }),
          ...(dto.dia_cobro_semana !== undefined && { dia_cobro_semana: dto.dia_cobro_semana ?? null }),
          ...(dto.dias_gracia_override !== undefined && { dias_gracia_override: dto.dias_gracia_override ?? null }),
          updated_at: new Date(),
        },
      });
      await this.logHistorial(tx, id, 'item_modificado', user_id, 'Datos de la solicitud actualizados', {
        campos: Object.keys(dto),
      });
    });

    return { message: 'Solicitud actualizada exitosamente', data: await this.findOneInternal(id) };
  }

  async remove(id: string, empresa_id: string, user_id: string) {
    await this.assertEditable(id, empresa_id);
    await this.prisma.solicitud_credito.update({ where: { id }, data: { active: false, updated_at: new Date() } });
    return { message: 'Solicitud eliminada exitosamente' };
  }

  // ═══════════ ITEMS ═══════════

  async addItem(solicitudId: string, dto: CreateSolicitudCreditoItemDto, empresa_id: string, user_id: string) {
    await this.assertEditable(solicitudId, empresa_id);

    await this.prisma.$transaction(async (tx) => {
      await this.addItemInternal(tx, solicitudId, dto, empresa_id);
      await this.recalcularTotales(tx, solicitudId);
      await this.logHistorial(tx, solicitudId, 'item_agregado', user_id, 'Producto agregado a solicitud', {
        producto_id: dto.producto_id,
      });
    });

    return { message: 'Item agregado', data: await this.findOneInternal(solicitudId) };
  }

  async updateItem(solicitudId: string, itemId: string, dto: Partial<CreateSolicitudCreditoItemDto>, empresa_id: string, user_id: string) {
    await this.assertEditable(solicitudId, empresa_id);

    const item = await this.prisma.solicitud_credito_det.findFirst({
      where: { id: itemId, solicitud_credito_id: solicitudId },
    });
    if (!item) throw new NotFoundException('Item no encontrado');

    await this.prisma.$transaction(async (tx) => {
      const precioTotal = dto.precio_total ?? (dto.cant_cuotas && dto.monto_cuota
        ? dto.cant_cuotas * dto.monto_cuota * (dto.cantidad || Number(item.cantidad))
        : undefined);

      await tx.solicitud_credito_det.update({
        where: { id: itemId },
        data: {
          ...(dto.cantidad !== undefined && { cantidad: dto.cantidad }),
          ...(dto.cant_cuotas !== undefined && { cant_cuotas: dto.cant_cuotas }),
          ...(dto.monto_cuota !== undefined && { monto_cuota: dto.monto_cuota }),
          ...(dto.cuota_inicial !== undefined && { cuota_inicial: dto.cuota_inicial }),
          ...(precioTotal !== undefined && { precio_total: precioTotal }),
          ...(dto.porcentaje_iva !== undefined && { porcentaje_iva: dto.porcentaje_iva }),
          ...(dto.observacion !== undefined && { observacion: dto.observacion }),
          ...(dto.producto_precio_cuota_id !== undefined && { producto_precio_cuota_id: dto.producto_precio_cuota_id }),
        },
      });
      await this.recalcularTotales(tx, solicitudId);
      await this.logHistorial(tx, solicitudId, 'item_modificado', user_id, 'Producto modificado', { item_id: itemId });
    });

    return { message: 'Item actualizado', data: await this.findOneInternal(solicitudId) };
  }

  async removeItem(solicitudId: string, itemId: string, empresa_id: string, user_id: string) {
    await this.assertEditable(solicitudId, empresa_id);

    const item = await this.prisma.solicitud_credito_det.findFirst({
      where: { id: itemId, solicitud_credito_id: solicitudId },
    });
    if (!item) throw new NotFoundException('Item no encontrado');

    await this.prisma.$transaction(async (tx) => {
      await tx.solicitud_credito_det.delete({ where: { id: itemId } });
      await this.recalcularTotales(tx, solicitudId);
      await this.logHistorial(tx, solicitudId, 'item_eliminado', user_id, 'Producto eliminado de solicitud', {
        producto_id: item.producto_id,
      });
    });

    return { message: 'Item eliminado', data: await this.findOneInternal(solicitudId) };
  }

  // ═══════════ FLUJO DE ESTADOS ═══════════

  async enviarAprobacion(id: string, empresa_id: string, user_id: string) {
    await this.assertEstado(id, empresa_id, EstadoSolicitud.BORRADOR);

    const items = await this.prisma.solicitud_credito_det.count({ where: { solicitud_credito_id: id } });
    if (items === 0) throw new BadRequestException('La solicitud no tiene productos');

    const solicitud = await this.prisma.solicitud_credito.findUnique({ where: { id }, select: { numeracion_id: true } });
    const numero = await this.prisma.$transaction(async (tx) => {
      const num = await this.reservarNumeroSolicitud(tx, empresa_id, solicitud?.numeracion_id);
      await tx.solicitud_credito.update({
        where: { id },
        data: {
          estado: EstadoSolicitud.PENDIENTE_APROBACION,
          numero_solicitud: num,
          updated_at: new Date(),
        },
      });
      await this.logHistorial(tx, id, 'enviado_aprobacion', user_id, `Solicitud enviada a aprobación: ${num}`, {
        numero_solicitud: num,
      });
      return num;
    });

    this.logger.log(`Solicitud enviada a aprobación: ${id} → ${numero}`, 'SolicitudesCreditoService');
    return { message: `Solicitud ${numero} enviada a aprobación`, data: await this.findOneInternal(id) };
  }

  async aprobar(id: string, empresa_id: string, user_id: string) {
    await this.assertEstado(id, empresa_id, EstadoSolicitud.PENDIENTE_APROBACION);

    await this.prisma.$transaction(async (tx) => {
      await tx.solicitud_credito.update({
        where: { id },
        data: {
          estado: EstadoSolicitud.APROBADA,
          fecha_aprobacion: new Date(),
          aprobado_por: user_id,
          updated_at: new Date(),
        },
      });
      await this.logHistorial(tx, id, 'aprobado', user_id, 'Solicitud aprobada');
    });

    this.logger.log(`Solicitud aprobada: ${id}`, 'SolicitudesCreditoService');
    return { message: 'Solicitud aprobada exitosamente', data: await this.findOneInternal(id) };
  }

  async rechazar(id: string, motivo: string, empresa_id: string, user_id: string) {
    await this.assertEstado(id, empresa_id, EstadoSolicitud.PENDIENTE_APROBACION);

    if (!motivo?.trim()) throw new BadRequestException('Debe indicar un motivo de rechazo');

    await this.prisma.$transaction(async (tx) => {
      await tx.solicitud_credito.update({
        where: { id },
        data: {
          estado: EstadoSolicitud.RECHAZADA,
          motivo_rechazo: motivo,
          updated_at: new Date(),
        },
      });
      await this.logHistorial(tx, id, 'rechazado', user_id, `Solicitud rechazada: ${motivo}`, { motivo });
    });

    return { message: 'Solicitud rechazada', data: await this.findOneInternal(id) };
  }

  async cancelar(id: string, empresa_id: string, user_id: string) {
    const solicitud = await this.prisma.solicitud_credito.findUnique({ where: { id } });
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('No pertenece a su empresa');
    if ([EstadoSolicitud.FACTURADA, EstadoSolicitud.CANCELADA].includes(solicitud.estado as EstadoSolicitud))
      throw new BadRequestException(`No se puede cancelar en estado "${solicitud.estado}"`);

    await this.prisma.$transaction(async (tx) => {
      await tx.solicitud_credito.update({
        where: { id },
        data: { estado: EstadoSolicitud.CANCELADA, updated_at: new Date() },
      });
      await this.logHistorial(tx, id, 'cancelado', user_id, 'Solicitud cancelada', {
        estado_anterior: solicitud.estado,
      });
    });

    return { message: 'Solicitud cancelada', data: await this.findOneInternal(id) };
  }

  async marcarFacturada(id: string, facturaId: string, empresa_id: string, user_id: string) {
    const solicitud = await this.prisma.solicitud_credito.findUnique({ where: { id } });
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('No pertenece a su empresa');
    if (solicitud.estado !== EstadoSolicitud.APROBADA)
      throw new BadRequestException(`No se puede facturar en estado "${solicitud.estado}"`);

    await this.prisma.$transaction(async (tx) => {
      await tx.solicitud_credito.update({
        where: { id },
        data: {
          estado: EstadoSolicitud.FACTURADA,
          factura_id: facturaId,
          updated_at: new Date(),
        },
      });
      await this.logHistorial(tx, id, 'facturado', user_id, `Facturada: ${facturaId}`, { factura_id: facturaId });
    });

    return { message: 'Solicitud marcada como facturada', data: await this.findOneInternal(id) };
  }

  // ═══════════ CONSULTAS ═══════════

  async findAprobadas(empresa_id: string, cliente_id?: string, sucursal_id?: string) {
    const where: Prisma.solicitud_creditoWhereInput = {
      empresa_id,
      active: true,
      estado: EstadoSolicitud.APROBADA,
    };
    if (cliente_id) where.cliente_id = cliente_id;
    if (sucursal_id) where.OR = [{ sucursal_id }, { sucursal_id: null }];

    const data = await this.prisma.solicitud_credito.findMany({
      where,
      include: {
        cliente: {
          select: { id: true, nombre_fantasia: true, personas: { select: { razon_social: true, ruc: true, telefono: true } } },
        },
        vendedor: { select: { id: true, nombre: true, apellido: true } },
        condicion_pago: { select: { id: true, descripcion: true } },
        _count: { select: { detalle: true } },
      },
      orderBy: { fecha_solicitud: 'desc' },
    });

    return data.map((item) => ({
      ...item,
      fecha_solicitud: formatDateResponse(item.fecha_solicitud),
      fecha_aprobacion: formatDateResponse(item.fecha_aprobacion),
    }));
  }

  async getHistorial(id: string, empresa_id: string) {
    const solicitud = await this.prisma.solicitud_credito.findUnique({ where: { id } });
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('No pertenece a su empresa');

    return this.prisma.solicitud_credito_historial.findMany({
      where: { solicitud_credito_id: id },
      include: { usuario: { select: { id: true, username: true } } },
      orderBy: { created_at: 'desc' },
    });
  }

  // ═══════════ INTERNALS ═══════════

  private async addItemInternal(
    tx: Prisma.TransactionClient,
    solicitudId: string,
    dto: CreateSolicitudCreditoItemDto,
    empresa_id: string,
  ) {
    const producto = await tx.productos.findFirst({
      where: { id: dto.producto_id, empresa_id },
      select: { id: true, descripcion: true, porcentaje_iva: true, precio: true },
    });
    if (!producto) throw new NotFoundException(`Producto no encontrado: ${dto.producto_id}`);

    let cantCuotas = dto.cant_cuotas;
    let montoCuota = dto.monto_cuota;
    let cuotaInicial = dto.cuota_inicial;

    // Si hay un plan de cuotas seleccionado, traer datos
    if (dto.producto_precio_cuota_id) {
      const plan = await tx.producto_precio_cuota.findUnique({ where: { id: dto.producto_precio_cuota_id } });
      if (plan) {
        cantCuotas = cantCuotas ?? plan.cant_cuotas;
        montoCuota = montoCuota ?? Number(plan.monto_cuota);
        cuotaInicial = cuotaInicial ?? (plan.cuota_inicial ? Number(plan.cuota_inicial) : undefined);
      }
    }

    const cantidad = dto.cantidad || 1;
    const precioTotal = dto.precio_total ?? (cantCuotas && montoCuota ? cantCuotas * montoCuota * cantidad : Number(producto.precio || 0) * cantidad);
    const porcentajeIva = dto.porcentaje_iva ?? producto.porcentaje_iva ?? 10;
    const montoIva = precioTotal * (porcentajeIva / (100 + porcentajeIva));

    await tx.solicitud_credito_det.create({
      data: {
        solicitud_credito_id: solicitudId,
        producto_id: dto.producto_id,
        producto_precio_cuota_id: dto.producto_precio_cuota_id ?? null,
        descripcion: dto.descripcion || producto.descripcion,
        cantidad,
        cant_cuotas: cantCuotas ?? null,
        monto_cuota: montoCuota ?? null,
        cuota_inicial: cuotaInicial ?? null,
        precio_total: precioTotal,
        porcentaje_iva: porcentajeIva,
        monto_iva: montoIva,
        observacion: dto.observacion ?? null,
      },
    });
  }

  private async recalcularTotales(tx: Prisma.TransactionClient, solicitudId: string) {
    const solicitud = await tx.solicitud_credito.findUnique({
      where: { id: solicitudId },
      select: { cuota_inicial_total: true, cant_cuotas_total: true, cronograma: true },
    });
    const items = await tx.solicitud_credito_det.findMany({ where: { solicitud_credito_id: solicitudId } });
    const montoTotal = items.reduce((sum, i) => sum + Number(i.precio_total), 0);
    const maxCuotas = items.reduce((max, i) => Math.max(max, i.cant_cuotas || 0), 0);
    const entregaInicial = Number(solicitud?.cuota_inicial_total || 0);
    const saldoFinanciar = Math.max(0, montoTotal - entregaInicial);

    const cronogramaLen = Array.isArray(solicitud?.cronograma) ? solicitud.cronograma.length : 0;
    const cantCuotasTotal = maxCuotas || solicitud?.cant_cuotas_total || cronogramaLen || null;

    await tx.solicitud_credito.update({
      where: { id: solicitudId },
      data: {
        monto_total: montoTotal,
        cant_cuotas_total: cantCuotasTotal,
        saldo_financiar: saldoFinanciar,
        updated_at: new Date(),
      },
    });
  }

  private async findOneInternal(id: string) {
    const data = await this.prisma.solicitud_credito.findUnique({
      where: { id },
      include: {
        cliente: {
          select: {
            id: true, nombre_fantasia: true,
            personas: { select: { razon_social: true, ruc: true, nro_documento: true, telefono: true, direccion: true } },
          },
        },
        vendedor: { select: { id: true, nombre: true, apellido: true } },
        condicion_pago: { select: { id: true, descripcion: true } },
        aprobador: { select: { id: true, username: true } },
        creador: { select: { id: true, username: true } },
        detalle: {
          include: {
            producto: {
              select: { id: true, descripcion: true, cod_producto: true, codigo_barra: true },
            },
            precio_cuota: { select: { id: true, nombre: true, cant_cuotas: true, monto_cuota: true, monto_total: true } },
          },
          orderBy: { created_at: 'asc' },
        },
        historial: {
          orderBy: { created_at: 'desc' },
          take: 10,
          include: { usuario: { select: { id: true, username: true } } },
        },
      },
    });
    if (!data) return null;
    return {
      ...data,
      fecha_solicitud: formatDateResponse(data.fecha_solicitud),
      fecha_aprobacion: formatDateResponse(data.fecha_aprobacion),
    };
  }

  private async assertEditable(id: string, empresa_id: string) {
    const solicitud = await this.prisma.solicitud_credito.findUnique({ where: { id } });
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('No pertenece a su empresa');
    if (solicitud.estado !== EstadoSolicitud.BORRADOR)
      throw new BadRequestException(`No se puede editar en estado "${solicitud.estado}"`);
    return solicitud;
  }

  private async assertEstado(id: string, empresa_id: string, estadoEsperado: string) {
    const solicitud = await this.prisma.solicitud_credito.findUnique({ where: { id } });
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('No pertenece a su empresa');
    if (solicitud.estado !== estadoEsperado)
      throw new BadRequestException(`La solicitud debe estar en estado "${estadoEsperado}" (actual: "${solicitud.estado}")`);
    return solicitud;
  }

  /**
   * Reserva el siguiente número de solicitud usando numeraciones_documento.
   * Usa el mismo patrón de bloqueo pesimista (SELECT FOR UPDATE) que facturas/recibos.
   * Formato: 001-001-0000001 (est-pexp-numero)
   *
   * @param tx - Cliente de transacción Prisma
   * @param numeracionId - ID de la numeración (de numeraciones_documento)
   * @param empresa_id - ID de la empresa (fallback si no hay numeracion_id)
   */
  private async reservarNumeroSolicitud(
    tx: Prisma.TransactionClient,
    empresa_id: string,
    numeracionId?: string | null,
  ): Promise<string> {
    // Si hay numeracion_id, usar el sistema formal de numeraciones
    if (numeracionId) {
      const numeraciones = await tx.$queryRaw<
        Array<{ id: string; numero_actual: number; numero_final: number | null; active: boolean; punto_expedicion_id: string }>
      >`SELECT id, numero_actual, numero_final, active, punto_expedicion_id
        FROM numeraciones_documento
        WHERE id = ${numeracionId}::uuid
        FOR UPDATE`;

      if (numeraciones.length > 0) {
        const num = numeraciones[0];

        if (!num.active) throw new BadRequestException('La numeración de solicitud está inactiva');
        if (num.numero_final && num.numero_actual >= num.numero_final) {
          throw new BadRequestException('Se alcanzó el número final de la numeración de solicitud');
        }

        const numeroReservado = num.numero_actual;

        // Incrementar atómicamente
        await tx.numeraciones_documento.update({
          where: { id: numeracionId },
          data: { numero_actual: numeroReservado + 1 },
        });

        // Obtener punto expedición para formato est-pexp-num
        const pe = await tx.empresas_puntos_expedicion.findUnique({
          where: { id: num.punto_expedicion_id },
          include: { empresas_sucursales: true },
        });

        const est = pe?.empresas_sucursales?.punto_establecimiento || '001';
        const pexp = pe?.punto_expedicion || '001';
        const dnumdoc = String(numeroReservado).padStart(7, '0');

        return `${est}-${pexp}-${dnumdoc}`;
      }
    }

    // Fallback: numeración secuencial simple (SC-000001)
    const last = await tx.solicitud_credito.findFirst({
      where: { empresa_id, numero_solicitud: { not: null } },
      orderBy: { numero_solicitud: 'desc' },
      select: { numero_solicitud: true },
    });

    let nextNum = 1;
    if (last?.numero_solicitud) {
      const match = last.numero_solicitud.match(/(\d+)$/);
      if (match) nextNum = parseInt(match[1], 10) + 1;
    }

    return `SC-${String(nextNum).padStart(6, '0')}`;
  }

  // ═══════════ PDF ═══════════

  async generatePdf(id: string, empresa_id: string, tipo_retorno: string = 'view') {
    const solicitud = await this.findOneInternal(id);
    if (!solicitud) throw new NotFoundException('Solicitud no encontrada');
    if (solicitud.empresa_id !== empresa_id) throw new BadRequestException('No pertenece a su empresa');

    const empresa = await this.prisma.empresas.findUnique({
      where: { id: empresa_id },
      include: { personas: true },
    });

    const persona = empresa?.personas as any;
    // Sucursal de la solicitud: su logo y nombre comercial encabezan el PDF.
    const sucursalSolicitud = (solicitud as any).sucursal_id
      ? await this.prisma.empresas_sucursales.findFirst({
          where: { id: (solicitud as any).sucursal_id, empresa_id },
          select: { logo: true, nombre_fantasia: true },
        })
      : null;

    const sol = solicitud as any;
    const payload = {
      tipo: tipo_retorno,
      numero_solicitud: solicitud.numero_solicitud || 'Borrador',
      estado: solicitud.estado,
      fecha_solicitud: solicitud.fecha_solicitud,
      moneda: solicitud.moneda || 'PYG',
      monto_total: solicitud.monto_total ? Number(solicitud.monto_total) : 0,
      cant_cuotas_total: solicitud.cant_cuotas_total,
      observaciones: solicitud.observaciones,
      // Condiciones del crédito
      intervalo_dias: sol.intervalo_dias || 30,
      cuota_inicial_total: sol.cuota_inicial_total ? Number(sol.cuota_inicial_total) : 0,
      saldo_financiar: sol.saldo_financiar ? Number(sol.saldo_financiar) : 0,
      fecha_primera_cuota: sol.fecha_primera_cuota || null,
      dia_fijo_pago_1: sol.dia_fijo_pago_1 || null,
      dia_fijo_pago_2: sol.dia_fijo_pago_2 || null,
      dia_cobro_semana: sol.dia_cobro_semana || null,
      dias_gracia_override: sol.dias_gracia_override ?? null,
      // Cronograma precalculado
      cronograma: sol.cronograma || [],
      empresa: {
        razon_social: persona?.razon_social || empresa?.razon_social || '',
        ruc: empresa?.ruc || '',
        dv: empresa?.dv || '',
        direccion: persona?.direccion || '',
        telefono: persona?.telefono || persona?.celular || empresa?.celular || '',
        email: persona?.email || empresa?.email || '',
        // Logo y nombre comercial de la sucursal del documento (si no tiene, los de la empresa).
        nombre_fantasia: nombreFantasiaEmisor(sucursalSolicitud, empresa),
        logo_url: logoEmisor(sucursalSolicitud, empresa as { logo?: string | null }) || (empresa as any)?.logo_url || null,
      },
      cliente: {
        razon_social: solicitud.cliente?.personas?.razon_social || solicitud.cliente?.nombre_fantasia || '',
        documento: solicitud.cliente?.personas?.ruc || solicitud.cliente?.personas?.nro_documento || '',
        ruc: solicitud.cliente?.personas?.ruc || '',
        telefono: solicitud.cliente?.personas?.telefono || '',
        direccion: solicitud.cliente?.personas?.direccion || '',
      },
      vendedor: solicitud.vendedor || null,
      condicion_pago: solicitud.condicion_pago || null,
      aprobador: solicitud.aprobador || null,
      detalle: (solicitud.detalle || []).map((d: any) => ({
        descripcion: d.descripcion || d.producto?.descripcion || '',
        cantidad: Number(d.cantidad),
        cant_cuotas: d.cant_cuotas,
        monto_cuota: d.monto_cuota ? Number(d.monto_cuota) : null,
        cuota_inicial: d.cuota_inicial ? Number(d.cuota_inicial) : null,
        precio_total: Number(d.precio_total),
        producto: d.producto,
      })),
    };

    const url = `${envs.apiGeneradorPDF}/api/solicitud-credito/generate-pdf`;

    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new BadRequestException(`Error al generar PDF: ${errorBody}`);
      }

      if (tipo_retorno === 'base64') {
        const json = (await response.json()) as { status: string; pdf_base64?: string; message?: string };
        if (json.status !== 'success') throw new BadRequestException(json.message || 'Error al generar PDF');
        return { base64: json.pdf_base64, contentType: 'application/json' };
      }

      const arrayBuffer = await response.arrayBuffer();
      const pdfBuffer = Buffer.from(arrayBuffer);
      return { pdfBuffer, contentType: 'application/pdf' };
    } catch (error) {
      if (error instanceof BadRequestException) throw error;
      const message = error instanceof Error ? error.message : 'Error desconocido';
      throw new BadRequestException(`No se pudo generar el PDF: ${message}`);
    }
  }

  private async logHistorial(
    tx: Prisma.TransactionClient,
    solicitudId: string,
    accion: string,
    userId: string,
    descripcion: string,
    datos?: any,
  ) {
    await tx.solicitud_credito_historial.create({
      data: {
        solicitud_credito_id: solicitudId,
        accion,
        descripcion,
        usuario_id: userId,
        datos: datos ?? undefined,
      },
    });
  }
}
