import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
import { envs } from 'src/config';
import { MiddlewareSifenService } from 'src/middleware-sifen/middleware-sifen.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { filtroNumeroDocumento } from 'src/utils/numero-documento';
import { alcanceSucursalUsuario, filtroFiscalPorSucursal } from 'src/utils/alcance-sucursal';
import { CreateNotaRemisionDto } from './dto/create-nota-remision.dto';
import { NotaRemisionSifenPayloadService } from './nota-remision-sifen-payload.service';
import { NotaRemisionValidacionService } from './nota-remision-validacion.service';
import { nowAsuncionNaive, toPrismaDate } from 'src/utils/utilidades';
import { AuditService } from 'src/audit/audit.service';
import { ReversionRemisionService } from './reversion-remision.service';

export interface FindAllRemisionParams {
  estado?: string;
  estado_sifen?: string;
  search?: string;
  fechaDesde?: string;
  fechaHasta?: string;
  cliente_id?: string;
  transportista_id?: string;
  chofer_id?: string;
  vehiculo_id?: string;
  agente_id?: string;
  /** Quién consulta. Define qué sucursales puede ver. */
  usuarioId?: string;
  sucursalId?: string;
}

const cabInclude = {
  clientes: { include: { personas: true } },
  transportistas: { include: { personas: true } },
  choferes: { include: { personas: true } },
  vehiculos: { include: { transportistas: { include: { personas: true } } } },
  agentes_transporte: { include: { personas: true } },
  numeraciones: true,
  nota_remision_det: true,
  nota_remision_factura: { include: { factura_cab: { select: { id: true, dnumdoc: true, dest: true, dpunexp: true } } } },
};

