import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from 'src/audit/audit.service';
import { isContabilidadEnabledForEmpresa } from 'src/common/utils/module-access.util';
import { envs } from 'src/config/envs';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario, filtroSucursalOperativo } from 'src/utils/alcance-sucursal';
import { CancelarOrdenCompraDto } from './dto/cancelar-orden-compra.dto';
import { CreateOrdenCompraDto, OrdenCompraItemDto } from './dto/create-orden-compra.dto';
import { UpdateOrdenCompraDto } from './dto/update-orden-compra.dto';
import { logoEmisor, nombreFantasiaEmisor } from 'src/utils/emisor-comprobante';

type Tx = Prisma.TransactionClient;

type NormalizedItem = {
  centro_costo_id: string | null;
  requisicion_det_id: string | null;
  producto_id: string | null;
  descripcion: string;
  cantidad: number;
  precio_unitario: number;
  porcentaje_iva: number;
  subtotal: number;
};

type ModoFlujoCompra = 'directo' | 'requisicion_opcional' | 'requisicion_obligatoria';
type Matching3ViasEstado = 'no_aplica' | 'pendiente' | 'parcial' | 'completo' | 'inconsistente';

@Injectable()
export class OrdenesCompraService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly auditService: AuditService,
  ) {}

  async create(dto: CreateOrdenCompraDto, empresaId: string, userId?: string) {
    this.validatePayload(dto, true);

    const createdMeta = await this.prisma.$transaction(async (tx) => {
      await this.validateCabeceraRefs(tx, empresaId, dto.cabecera);
      const config = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!Boolean(config.habilitar_orden_compra ?? true)) {
        throw new BadRequestException('La empresa tiene deshabilitada la Orden de Compra en Configuración de Compras');
      }
      const modoFlujo = this.normalizeModoFlujoCompra(config.modo_flujo_compra);
      if (modoFlujo === 'requisicion_obligatoria' && !dto.cabecera.requisicion_id) {
        throw new BadRequestException(
          'La configuración exige requisición obligatoria. Debés informar cabecera.requisicion_id',
        );
      }
      const requiereAprobacion = Boolean(config.requiere_aprobacion || config.workflow_obligatorio || config.usar_workflow);

      const contabilidadEnabled = await isContabilidadEnabledForEmpresa(tx, empresaId);
      const items = this.normalizeItems(dto.items || [], contabilidadEnabled);
      await this.validateCentroCostoRefs(tx, empresaId, items);
      await this.assertRequisicionCompraValida(tx, empresaId, dto.cabecera.requisicion_id || null, items);
      const totals = this.calculateTotals(items);
      const numeroOrden = await this.getNextNumeroOrdenCompra(tx, empresaId, dto.cabecera.sucursal_id);

      const cabecera = await tx.orden_compra_cab.create({
        data: {
          empresa_id: empresaId,
          sucursal_id: dto.cabecera.sucursal_id,
          proveedor_id: dto.cabecera.proveedor_id,
          requisicion_id: dto.cabecera.requisicion_id || null,
          usuario_id: userId || null,
          moneda_id: dto.cabecera.moneda_id || null,
          numero_orden: numeroOrden,
          fecha_emision: dto.cabecera.fecha_emision ? new Date(dto.cabecera.fecha_emision) : new Date(),
          fecha_entrega: dto.cabecera.fecha_entrega ? new Date(dto.cabecera.fecha_entrega) : null,
          observaciones: dto.cabecera.observaciones || null,
          subtotal: totals.subtotal,
          total_iva: totals.total_iva,
          total: totals.total,
          cotizacion: this.round(Number(dto.cabecera.cotizacion ?? 1), 6),
          estado: requiereAprobacion ? 'pendiente_aprobacion' : 'pendiente',
          activo: true,
        },
      });

      if (items.length > 0) {
        await tx.orden_compra_det.createMany({
          data: items.map((item) => ({
            orden_compra_id: cabecera.id,
            centro_costo_id: item.centro_costo_id,
            requisicion_det_id: item.requisicion_det_id,
            producto_id: item.producto_id,
            descripcion: item.descripcion,
            cantidad: item.cantidad,
            cantidad_recibida: 0,
            precio_unitario: item.precio_unitario,
            porcentaje_iva: item.porcentaje_iva,
            subtotal: item.subtotal,
          })),
        });
      }

      await this.recalculateOrdenCompraEstado(tx, cabecera.id);
      return {
        id: cabecera.id,
        numero_orden: cabecera.numero_orden,
        estado: cabecera.estado,
        total: this.toNumber(cabecera.total),
      };
    });
    const created = await this.findOne(createdMeta.id, empresaId);

    await this.logOrdenCompraEvent({
      empresaId,
      userId: userId || null,
      action: 'CREATE',
      entityId: createdMeta.id,
      descripcion: `Orden de compra ${createdMeta.numero_orden || createdMeta.id} creada en estado ${String(
        createdMeta.estado || 'pendiente',
      ).toUpperCase()}`,
      newValue: {
        numero_orden: createdMeta.numero_orden,
        estado: createdMeta.estado,
        total: createdMeta.total,
      },
    });

    return created;
  }

  async findAll(
    empresaId: string,
    query?: {
      page?: string;
      limit?: string;
      fecha_desde?: string;
      fecha_hasta?: string;
      proveedor_id?: string;
      sucursal_id?: string;
      estado?: string;
      search?: string;
      /** Quién consulta. Define qué sucursales puede ver. */
      usuarioId?: string;
    },
  ) {
    const take = Math.max(1, Math.min(100, Number(query?.limit) || 20));
    const currentPage = Math.max(1, Number(query?.page) || 1);
    const skip = (currentPage - 1) * take;

    const where: Prisma.orden_compra_cabWhereInput = {
      empresa_id: empresaId,
      activo: true,
    };

    if (query?.proveedor_id) where.proveedor_id = query.proveedor_id;

    // Ver comentario equivalente en compras.service.ts.
    const alcance = await alcanceSucursalUsuario(this.prisma, query?.usuarioId, empresaId);
    const filtroSucursal = filtroSucursalOperativo(alcance, query?.sucursal_id);
    if (filtroSucursal !== undefined) where.sucursal_id = filtroSucursal;
    if (query?.estado) where.estado = query.estado;
    if (query?.fecha_desde || query?.fecha_hasta) {
      where.fecha_emision = {};
      if (query?.fecha_desde) where.fecha_emision.gte = this.parseDateBoundary(query.fecha_desde, false);
      if (query?.fecha_hasta) where.fecha_emision.lte = this.parseDateBoundary(query.fecha_hasta, true);
    }
    if (query?.search?.trim()) {
      const q = query.search.trim();
      where.OR = [
        { numero_orden: { contains: q, mode: 'insensitive' } },
        { observaciones: { contains: q, mode: 'insensitive' } },
        { proveedores: { personas: { razon_social: { contains: q, mode: 'insensitive' } } } },
        { proveedores: { personas: { ruc: { contains: q, mode: 'insensitive' } } } },
      ];
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.orden_compra_cab.findMany({
        where,
        include: {
          proveedores: {
            select: {
              id: true,
              persona_id: true,
              personas: {
                select: { razon_social: true, ruc: true, dv: true },
              },
            },
          },
          empresas_sucursales: {
            select: { id: true, descripcion: true, punto_establecimiento: true },
          },
          moneda: { select: { id: true, codigo: true, descripcion: true, simbolo: true } },
          orden_compra_det: {
            select: {
              id: true,
              centro_costo_id: true,
              producto_id: true,
              descripcion: true,
              cantidad: true,
              cantidad_recibida: true,
              precio_unitario: true,
              subtotal: true,
              cont_centros_costo: {
                select: { id: true, codigo: true, descripcion: true },
              },
            },
            orderBy: { created_at: 'asc' },
          },
        },
        orderBy: [{ fecha_emision: 'desc' }, { created_at: 'desc' }],
        skip,
        take,
      }),
      this.prisma.orden_compra_cab.count({ where }),
    ]);

    const enriched = await this.enrichOrdenes(items, empresaId);

    // KPIs sobre TODO el filtro (no la página): conteo por estado + monto total.
    const [estadoGroups, montoAgg] = await Promise.all([
      this.prisma.orden_compra_cab.groupBy({ by: ['estado'], where, _count: { _all: true } }),
      this.prisma.orden_compra_cab.aggregate({ where, _sum: { total: true } }),
    ]);
    const stats = { total, pendiente_aprobacion: 0, pendiente: 0, parcial: 0, completada: 0, cancelada: 0 };
    for (const g of estadoGroups) {
      const e = String(g.estado ?? '').toLowerCase();
      const c = g._count._all;
      if (e === 'pendiente_aprobacion') stats.pendiente_aprobacion = c;
      else if (e === 'pendiente') stats.pendiente = c;
      else if (e === 'parcial') stats.parcial = c;
      else if (e === 'completada') stats.completada = c;
      else if (e === 'cancelada') stats.cancelada = c;
    }
    const montoTotal = this.toNumber(montoAgg._sum.total);

    return {
      data: enriched,
      total,
      page: currentPage,
      limit: take,
      lastPage: Math.max(1, Math.ceil(total / take)),
      stats,
      montoTotal,
    };
  }

  async findOne(id: string, empresaId: string, tx?: Tx) {
    const db = tx ?? this.prisma;
    const orden = await db.orden_compra_cab.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      include: {
        proveedores: {
          select: {
            id: true,
            persona_id: true,
            personas: {
              select: {
                razon_social: true,
                ruc: true,
                dv: true,
                telefono: true,
                email: true,
              },
            },
          },
        },
        empresas_sucursales: {
          select: { id: true, descripcion: true, punto_establecimiento: true },
        },
        moneda: { select: { id: true, codigo: true, descripcion: true, simbolo: true } },
        orden_compra_det: {
          include: {
            productos: { select: { id: true, descripcion: true, cod_producto: true } },
            cont_centros_costo: { select: { id: true, codigo: true, descripcion: true } },
          },
          orderBy: { created_at: 'asc' },
        },
      },
    });

    if (!orden) throw new NotFoundException('Orden de compra no encontrada');
    const [enriched] = await this.enrichOrdenes([orden], empresaId, tx);
    return enriched;
  }

  async getHistorial(id: string, empresaId: string) {
    const orden = await this.prisma.orden_compra_cab.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      select: {
        id: true,
        numero_orden: true,
        estado: true,
        observaciones: true,
        created_at: true,
        updated_at: true,
        usuario: {
          select: {
            id: true,
            nombres: true,
            apellidos: true,
            username: true,
          },
        },
      },
    });
    if (!orden) throw new NotFoundException('Orden de compra no encontrada');

    const logs = await this.prisma.audit_logs.findMany({
      where: {
        empresa_id: empresaId,
        entity_type: 'orden_compra',
        entity_id: id,
      },
      orderBy: { created_at: 'desc' },
      take: 200,
    });

    const userIds = Array.from(
      new Set((logs || []).map((log) => log.user_id).filter((value): value is string => Boolean(value))),
    );
    const users = userIds.length
      ? await this.prisma.usuario.findMany({
          where: { id: { in: userIds } },
          select: {
            id: true,
            nombres: true,
            apellidos: true,
            username: true,
          },
        })
      : [];
    const usersMap = new Map(users.map((u) => [u.id, u]));

    const events = logs.map((log) => ({
      id: log.id,
      accion: String(log.action || 'UPDATE').toLowerCase(),
      comentario: log.descripcion || this.defaultComentarioAccion(log.action),
      created_at: log.created_at,
      usuario: log.user_id ? usersMap.get(log.user_id) || null : null,
    }));

    const hasCreateEvent = events.some((event) => event.accion === 'create' || event.accion === 'creada');
    if (!hasCreateEvent) {
      events.push({
        id: `oc-created-${orden.id}`,
        accion: 'create',
        comentario: `Orden de compra ${orden.numero_orden || orden.id} creada`,
        created_at: orden.created_at || orden.updated_at || new Date(),
        usuario: orden.usuario || null,
      });
    }

    const hasCancelEvent = events.some((event) => String(event.accion).toLowerCase() === 'cancel');
    if (
      !hasCancelEvent &&
      String(orden.estado || '').toLowerCase() === 'cancelada' &&
      String(orden.observaciones || '').toLowerCase().includes('cancelada:')
    ) {
      events.push({
        id: `oc-cancelada-${orden.id}`,
        accion: 'cancel',
        comentario: orden.observaciones || 'Orden cancelada',
        created_at: orden.updated_at || orden.created_at || new Date(),
        usuario: null,
      });
    }

    const sorted = events.sort((a, b) => {
      const ta = new Date(a.created_at || 0).getTime();
      const tb = new Date(b.created_at || 0).getTime();
      return tb - ta;
    });

    return {
      orden_compra_id: orden.id,
      numero_orden: orden.numero_orden,
      events: sorted,
    };
  }

  async getMatching3Vias(id: string, empresaId: string) {
    return this.prisma.$transaction(async (tx) => {
      const [config, orden] = await Promise.all([
        this.getOrCreateConfigCompras(tx, empresaId),
        tx.orden_compra_cab.findFirst({
          where: { id, empresa_id: empresaId, activo: true },
          include: {
            orden_compra_det: {
              include: {
                productos: { select: { id: true, descripcion: true, cod_producto: true } },
              },
              orderBy: { created_at: 'asc' },
            },
          },
        }),
      ]);

      if (!orden) {
        throw new NotFoundException('Orden de compra no encontrada');
      }

      const detalleIds = (orden.orden_compra_det || []).map((det) => det.id).filter(Boolean);
      const [recepciones, facturas] = await Promise.all([
        detalleIds.length
          ? tx.recepcion_compra_det.findMany({
              where: {
                orden_compra_det_id: { in: detalleIds },
                recepcion_compra_cab: {
                  empresa_id: empresaId,
                  activo: true,
                  anulado: false,
                },
              },
              select: {
                orden_compra_det_id: true,
                recepcion_id: true,
                cantidad_recibida: true,
                recepcion_compra_cab: {
                  select: {
                    fecha_recepcion: true,
                    estado: true,
                  },
                },
              },
              orderBy: [{ recepcion_compra_cab: { fecha_recepcion: 'asc' } }, { created_at: 'asc' }],
            })
          : [],
        detalleIds.length
          ? tx.compra_det.findMany({
              where: {
                orden_compra_det_id: { in: detalleIds },
                compra_cab: {
                  empresa_id: empresaId,
                  activo: true,
                  anulado: false,
                },
              },
              select: {
                orden_compra_det_id: true,
                compra_id: true,
                cantidad: true,
                precio_unitario: true,
                compra_cab: {
                  select: {
                    fecha_emision: true,
                    timbrado_proveedor: true,
                    establecimiento: true,
                    punto_expedicion: true,
                    numero_factura: true,
                  },
                },
              },
              orderBy: [{ compra_cab: { fecha_emision: 'asc' } }, { created_at: 'asc' }],
            })
          : [],
      ]);

      return this.buildMatching3ViasReport({
        orden,
        config,
        recepciones,
        facturas,
      });
    });
  }

  async generatePdf(id: string, empresaId: string) {
    const orden = await this.prisma.orden_compra_cab.findFirst({
      where: { id, empresa_id: empresaId, activo: true },
      include: {
        empresas: {
          select: {
            razon_social: true,
            nombre_fantasia: true,
            ruc: true,
            dv: true,
            celular: true,
            email: true,
            logo: true,
          },
        },
        empresas_sucursales: {
          select: {
            descripcion: true,
            punto_establecimiento: true,
            direccion: true,
            telefono: true,
            email: true,
            logo: true,
            nombre_fantasia: true,
          },
        },
        proveedores: {
          include: {
            personas: {
              select: {
                razon_social: true,
                ruc: true,
                dv: true,
                direccion: true,
                telefono: true,
                email: true,
              },
            },
          },
        },
        moneda: {
          select: {
            codigo: true,
            descripcion: true,
            simbolo: true,
          },
        },
        requisicion_compra_cab: {
          select: {
            numero_requisicion: true,
          },
        },
        orden_compra_det: {
          include: {
            productos: {
              select: {
                cod_producto: true,
                descripcion: true,
              },
            },
          },
          orderBy: { created_at: 'asc' },
        },
      },
    });

    if (!orden) {
      throw new NotFoundException('Orden de compra no encontrada');
    }
    const estadoActual = String(orden.estado || '').toLowerCase();
    const estadosPermitidos = new Set(['pendiente', 'parcial', 'completada']);
    if (!estadosPermitidos.has(estadoActual)) {
      throw new BadRequestException(
        'El PDF solo está disponible para órdenes de compra aprobadas (pendiente, parcial o completada)',
      );
    }

    const moneda = String(orden.moneda?.codigo || 'PYG').toUpperCase();
    const detalles = (orden.orden_compra_det || []).map((det, index) => {
      const cantidad = this.round(this.toNumber(det.cantidad), 4);
      const precioUnitario = this.round(this.toNumber(det.precio_unitario), 4);
      const subtotal = this.round(this.toNumber(det.subtotal) || cantidad * precioUnitario, 2);
      const porcentajeIva = this.toNumber(det.porcentaje_iva);
      const ivaMonto = porcentajeIva > 0 ? this.round(this.computeIva(subtotal, porcentajeIva), 2) : 0;

      return {
        item: index + 1,
        codigo: det.productos?.cod_producto || '',
        descripcion: String(det.descripcion || det.productos?.descripcion || '').trim() || `Ítem ${index + 1}`,
        unidad: 'UNI',
        cantidad,
        precio_unitario: precioUnitario,
        porcentaje_iva: porcentajeIva,
        iva_monto: ivaMonto,
        subtotal,
      };
    });

    if (!detalles.length) {
      throw new BadRequestException('La orden de compra no tiene ítems para generar el PDF');
    }

    const subtotal = this.round(detalles.reduce((acc, row) => acc + this.toNumber(row.subtotal), 0), 2);
    const totalIva = this.round(detalles.reduce((acc, row) => acc + this.toNumber(row.iva_monto), 0), 2);
    const subtotalIva5 = this.round(
      detalles
        .filter((row) => this.toNumber(row.porcentaje_iva) === 5)
        .reduce((acc, row) => acc + this.toNumber(row.subtotal), 0),
      2,
    );
    const subtotalIva10 = this.round(
      detalles
        .filter((row) => this.toNumber(row.porcentaje_iva) === 10)
        .reduce((acc, row) => acc + this.toNumber(row.subtotal), 0),
      2,
    );
    const totalIva5 = this.round(
      detalles
        .filter((row) => this.toNumber(row.porcentaje_iva) === 5)
        .reduce((acc, row) => acc + this.toNumber(row.iva_monto), 0),
      2,
    );
    const totalIva10 = this.round(
      detalles
        .filter((row) => this.toNumber(row.porcentaje_iva) === 10)
        .reduce((acc, row) => acc + this.toNumber(row.iva_monto), 0),
      2,
    );
    const totalOrden = this.round(this.toNumber(orden.total) || subtotal, 2);

    const payload = {
      tipo: 'view',
      documento_tipo: 'orden_compra',
      empresa: {
        razon_social: orden.empresas?.razon_social || '',
        ruc: orden.empresas?.ruc || '',
        dv: orden.empresas?.dv || '',
        direccion: orden.empresas_sucursales?.direccion || '',
        telefono: orden.empresas_sucursales?.telefono || orden.empresas?.celular || '',
        email: orden.empresas_sucursales?.email || orden.empresas?.email || '',
        // Logo y nombre comercial de la sucursal del documento (si no tiene, los de la empresa).
        nombre_fantasia: nombreFantasiaEmisor(orden.empresas_sucursales, orden.empresas),
        logo_url: logoEmisor(orden.empresas_sucursales, orden.empresas) || undefined,
      },
      proveedor: {
        razon_social: orden.proveedores?.personas?.razon_social || '',
        ruc: orden.proveedores?.personas?.ruc || '',
        dv: orden.proveedores?.personas?.dv || '',
        direccion: orden.proveedores?.personas?.direccion || '',
        telefono: orden.proveedores?.personas?.telefono || '',
        email: orden.proveedores?.personas?.email || '',
      },
      orden_compra: {
        numero: orden.numero_orden || orden.id,
        fecha_emision: orden.fecha_emision ? new Date(orden.fecha_emision).toISOString() : new Date().toISOString(),
        fecha_entrega: orden.fecha_entrega ? new Date(orden.fecha_entrega).toISOString() : null,
        estado: String(orden.estado || 'pendiente').toUpperCase(),
        sucursal: orden.empresas_sucursales?.descripcion || '',
        punto_establecimiento: orden.empresas_sucursales?.punto_establecimiento || '',
        moneda,
        moneda_descripcion: orden.moneda?.descripcion || moneda,
        cotizacion: this.toNumber(orden.cotizacion || 1),
        observaciones: orden.observaciones || '',
        requisicion_numero: orden.requisicion_compra_cab?.numero_requisicion || null,
      },
      items: detalles,
      totales: {
        subtotal,
        total_iva: totalIva,
        subtotal_iva5: subtotalIva5,
        subtotal_iva10: subtotalIva10,
        total_iva5: totalIva5,
        total_iva10: totalIva10,
        total: totalOrden,
      },
    };

    const url = `${envs.apiGeneradorPDF}/api/orden-compra/generate-pdf`;
    let pdfBuffer: Buffer;
    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 de orden de compra: ${errorBody}`);
      }

      const arrayBuffer = await response.arrayBuffer();
      pdfBuffer = Buffer.from(arrayBuffer);
    } 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 de la orden de compra: ${message}`);
    }

    const numeroOrden = String(orden.numero_orden || orden.id || 'orden-compra').replace(/\s+/g, '-');
    const filename = `orden-compra-${numeroOrden}.pdf`;
    return { pdfBuffer, filename };
  }

  async update(id: string, dto: UpdateOrdenCompraDto, empresaId: string, userId?: string) {
    this.validatePayload(dto, false);

    const updateMeta = await this.prisma.$transaction(async (tx) => {
      const config = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!Boolean(config.habilitar_orden_compra ?? true)) {
        throw new BadRequestException('La empresa tiene deshabilitada la Orden de Compra en Configuración de Compras');
      }
      const modoFlujo = this.normalizeModoFlujoCompra(config.modo_flujo_compra);
      const contabilidadEnabled = await isContabilidadEnabledForEmpresa(tx, empresaId);

      const existing = await tx.orden_compra_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        include: {
          orden_compra_det: true,
        },
      });

      if (!existing) throw new NotFoundException('Orden de compra no encontrada');
      if (String(existing.estado || '').toLowerCase() === 'cancelada') {
        throw new BadRequestException('No se puede editar una orden de compra cancelada');
      }
      if (String(existing.estado || '').toLowerCase() === 'completada') {
        throw new BadRequestException('No se puede editar una orden de compra completada');
      }

      const [recepcionesActivas, comprasActivas] = await Promise.all([
        tx.recepcion_compra_cab.count({
          where: { empresa_id: empresaId, orden_compra_id: id, activo: true, anulado: false },
        }),
        tx.compra_cab.count({
          where: { empresa_id: empresaId, orden_compra_id: id, activo: true, anulado: false },
        }),
      ]);

      if (recepcionesActivas > 0 || comprasActivas > 0) {
        throw new BadRequestException(
          'No se puede editar la orden porque ya tiene recepciones o facturas asociadas',
        );
      }

      const nextCabecera = {
        sucursal_id: dto.cabecera?.sucursal_id ?? existing.sucursal_id,
        proveedor_id: dto.cabecera?.proveedor_id ?? existing.proveedor_id,
        requisicion_id: dto.cabecera?.requisicion_id ?? existing.requisicion_id,
        moneda_id: dto.cabecera?.moneda_id ?? existing.moneda_id,
        fecha_emision: dto.cabecera?.fecha_emision
          ? new Date(dto.cabecera.fecha_emision)
          : (existing.fecha_emision ?? new Date()),
        fecha_entrega: dto.cabecera?.fecha_entrega
          ? new Date(dto.cabecera.fecha_entrega)
          : existing.fecha_entrega,
        cotizacion:
          dto.cabecera?.cotizacion !== undefined
            ? this.round(Number(dto.cabecera.cotizacion || 1), 6)
            : this.toNumber(existing.cotizacion) || 1,
        observaciones: dto.cabecera?.observaciones ?? existing.observaciones,
      };

      if (modoFlujo === 'requisicion_obligatoria' && !nextCabecera.requisicion_id) {
        throw new BadRequestException(
          'La configuración exige requisición obligatoria. Debés informar cabecera.requisicion_id',
        );
      }

      await this.validateCabeceraRefs(tx, empresaId, {
        sucursal_id: nextCabecera.sucursal_id || '',
        proveedor_id: nextCabecera.proveedor_id || '',
        moneda_id: nextCabecera.moneda_id || undefined,
      });

      const items = dto.items?.length
        ? this.normalizeItems(dto.items, contabilidadEnabled)
        : existing.orden_compra_det.map((det) => ({
            centro_costo_id: det.centro_costo_id || null,
            requisicion_det_id: det.requisicion_det_id || null,
            producto_id: det.producto_id || null,
            descripcion: String(det.descripcion || ''),
            cantidad: this.toNumber(det.cantidad),
            precio_unitario: this.toNumber(det.precio_unitario),
            porcentaje_iva: this.normalizeIva(det.porcentaje_iva),
            subtotal: this.round(this.toNumber(det.subtotal), 2),
          }));
      if (items.length === 0) {
        throw new BadRequestException('La orden de compra debe tener al menos un ítem');
      }
      await this.validateCentroCostoRefs(tx, empresaId, items);
      await this.assertRequisicionCompraValida(tx, empresaId, nextCabecera.requisicion_id || null, items);
      const totals = this.calculateTotals(items);

      await tx.orden_compra_cab.update({
        where: { id },
        data: {
          sucursal_id: nextCabecera.sucursal_id,
          proveedor_id: nextCabecera.proveedor_id,
          requisicion_id: nextCabecera.requisicion_id || null,
          moneda_id: nextCabecera.moneda_id,
          fecha_emision: nextCabecera.fecha_emision,
          fecha_entrega: nextCabecera.fecha_entrega || null,
          cotizacion: nextCabecera.cotizacion,
          observaciones: nextCabecera.observaciones || null,
          subtotal: totals.subtotal,
          total_iva: totals.total_iva,
          total: totals.total,
          usuario_id: userId || existing.usuario_id,
          updated_at: new Date(),
        },
      });

      if (dto.items?.length) {
        await tx.orden_compra_det.deleteMany({ where: { orden_compra_id: id } });
        await tx.orden_compra_det.createMany({
          data: items.map((item) => ({
            orden_compra_id: id,
            centro_costo_id: item.centro_costo_id,
            requisicion_det_id: item.requisicion_det_id,
            producto_id: item.producto_id,
            descripcion: item.descripcion,
            cantidad: item.cantidad,
            cantidad_recibida: 0,
            precio_unitario: item.precio_unitario,
            porcentaje_iva: item.porcentaje_iva,
            subtotal: item.subtotal,
          })),
        });
      }

      await this.recalculateOrdenCompraEstado(tx, id);
      const refreshed = await tx.orden_compra_cab.findUnique({
        where: { id },
        select: { id: true, numero_orden: true, estado: true, total: true },
      });
      return {
        numero_orden: refreshed?.numero_orden || existing.numero_orden || id,
        old_state: String(existing.estado || ''),
        new_state: String(refreshed?.estado || existing.estado || ''),
        old_total: this.toNumber(existing.total),
        new_total: this.toNumber(refreshed?.total),
      };
    });

    await this.logOrdenCompraEvent({
      empresaId,
      userId: userId || null,
      action: 'UPDATE',
      entityId: id,
      descripcion: `Orden de compra ${updateMeta.numero_orden} actualizada`,
      oldValue: { estado: updateMeta.old_state, total: updateMeta.old_total },
      newValue: { estado: updateMeta.new_state, total: updateMeta.new_total },
    });

    return this.findOne(id, empresaId);
  }

  async aprobar(id: string, empresaId: string, userId?: string) {
    const approveMeta = await this.prisma.$transaction(async (tx) => {
      const config = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!Boolean(config.habilitar_orden_compra ?? true)) {
        throw new BadRequestException('La empresa tiene deshabilitada la Orden de Compra en Configuración de Compras');
      }

      const orden = await tx.orden_compra_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        select: { id: true, estado: true, observaciones: true, requisicion_id: true },
      });
      if (!orden) throw new NotFoundException('Orden de compra no encontrada');

      const estadoActual = String(orden.estado || '').toLowerCase();
      if (estadoActual === 'cancelada') {
        throw new BadRequestException('No se puede aprobar una orden cancelada');
      }
      if (estadoActual === 'completada') {
        throw new BadRequestException('No se puede aprobar una orden completada');
      }
      if (estadoActual === 'pendiente_aprobacion') {
        await tx.orden_compra_cab.update({
          where: { id },
          data: { estado: 'pendiente', updated_at: new Date() },
        });
        return {
          numero_orden: orden.id,
          changed: true,
          old_state: 'pendiente_aprobacion',
          new_state: 'pendiente',
        };
      }
      return {
        numero_orden: orden.id,
        changed: false,
        old_state: estadoActual,
        new_state: estadoActual,
      };
    });

    if (approveMeta.changed) {
      await this.logOrdenCompraEvent({
        empresaId,
        userId: userId || null,
        action: 'APPROVE',
        entityId: id,
        descripcion: `Orden de compra aprobada`,
        oldValue: { estado: approveMeta.old_state },
        newValue: { estado: approveMeta.new_state },
      });
    }

    return this.findOne(id, empresaId);
  }

  async cancelar(id: string, dto: CancelarOrdenCompraDto, empresaId: string, userId?: string) {
    const cancelMeta = await this.prisma.$transaction(async (tx) => {
      const config = await this.getOrCreateConfigCompras(tx, empresaId);
      if (!Boolean(config.habilitar_orden_compra ?? true)) {
        throw new BadRequestException('La empresa tiene deshabilitada la Orden de Compra en Configuración de Compras');
      }

      const orden = await tx.orden_compra_cab.findFirst({
        where: { id, empresa_id: empresaId, activo: true },
        select: { id: true, estado: true, observaciones: true, requisicion_id: true },
      });
      if (!orden) throw new NotFoundException('Orden de compra no encontrada');

      const estadoActual = String(orden.estado || '').toLowerCase();
      if (estadoActual === 'cancelada') {
        throw new BadRequestException('La orden de compra ya está cancelada');
      }

      const [recepcionesActivas, comprasActivas] = await Promise.all([
        tx.recepcion_compra_cab.count({
          where: { empresa_id: empresaId, orden_compra_id: id, activo: true, anulado: false },
        }),
        tx.compra_cab.count({
          where: { empresa_id: empresaId, orden_compra_id: id, activo: true, anulado: false },
        }),
      ]);

      if (recepcionesActivas > 0) {
        throw new BadRequestException('No se puede cancelar: la orden ya tiene recepciones aplicadas');
      }
      if (comprasActivas > 0) {
        throw new BadRequestException('No se puede cancelar: la orden ya tiene facturas de compra asociadas');
      }

      await tx.orden_compra_cab.update({
        where: { id },
        data: {
          estado: 'cancelada',
          observaciones: dto?.motivo
            ? `${String(orden.observaciones || '').trim()}${String(orden.observaciones || '').trim() ? ' | ' : ''}Cancelada: ${dto.motivo}`
            : orden.observaciones,
          updated_at: new Date(),
        },
      });

      await this.syncRequisicionOnOrdenCompraCancelada(tx, empresaId, {
        ordenCompraId: orden.id,
        requisicionId: orden.requisicion_id || null,
        motivo: dto?.motivo || null,
      });

      return {
        numero_orden: orden.id,
        old_state: estadoActual,
        motivo: dto?.motivo || null,
      };
    });

    await this.logOrdenCompraEvent({
      empresaId,
      userId: userId || null,
      action: 'CANCEL',
      entityId: id,
      descripcion: cancelMeta.motivo
        ? `Orden de compra cancelada. Motivo: ${cancelMeta.motivo}`
        : 'Orden de compra cancelada',
      oldValue: { estado: cancelMeta.old_state },
      newValue: { estado: 'cancelada', motivo: cancelMeta.motivo || null },
    });

    return this.findOne(id, empresaId);
  }

  async recalculateOrdenCompraEstado(tx: Tx, ordenCompraId: string) {
    const orden = await tx.orden_compra_cab.findUnique({
      where: { id: ordenCompraId },
      select: {
        id: true,
        estado: true,
        orden_compra_det: {
          select: {
            id: true,
            cantidad: true,
            cantidad_recibida: true,
          },
        },
      },
    });

    if (!orden) return;
    const estadoActual = String(orden.estado || '').toLowerCase();
    if (estadoActual === 'cancelada') return;

    const detalleIds = orden.orden_compra_det.map((det) => det.id);
    const facturadoMap = await this.getFacturadoMapByDetalleIds(tx, detalleIds);

    const detalles = orden.orden_compra_det || [];
    let anyProgress = false;
    let allCompleted = detalles.length > 0;

    for (const det of detalles) {
      const ordered = this.toNumber(det.cantidad);
      const recibido = this.toNumber(det.cantidad_recibida);
      const facturado = this.toNumber(facturadoMap.get(det.id));

      if (recibido > 0 || facturado > 0) anyProgress = true;
      if (recibido + 0.0001 < ordered || facturado + 0.0001 < ordered) {
        allCompleted = false;
      }
    }

    let newState = estadoActual === 'pendiente_aprobacion' ? 'pendiente_aprobacion' : 'pendiente';
    if (allCompleted) newState = 'completada';
    else if (anyProgress) newState = 'parcial';

    if (newState !== estadoActual) {
      await tx.orden_compra_cab.update({
        where: { id: ordenCompraId },
        data: {
          estado: newState,
          updated_at: new Date(),
        },
      });
    }
  }

  private buildMatching3ViasReport(input: {
    orden: any;
    config: any;
    recepciones: Array<{
      orden_compra_det_id: string;
      recepcion_id: string;
      cantidad_recibida: Prisma.Decimal | number | null;
      recepcion_compra_cab?: {
        fecha_recepcion?: Date | null;
        estado?: string | null;
      } | null;
    }>;
    facturas: Array<{
      orden_compra_det_id: string | null;
      compra_id: string;
      cantidad: Prisma.Decimal | number | null;
      precio_unitario: Prisma.Decimal | number | null;
      compra_cab?: {
        fecha_emision?: Date | null;
        timbrado_proveedor?: string | null;
        establecimiento?: string | null;
        punto_expedicion?: string | null;
        numero_factura?: string | null;
      } | null;
    }>;
  }) {
    const toleranceQty = 0.0001;
    const tolerancePrice = 0.0001;
    const matchingEnabled = Boolean(input.config?.habilitar_matching_3_vias ?? true);
    const recepcionesByDet = new Map<string, typeof input.recepciones>();
    const facturasByDet = new Map<string, typeof input.facturas>();

    for (const row of input.recepciones || []) {
      const key = String(row.orden_compra_det_id || '');
      if (!key) continue;
      const list = recepcionesByDet.get(key) || [];
      list.push(row);
      recepcionesByDet.set(key, list);
    }
    for (const row of input.facturas || []) {
      const key = String(row.orden_compra_det_id || '');
      if (!key) continue;
      const list = facturasByDet.get(key) || [];
      list.push(row);
      facturasByDet.set(key, list);
    }

    let lineasCompletas = 0;
    let lineasPendientes = 0;
    let lineasParciales = 0;
    let lineasInconsistentes = 0;
    let lineasConDesvioPrecio = 0;
    let lineasConExcesoRecepcion = 0;
    let lineasConExcesoFacturacion = 0;
    let totalOrdenado = 0;
    let totalRecibido = 0;
    let totalFacturado = 0;

    const hallazgos: Array<{
      nivel: 'info' | 'warning' | 'error';
      codigo: string;
      mensaje: string;
      orden_compra_det_id?: string;
    }> = [];

    const lineas = (input.orden.orden_compra_det || []).map((det: any, idx: number) => {
      const detalleId = String(det.id || '');
      const recibos = recepcionesByDet.get(detalleId) || [];
      const facturas = facturasByDet.get(detalleId) || [];
      const ordered = this.round(this.toNumber(det.cantidad), 4);
      const recibidoFromDet = this.round(this.toNumber(det.cantidad_recibida), 4);
      const recibidoFromRows = this.round(
        recibos.reduce((acc, row) => acc + this.toNumber(row.cantidad_recibida), 0),
        4,
      );
      const recibido = this.round(Math.max(recibidoFromDet, recibidoFromRows), 4);
      const facturado = this.round(facturas.reduce((acc, row) => acc + this.toNumber(row.cantidad), 0), 4);
      const pendienteRecepcion = this.round(Math.max(ordered - recibido, 0), 4);
      const pendienteFactura = this.round(Math.max(recibido - facturado, 0), 4);
      const excesoRecepcion = this.round(Math.max(recibido - ordered, 0), 4);
      const excesoFacturacion = this.round(Math.max(facturado - recibido, 0), 4);
      const precioOrden = this.round(this.toNumber(det.precio_unitario), 4);
      const precioFacturadoPromedio =
        facturado > toleranceQty
          ? this.round(
              facturas.reduce((acc, row) => acc + this.toNumber(row.precio_unitario) * this.toNumber(row.cantidad), 0) /
                facturado,
              4,
            )
          : null;
      const hayDesvioPrecio = facturas.some((row) => {
        const precioFactura = this.round(this.toNumber(row.precio_unitario), 4);
        return !this.equalsAmount(precioFactura, precioOrden, tolerancePrice);
      });

      if (hayDesvioPrecio) {
        lineasConDesvioPrecio += 1;
        hallazgos.push({
          nivel: matchingEnabled ? 'error' : 'warning',
          codigo: 'PRICE_DEVIATION',
          mensaje: `Desvío de precio detectado en línea ${idx + 1}`,
          orden_compra_det_id: detalleId,
        });
      }

      if (Math.abs(recibidoFromDet - recibidoFromRows) > toleranceQty) {
        hallazgos.push({
          nivel: 'warning',
          codigo: 'RECEIPT_DRIFT',
          mensaje: `Desalineación entre acumulado de recepción y cantidad_recibida en línea ${idx + 1}`,
          orden_compra_det_id: detalleId,
        });
      }

      if (excesoRecepcion > toleranceQty) {
        lineasConExcesoRecepcion += 1;
        hallazgos.push({
          nivel: matchingEnabled ? 'error' : 'warning',
          codigo: 'RECEIPT_OVERFLOW',
          mensaje: `Recepción excedida en línea ${idx + 1}`,
          orden_compra_det_id: detalleId,
        });
      }

      if (excesoFacturacion > toleranceQty) {
        lineasConExcesoFacturacion += 1;
        hallazgos.push({
          nivel: matchingEnabled ? 'error' : 'warning',
          codigo: 'BILLING_OVERFLOW',
          mensaje: `Facturación excedida en línea ${idx + 1}`,
          orden_compra_det_id: detalleId,
        });
      }

      const isInconsistent = excesoRecepcion > toleranceQty || excesoFacturacion > toleranceQty || hayDesvioPrecio;
      const isComplete =
        !isInconsistent &&
        pendienteRecepcion <= toleranceQty &&
        pendienteFactura <= toleranceQty &&
        receivedAtLeast(ordered, recibido, toleranceQty) &&
        receivedAtLeast(ordered, facturado, toleranceQty);
      const isPending = recibido <= toleranceQty && facturado <= toleranceQty && !isInconsistent;

      let estado: Matching3ViasEstado = 'parcial';
      if (isInconsistent) estado = 'inconsistente';
      else if (isComplete) estado = 'completo';
      else if (isPending) estado = 'pendiente';

      if (estado === 'completo') lineasCompletas += 1;
      else if (estado === 'pendiente') lineasPendientes += 1;
      else if (estado === 'inconsistente') lineasInconsistentes += 1;
      else lineasParciales += 1;

      totalOrdenado += ordered;
      totalRecibido += recibido;
      totalFacturado += facturado;

      return {
        orden_compra_det_id: detalleId,
        linea: idx + 1,
        producto_id: det.producto_id || null,
        codigo_producto: det.productos?.cod_producto || null,
        descripcion: String(det.descripcion || det.productos?.descripcion || ''),
        precio_ordenado: precioOrden,
        precio_facturado_promedio: precioFacturadoPromedio,
        cantidad_ordenada: ordered,
        cantidad_recibida: recibido,
        cantidad_facturada: facturado,
        cantidad_pendiente_recepcion: pendienteRecepcion,
        cantidad_pendiente_factura: pendienteFactura,
        exceso_recepcion: excesoRecepcion,
        exceso_facturacion: excesoFacturacion,
        desvio_precio: hayDesvioPrecio,
        estado_matching: matchingEnabled ? estado : 'no_aplica',
        recepciones: recibos.map((row) => ({
          recepcion_id: row.recepcion_id,
          fecha_recepcion: row.recepcion_compra_cab?.fecha_recepcion || null,
          estado: row.recepcion_compra_cab?.estado || null,
          cantidad_recibida: this.round(this.toNumber(row.cantidad_recibida), 4),
        })),
        facturas: facturas.map((row) => ({
          compra_id: row.compra_id,
          fecha_emision: row.compra_cab?.fecha_emision || null,
          documento:
            row.compra_cab?.establecimiento && row.compra_cab?.punto_expedicion && row.compra_cab?.numero_factura
              ? `${row.compra_cab.establecimiento}-${row.compra_cab.punto_expedicion}-${row.compra_cab.numero_factura}`
              : null,
          timbrado: row.compra_cab?.timbrado_proveedor || null,
          precio_unitario: this.round(this.toNumber(row.precio_unitario), 4),
          cantidad: this.round(this.toNumber(row.cantidad), 4),
        })),
      };
    });

    const totalLineas = lineas.length;
    const pendingReception = Math.max(this.round(totalOrdenado - totalRecibido, 4), 0);
    const pendingBilling = Math.max(this.round(totalRecibido - totalFacturado, 4), 0);
    const completionRatio = totalOrdenado > toleranceQty ? Math.min(totalFacturado / totalOrdenado, 1) : 0;

    let estadoGlobal: Matching3ViasEstado = 'parcial';
    if (!matchingEnabled) {
      estadoGlobal = 'no_aplica';
    } else if (totalLineas === 0) {
      estadoGlobal = 'pendiente';
    } else if (lineasInconsistentes > 0) {
      estadoGlobal = 'inconsistente';
    } else if (lineasCompletas === totalLineas) {
      estadoGlobal = 'completo';
    } else if (lineasPendientes === totalLineas) {
      estadoGlobal = 'pendiente';
    } else {
      estadoGlobal = 'parcial';
    }

    if (!matchingEnabled) {
      hallazgos.unshift({
        nivel: 'info',
        codigo: 'MATCHING_DISABLED',
        mensaje: 'Matching 3 vías deshabilitado por configuración de empresa',
      });
    }

    return {
      orden_compra_id: input.orden.id,
      estado_orden_compra: String(input.orden.estado || ''),
      config: {
        habilitar_orden_compra: Boolean(input.config?.habilitar_orden_compra ?? true),
        habilitar_recepcion_compra: Boolean(input.config?.habilitar_recepcion_compra ?? true),
        habilitar_matching_3_vias: matchingEnabled,
      },
      estado_matching: estadoGlobal,
      resumen: {
        lineas_totales: totalLineas,
        lineas_completas: lineasCompletas,
        lineas_pendientes: lineasPendientes,
        lineas_parciales: lineasParciales,
        lineas_inconsistentes: lineasInconsistentes,
        lineas_con_desvio_precio: lineasConDesvioPrecio,
        lineas_con_exceso_recepcion: lineasConExcesoRecepcion,
        lineas_con_exceso_facturacion: lineasConExcesoFacturacion,
        cantidad_ordenada: this.round(totalOrdenado, 4),
        cantidad_recibida: this.round(totalRecibido, 4),
        cantidad_facturada: this.round(totalFacturado, 4),
        cantidad_pendiente_recepcion: this.round(pendingReception, 4),
        cantidad_pendiente_factura: this.round(pendingBilling, 4),
        cumplimiento_porcentaje: this.round(completionRatio * 100, 2),
      },
      lineas,
      hallazgos,
      actualizado_en: new Date().toISOString(),
    };

    function receivedAtLeast(target: number, current: number, tolerance: number) {
      return current + tolerance >= target;
    }
  }

  private async enrichOrdenes(rawItems: any[], empresaId: string, tx?: Tx) {
    if (!rawItems?.length) return [];
    const db = tx ?? this.prisma;

    const detailIds = rawItems.flatMap((item) =>
      (item.orden_compra_det || []).map((det: any) => det.id).filter(Boolean),
    );
    const facturadoMap = await this.getFacturadoMapByDetalleIds(db, detailIds, empresaId);

    return rawItems.map((item) => {
      const detalles = (item.orden_compra_det || []).map((det: any) => {
        const cantidad = this.toNumber(det.cantidad);
        const cantidadRecibida = this.toNumber(det.cantidad_recibida);
        const cantidadFacturada = this.toNumber(facturadoMap.get(det.id));
        return {
          ...det,
          cantidad_facturada: this.round(cantidadFacturada, 4),
          cantidad_pendiente_recepcion: this.round(Math.max(cantidad - cantidadRecibida, 0), 4),
          cantidad_pendiente_factura: this.round(Math.max(cantidadRecibida - cantidadFacturada, 0), 4),
        };
      });

      const totalOrdenado = detalles.reduce((acc: number, d: any) => acc + this.toNumber(d.cantidad), 0);
      const totalRecibido = detalles.reduce((acc: number, d: any) => acc + this.toNumber(d.cantidad_recibida), 0);
      const totalFacturado = detalles.reduce((acc: number, d: any) => acc + this.toNumber(d.cantidad_facturada), 0);

      return {
        ...item,
        orden_compra_det: detalles,
        progreso: {
          ordenado: this.round(totalOrdenado, 4),
          recibido: this.round(totalRecibido, 4),
          facturado: this.round(totalFacturado, 4),
          pendiente_recepcion: this.round(Math.max(totalOrdenado - totalRecibido, 0), 4),
          pendiente_factura: this.round(Math.max(totalRecibido - totalFacturado, 0), 4),
        },
      };
    });
  }

  private validatePayload(dto: Partial<CreateOrdenCompraDto>, isCreate: boolean) {
    if (isCreate && !dto?.cabecera) {
      throw new BadRequestException('cabecera es obligatoria');
    }
    if (isCreate && !dto?.items?.length) {
      throw new BadRequestException('Debe enviar al menos un ítem de orden de compra');
    }
    if (!isCreate && !dto?.cabecera && dto?.items === undefined) {
      throw new BadRequestException('Debe enviar cabecera y/o items para actualizar');
    }
  }

  private normalizeItems(items: OrdenCompraItemDto[], requireCentroCosto: boolean): NormalizedItem[] {
    const normalized = items.map((item, index) => {
      const descripcion = String(item.descripcion || '').trim();
      const cantidad = this.round(Number(item.cantidad || 0), 4);
      const precio = this.round(Number(item.precio_unitario || 0), 4);
      const porcentajeIva = this.normalizeIva(item.porcentaje_iva);
      const centroCostoId = String(item.centro_costo_id || '').trim() || null;

      if (!descripcion) {
        throw new BadRequestException(`Ítem ${index + 1}: descripción obligatoria`);
      }
      if (requireCentroCosto && !centroCostoId) {
        throw new BadRequestException(
          `Ítem ${index + 1}: centro_costo_id es obligatorio cuando CONTABILIDAD está habilitado`,
        );
      }
      if (cantidad <= 0) {
        throw new BadRequestException(`Ítem ${index + 1}: cantidad debe ser mayor a cero`);
      }
      if (precio < 0) {
        throw new BadRequestException(`Ítem ${index + 1}: precio_unitario inválido`);
      }

      const subtotal = this.round(cantidad * precio, 2);
      return {
        centro_costo_id: centroCostoId,
        requisicion_det_id: item.requisicion_det_id || null,
        producto_id: item.producto_id || null,
        descripcion,
        cantidad,
        precio_unitario: precio,
        porcentaje_iva: porcentajeIva,
        subtotal,
      };
    });

    return normalized;
  }

  private async validateCentroCostoRefs(tx: Tx, empresaId: string, items: NormalizedItem[]) {
    const ids = Array.from(
      new Set(items.map((item) => String(item.centro_costo_id || '').trim()).filter(Boolean)),
    );
    if (!ids.length) return;

    const centros = await tx.cont_centros_costo.findMany({
      where: {
        empresa_id: empresaId,
        activo: true,
        id: { in: ids },
      },
      select: { id: true },
    });
    const validIds = new Set(centros.map((cc) => cc.id));
    const invalid = ids.find((id) => !validIds.has(id));
    if (invalid) {
      throw new BadRequestException(
        `centro_costo_id inválido o inactivo para la empresa: ${invalid}`,
      );
    }
  }

  private calculateTotals(items: NormalizedItem[]) {
    let subtotal = 0;
    let totalIva = 0;
    for (const item of items) {
      subtotal += this.toNumber(item.subtotal);
      totalIva += this.computeIva(item.subtotal, item.porcentaje_iva);
    }
    return {
      subtotal: this.round(subtotal, 2),
      total_iva: this.round(totalIva, 2),
      total: this.round(subtotal, 2),
    };
  }

  private async validateCabeceraRefs(
    tx: Tx,
    empresaId: string,
    cabecera: { sucursal_id: string; proveedor_id: string; moneda_id?: string },
  ) {
    if (!cabecera?.sucursal_id) {
      throw new BadRequestException('sucursal_id es obligatorio en órdenes de compra');
    }
    const [sucursal, proveedor] = await Promise.all([
      tx.empresas_sucursales.findFirst({
        where: { id: cabecera.sucursal_id, empresa_id: empresaId, active: true },
        select: { id: true },
      }),
      tx.proveedores.findFirst({
        where: { id: cabecera.proveedor_id, empresa_id: empresaId, activo: true },
        select: { id: true, tipo_entidad: true },
      }),
    ]);

    if (!sucursal) throw new NotFoundException('Sucursal no encontrada para la empresa');
    if (!proveedor) throw new NotFoundException('Proveedor no encontrado para la empresa');
    // FASE 2: bloqueo de ACREEDOR_VARIO — solo puede operar vía Gastos.
    if ((proveedor as any).tipo_entidad === 'ACREEDOR_VARIO') {
      throw new BadRequestException(
        'Este tercero está clasificado como "Acreedor vario" y no puede usarse en órdenes de compra. Registrá la operación como Gasto.',
      );
    }

    if (cabecera.moneda_id) {
      const moneda = await tx.moneda.findUnique({
        where: { id: cabecera.moneda_id },
        select: { id: true },
      });
      if (!moneda) throw new NotFoundException('Moneda no encontrada');
    }
  }

  private async getOrCreateConfigCompras(tx: Tx, empresaId: string) {
    const existing = await tx.config_compras.findUnique({
      where: { empresa_id: empresaId },
    });
    if (existing) return existing;

    return tx.config_compras.create({
      data: {
        empresa_id: empresaId,
        habilitar_requisicion_compra: false,
        modo_flujo_compra: 'directo',
        afectar_stock_en_compra_directa: true,
        habilitar_orden_compra: true,
        habilitar_recepcion_compra: true,
        habilitar_matching_3_vias: true,
        habilitar_orden_pago: true,
        requiere_aprobacion: false,
        permite_pago_sin_aprobacion: true,
        generar_cxp_automatico: true,
        permitir_edicion_cxp: true,
        permitir_pago_parcial: false,
        usar_workflow: false,
        workflow_obligatorio: false,
        aplicar_pago_al_guardar: true,
      },
    });
  }

  private normalizeModoFlujoCompra(value: string | null | undefined): ModoFlujoCompra {
    if (value === 'requisicion_opcional' || value === 'requisicion_obligatoria') {
      return value;
    }
    return 'directo';
  }

  private async assertRequisicionCompraValida(
    tx: Tx,
    empresaId: string,
    requisicionId: string | null,
    items: NormalizedItem[],
  ) {
    if (!requisicionId) {
      if (items.some((item) => Boolean(item.requisicion_det_id))) {
        throw new BadRequestException(
          'Si enviás requisicion_det_id en ítems, también debés informar cabecera.requisicion_id',
        );
      }
      return;
    }

    const requisicion = await tx.requisicion_cab.findFirst({
      where: { id: requisicionId, empresa_id: empresaId, activo: true },
      include: {
        requisicion_det: {
          select: { id: true },
        },
      },
    });
    if (!requisicion) {
      throw new BadRequestException('requisicion_id inválido para la empresa');
    }
    if (String(requisicion.origen_tipo) !== 'compra') {
      throw new BadRequestException('La requisición indicada no corresponde al origen compra');
    }
    if (!['aprobada', 'ejecutada_parcial'].includes(String(requisicion.estado))) {
      throw new BadRequestException('La requisición debe estar aprobada para generar orden de compra');
    }

    const detalles = new Set(requisicion.requisicion_det.map((det) => det.id));
    for (const item of items) {
      if (!item.requisicion_det_id) {
        throw new BadRequestException(
          'Cuando la orden está vinculada a requisición, cada ítem debe incluir requisicion_det_id',
        );
      }
      if (!detalles.has(item.requisicion_det_id)) {
        throw new BadRequestException(
          `requisicion_det_id inválido o no pertenece a la requisición: ${item.requisicion_det_id}`,
        );
      }
    }
  }

  private async syncRequisicionOnOrdenCompraCancelada(
    tx: Tx,
    empresaId: string,
    input: {
      ordenCompraId: string;
      requisicionId: string | null;
      motivo?: string | null;
    },
  ) {
    if (!input.requisicionId) return;

    const req = await tx.requisicion_cab.findFirst({
      where: { id: input.requisicionId, empresa_id: empresaId, activo: true },
      select: { id: true, estado: true },
    });
    if (!req) return;

    const motivo = String(input.motivo || '').trim();

    await tx.requisicion_historial.create({
      data: {
        requisicion_id: req.id,
        accion: 'oc_cancelada',
        estado_anterior: String(req.estado || ''),
        estado_nuevo: String(req.estado || ''),
        comentario: motivo
          ? `Orden de compra ${input.ordenCompraId} cancelada: ${motivo}`
          : `Orden de compra ${input.ordenCompraId} cancelada`,
        datos: {
          orden_compra_id: input.ordenCompraId,
          motivo: motivo || null,
        } as Prisma.InputJsonValue,
      },
    });

    const ordenes = await tx.orden_compra_cab.findMany({
      where: {
        empresa_id: empresaId,
        requisicion_id: req.id,
        activo: true,
      },
      select: { id: true, estado: true },
    });
    if (!ordenes.length) return;

    const hasOrdenNoCancelada = ordenes.some(
      (oc) => String(oc.estado || '').toLowerCase() !== 'cancelada',
    );
    if (hasOrdenNoCancelada) return;

    const ordenIds = ordenes.map((oc) => oc.id);
    const [recepcionesActivas, comprasActivas, gastosActivos] = await Promise.all([
      tx.recepcion_compra_cab.count({
        where: {
          empresa_id: empresaId,
          orden_compra_id: { in: ordenIds },
          activo: true,
          anulado: false,
        },
      }),
      tx.compra_cab.count({
        where: {
          empresa_id: empresaId,
          OR: [{ requisicion_id: req.id }, { orden_compra_id: { in: ordenIds } }],
          activo: true,
          anulado: false,
        },
      }),
      tx.gasto_cab.count({
        where: {
          empresa_id: empresaId,
          requisicion_id: req.id,
          activo: true,
          anulado: false,
        },
      }),
    ]);

    if (recepcionesActivas > 0 || comprasActivas > 0 || gastosActivos > 0) return;

    const estadoActualReq = String(req.estado || '').toLowerCase();
    if (['cancelada', 'ejecutada_total', 'rechazada'].includes(estadoActualReq)) return;

    const motivoRechazo = motivo
      ? `Todas las OCs vinculadas fueron canceladas. Último motivo: ${motivo}`
      : 'Todas las órdenes de compra vinculadas fueron canceladas';

    await tx.requisicion_cab.update({
      where: { id: req.id },
      data: {
        estado: 'rechazada',
        motivo_rechazo: motivoRechazo,
        updated_at: new Date(),
      },
    });

    await tx.requisicion_historial.create({
      data: {
        requisicion_id: req.id,
        accion: 'rechazada_por_oc_cancelada',
        estado_anterior: String(req.estado || ''),
        estado_nuevo: 'rechazada',
        comentario: motivoRechazo,
        datos: {
          orden_compra_id: input.ordenCompraId,
          motivo: motivo || null,
        } as Prisma.InputJsonValue,
      },
    });
  }

  private async getNextNumeroOrdenCompra(tx: Tx, empresaId: string, sucursalId: string): Promise<string> {
    const rows = await tx.$queryRaw<
      Array<{
        id: string;
        numero_actual: number;
        numero_final: number | null;
        punto_expedicion: string | null;
        punto_establecimiento: string | null;
      }>
    >`
      SELECT
        nd.id,
        nd.numero_actual,
        nd.numero_final,
        epe.punto_expedicion,
        es.punto_establecimiento
      FROM numeraciones_documento nd
      INNER JOIN tipo_documento td ON td.id = nd.tipo_documento_id
      INNER JOIN empresas_puntos_expedicion epe ON epe.id = nd.punto_expedicion_id
      INNER JOIN empresas_sucursales es ON es.id = epe.sucursal_id
      WHERE nd.empresa_id = ${empresaId}::uuid
        AND epe.sucursal_id = ${sucursalId}::uuid
        AND nd.active = true
        AND (
          td.codigo = 401
          OR LOWER(COALESCE(td.descripcion, '')) LIKE '%orden%compra%'
        )
      ORDER BY nd.created_at ASC
      LIMIT 1
      FOR UPDATE
    `;

    if (!rows.length) {
      const next = (await tx.orden_compra_cab.count({ where: { empresa_id: empresaId } })) + 1;
      return `OC-${String(next).padStart(8, '0')}`;
    }

    const numeracion = rows[0];
    const numeroActual = Number(numeracion.numero_actual || 0);
    const numeroFinal = numeracion.numero_final === null ? null : Number(numeracion.numero_final);

    if (numeroFinal !== null && numeroActual > numeroFinal) {
      throw new BadRequestException('Se alcanzó el número final de la numeración para Orden de Compra');
    }

    const dest = String(numeracion.punto_establecimiento || '001').padStart(3, '0').slice(0, 3);
    const pexp = String(numeracion.punto_expedicion || '001').padStart(3, '0').slice(0, 3);
    const sec = String(numeroActual).padStart(7, '0');
    const numero = `${dest}-${pexp}-${sec}`;

    await tx.numeraciones_documento.update({
      where: { id: numeracion.id },
      data: { numero_actual: numeroActual + 1 },
    });

    return numero;
  }

  private async getFacturadoMapByDetalleIds(
    tx: Tx | PrismaService,
    detalleIds: string[],
    empresaId?: string,
  ): Promise<Map<string, number>> {
    const ids = Array.from(new Set((detalleIds || []).filter(Boolean)));
    if (ids.length === 0) return new Map();

    const detalles = await tx.compra_det.findMany({
      where: {
        orden_compra_det_id: { in: ids },
        compra_cab: {
          ...(empresaId ? { empresa_id: empresaId } : {}),
          activo: true,
          anulado: false,
        },
      },
      select: {
        orden_compra_det_id: true,
        cantidad: true,
      },
    });

    const map = new Map<string, number>();
    for (const det of detalles) {
      const key = String(det.orden_compra_det_id || '');
      if (!key) continue;
      map.set(key, this.toNumber(map.get(key)) + this.toNumber(det.cantidad));
    }
    return map;
  }

  private async logOrdenCompraEvent(input: {
    empresaId: string;
    userId?: string | null;
    action: string;
    entityId: string;
    descripcion: string;
    oldValue?: Record<string, unknown>;
    newValue?: Record<string, unknown>;
  }) {
    await this.auditService.log({
      empresa_id: input.empresaId,
      user_id: input.userId || undefined,
      action: String(input.action || 'UPDATE').toUpperCase(),
      entity_type: 'orden_compra',
      entity_id: input.entityId,
      descripcion: input.descripcion,
      old_value: input.oldValue,
      new_value: input.newValue,
    });
  }

  private defaultComentarioAccion(action: string | null | undefined) {
    const normalized = String(action || '').toUpperCase();
    if (normalized === 'CREATE') return 'Orden de compra creada';
    if (normalized === 'APPROVE') return 'Orden de compra aprobada';
    if (normalized === 'CANCEL') return 'Orden de compra cancelada';
    return 'Orden de compra actualizada';
  }

  private normalizeIva(value: Prisma.Decimal | number | null | undefined): number {
    const parsed = this.toNumber(value);
    if (parsed === 0) return 0;
    if (parsed === 5) return 5;
    return 10;
  }

  private computeIva(subtotal: number, porcentajeIva: number): number {
    if (porcentajeIva <= 0) return 0;
    const divisor = 1 + porcentajeIva / 100;
    const base = subtotal / divisor;
    return this.round(subtotal - base, 2);
  }

  private parseDateBoundary(value: string, endOfDay: boolean): Date {
    const parts = String(value || '')
      .split('-')
      .map(Number);
    if (parts.length === 3 && parts.every((part) => Number.isFinite(part))) {
      const [year, month, day] = parts;
      return endOfDay
        ? new Date(year, month - 1, day, 23, 59, 59, 999)
        : new Date(year, month - 1, day, 0, 0, 0, 0);
    }
    const parsed = new Date(value);
    if (Number.isNaN(parsed.getTime())) {
      return endOfDay ? new Date(9999, 0, 1, 23, 59, 59, 999) : new Date(1970, 0, 1);
    }
    if (endOfDay) parsed.setHours(23, 59, 59, 999);
    else parsed.setHours(0, 0, 0, 0);
    return parsed;
  }

  private toNumber(value: Prisma.Decimal | number | string | null | undefined): number {
    const parsed = Number(value || 0);
    return Number.isFinite(parsed) ? parsed : 0;
  }

  private round(value: number, decimals = 2): number {
    const factor = 10 ** decimals;
    return Math.round((Number(value || 0) + Number.EPSILON) * factor) / factor;
  }

  private equalsAmount(a: number, b: number, tolerance = 0.0001): boolean {
    return Math.abs(this.toNumber(a) - this.toNumber(b)) <= tolerance;
  }
}
