import { sincronizarCuentaCobrar, type TxSincronizacion } from 'src/common/utils/sincronizar-cuenta.util';
import { BadRequestException, ConflictException, Injectable, NotFoundException, Logger } from '@nestjs/common';
import { AuditService } from 'src/audit/audit.service';
import { envs } from 'src/config/envs';
import { LotesService } from 'src/lotes/lotes.service';
import { PlanLimitsService } from 'src/plan-limits/plan-limits.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { filtroNumeroDocumento } from 'src/utils/numero-documento';
import { alcanceSucursalUsuario, filtroFiscalPorSucursal } from 'src/utils/alcance-sucursal';
import { QueuesService } from 'src/queues/queues.service';
import { MiddlewareSifenService } from 'src/middleware-sifen/middleware-sifen.service';
import { AsignacionFacturasService } from 'src/vendedores-cobradores/asignacion-facturas.service';
import { formatDateResponse, nowAsuncionNaive, parseYmdValida, toPrismaDate } from 'src/utils/utilidades';
import { CreateNotaCreditoDto } from './dto/create-nota-credito.dto';
import { aplicarNcAlEmitir, revertirNcDeCuotas, saldoPendienteDeCuotas, type ModoAplicacionNc } from './aplicacion-nc';

@Injectable()
export class NotaCreditosService {
  private readonly logger = new Logger(NotaCreditosService.name);
  constructor(
    private prisma: PrismaService,
    private queuesService: QueuesService,
    private readonly auditService: AuditService,
    private readonly planLimits: PlanLimitsService,
    private readonly lotesService: LotesService,
    private readonly middlewareSifenService: MiddlewareSifenService,
    private readonly asignacionFacturas: AsignacionFacturasService,
  ) {}