// Include enriquecido para la vista de detalle (findOne): trae descripciones de
// catálogos y geo para mostrar toda la info en el modal, sin recargar la lista.
const cabIncludeDetalle = {
  clientes: {
    include: {
      personas: {
        include: {
          naturaleza_receptor: true,
          tipo_contribuyente: true,
          tipo_documento_identidad: true,
          ciudades: { include: { distritos: { include: { departamentos: true } } } },
        },
      },
      tipo_operacion: true,
    },
  },
  motivo_emision_remision: true,
  responsable_emision: true,
  responsable_flete: true,
  tipo_transporte: true,
  modalidad_transporte: true,
  condicion_negociacion: true,
  transportistas: {
    include: { personas: { include: { naturaleza_receptor: true, tipo_documento_identidad: true, paises: true } } },
  },
  choferes: { include: { personas: true } },
  vehiculos: { include: { transportistas: { include: { personas: true } } } },
  agentes_transporte: { include: { personas: true } },
  ciudades_nota_remision_cab_ciudad_salida_idTociudades: {
    include: { distritos: { include: { departamentos: true } } },
  },
  ciudades_nota_remision_cab_ciudad_entrega_idTociudades: {
    include: { distritos: { include: { departamentos: true } } },
  },
  numeraciones: true,
  nota_remision_det: {
    include: {
      productos: { include: { unidades_de_medida: true } },
      // Factura de origen de cada ítem (para remisiones de N facturas).
      nota_remision_det_aplicacion: {
        include: {
          factura_det: {
            select: { factura_cab: { select: { dest: true, dpunexp: true, dnumdoc: true } } },
          },
        },
      },
    },
  },
  nota_remision_factura: {
    include: {
      factura_cab: {
        select: {
          id: true,
          dest: true,
          dpunexp: true,
          dnumdoc: true,
          dfeemide: true,
          cdc: true,
          estado_sifen: true,
          moneda: { select: { codigo: true } },
          factura_subtotales: { select: { dtotope: true } },
        },
      },
    },
  },
};

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

  constructor(
    private prisma: PrismaService,
    private middlewareSifen: MiddlewareSifenService,
    private sifenPayload: NotaRemisionSifenPayloadService,
    private validacion: NotaRemisionValidacionService,
    private audit: AuditService,
    private reversionRemision: ReversionRemisionService,
  ) {}

  async create(dto: CreateNotaRemisionDto, empresa_id: string, usuario?: string, userId?: string) {
    const { cabecera, items, facturas_ids } = dto;

    if (!items || items.length === 0) {
      throw new BadRequestException('La nota de remisión debe tener al menos un ítem');
    }

    // Validar transportista, chofer, vehículo existen y pertenecen a la empresa
    const [transportista, chofer, vehiculo] = await Promise.all([
      this.prisma.transportistas.findFirst({ where: { id: cabecera.transportista_id, empresa_id } }),
      this.prisma.choferes.findFirst({ where: { id: cabecera.chofer_id, empresa_id } }),
      this.prisma.vehiculos.findFirst({ where: { id: cabecera.vehiculo_id, empresa_id } }),
    ]);
    if (!transportista) throw new NotFoundException('Transportista no encontrado');
    if (!chofer) throw new NotFoundException('Chofer no encontrado');
    if (!vehiculo) throw new NotFoundException('Vehículo no encontrado');

    if (cabecera.agente_id) {
      const agente = await this.prisma.agentes_transporte.findFirst({ where: { id: cabecera.agente_id, empresa_id } });
      if (!agente) throw new NotFoundException('Agente de transporte no encontrado');
    }

    // Validación de negocio ANTES de crear (igual que factura/NC): si falla,
    // no se persiste nada y el error vuelve al frontend.
    const errores = await this.validacion.validarCreacion(cabecera, empresa_id);
    if (errores.length > 0) {
      throw new BadRequestException({
        message: 'La remisión tiene datos incompletos o inválidos. Corregilos antes de emitir:',
        errores,
      });
    }

    const result = await this.prisma.$transaction(async (tx) => {
      // Bloqueo de numeración
      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 = ${cabecera.numeracion_id}::uuid
        FOR UPDATE`;

      if (numeraciones.length === 0) throw new NotFoundException('Numeración no encontrada');
      const num = numeraciones[0];
      if (!num.active) throw new BadRequestException('La numeración está inactiva');
      if (num.numero_final && num.numero_actual > num.numero_final) {
        throw new BadRequestException('La numeración ha alcanzado su límite');
      }

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

      const dest = puntoExp?.empresas_sucursales?.punto_establecimiento ?? '001';
      const dpunexp = puntoExp?.punto_expedicion ?? '001';
      const dnumdoc = String(num.numero_actual).padStart(7, '0');

      // Construir doc asociado
      let tipo_documento_asociado: string | null = null;
      let cdc_asociado: string | null = null;
      let timbrado_asociado: string | null = null;
      let establecimiento_asociado: string | null = null;
      let expedicion_asociado: string | null = null;
      let nro_doc_asociado: string | null = null;
      let tipo_doc_impreso_asociado: string | null = null;
      let fecha_doc_asociado: Date | null = null;

      if (cabecera.documento_asociado) {
        const docAsoc = cabecera.documento_asociado;
        tipo_documento_asociado = String(docAsoc.itipdocaso ?? '');
        if (docAsoc.itipdocaso === '1') {
          cdc_asociado = docAsoc.dcdcderef ?? null;
        } else if (docAsoc.itipdocaso === '2' || docAsoc.itipdocaso === '3') {
          timbrado_asociado = docAsoc.dntimdi ?? null;
          establecimiento_asociado = docAsoc.destdocaso ?? null;
          expedicion_asociado = docAsoc.dpexpdocaso ?? null;
          nro_doc_asociado = docAsoc.dnumdocaso ?? null;
          tipo_doc_impreso_asociado = String(docAsoc.itipodocaso ?? '');
          fecha_doc_asociado = docAsoc.dfecemidi ? new Date(docAsoc.dfecemidi) : null;
        }
      }

      // === GENERAR CDC (Código de Control) + enlace QR ===
      // Se calcula localmente al crear (paridad con factura/NC), no se espera a SIFEN.
      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?.ruc
        ? this.generarCdc({
            iTiDE: 7,
            dRucEm: empresa.ruc,
            dDVEmi: empresa.dv ?? '0',
            dEst: dest,
            dPunExp: dpunexp,
            dNumDoc: dnumdoc,
            iTipCont,
            dFeEmiDE: fechaEmision,
            iTipEmi: 1,
          })
        : null;
      const enlace_qr = cdc ? this.generarEnlaceQr(cdc) : null;

      // Crear cabecera
      const cab = await tx.nota_remision_cab.create({
        data: {
          empresa_id,
          numeracion_id: cabecera.numeracion_id,
          cliente_id: cabecera.cliente_id,
          transportista_id: cabecera.transportista_id,
          chofer_id: cabecera.chofer_id,
          vehiculo_id: cabecera.vehiculo_id,
          agente_id: cabecera.agente_id ?? null,
          dest,
          dpunexp,
          dnumdoc,
          dserienum: null,
          cdc,
          enlace_qr,
          motivo_emision_id: cabecera.motivo_emision_id ?? null,
          responsable_emision_id: cabecera.responsable_emision_id ?? null,
          tipo_transporte_id: cabecera.tipo_transporte_id ?? null,
          modalidad_transporte_id: cabecera.modalidad_transporte_id ?? null,
          condicion_negociacion_id: cabecera.condicion_negociacion_id ?? null,
          ciudad_salida_id: cabecera.ciudad_salida_id ?? null,
          ciudad_entrega_id: cabecera.ciudad_entrega_id ?? null,
          dfeemide: fechaEmision,
          dinitras: toPrismaDate(cabecera.fecha_inicio_traslado),
          dfintras: toPrismaDate(cabecera.fecha_fin_traslado),
          dfecem: cabecera.fecha_futura_emision ? toPrismaDate(cabecera.fecha_futura_emision) : null,
          dkmr: cabecera.kilometros ?? 0,
          ddirlocsal: cabecera.direccion_salida,
          dnumcassal: cabecera.nro_casa_salida ?? 0,
          dcomp1sal: cabecera.complemento1_salida,
          dtelsal: cabecera.telefono_salida,
          ddirlocent: cabecera.direccion_entrega,
          dnumcasent: cabecera.nro_casa_entrega ?? 0,
          dcomp1ent: cabecera.complemento1_entrega,
          dcomp2ent: cabecera.complemento2_entrega ?? null,
          dtelent: cabecera.telefono_entrega,
          tipo_documento_asociado,
          cdc_asociado,
          timbrado_asociado,
          establecimiento_asociado,
          expedicion_asociado,
          nro_doc_asociado,
          tipo_doc_impreso_asociado,
          fecha_doc_asociado,
          dinfadic: cabecera.info_adicional,
          usuario,
          estado: 'Pendiente',
        } as any,
      });

      // Precargar TODOS los factura_det referenciados por las aplicaciones en una sola
      // query (antes: un findUnique por aplicación dentro de la transacción → N+1 que
      // extendía el hold del lock). El disponible se valida contra este mapa + un
      // acumulador en memoria de lo aplicado en esta request (equivalente a re-leer,
      // porque los UPDATE incrementales viven en la misma transacción).
      const aplicFacturaDetIds = [
        ...new Set(items.flatMap((it) => (it.aplicaciones ?? []).map((a) => a.factura_det_id))),
      ];
      const facturaDetMap = new Map<string, any>();
      if (aplicFacturaDetIds.length > 0) {
        const rows = await tx.factura_det.findMany({ where: { id: { in: aplicFacturaDetIds } } });
        for (const r of rows) facturaDetMap.set(r.id, r);
      }
      const aplicadoEnRequest = new Map<string, number>();

      // Crear detalles
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        const det = await tx.nota_remision_det.create({
          data: {
            nota_remision_cab_id: cab.id,
            producto_id: item.producto_id,
            ddesproser: item.descripcion,
            cunimed: item.unidad_medida,
            dcantproser: item.cantidad,
            precio_unitario: item.precio_unitario ?? 0,
            cantidad_aplicada: 0,
            item: i + 1,
          } as any,
        });

        // Aplicaciones many-to-many detalle
        if (item.aplicaciones && item.aplicaciones.length > 0) {
          let totalAplicado = 0;
          for (const aplic of item.aplicaciones) {
            // Validar cantidad disponible en factura_det (mapa precargado + acumulador).
            const facturaDet = facturaDetMap.get(aplic.factura_det_id);
            if (!facturaDet) throw new NotFoundException(`factura_det ${aplic.factura_det_id} no encontrado`);
            const yaAplicadoReq = aplicadoEnRequest.get(aplic.factura_det_id) ?? 0;
            const disponible =
              Number(facturaDet.dcantproser) -
              Number(facturaDet.cantidad_aplicada_remision ?? 0) -
              yaAplicadoReq;
            if (aplic.cantidad > disponible) {
              throw new BadRequestException(
                `Cantidad a aplicar (${aplic.cantidad}) supera la disponible (${disponible}) en ítem de factura`,
              );
            }
            aplicadoEnRequest.set(aplic.factura_det_id, yaAplicadoReq + aplic.cantidad);
            await tx.nota_remision_det_aplicacion.create({
              data: {
                nota_remision_det_id: det.id,
                factura_det_id: aplic.factura_det_id,
                cantidad_aplicada: aplic.cantidad,
              },
            });
            // Actualizar cantidad_aplicada_remision en factura_det
            await tx.$executeRaw`
              UPDATE factura_det
              SET cantidad_aplicada_remision = cantidad_aplicada_remision + ${aplic.cantidad}
              WHERE id = ${aplic.factura_det_id}::uuid`;
            totalAplicado += aplic.cantidad;
          }
          // Actualizar cantidad_aplicada en nota_remision_det
          await tx.$executeRaw`
            UPDATE nota_remision_det
            SET cantidad_aplicada = ${totalAplicado}
            WHERE id = ${det.id}::uuid`;
        }
      }

      // Vincular facturas a nivel de cabecera
      const validFacturasIds = (facturas_ids || []).filter((id) => !!id);
      for (const factura_cab_id of validFacturasIds) {
        await tx.nota_remision_factura.create({
          data: { nota_remision_cab_id: cab.id, factura_cab_id },
        });
      }

      // Inventario: solo la remisión SIN factura asociada descuenta stock (la
      // mercadería sale físicamente y ninguna factura la movió aún). Si tiene
      // factura asociada, la factura ya descontó → no se toca stock (evita doble).
      if (validFacturasIds.length === 0) {
        const numeroRem = `${dest}-${dpunexp}-${dnumdoc}`;
        await this.descontarStockRemision(tx, cab.id, items, empresa_id, puntoExp?.empresas_sucursales?.id ?? null, cabecera.deposito_id ?? null, numeroRem);
      }

      // Incrementar numeración
      await tx.numeraciones_documento.update({
        where: { id: cabecera.numeracion_id },
        data: { numero_actual: { increment: 1 } },
      });

      return tx.nota_remision_cab.findUnique({ where: { id: cab.id }, include: cabInclude });
    });

    // Si la empresa no usa SIFEN, aprobar localmente como MANUAL; si usa SIFEN, enviar
    const usaSifen = await this.middlewareSifen.empresaUsaSifen(empresa_id);
    if (!usaSifen) {
      const ahora = new Date();
      await this.prisma.nota_remision_cab.update({
        where: { id: (result as any).id },
        data: {
          estado: 'Aprobado',
          estado_sifen: 'Aprobado',
          mensaje_sifen: 'Aprobado localmente — empresa sin facturación electrónica',
          tipo_aprobacion: 'MANUAL',
          fecha_envio_sifen: ahora,
          fecha_firma_sifen: ahora,
          fecha_registro_sifen: ahora,
        } as any,
      });
    } else {
      this.enviarSifen((result as any).id, empresa_id, userId).catch((err) =>
        this.logger.error(
          `Error enviando NR ${(result as any).id} a SIFEN: ${err instanceof Error ? err.message : err}`,
          'NotaRemisionService',
        ),
      );
    }

    // ── Auditoría detallada de la creación ──
    const r = result as any;
    const numero = `${r?.dest}-${r?.dpunexp}-${r?.dnumdoc}`;
    const conFactura = (dto.facturas_ids || []).filter(Boolean).length > 0;
    await this.audit.log({
      empresa_id,
      user_id: userId,
      action: 'CREATE',
      entity_type: 'nota_remision',
      entity_id: r?.id,
      descripcion:
        `Nota de remisión creada ${numero}` +
        ` | Cliente: ${r?.clientes?.personas?.razon_social ?? cabecera.cliente_id}` +
        ` | Ítems: ${items.length}` +
        ` | ${conFactura ? 'Con factura(s) asociada(s) (no mueve stock)' : `Sin factura → descuenta stock${cabecera.deposito_id ? ` (depósito ${cabecera.deposito_id})` : ' (depósito principal)'}`}` +
        (usuario ? ` | Usuario: ${usuario}` : ''),
      new_value: {
        numero,
        cliente_id: cabecera.cliente_id,
        transportista_id: cabecera.transportista_id,
        chofer_id: cabecera.chofer_id,
        vehiculo_id: cabecera.vehiculo_id,
        agente_id: cabecera.agente_id ?? null,
        motivo_emision_id: cabecera.motivo_emision_id ?? null,
        tipo_transporte_id: cabecera.tipo_transporte_id ?? null,
        modalidad_transporte_id: cabecera.modalidad_transporte_id ?? null,
        kilometros: cabecera.kilometros ?? null,
        fecha_inicio_traslado: cabecera.fecha_inicio_traslado ?? null,
        fecha_fin_traslado: cabecera.fecha_fin_traslado ?? null,
        direccion_salida: cabecera.direccion_salida ?? null,
        direccion_entrega: cabecera.direccion_entrega ?? null,
        tipo_documento_asociado: conFactura ? 'electronico' : (cabecera.documento_asociado?.itipdocaso ?? 'sin_documento'),
        facturas_asociadas: (dto.facturas_ids || []).filter(Boolean),
        afecta_stock: !conFactura,
        deposito_id: cabecera.deposito_id ?? null,
        items: items.map((it) => ({ producto_id: it.producto_id, cantidad: it.cantidad })),
      },
    });

    return result;
  }

  /**
   * `filtroSucursal` va como AND aparte: `where.OR` ya lo usa la búsqueda por
   * texto y mezclarlos anularía la restricción.
   */
  private buildWhere(
    empresa_id: string,
    params: FindAllRemisionParams,
    filtroSucursal?: { dest: { in: string[] } } | null,
  ) {
    const where: any = { empresa_id };
    if (filtroSucursal) where.AND = [filtroSucursal];
    if (params.estado) where.estado = params.estado;
    if (params.estado_sifen === 'none') where.estado_sifen = null;
    else if (params.estado_sifen) where.estado_sifen = params.estado_sifen;
    if (params.cliente_id) where.cliente_id = params.cliente_id;
    if (params.transportista_id) where.transportista_id = params.transportista_id;
    if (params.chofer_id) where.chofer_id = params.chofer_id;
    if (params.vehiculo_id) where.vehiculo_id = params.vehiculo_id;
    if (params.agente_id) where.agente_id = params.agente_id;
    if (params.fechaDesde || params.fechaHasta) {
      where.dfeemide = {};
      if (params.fechaDesde) where.dfeemide.gte = new Date(params.fechaDesde);
      if (params.fechaHasta) where.dfeemide.lte = new Date(params.fechaHasta);
    }
    if (params.search) {
      const q = params.search;
      const numero = filtroNumeroDocumento(q);
      where.OR = [
        ...(numero ? [numero] : []),
        { dnumdoc: { contains: q, mode: 'insensitive' } },
        { dest: { contains: q, mode: 'insensitive' } },
        { cdc: { contains: q, mode: 'insensitive' } },
        { clientes: { personas: { razon_social: { contains: q, mode: 'insensitive' } } } },
        { transportistas: { personas: { razon_social: { contains: q, mode: 'insensitive' } } } },
      ];
    }
    return where;
  }

  async findAll(empresa_id: string, page = 1, limit = 20, params: FindAllRemisionParams = {}) {
    const alcance = await alcanceSucursalUsuario(this.prisma, params.usuarioId, empresa_id);
    const where = this.buildWhere(
      empresa_id,
      params,
      await filtroFiscalPorSucursal(this.prisma, alcance, params.sucursalId),
    );
    const [items, total] = await Promise.all([
      this.prisma.nota_remision_cab.findMany({
        where,
        include: cabInclude,
        skip: (page - 1) * limit,
        take: limit,
        orderBy: { created_at: 'desc' },
      }),
      this.prisma.nota_remision_cab.count({ where }),
    ]);
    return { items, total, page, limit, lastPage: Math.ceil(total / limit) };
  }

  async getResumen(empresa_id: string, params: FindAllRemisionParams = {}) {
    const alcance = await alcanceSucursalUsuario(this.prisma, params.usuarioId, empresa_id);
    const base = this.buildWhere(
      empresa_id,
      params,
      await filtroFiscalPorSucursal(this.prisma, alcance, params.sucursalId),
    );
    const [total, pendientesSifen, aprobadosSifen, rechazadosSifen, anuladas] = await Promise.all([
      this.prisma.nota_remision_cab.count({ where: base }),
      this.prisma.nota_remision_cab.count({ where: { ...base, estado_sifen: null, estado: { not: 'Anulado' } } }),
      this.prisma.nota_remision_cab.count({ where: { ...base, estado_sifen: 'Aprobado' } }),
      this.prisma.nota_remision_cab.count({ where: { ...base, estado_sifen: 'Rechazado' } }),
      this.prisma.nota_remision_cab.count({ where: { ...base, estado: 'Anulado' } }),
    ]);
    return { total, pendientesSifen, aprobadosSifen, rechazadosSifen, anuladas };
  }

  async findOne(id: string, empresa_id: string) {
    const item = await this.prisma.nota_remision_cab.findFirst({ where: { id, empresa_id }, include: cabIncludeDetalle });
    if (!item) throw new NotFoundException('Nota de remisión no encontrada');
    return item;
  }

  /**
   * Remisiones vinculadas a una factura (1 factura → N remisiones), con las
   * cantidades aplicadas por ítem de esa factura (vía las aplicaciones).
   */
  async findByFactura(facturaId: string, empresa_id: string) {
    const vinculos = await this.prisma.nota_remision_factura.findMany({
      where: { factura_cab_id: facturaId, nota_remision_cab: { empresa_id } },
      include: {
        nota_remision_cab: {
          select: {
            id: true,
            dest: true,
            dpunexp: true,
            dnumdoc: true,
            dfeemide: true,
            estado: true,
            estado_sifen: true,
            cdc: true,
            created_at: true,
          },
        },
      },
      orderBy: { created_at: 'desc' },
    });

    // Cantidad aplicada por ítem de factura (para el detalle por ítem).
    const aplicaciones = await this.prisma.nota_remision_det_aplicacion.findMany({
      where: { factura_det: { factura_cab_id: facturaId } },
      select: {
        factura_det_id: true,
        cantidad_aplicada: true,
        nota_remision_det: {
          select: { nota_remision_cab: { select: { id: true, dest: true, dpunexp: true, dnumdoc: true } } },
        },
      },
    });

    return {
      remisiones: vinculos.map((v) => v.nota_remision_cab).filter(Boolean),
      aplicaciones,
    };
  }

  /**
   * Remisiones facturables de un cliente (flujo Remisión → Factura):
   * aprobadas, SIN documento asociado (salieron sin factura y ya descontaron
   * stock), con al menos un ítem con saldo por facturar (dcantproser > cantidad_facturada).
   * Devuelve por ítem la cantidad disponible y datos del producto para resolver precio.
   */
  async findFacturables(empresa_id: string, clienteId: string) {
    const remisiones = await this.prisma.nota_remision_cab.findMany({
      where: {
        empresa_id,
        cliente_id: clienteId,
        estado: 'Aprobado',
        tipo_documento_asociado: null,
      },
      select: {
        id: true,
        dest: true,
        dpunexp: true,
        dnumdoc: true,
        dfeemide: true,
        created_at: true,
        nota_remision_det: {
          select: {
            id: true,
            producto_id: true,
            ddesproser: true,
            cunimed: true,
            dcantproser: true,
            cantidad_facturada: true,
            precio_unitario: true,
            productos: {
              select: {
                id: true,
                cod_producto: true,
                descripcion: true,
                precio: true,
                porcentaje_iva: true,
                unidades_de_medida: { select: { codigo: true, representacion: true } },
              },
            },
          },
        },
      },
      orderBy: { dfeemide: 'asc' },
    });

    // Mapear a items con cantidad_disponible y descartar remisiones sin saldo.
    return remisiones
      .map((r) => {
        const items = r.nota_remision_det
          .map((d) => {
            const disponible = Number(d.dcantproser) - Number(d.cantidad_facturada || 0);
            return {
              nota_remision_det_id: d.id,
              producto_id: d.producto_id,
              cod_producto: d.productos?.cod_producto ?? null,
              descripcion: d.ddesproser ?? d.productos?.descripcion ?? '',
              // cunimed es el código SIFEN de unidad como String. Priorizar el de
              // la remisión; fallback al código del producto convertido a String.
              unidad_medida:
                d.cunimed ??
                (d.productos?.unidades_de_medida?.codigo != null
                  ? String(d.productos.unidades_de_medida.codigo)
                  : 'UNI'),
              cantidad_disponible: disponible,
              precio: Number(d.productos?.precio ?? 0),
              porcentaje_iva: d.productos?.porcentaje_iva ?? null,
            };
          })
          .filter((it) => it.cantidad_disponible > 0);
        return {
          id: r.id,
          numero: `${r.dest}-${r.dpunexp}-${r.dnumdoc}`,
          dest: r.dest,
          dpunexp: r.dpunexp,
          dnumdoc: r.dnumdoc,
          dfeemide: r.dfeemide,
          items,
        };
      })
      .filter((r) => r.items.length > 0);
  }

  async anular(id: string, empresa_id: string, userId?: string) {
    const remision = await this.findOne(id, empresa_id);
    const rem = remision as any;
    if (rem.estado === 'Anulado') throw new BadRequestException('La nota de remisión ya está anulada');
    const numero = `${rem.dest}-${rem.dpunexp}-${rem.dnumdoc}`;
    const estadoAnterior = rem.estado;

    // Libera lo aplicado a facturas y repone stock; misma reversión que un ECAN/EINU aprobado por SIFEN.
    const { revertida, stockRepuesto } = await this.reversionRemision.revertirRemision(id, empresa_id);
    if (!revertida) throw new BadRequestException('La nota de remisión ya está anulada');

    await this.audit.log({
      empresa_id,
      user_id: userId,
      action: 'ANULAR',
      entity_type: 'nota_remision',
      entity_id: id,
      descripcion:
        `Nota de remisión ${numero} anulada` +
        (stockRepuesto > 0 ? ` | Stock repuesto en ${stockRepuesto} ítem(s)` : ' | Sin reposición de stock (tenía factura asociada o no maneja inventario)'),
      old_value: { estado: estadoAnterior },
      new_value: { estado: 'Anulado', stock_repuesto: stockRepuesto },
    });

    return { message: 'Nota de remisión anulada', stock_repuesto: stockRepuesto };
  }

  /** Tipo de movimiento de inventario para salida por remisión (find-or-create). */
  private async tipoMovRemision(tx: any, codigo: string, descripcion: string, afecta: number): Promise<{ id: string }> {
    let tipo = await tx.tipo_movimiento_inventario.findFirst({ where: { codigo } });
    if (!tipo) {
      tipo = await tx.tipo_movimiento_inventario.upsert({
        where: { codigo },
        update: {},
        create: { codigo, descripcion, afecta_stock: afecta },
      });
    }
    return tipo;
  }

  /**
   * Descuenta stock por una remisión SIN factura asociada (la mercadería sale
   * físicamente del depósito principal de la sucursal). Permite negativo, igual
   * que factura. Solo productos con maneja_inventario.
   */
  private async descontarStockRemision(
    tx: any,
    remisionId: string,
    itemsInput: Array<{ producto_id: string; cantidad: number }>,
    empresaId: string,
    sucursalId: string | null,
    depositoIdPreferido: string | null,
    numeroRemision?: string,
  ): Promise<void> {
    if (!sucursalId) return;
    // Usar el depósito indicado (validado por empresa+activo); si no vino o no es
    // válido, caer al principal de la sucursal (mismo criterio que factura).
    let deposito: { id: string } | null = null;
    if (depositoIdPreferido) {
      deposito = await tx.depositos.findFirst({
        where: { id: depositoIdPreferido, empresa_id: empresaId, active: true },
        select: { id: true },
      });
    }
    if (!deposito) {
      deposito = await tx.depositos.findFirst({
        where: { sucursal_id: sucursalId, empresa_id: empresaId, es_principal: true, active: true },
        select: { id: true },
      });
    }
    if (!deposito) return;

    // Política de stock de la sucursal: 'estricto' bloquea si no hay disponible;
    // 'advertencia'/'libre' permiten negativo (igual que factura).
    const sucConfig = await tx.empresas_sucursales.findUnique({
      where: { id: sucursalId },
      select: { config: true },
    });
    const politicaStock = (sucConfig?.config as Record<string, any> | null)?.politica_stock || 'advertencia';

    const tipoMov = await this.tipoMovRemision(tx, 'SALIDA_REMISION', 'Salida por remisión (sin factura)', -1);

    for (const item of itemsInput) {
      const cantidad = Number(item.cantidad || 0);
      if (cantidad <= 0) continue;
      const producto = await tx.productos.findUnique({
        where: { id: item.producto_id },
        select: { maneja_inventario: true, precio_costo: true, descripcion: true },
      });
      if (!producto?.maneja_inventario) continue;

      const stockActual = await tx.stock_deposito.findUnique({
        where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: item.producto_id } },
      });
      const disponible = stockActual
        ? Number(stockActual.cantidad_disponible) - Number(stockActual.cantidad_reservada || 0)
        : 0;
      if (politicaStock === 'estricto' && disponible < cantidad) {
        throw new BadRequestException(
          `Stock insuficiente para "${producto.descripcion}". Disponible: ${disponible}, requerido: ${cantidad}`,
        );
      }

      const nueva = (stockActual ? Number(stockActual.cantidad_disponible) : 0) - cantidad;
      await tx.stock_deposito.upsert({
        where: { deposito_id_producto_id: { deposito_id: deposito.id, producto_id: item.producto_id } },
        create: { deposito_id: deposito.id, producto_id: item.producto_id, cantidad_disponible: -cantidad },
        update: { cantidad_disponible: nueva, updated_at: new Date() },
      });
      await tx.movimientos_inventario.create({
        data: {
          deposito_origen_id: deposito.id,
          producto_id: item.producto_id,
          tipo_movimiento_id: tipoMov.id,
          cantidad,
          precio_unitario: Number(producto.precio_costo || 0),
          documento_origen: 'nota_remision',
          documento_id: remisionId,
          observaciones: numeroRemision ? `Salida por remisión ${numeroRemision} (sin factura)` : 'Salida por remisión sin factura',
        },
      });
    }
  }

  async enviarSifen(id: string, empresaId: string, userId?: string): Promise<{ success: boolean; message: string }> {
    const rem = await this.prisma.nota_remision_cab.findFirst({
      where: { id, empresa_id: empresaId },
      select: { id: true, dest: true, dpunexp: true, dnumdoc: true, estado_sifen: true },
    });
    if (!rem) return { success: false, message: 'Nota de remisión no encontrada' };
    const numero = `${rem.dest}-${rem.dpunexp}-${rem.dnumdoc}`;

    try {
      // Validación de negocio antes de enviar al middleware. Da un mensaje
      // accionable (por entidad) en vez del 400 críptico de SIFEN.
      const errores = await this.validacion.validar(id, empresaId);
      if (errores.length > 0) {
        throw new BadRequestException(
          `No se puede enviar la remisión, corrija estos datos: ${errores.join(' | ')}`,
        );
      }

      const payload = await this.sifenPayload.buildPayload(id, empresaId);
      const nroLote = `facvoice-nr-${Date.now()}`;

      const middlewarePayload = {
        tipOpe: '1',
        nro_lote: nroLote,
        iTiDE: '7',
        comprobantes: [payload],
      };
      // Deja el payload en físico (logs/sifen) para trazabilidad, igual que factura/NC.
      this.savePayloadToLog(middlewarePayload);

      const response = await this.middlewareSifen.sendDocumentToMiddlewareSifen(middlewarePayload as any);

      await this.prisma.nota_remision_cab.update({
        where: { id },
        data: {
          nro_lote: nroLote,
          estado_sifen: 'Enviado',
          fecha_envio_sifen: new Date(),
        } as any,
      });

      this.logger.log(
        `NR ${rem.dest}-${rem.dpunexp}-${rem.dnumdoc} enviada a SIFEN | lote: ${nroLote}`,
        'NotaRemisionService',
      );
      await this.audit.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'UPDATE',
        entity_type: 'nota_remision_sifen_envio',
        entity_id: id,
        descripcion: `[OK] Remisión ${numero} enviada a SIFEN | lote: ${nroLote}`,
        new_value: { estado_sifen: 'Enviado', nro_lote: nroLote },
      });
      return { success: true, message: 'Nota de remisión enviada a SIFEN', ...(response ? { data: response } : {}) } as any;
    } catch (err) {
      const msg = err instanceof Error ? err.message : 'Error desconocido';
      this.logger.error(`Error enviando NR ${id} a SIFEN: ${msg}`, 'NotaRemisionService');

      await this.prisma.nota_remision_cab.update({
        where: { id },
        data: { estado_sifen: 'Error', mensaje_sifen: msg } as any,
      });

      await this.audit.log({
        empresa_id: empresaId,
        user_id: userId,
        action: 'UPDATE',
        entity_type: 'nota_remision_sifen_envio',
        entity_id: id,
        descripcion: `[ERROR] Remisión ${numero} rechazada al enviar a SIFEN: ${msg}`,
        new_value: { estado_sifen: 'Error', mensaje_sifen: msg },
      });

      return { success: false, message: msg };
    }
  }

  async sincronizarSifen(empresaId: string): Promise<{ actualizados: number; detalles: string[] }> {
    const documentos = await this.sincronizarDocumentosSifen(empresaId);
    // Eventos (ECAN/EINU) pendientes: mismo sync que el job, que al aprobarse
    // anula la remisión y revierte stock / cantidades aplicadas a facturas.
    try {
      const eventos = await this.middlewareSifen.sincronizarSifen(empresaId, 'nota_remision', 'evento');
      if (eventos.actualizados === 0 && Object.keys(eventos.casosEvento).length === 0) return documentos;
      return {
        actualizados: documentos.actualizados + eventos.actualizados,
        detalles: [...documentos.detalles, ...eventos.detalles],
      };
    } catch (e) {
      return {
        ...documentos,
        detalles: [...documentos.detalles, `Error consultando eventos: ${e instanceof Error ? e.message : e}`],
      };
    }
  }

  private async sincronizarDocumentosSifen(empresaId: string): Promise<{ actualizados: number; detalles: string[] }> {
    // Mismos estados para DETECTAR pendientes y para MATCHEAR el registro local
    // a actualizar; si difieren, se detectan documentos que nunca se actualizan.
    const ESTADOS_SIFEN_PENDIENTES = ['Enviado', 'Pendiente', 'Documento firmado'];
    const pendientes = await this.prisma.nota_remision_cab.findMany({
      where: {
        empresa_id: empresaId,
        nro_lote: { not: null },
        estado_sifen: { in: ESTADOS_SIFEN_PENDIENTES },
      },
      select: { id: true, nro_lote: true, dest: true, dpunexp: true, dnumdoc: true },
    });

    if (pendientes.length === 0) {
      return { actualizados: 0, detalles: ['No hay notas de remisión pendientes de sincronización'] };
    }

    const lotes = [...new Set(pendientes.map((r) => r.nro_lote).filter(Boolean))];
    const detalles: string[] = [];
    let actualizados = 0;

    const responseLogin = await this.middlewareSifen.login(empresaId);
    const token = (responseLogin as { token?: string })?.token ?? null;
    const url = envs.middlewareSifenUrl;
    if (!url) throw new Error('MIDDLEWARE_SIFEN_URL no configurado');

    const parseFecha = (s: string | null): Date | null => {
      if (!s) return null;
      const [fecha, hora] = s.split(' ');
      const [d, m, y] = fecha.split('-');
      return new Date(`${y}-${m}-${d}T${hora}`);
    };

    for (const lote of lotes) {
      try {
        const res = await fetch(`${url}/api/maintenance`, {
          method: 'POST',
          headers: {
            'content-type': 'application/json',
            ...(token ? { authorization: `Bearer ${token}` } : {}),
          },
          body: JSON.stringify({ tipOpe: '2', tipo: 'documento', nro_lote: lote, fecha_inicial: null, fecha_final: null, iTiDE: null, dEst: null, dPunExp: null, dNumDoc: null, cdc: null }),
        });
        const data = (await res.json()) as { status: string; response: Array<{ cdc: string; enlaceQR: string; fecha_firma: string; fecha_registro: string; estado_documento: string; retorno_sifen?: { estado: string; mensaje: string } }> };

        if (data.status === 'success' && Array.isArray(data.response)) {
          for (const doc of data.response) {
            const cdc = doc.cdc;
            if (!cdc || cdc.length < 24) continue;
            const dest = cdc.substring(11, 14);
            const dpunexp = cdc.substring(14, 17);
            const dnumdoc = cdc.substring(17, 24);

            const registro = await this.prisma.nota_remision_cab.findFirst({
              where: { empresa_id: empresaId, dest, dpunexp, dnumdoc, estado_sifen: { in: ESTADOS_SIFEN_PENDIENTES } },
            });
            if (!registro) { detalles.push(`CDC ${cdc}: NR no encontrada`); continue; }

            const estadoSifen = doc.retorno_sifen?.estado || doc.estado_documento || 'Desconocido';
            const mensajeSifen = doc.retorno_sifen?.mensaje || null;

            try {
              await this.prisma.nota_remision_cab.update({
                where: { id: registro.id },
                data: {
                  cdc: doc.cdc,
                  enlace_qr: doc.enlaceQR,
                  fecha_firma_sifen: parseFecha(doc.fecha_firma),
                  fecha_registro_sifen: parseFecha(doc.fecha_registro),
                  estado_sifen: estadoSifen,
                  mensaje_sifen: mensajeSifen,
                  ...(estadoSifen === 'Aprobado' ? { estado: 'Aprobado' } : {}),
                  ...(estadoSifen === 'Rechazado' ? { estado: 'Rechazado' } : {}),
                } as any,
              });
              actualizados++;
              detalles.push(`NR ${dest}-${dpunexp}-${dnumdoc}: ${estadoSifen}`);
              await this.audit.log({
                empresa_id: empresaId,
                action: 'UPDATE',
                entity_type: 'nota_remision_sifen_sync',
                entity_id: registro.id,
                descripcion: `Remisión ${dest}-${dpunexp}-${dnumdoc} → SIFEN ${estadoSifen}${mensajeSifen ? ` | ${mensajeSifen}` : ''}`,
                new_value: { estado_sifen: estadoSifen, cdc: doc.cdc, mensaje_sifen: mensajeSifen },
              });
            } catch (e) {
              const msg = e instanceof Error ? e.message : 'Error';
              detalles.push(`Error actualizando NR ${dest}-${dpunexp}-${dnumdoc}: ${msg}`);
            }
          }
        }
      } catch (e) {
        detalles.push(`Error consultando lote ${lote}: ${e instanceof Error ? e.message : e}`);
      }
    }

    this.logger.log(`Sincronización SIFEN NR: ${actualizados} actualizados | empresa: ${empresaId}`, 'NotaRemisionService');
    return { actualizados, detalles };
  }

  async updateSifen(id: string, sifenData: {
    cdc?: string;
    enlace_qr?: string;
    estado_sifen?: string;
    fecha_envio_sifen?: Date;
    fecha_firma_sifen?: Date;
    fecha_registro_sifen?: Date;
    mensaje_sifen?: string;
    nro_lote?: string;
    estado?: string;
  }) {
    return this.prisma.$executeRaw`
      UPDATE nota_remision_cab SET
        cdc = COALESCE(${sifenData.cdc ?? null}, cdc),
        enlace_qr = COALESCE(${sifenData.enlace_qr ?? null}, enlace_qr),
        estado_sifen = COALESCE(${sifenData.estado_sifen ?? null}, estado_sifen),
        fecha_envio_sifen = COALESCE(${sifenData.fecha_envio_sifen ?? null}::timestamptz, fecha_envio_sifen),
        fecha_firma_sifen = COALESCE(${sifenData.fecha_firma_sifen ?? null}::timestamptz, fecha_firma_sifen),
        fecha_registro_sifen = COALESCE(${sifenData.fecha_registro_sifen ?? null}::timestamptz, fecha_registro_sifen),
        mensaje_sifen = COALESCE(${sifenData.mensaje_sifen ?? null}, mensaje_sifen),
        nro_lote = COALESCE(${sifenData.nro_lote ?? null}, nro_lote),
        estado = COALESCE(${sifenData.estado ?? null}, estado),
        updated_at = NOW()
      WHERE id = ${id}::uuid`;
  }

  /**
   * Guarda el payload enviado al middleware en físico (logs/sifen), igual que
   * los processors de factura/NC. Nunca lanza: si falla, solo loguea.
   */
  private savePayloadToLog(payload: Record<string, unknown>): void {
    try {
      const logsDir = join(process.cwd(), 'logs', 'sifen');
      if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
      const filepath = join(logsDir, `payload_nr_${timestamp}.json`);
      writeFileSync(filepath, JSON.stringify(payload, null, 2), 'utf-8');
      this.logger.log(`Payload NR guardado en: ${filepath}`, 'NotaRemisionService');
    } catch (error) {
      this.logger.error(`Error guardando payload NR en log: ${error}`, 'NotaRemisionService');
    }
  }

  /**
   * Calcular Dígito Verificador usando Módulo 11 (algoritmo SIFEN Paraguay).
   * Réplica del usado en facturas/NC (multiplicadores cíclicos 2..11).
   */
  private calcularDVModulo11(p_numero: string, p_basemax = 11): number {
    let v_numero_al = '';
    for (let i = 0; i < p_numero.length; i++) {
      const c = p_numero.charAt(i);
      v_numero_al += /\d/.test(c) ? c : c.charCodeAt(0);
    }
    let v_total = 0;
    let k = 2;
    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;
  }

  /**
   * Generar CDC (Código de Control) de 44 dígitos.
   * iTiDE(2)+dRucEm(8)+dDVEmi(1)+dEst(3)+dPunExp(3)+dNumDoc(7)+iTipCont(1)+dFeEmiDE(8)+iTipEmi(1)+dCodSeg(9)+dDVId(1)
   */
  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;
    // dfeemide se persiste tratando el wall-clock como UTC (toPrismaDate le
    // agrega 'Z'). El CDC debe usar los MISMOS componentes UTC para que la fecha
    // coincida con dFeEmiDE del payload; con getDate() local se corría un día
    // cerca de medianoche (ej. 2026-07-23T00:09Z -> día 22 en Asunción).
    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}`;
  }
}