  async create(createNotaCreditoDto: CreateNotaCreditoDto, empresa_id: string, user_id?: string) {
    try {
      await this.planLimits.checkLimit(empresa_id, 'documentos');
      const cabecera = createNotaCreditoDto.cabecera;
      const items = createNotaCreditoDto.items;
      const subtotal = createNotaCreditoDto.subtotal;

      // Validaciones básicas
      if (items.length === 0) throw new BadRequestException('No se puede crear una nota de crédito sin items');

      // Validar que se envíe numeracion_id
      if (!cabecera.numeracion_id) {
        throw new BadRequestException('numeracion_id es obligatorio');
      }

      const cliente = await this.prisma.clientes.findUnique({ where: { id: cabecera.cliente_id } });
      if (!cliente) throw new NotFoundException(`El cliente ${cabecera.cliente_id} no existe`);

      const moneda = await this.prisma.moneda.findUnique({ where: { id: cabecera.moneda_id } });
      if (!moneda) throw new NotFoundException(`La moneda ${cabecera.moneda_id} no existe`);

      const motivoEmision = await this.prisma.motivo_emision_nota_credito_y_debito.findUnique({
        where: { id: cabecera.motivo_emision_id },
      });
      if (!motivoEmision) throw new NotFoundException(`El motivo de emisión ${cabecera.motivo_emision_id} no existe`);

      if (moneda.codigo !== 'PYG') {
        if (!cabecera.dticam || cabecera.dticam === 0)
          throw new BadRequestException('La nota de crédito debe tener tipo de cambio y un valor mayor a 0');
        if (!cabecera.dcondticam)
          throw new BadRequestException(
            'La nota de crédito debe tener condición de tipo de cambio si es Global o por item',
          );
      }

      // Pre-cargar productos para validar stock
      const productosIds = items.map((item) => item.producto_id);
      const productos = await this.prisma.productos.findMany({
        where: { id: { in: productosIds } },
      });
      const productosMap = new Map(productos.map((p) => [p.id, p]));

      // === VALIDACIÓN ITEMS vs FACTURA ASOCIADA ===
      // Reglas:
      //   - Item con producto presente en factura: cantidad ≤ disponible y precio ≤ original.
      //   - Item con producto NO presente: se permite como "adicional".
      //   - Suma total NC ≤ saldo disponible global de la factura
      //     (= total_factura − total NCs previas aplicadas).
      const itemsEsAdicional = new Array<boolean>(items.length).fill(false);
      if (cabecera.factura_cab_id) {
        const facturaRef = await this.prisma.factura_cab.findUnique({
          where: { id: cabecera.factura_cab_id },
          select: {
            total_factura: true,
            total_notas_credito: true,
            factura_subtotales: { select: { dtotope: true, dtotgralope: true } },
            factura_det: { select: { producto_id: true, dcantproser: true, duniproser: true, cantidad_nc_aplicada: true } as any },
          },
        });
        if (facturaRef) {
          const detByProducto = new Map<string, { dcantproser: number; duniproser: number; cantidad_nc_aplicada: number }>();
          for (const d of (facturaRef.factura_det ?? []) as any[]) {
            detByProducto.set(d.producto_id, {
              dcantproser:          Number(d.dcantproser) || 0,
              duniproser:           Number(d.duniproser) || 0,
              cantidad_nc_aplicada: Number(d.cantidad_nc_aplicada) || 0,
            });
          }

          items.forEach((it, idx) => {
            const det = detByProducto.get(it.producto_id);
            if (!det) {
              itemsEsAdicional[idx] = true;
              return;
            }
            const disponible = det.dcantproser - det.cantidad_nc_aplicada;
            if (Number(it.cantidad) > disponible + 1e-6) {
              throw new BadRequestException(
                `Item "${it.descripcion}": cantidad ${it.cantidad} supera el disponible (${disponible}) en la factura asociada.`,
              );
            }
            if (Number(it.precio_unitario) > det.duniproser + 1e-6) {
              throw new BadRequestException(
                `Item "${it.descripcion}": precio unitario ${it.precio_unitario} supera el precio original (${det.duniproser}).`,
              );
            }
          });

          const subRef = (facturaRef as any).factura_subtotales?.[0];
          const totalFactura =
            Number(facturaRef.total_factura) ||
            Number(subRef?.dtotgralope) ||
            Number(subRef?.dtotope) ||
            0;
          const totalNcPrevio = Number((facturaRef as any).total_notas_credito) || 0;
          const saldoDisponibleNC = Math.max(0, totalFactura - totalNcPrevio);
          const montoNC = Number(subtotal.total_general_operacion) || 0;
          if (montoNC > saldoDisponibleNC + 1e-2) {
            throw new BadRequestException(
              `El total de la NC (${montoNC}) supera el saldo disponible de la factura para emitir notas de crédito (${saldoDisponibleNC}).`,
            );
          }
        }
      }
      const numeracionContext = await this.prisma.numeraciones_documento.findUnique({
        where: { id: cabecera.numeracion_id },
        select: {
          punto_expedicion: {
            select: {
              empresas_sucursales: {
                select: { id: true },
              },
            },
          },
        },
      });
      const sucursalLotesId = numeracionContext?.punto_expedicion?.empresas_sucursales?.id || null;
      const lotesEnabled = sucursalLotesId
        ? await this.lotesService.isEnabledForEmpresa(empresa_id, sucursalLotesId)
        : false;

      const notaCreditoId = await this.prisma.$transaction(async (tx) => {
        // === MANEJO DE NUMERACIÓN (igual que facturas) ===
        let dest: string | null = null;
        let dpunexp: string | null = null;
        let dnumdoc: string | null = null;
        let puntoExpedicion: {
          id: string;
          punto_expedicion: string | null;
          empresas_sucursales: {
            id: string;
            punto_establecimiento: string | null;
            config: any;
          } | null;
        } | null = null;

        // Bloqueo pesimista: SELECT FOR UPDATE
        const numeraciones = await tx.$queryRaw<
          Array<{
            id: string;
            numero_actual: number;
            numero_final: number | null;
            active: boolean;
            punto_expedicion_id: string;
            timbrado_id: string | null;
          }>
        >`SELECT id, numero_actual, numero_final, active, punto_expedicion_id, timbrado_id 
          FROM numeraciones_documento 
          WHERE id = ${cabecera.numeracion_id}::uuid 
          FOR UPDATE`;

        if (numeraciones.length === 0) {
          throw new NotFoundException('Numeración no encontrada');
        }

        const numeracion = numeraciones[0];

        if (!numeracion.active) {
          throw new ConflictException('La numeración está inactiva');
        }

        puntoExpedicion = await tx.empresas_puntos_expedicion.findUnique({
          where: { id: numeracion.punto_expedicion_id },
          include: {
            empresas_sucursales: {
              select: {
                id: true,
                punto_establecimiento: true,
                config: true,
              },
            },
          },
        });

        // Asignar valores de numeración
        dnumdoc = String(numeracion.numero_actual).padStart(7, '0');
        dest = puntoExpedicion?.empresas_sucursales?.punto_establecimiento || '001';
        dpunexp = puntoExpedicion?.punto_expedicion || '001';

        // Incrementar número para la siguiente NC
        await tx.numeraciones_documento.update({
          where: { id: cabecera.numeracion_id },
          data: { numero_actual: numeracion.numero_actual + 1 },
        });

        // Validar que tengamos los datos de numeración
        if (!dest || !dpunexp || !dnumdoc) {
          throw new BadRequestException('Error al obtener datos de numeración');
        }

        // Verificar que no exista la NC
        const notaCreditoFind = await tx.nota_credito_cab.findFirst({
          where: { dest, dpunexp, dnumdoc, empresa_id },
        });

        if (notaCreditoFind) {
          throw new ConflictException(`La nota de crédito ${dest}-${dpunexp}-${dnumdoc} ya existe`);
        }

        // === GENERAR CDC ===
        const empresa = await tx.empresas.findUnique({
          where: { id: empresa_id },
          select: { ruc: true, dv: true, tipo_contribuyente: true },
        });
        let iTipCont = 1;
        if (empresa?.tipo_contribuyente) {
          const tipContrib = await tx.tipo_contribuyente.findUnique({
            where: { id: empresa.tipo_contribuyente },
            select: { codigo: true },
          });
          if (tipContrib?.codigo) iTipCont = tipContrib.codigo;
        }
        const fechaEmision = toPrismaDate(cabecera.dfeemide) ?? nowAsuncionNaive();
        const cdc = empresa
          ? this.generarCdc({
              iTiDE: 5,
              dRucEm: empresa.ruc,
              dDVEmi: empresa.dv,
              dEst: dest,
              dPunExp: dpunexp,
              dNumDoc: dnumdoc,
              iTipCont,
              dFeEmiDE: fechaEmision instanceof Date ? fechaEmision : new Date(fechaEmision),
              iTipEmi: 1,
            })
          : null;
        const enlace_qr = cdc ? this.generarEnlaceQr(cdc) : null;

        // Modo de aplicación efectivo, resuelto y PERSISTIDO (nunca NULL). Guardar
        // NULL hacía que el wizard de recibos tratara la NC como ESTRICTO (no la
        // dejaba aplicar a otra factura del cliente) aunque la empresa fuera FLEXIBLE.
        // Reglas: sin factura origen no hay ESTRICTO posible → FLEXIBLE; si el usuario
        // eligió un modo válido se respeta; si no ('Default empresa'), el de la empresa.
        const configEmpNc = await tx.rec_config_empresa.findUnique({
          where: { empresa_id },
          select: { nc_modo_default: true },
        });
        const modoElegido = String(cabecera?.rec_modo_aplicacion ?? '').trim().toUpperCase();
        const modoAplicacionResuelto = !cabecera?.factura_cab_id
          ? 'FLEXIBLE'
          : modoElegido === 'ESTRICTO' || modoElegido === 'FLEXIBLE'
          ? modoElegido
          : String(configEmpNc?.nc_modo_default ?? 'ESTRICTO').toUpperCase();

        // === CREAR CABECERA ===
        const notaCreditoCab = await tx.nota_credito_cab.create({
          data: {
            dest,
            dpunexp,
            dnumdoc,
            empresa_id: empresa_id,
            cdc,
            enlace_qr,
            cliente_id: cabecera.cliente_id,
            moneda_id: cabecera.moneda_id,
            tipo_impuesto_id: cabecera.tipo_impuesto_id,
            tipo_operacion_id: cabecera?.tipo_operacion_id ?? null,
            motivo_emision_id: cabecera.motivo_emision_id,
            factura_cab_id: cabecera?.factura_cab_id ?? null,
            dinfoemi: cabecera?.dinfoemi ?? null,
            dinfofisc: cabecera?.dinfofisc ?? null,
            ddirrec: cabecera?.ddirrec ?? null,
            cliente_direccion_id: cabecera?.cliente_direccion_id ?? null,
            dfeemide: fechaEmision,
            dcondticam: cabecera?.dcondticam ?? null,
            dticam: cabecera?.dticam ?? null,
            dinfadic: cabecera?.dinfadic ?? null,
            rec_modo_aplicacion: modoAplicacionResuelto,
            documentos_asociados: cabecera.documentos_asociados
              ? JSON.parse(JSON.stringify(cabecera.documentos_asociados))
              : undefined,
            estado: 'Pendiente',
          },
        });

        // === RESOLVER DEPÓSITOS POR ITEM ===
        const sucursalId = puntoExpedicion?.empresas_sucursales?.id;
        const esElectronico = cabecera.tipo_documento === 'electronico';

        let depositoFallback: { id: string; descripcion: string } | null = null;
        if (!esElectronico && cabecera.deposito_id) {
          depositoFallback = await tx.depositos.findFirst({
            where: { id: cabecera.deposito_id, empresa_id: empresa_id, active: true },
          });
        }
        if (!depositoFallback && sucursalId) {
          depositoFallback = await tx.depositos.findFirst({
            where: { sucursal_id: sucursalId, empresa_id: empresa_id, es_principal: true, active: true },
          });
        }

        const depositosPorItem: Map<string, string> = new Map();
        const facturaDetPorProducto: Map<string, { id: string; deposito_id: string | null }> = new Map();
        if (cabecera.factura_cab_id) {
          const detallesFactura = await tx.factura_det.findMany({
            where: { factura_cab_id: cabecera.factura_cab_id },
            select: { id: true, producto_id: true, deposito_id: true },
          });
          for (const det of detallesFactura) {
            if (det.deposito_id) {
              depositosPorItem.set(det.producto_id, det.deposito_id);
            }
            if (!facturaDetPorProducto.has(det.producto_id)) {
              facturaDetPorProducto.set(det.producto_id, { id: det.id, deposito_id: det.deposito_id });
            }
          }
        }

        // === CREAR DETALLE ===
        await tx.nota_credito_det.createMany({
          data: items.map((item, index) => ({
            nota_credito_cab_id: notaCreditoCab.id,
            producto_id: item.producto_id,
            ddesproser: item.descripcion,
            cunimed: item.unidad_medida,
            dcantproser: item.cantidad,
            cpaisorig: item?.pais_origen ?? null,
            duniproser: item.precio_unitario,
            dticamit: item?.tipo_cambio_item ?? null,
            dtotbruopeitem: item.total_bruto,
            dtotopeitem: item.total_operacion,
            dtotopegs: item?.total_gs ?? null,
            iafeciva: item.afectacion_iva,
            dtasiva: item.porcentaje_iva,
            dpropiva: item.proporcion_iva,
            dbasgraviva: item.base_gravada,
            dliqivaitem: item.liquidacion_iva,
            ddescitem: item?.descuento ?? null,
            dporcdesit: item?.porcentaje_descuento ?? null,
            dbasexe: item?.base_gravada_exenta ?? null,
            dinfitem: item?.info_adicional_item ?? null,
            item: item?.item ?? index + 1,
            deposito_id: depositosPorItem.get(item.producto_id) || depositoFallback?.id || null,
            es_adicional: itemsEsAdicional[index] ?? false,
          })),
        });

        // === CREAR SUBTOTALES ===
        await tx.nota_credito_subtotal.create({
          data: {
            nota_credito_cab_id: notaCreditoCab.id,
            dsubexe: subtotal?.subtotal_exento ?? 0,
            dsubexo: subtotal?.subtotal_exonerado ?? 0,
            dsub5: subtotal?.subtotal_5 ?? 0,
            dsub10: subtotal?.subtotal_10 ?? 0,
            dtotope: subtotal.total_operacion,
            dtotdesc: subtotal.total_descuento,
            dtotdescglotem: subtotal.total_descuento_global_item,
            dtotantitem: subtotal.total_anticipo_item,
            dtotant: subtotal.total_anticipo_global,
            dporcdesctotal: subtotal.porcentaje_descuento_total,
            danticipo: subtotal.total_anticipo,
            dredon: subtotal.total_redondeo,
            dcomi: subtotal?.total_comision ?? 0,
            dtotgralope: subtotal.total_general_operacion,
            diva5: subtotal?.liquidacion_iva_5 ?? 0,
            diva10: subtotal?.liquidacion_iva_10 ?? 0,
            dliqtotiva5: subtotal?.liquidacion_redondeo_5 ?? 0,
            dliqtotiva10: subtotal?.liquidacion_redondeo_10 ?? 0,
            divacomi: subtotal?.liquidacion_iva_comision ?? 0,
            dtotiva: subtotal?.total_iva ?? 0,
            dbasegrav5: subtotal?.total_base_gravada_5 ?? 0,
            dbasegrav10: subtotal?.total_base_gravada_10 ?? 0,
            dtbasgraiva: subtotal?.total_base_gravada ?? 0,
            dtotalgs: subtotal?.total_gs ?? 0,
          },
        });

        // === INCREMENTO DE INVENTARIO (NC devuelve stock) ===
        // Buscar tipo de movimiento "Devolución" o "Entrada"
        const tipoMovDevolucion = await tx.tipo_movimiento_inventario.findFirst({
          where: {
            OR: [
              { codigo: 'DEVOLUCION' },
              { descripcion: { contains: 'Devolución', mode: 'insensitive' } },
              { descripcion: { contains: 'Entrada', mode: 'insensitive' } },
            ],
          },
        });

        // Procesar cada item que maneje inventario
        for (const item of items) {
          const producto = productosMap.get(item.producto_id);

          if (producto?.maneja_inventario) {
            // Resolver depósito: ítem específico de factura_det > fallback
            const depositoIdItem = depositosPorItem.get(item.producto_id);
            let depositoParaInventario: { id: string; descripcion: string } | null = null;

            if (depositoIdItem) {
              depositoParaInventario = await tx.depositos.findFirst({
                where: { id: depositoIdItem, empresa_id: empresa_id, active: true },
              });
            }
            if (!depositoParaInventario) {
              depositoParaInventario = depositoFallback;
            }

            if (depositoParaInventario) {
              // Buscar stock actual del producto en el depósito
              const stockActual = await tx.stock_deposito.findUnique({
                where: {
                  deposito_id_producto_id: {
                    deposito_id: depositoParaInventario.id,
                    producto_id: item.producto_id,
                  },
                },
              });

              const cantidadActual = stockActual ? Number(stockActual.cantidad_disponible) : 0;
              const nuevaCantidad = cantidadActual + item.cantidad; // NC suma stock

              // Actualizar o crear registro de stock
              await tx.stock_deposito.upsert({
                where: {
                  deposito_id_producto_id: {
                    deposito_id: depositoParaInventario.id,
                    producto_id: item.producto_id,
                  },
                },
                create: {
                  deposito_id: depositoParaInventario.id,
                  producto_id: item.producto_id,
                  cantidad_disponible: item.cantidad,
                },
                update: {
                  cantidad_disponible: nuevaCantidad,
                  updated_at: new Date(),
                },
              });

              if (lotesEnabled) {
                const facturaDet = facturaDetPorProducto.get(item.producto_id);
                if (facturaDet?.id) {
                  await this.lotesService.restoreFacturaDetLotes(tx, {
                    facturaDetId: facturaDet.id,
                    depositoId: depositoParaInventario.id,
                    cantidad: item.cantidad,
                  });
                }
              }

              // Registrar movimiento de inventario
              if (tipoMovDevolucion) {
                const numeroNC = `${notaCreditoCab.dest}-${notaCreditoCab.dpunexp}-${notaCreditoCab.dnumdoc}`;
                await tx.movimientos_inventario.create({
                  data: {
                    deposito_origen_id: depositoParaInventario.id,
                    deposito_destino_id: depositoParaInventario.id,
                    producto_id: item.producto_id,
                    tipo_movimiento_id: tipoMovDevolucion.id,
                    cantidad: item.cantidad,
                    documento_origen: 'nota_credito',
                    documento_id: notaCreditoCab.id,
                    observaciones: `Devolución NC ${numeroNC}`,
                  },
                });
              }
            }
          }
        }

        // === ACTUALIZAR FACTURA ASOCIADA (si existe) ===
        // Nota: El movimiento de caja NO se realiza automáticamente con la NC.
        // La NC genera un saldo a favor del cliente que puede ser:
        // - Devuelto en efectivo (movimiento manual de caja)
        // - Compensado con otras facturas
        // - Aplicado como descuento en futuras ventas
        if (cabecera.factura_cab_id) {
          const facturaAsociada = await tx.factura_cab.findUnique({
            where: { id: cabecera.factura_cab_id },
          });

          if (facturaAsociada) {
            const montoNC = subtotal.total_general_operacion;

            const totalNCActual = Number((facturaAsociada as any).total_notas_credito) || 0;
            const totalFactura = Number(facturaAsociada.total_factura) || 0;

            await tx.factura_cab.update({
              where: { id: cabecera.factura_cab_id },
              data: {
                total_notas_credito: totalNCActual + montoNC,
                saldo_disponible: totalFactura - (totalNCActual + montoNC),
                updated_at: new Date(),
              } as any,
            });

            // Actualizar cantidad_nc_aplicada en factura_det si hay items asociados
            for (const item of items) {
              // Buscar el item correspondiente en la factura
              const facturaDetItem = await tx.factura_det.findFirst({
                where: {
                  factura_cab_id: cabecera.factura_cab_id,
                  producto_id: item.producto_id,
                },
              });

              if (facturaDetItem) {
                const cantidadNCActual = Number((facturaDetItem as any).cantidad_nc_aplicada) || 0;
                const cantidadOriginal = Number(facturaDetItem.dcantproser) || 0;

                await tx.factura_det.update({
                  where: { id: facturaDetItem.id },
                  data: {
                    cantidad_nc_aplicada: cantidadNCActual + item.cantidad,
                    cantidad_disponible: cantidadOriginal - (cantidadNCActual + item.cantidad),
                    updated_at: new Date(),
                  } as any,
                });
              }
            }

            // === APLICACIÓN AL EMITIR contra las cuotas de la factura origen ===
            // ESTRICTO siempre; FLEXIBLE solo si la NC cubre todo el saldo pendiente
            // (anulación total). Regla y tests en aplicacion-nc.ts.
            await aplicarNcAlEmitir(tx, {
              empresaId: empresa_id,
              notaCreditoId: notaCreditoCab.id,
              facturaId: cabecera.factura_cab_id,
              modo: modoAplicacionResuelto as ModoAplicacionNc,
              montoNc: Number(montoNC) || 0,
            });
            // La NC bajó el saldo de las cuotas: la cuenta y el cliente tienen que
            // enterarse, o siguen reclamando lo que la NC ya perdonó.
            await sincronizarCuentaCobrar(tx as unknown as TxSincronizacion, cabecera.factura_cab_id);
          }
        }

        return {
          notaCreditoId: notaCreditoCab.id,
          sucursalConfig: puntoExpedicion?.empresas_sucursales?.config as Record<string, unknown> | null,
        };
      });

      // Recalcular comisiones de la factura asociada — la NC redujo el neto,
      // hay que ajustar monto_base y monto_comision de las comisiones pendientes.
      if (cabecera.factura_cab_id) {
        this.asignacionFacturas
          .recalcularComisionesPorNC(cabecera.factura_cab_id, empresa_id)
          .catch((err: Error) =>
            this.logger.error?.(
              `Error recalculando comisiones por NC en factura ${cabecera.factura_cab_id}: ${err.message}`,
            ),
          );
      }

      // Empresas sin facturación electrónica habilitada: aprobamos localmente.
      const usaSifen = await this.middlewareSifenService.empresaUsaSifen(empresa_id);
      // Default envio_lote_middleware = true (no enviar automáticamente)
      const envioLoteMiddleware = notaCreditoId.sucursalConfig?.envio_lote_middleware ?? true;
      if (!usaSifen) {
        await this.middlewareSifenService.aprobarNCSinSifen(notaCreditoId.notaCreditoId);
      } else if (!envioLoteMiddleware) {
        // Solo enviar automáticamente si envio_lote_middleware es false
        try {
          await this.queuesService.enqueueSifenNotaCredito(notaCreditoId.notaCreditoId, empresa_id);
        } catch (queueError) {
          console.error('Error al encolar NC para SIFEN:', queueError);
          // No lanzamos error para no afectar la creación de la NC
        }
      }

      this.logger.log(
        `Nota de crédito creada: ${notaCreditoId.notaCreditoId} | Empresa: ${empresa_id}`,
        'NotaCreditosService',
      );
      await this.auditService.log({
        empresa_id,
        user_id,
        action: 'CREATE',
        entity_type: 'nota_credito',
        entity_id: notaCreditoId.notaCreditoId,
        descripcion: `Nota de crédito creada | Empresa: ${empresa_id}`,
        new_value: {
          empresa_id,
          cliente_id: createNotaCreditoDto.cabecera.cliente_id,
          total: createNotaCreditoDto.subtotal.total_general_operacion,
        },
      });
      return {
        message: 'Nota de crédito creada exitosamente',
        data: { id: notaCreditoId.notaCreditoId, envio_automatico: !envioLoteMiddleware },
      };
    } catch (error: unknown) {
      const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
      console.log('Error al crear nota de crédito:', errorMsg);
      if (error instanceof NotFoundException) throw error;
      if (error instanceof ConflictException) throw error;
      if (error instanceof BadRequestException) throw error;
      throw new Error(`Error al crear nota de crédito: ${errorMsg}`);
    }
  }

  /**
   * Revierte los efectos contables/saldos que la NC aplicó sobre la factura
   * y los detalles de la misma. Se llama cuando SIFEN rechaza la NC, o cuando
   * la NC es anulada/inutilizada por evento aprobado.
   */
  async revertirAplicacionFactura(notaCreditoId: string): Promise<void> {
    const nc = await this.prisma.nota_credito_cab.findUnique({
      where: { id: notaCreditoId },
      select: {
        id: true,
        factura_cab_id: true,
        rec_aplicacion_reversada: true as any,
        created_at: true,
        dest: true,
        dpunexp: true,
        dnumdoc: true,
      },
    });
    if (!nc || !nc.factura_cab_id) return;
    if ((nc as any).rec_aplicacion_reversada) {
      this.logger.log(`NC ${notaCreditoId} ya tiene aplicación reversada — skip`, 'NotaCreditosService');
      return;
    }

    const subtotales = await this.prisma.nota_credito_subtotal.findMany({
      where: { nota_credito_cab_id: notaCreditoId },
      select: { dtotgralope: true },
    });
    const montoNC = subtotales.reduce((acc, s) => acc + Number(s.dtotgralope || 0), 0);

    const items = await this.prisma.nota_credito_det.findMany({
      where: { nota_credito_cab_id: notaCreditoId },
      select: { producto_id: true, dcantproser: true },
    });

    await this.prisma.$transaction(async (tx) => {
      const fac = await tx.factura_cab.findUnique({ where: { id: nc.factura_cab_id! } });
      if (fac) {
        const totalNCActual = Number((fac as any).total_notas_credito) || 0;
        const totalFactura = Number(fac.total_factura) || 0;
        const nuevoTotalNC = Math.max(0, totalNCActual - montoNC);
        await tx.factura_cab.update({
          where: { id: fac.id },
          data: {
            total_notas_credito: nuevoTotalNC,
            saldo_disponible: totalFactura - nuevoTotalNC,
            updated_at: new Date(),
          } as any,
        });
      }

      for (const item of items) {
        const det = await tx.factura_det.findFirst({
          where: { factura_cab_id: nc.factura_cab_id!, producto_id: item.producto_id },
        });
        if (!det) continue;
        const cantidadNCActual = Number((det as any).cantidad_nc_aplicada) || 0;
        const cantidadOriginal = Number(det.dcantproser) || 0;
        const cantidadNC = Number(item.dcantproser) || 0;
        const nuevoNCAplicada = Math.max(0, cantidadNCActual - cantidadNC);
        await tx.factura_det.update({
          where: { id: det.id },
          data: {
            cantidad_nc_aplicada: nuevoNCAplicada,
            cantidad_disponible: cantidadOriginal - nuevoNCAplicada,
            updated_at: new Date(),
          } as any,
        });
      }

      // Revertir aplicaciones directas a cuotas (ESTRICTO o FLEXIBLE auto-aplicada).
      // Guard A4: si alguna cuota afectada recibió un cobro POSTERIOR a la NC
      // (recibo no anulado), abortamos. Devolver saldo generaría sobrepago
      // silencioso (cuota con saldo_pendiente > 0 aunque el cliente ya pagó).
      // El operador debe anular primero el/los cobros posteriores.
      const aplicaciones = await tx.nc_aplicaciones_cuota.findMany({
        where: { nota_credito_cab_id: notaCreditoId },
      });

      if (aplicaciones.length > 0 && nc.created_at) {
        const cuotasIds = aplicaciones.map((a) => a.factura_cuota_id);
        const cobrosPosteriores = await tx.recibo_cobro_detalle.findMany({
          where: {
            factura_cuota_id: { in: cuotasIds },
            created_at: { gt: nc.created_at },
            recibos_cobro: {
              estado: { notIn: ['anulado', 'Anulado', 'ANULADO'] },
            },
          },
          select: {
            recibos_cobro: {
              select: {
                numero_recibo: true,
                fecha_emision: true,
              },
            },
          },
          take: 5,
        });
        if (cobrosPosteriores.length > 0) {
          const ejemplos = cobrosPosteriores
            .map((c) => {
              const num = c.recibos_cobro?.numero_recibo ?? '?';
              const fecha =
                c.recibos_cobro?.fecha_emision?.toISOString().slice(0, 10) ?? '?';
              return `${num} (${fecha})`;
            })
            .join(', ');
          const ncNumero = `${nc.dest ?? '?'}-${nc.dpunexp ?? '?'}-${nc.dnumdoc ?? '?'}`;
          throw new BadRequestException(
            `No se puede anular la NC ${ncNumero}: las cuotas afectadas recibieron cobros posteriores. ` +
              `Recibo(s) que bloquean la anulación: ${ejemplos}. ` +
              `Anule primero esos recibos y vuelva a intentar.`,
          );
        }
      }

      await revertirNcDeCuotas(tx, notaCreditoId);

      // La deuda volvió a las cuotas: la cuenta y el cliente tienen que reflejarla.
      const ncRevertida = await tx.nota_credito_cab.findUnique({
        where: { id: notaCreditoId },
        select: { factura_cab_id: true },
      });
      if (ncRevertida?.factura_cab_id) {
        await sincronizarCuentaCobrar(tx as unknown as TxSincronizacion, ncRevertida.factura_cab_id);
      }

      await tx.nota_credito_cab.update({
        where: { id: notaCreditoId },
        data: { rec_saldo_disponible: 0, rec_aplicacion_reversada: true } as any,
      });
    });

    // Restaurar comisiones: al revertir la NC subió el neto de la factura, hay
    // que recalcular monto_base y monto_comision (o reactivar si quedó cancelada).
    const facturaCabId = nc.factura_cab_id!;
    const empresaId = (
      await this.prisma.factura_cab.findUnique({
        where: { id: facturaCabId },
        select: { empresa_id: true },
      })
    )?.empresa_id;
    if (empresaId) {
      this.asignacionFacturas
        .recalcularComisionesPorNC(facturaCabId, empresaId)
        .catch((err: Error) =>
          this.logger.error?.(
            `Error recalculando comisiones al revertir NC ${notaCreditoId}: ${err.message}`,
          ),
        );
    }

    this.logger.log(
      `NC ${notaCreditoId} aplicación reversada (monto ${montoNC}, items ${items.length})`,
      'NotaCreditosService',
    );
  }

  /**
   * Devuelve los saldos disponibles para emitir una NC contra una factura:
   * - A nivel cabecera: total_factura, total_nc_aplicado, monto_disponible.
   * - Por item: cantidad original, ya aplicada en NCs previas, disponible.
   * Considera sólo NCs no reversadas (las anuladas/inutilizadas no consumen saldo).
   */
  async saldosDisponiblesFactura(facturaId: string, empresa_id: string) {
    const factura = await this.prisma.factura_cab.findFirst({
      where: { id: facturaId, empresa_id },
      select: {
        id: true,
        total_factura: true,
        total_notas_credito: true,
        saldo_disponible: true,
        factura_subtotales: { select: { dtotope: true, dtotgralope: true } },
        factura_det: {
          select: {
            id: true,
            producto_id: true,
            ddesproser: true,
            cunimed: true,
            dcantproser: true,
            duniproser: true,
            cantidad_nc_aplicada: true,
            cantidad_disponible: true,
            dpropiva: true,
          } as any,
        },
      },
    });
    if (!factura) throw new NotFoundException('Factura no encontrada');

    const sub = (factura as any).factura_subtotales?.[0];
    const totalFactura =
      Number(factura.total_factura) ||
      Number(sub?.dtotgralope) ||
      Number(sub?.dtotope) ||
      0;
    const totalNcAplicado = Number((factura as any).total_notas_credito) || 0;
    const montoDisponible = Math.max(0, totalFactura - totalNcAplicado);

    // Lo que falta cobrar (no lo que falta acreditar): es lo que decide si una
    // NC FLEXIBLE se aplica al emitirse. Ver decidirAplicacionAlEmitir.
    const cuotasPendientes = await this.prisma.factura_cuotas.findMany({
      where: { factura_cab_id: facturaId, estado: { notIn: ['pagado', 'Pagado'] } },
      select: { saldo_pendiente: true, dmoncuota: true },
    });
    const saldoPendienteCobro = saldoPendienteDeCuotas(cuotasPendientes);

    // Modo por defecto de la empresa: la pantalla de emisión lo necesita para
    // explicar "Default empresa". Va acá y no en /recibos/config porque ese
    // endpoint exige permiso de Cobranzas, que quien emite NC puede no tener.
    const configRec = await this.prisma.rec_config_empresa.findUnique({
      where: { empresa_id },
      select: { nc_modo_default: true },
    });

    const items = (factura.factura_det ?? []).map((d: any) => {
      const cantOriginal = Number(d.dcantproser) || 0;
      const cantAplicada = Number(d.cantidad_nc_aplicada) || 0;
      const cantDisponible = Math.max(0, cantOriginal - cantAplicada);
      const precioUnit = Number(d.duniproser) || 0;
      return {
        factura_det_id:     d.id,
        producto_id:        d.producto_id,
        descripcion:        d.ddesproser,
        unidad_medida:      d.cunimed,
        cantidad_original:  cantOriginal,
        cantidad_aplicada:  cantAplicada,
        cantidad_disponible: cantDisponible,
        precio_unitario:    precioUnit,
        porcentaje_iva:     Number(d.dpropiva) || 0,
        monto_disponible:   cantDisponible * precioUnit,
      };
    });

    return {
      factura_id:        factura.id,
      total_factura:     totalFactura,
      total_nc_aplicado: totalNcAplicado,
      monto_disponible:  montoDisponible,
      saldo_pendiente_cobro: saldoPendienteCobro,
      nc_modo_default: String(configRec?.nc_modo_default ?? 'ESTRICTO').toUpperCase(),
      items,
    };
  }

  async getSiguienteNumeracion(
    est: string,
    exp: string,
    empresa_id: string,
  ): Promise<{ siguiente: string; ultimo: string | null }> {
    const last = await this.prisma.nota_credito_cab.findFirst({
      where: { dest: est, dpunexp: exp, empresa_id },
      orderBy: { dnumdoc: 'desc' },
      select: { dnumdoc: true },
    });
    if (!last || !last.dnumdoc) {
      return { siguiente: '0000001', ultimo: null };
    }
    const current = parseInt(last.dnumdoc, 10) || 0;
    const next = current + 1;
    return { siguiente: next.toString().padStart(7, '0'), ultimo: last.dnumdoc };
  }

  async findAll(
    page = 1,
    limit = 10,
    empresa_id: string,
    filters?: {
      search?: string;
      estado?: string;
      fechaDesde?: string;
      fechaHasta?: string;
      /** Quién consulta. Define qué sucursales puede ver. */
      usuarioId?: string;
      sucursalId?: string;
    },
  ) {
    const take = Math.max(1, Math.min(100, Number(limit) || 10));
    const currentPage = Math.max(1, Number(page) || 1);
    const skip = (currentPage - 1) * take;
    const where: any = { empresa_id };

    // Las notas de crédito viven en el mismo alcance que las facturas: se
    // filtran por `dest` (punto de establecimiento = sucursal).
    const alcance = await alcanceSucursalUsuario(this.prisma, filters?.usuarioId, empresa_id);
    const filtroSucursal = await filtroFiscalPorSucursal(this.prisma, alcance, filters?.sucursalId);

    if (filters?.fechaDesde || filters?.fechaHasta) {
      const gte = parseYmdValida(filters.fechaDesde, false);
      const lte = parseYmdValida(filters.fechaHasta, true);
      if (gte || lte) {
        where.dfeemide = {};
        if (gte) where.dfeemide.gte = gte;
        if (lte) where.dfeemide.lte = lte;
      }
    }

    if (filters?.estado) {
      const estadoCap =
        filters.estado.charAt(0).toUpperCase() + filters.estado.slice(1).toLowerCase();
      where.estado = estadoCap;
    }

    if (filters?.search) {
      const s = filters.search.trim();
      const numero = filtroNumeroDocumento(s);
      where.OR = [
        ...(numero ? [numero] : []),
        { dnumdoc: { contains: s, mode: 'insensitive' } },
        { dest: { contains: s, mode: 'insensitive' } },
        { dpunexp: { contains: s, mode: 'insensitive' } },
        { clientes: { personas: { razon_social: { contains: s, mode: 'insensitive' } } } },
        { clientes: { personas: { nro_documento: { contains: s, mode: 'insensitive' } } } },
      ];
    }

    if (filtroSucursal) {
      where.AND = [...(where.AND ?? []), filtroSucursal];
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.nota_credito_cab.findMany({
        where,
        include: {
          clientes: {
            include: {
              personas: true,
            },
          },
          empresas: true,
          moneda: true,
          tipo_impuesto: true,
          tipo_operacion: true,
          motivo_emision_nota_credito_y_debito: true,
          nota_credito_subtotal: true,
          nota_credito_det: {
            include: {
              productos: {
                include: {
                  unidades_de_medida: true,
                },
              },
            },
          },
          factura_cab: true,
        },
        orderBy: [{ dfeemide: 'desc' }, { created_at: 'desc' }],
        take,
        skip,
      }),
      this.prisma.nota_credito_cab.count({ where }),
    ]);

    const lastPage = Math.max(1, Math.ceil(total / take));
    /// formatear primero

    // Formatear fechas SIFEN para respuesta
    const formattedItems = items.map((item) => ({
      ...item,
      dfeemide: formatDateResponse(item.dfeemide),
      created_at: formatDateResponse(item.created_at),
      fecha_envio_sifen: formatDateResponse(item.fecha_envio_sifen),
      fecha_firma_sifen: formatDateResponse(item.fecha_firma_sifen),
      fecha_registro_sifen: formatDateResponse(item.fecha_registro_sifen),
    }));
    return {
      message: 'Notas de crédito obtenidas exitosamente',
      data: formattedItems,
      total,
      page: currentPage,
      limit: take,
      lastPage,
    };
  }

  async findOne(id: string, empresa_id: string) {
    const notaCredito = await this.prisma.nota_credito_cab.findFirst({
      where: { id, empresa_id },
      include: {
        clientes: {
          include: {
            personas: true,
          },
        },
        empresas: true,
        moneda: true,
        tipo_impuesto: true,
        tipo_operacion: true,
        motivo_emision_nota_credito_y_debito: true,
        nota_credito_det: {
          include: {
            productos: true,
          },
        },
        nota_credito_subtotal: true,
      },
    });
    if (!notaCredito) throw new NotFoundException(`La nota de crédito ${id} no existe`);
    return { message: 'Nota de crédito encontrada exitosamente', data: notaCredito };
  }

  /**
   * Encola notas de crédito para envío/reenvío a SIFEN.
   *
   * ⚠️ **Uso EXCLUSIVO por acción explícita del usuario** (endpoint HTTP). No
   * invocar desde jobs / cron / triggers automáticos: los documentos rechazados
   * por SIFEN requieren revisión humana antes de un reintento.
   *
   * Documentos aptos:
   *   - Estado local `Pendiente` (nunca llegó a SIFEN).
   *   - Estado local `Rechazado` (SIFEN rechazó y el usuario corrigió datos).
   */
  async enviarLoteMiddleware(notaCreditoIds: string[], empresa_id: string, user_id?: string) {
    if (!notaCreditoIds || notaCreditoIds.length === 0) {
      throw new BadRequestException('Debe proporcionar al menos una nota de crédito');
    }

    // Reenviables: pendientes locales o rechazadas por SIFEN (para reintentar
    // con datos corregidos). Cuando SIFEN devuelve rechazo, `estado` local se
    // sincroniza a "Rechazado" (ver middleware-sifen.service.ts).
    // Aprobadas y anuladas quedan fuera.
    const notasCredito = await this.prisma.nota_credito_cab.findMany({
      where: {
        id: { in: notaCreditoIds },
        empresa_id,
        estado: { in: ['Pendiente', 'Rechazado'] },
      },
      select: { id: true, estado: true },
    });

    if (notasCredito.length === 0) {
      throw new NotFoundException('No se encontraron notas de crédito reenviables (pendientes o rechazadas por SIFEN).');
    }

    const notasEncontradas = notasCredito.map((nc) => nc.id);
    const notasNoEncontradas = notaCreditoIds.filter((id) => !notasEncontradas.includes(id));

    // Encolar cada nota de crédito para envío
    const resultados: { notaCreditoId: string; status: string }[] = [];

    for (const notaCreditoId of notasEncontradas) {
      try {
        await this.queuesService.enqueueSifenNotaCredito(notaCreditoId, empresa_id);
        resultados.push({ notaCreditoId, status: 'encolado' });
      } catch (error) {
        const msg = error instanceof Error ? error.message : 'Error desconocido';
        resultados.push({ notaCreditoId, status: `error: ${msg}` });
      }
    }

    this.logger.log(
      `Lote NC enviado a SIFEN: ${notasEncontradas.length} documentos | Empresa: ${empresa_id}`,
      'NotaCreditosService',
    );
    await this.auditService.log({
      empresa_id,
      user_id,
      action: 'UPDATE',
      entity_type: 'nota_credito',
      entity_id: empresa_id,
      descripcion: `Lote de ${notasEncontradas.length} nota(s) de crédito encolada(s) para envío SIFEN`,
      new_value: { enviadas: notasEncontradas.length, no_encontradas: notasNoEncontradas, resultados },
    });

    return {
      enviadas: notasEncontradas.length,
      no_encontradas: notasNoEncontradas,
      resultados,
    };
  }

  private calcularDVModulo11(p_numero: string, p_basemax = 11): number {
    let v_total = 0;
    let k = 2;
    let v_numero_al = '';
    for (let i = 0; i < p_numero.length; i++) {
      const c = p_numero.charAt(i);
      if (/\d/.test(c)) v_numero_al += c;
      else v_numero_al += c.charCodeAt(0);
    }
    for (let i = v_numero_al.length - 1; i >= 0; i--) {
      k = k > p_basemax ? 2 : k;
      v_total += parseInt(v_numero_al.charAt(i), 10) * k++;
    }
    const v_resto = v_total % 11;
    return v_resto > 1 ? 11 - v_resto : 0;
  }

  private generarCdc(params: {
    iTiDE: number;
    dRucEm: string;
    dDVEmi: string;
    dEst: string;
    dPunExp: string;
    dNumDoc: string;
    iTipCont: number;
    dFeEmiDE: Date;
    iTipEmi?: number;
  }): string {
    const tipoDoc = String(params.iTiDE).padStart(2, '0');
    const ruc = params.dRucEm.padStart(8, '0');
    const dv = String(params.dDVEmi);
    const est = params.dEst.padStart(3, '0');
    const punExp = params.dPunExp.padStart(3, '0');
    const numDoc = params.dNumDoc.padStart(7, '0');
    const tipCont = String(params.iTipCont);
    const fecha = params.dFeEmiDE;
    // Componentes UTC: dfeemide se persiste como wall-clock UTC (toPrismaDate) y
    // dFeEmiDE del payload usa toISOString() (UTC). El CDC debe coincidir; con
    // getDate() local se corría un día cerca de medianoche.
    const fechaStr = `${fecha.getUTCFullYear()}${String(fecha.getUTCMonth() + 1).padStart(2, '0')}${String(fecha.getUTCDate()).padStart(2, '0')}`;
    const tipEmi = String(params.iTipEmi ?? 1);
    const codSeg = String(Math.floor(Math.random() * 999999999)).padStart(9, '0');
    const cdcSinDV = `${tipoDoc}${ruc}${dv}${est}${punExp}${numDoc}${tipCont}${fechaStr}${tipEmi}${codSeg}`;
    return `${cdcSinDV}${this.calcularDVModulo11(cdcSinDV)}`;
  }

  private generarEnlaceQr(cdc: string): string {
    return `${envs.urlPanelFrontend}/consulta/documento/${cdc}`;
  }
}
