import {
  BadRequestException,
  ForbiddenException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import { Prisma, presupuesto_estado } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import { alcanceSucursalUsuario, filtroSucursalOperativo } from 'src/utils/alcance-sucursal';
import { CalcItemInput, calcItem, calcTotals } from './presupuestos.calc';
import { CreatePresupuestoDto } from './dto/create-presupuesto.dto';
import { PresupuestoItemDto } from './dto/presupuesto-item.dto';
import { QueryPresupuestosDto } from './dto/query-presupuestos.dto';
import { UpdatePresupuestoDto } from './dto/update-presupuesto.dto';

/** Include estándar para devolver un presupuesto completo. */
const FULL_INCLUDE = {
  items: { orderBy: { orden: 'asc' as const } },
  historial: { orderBy: { fecha: 'desc' as const } },
  emails: { orderBy: { fecha_envio: 'desc' as const } },
  aprobaciones: { orderBy: { fecha: 'asc' as const } },
  clientes: { include: { personas: { select: { razon_social: true, ruc: true, nro_documento: true, email: true, telefono: true } } } },
  vendedor: { select: { id: true, nombre: true, apellido: true, email: true, telefono: true } },
  sucursal: { select: { id: true, descripcion: true, punto_establecimiento: true } },
  moneda: { select: { id: true, codigo: true, descripcion: true, simbolo: true, decimales: true } },
} satisfies Prisma.presupuesto_cabInclude;
const PRESUPUESTO_ESTADO_FACTURADO = 'FACTURADO' as unknown as presupuesto_estado;

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

  constructor(private readonly prisma: PrismaService) {}

  // ── Configuración (M36) ─────────────────────────────────────────────────────

  /** Devuelve la config del módulo para la empresa, creándola con defaults si no existe. */
  async getConfig(empresa_id: string) {
    const existing = await this.prisma.presupuesto_config.findUnique({ where: { empresa_id } });
    if (existing) return existing;
    return this.prisma.presupuesto_config.create({ data: { empresa_id } });
  }

  /** Actualiza (o crea) los parámetros globales del módulo para la empresa. */
  async updateConfig(empresa_id: string, dto: Partial<Prisma.presupuesto_configUncheckedUpdateInput>) {
    await this.getConfig(empresa_id);
    return this.prisma.presupuesto_config.update({
      where: { empresa_id },
      data: { ...dto, updated_at: new Date() },
    });
  }

  // ── Helpers internos ──────────────────────────────────────────────────────

  /**
   * Reserva atómicamente el número del presupuesto siguiendo el estándar de numeraciones
   * por sucursal (igual que Pedidos/OV): busca una numeración activa de tipo "Presupuesto"
   * (tipo_documento.codigo = 500) para la sucursal —con fallback a la empresa— y arma
   * `PRES-{YYYY}-{est}-{pexp}-{correlativo}` (ej: PRES-2026-001-001-0000007).
   * Si la empresa aún no configuró numeración, cae al correlativo previo a nivel empresa.
   */
  private async reservarNumeroPresupuesto(
    tx: Prisma.TransactionClient,
    empresa_id: string,
    sucursal_id: string | null | undefined,
    prefijoTpl: string,
  ): Promise<string> {
    const now = new Date();
    const prefijo = prefijoTpl
      .replace('{YYYY}', String(now.getFullYear()))
      .replace('{YY}', String(now.getFullYear()).slice(-2));

    const tipoPres = await tx.tipo_documento.findFirst({ where: { codigo: 500 } });

    let numeracionId: string | null = null;
    if (tipoPres) {
      if (sucursal_id) {
        const numSuc = await tx.$queryRaw<Array<{ id: string }>>`
          SELECT nd.id FROM numeraciones_documento nd
          JOIN empresas_puntos_expedicion epe ON epe.id = nd.punto_expedicion_id
          WHERE nd.empresa_id = ${empresa_id}::uuid
            AND nd.tipo_documento_id = ${tipoPres.id}::uuid
            AND nd.active = true
            AND epe.sucursal_id = ${sucursal_id}::uuid
          LIMIT 1`;
        if (numSuc.length > 0) numeracionId = numSuc[0].id;
      }
      if (!numeracionId) {
        const numEmp = await tx.$queryRaw<Array<{ id: string }>>`
          SELECT id FROM numeraciones_documento
          WHERE empresa_id = ${empresa_id}::uuid
            AND tipo_documento_id = ${tipoPres.id}::uuid
            AND active = true
          LIMIT 1`;
        if (numEmp.length > 0) numeracionId = numEmp[0].id;
      }
    }

    if (numeracionId) {
      const rows = await tx.$queryRaw<
        Array<{ id: string; numero_actual: number; numero_final: number | null; punto_expedicion_id: string }>
      >`SELECT id, numero_actual, numero_final, punto_expedicion_id
        FROM numeraciones_documento WHERE id = ${numeracionId}::uuid FOR UPDATE`;
      if (rows.length > 0) {
        const n = rows[0];
        if (n.numero_final && n.numero_actual >= n.numero_final) {
          throw new BadRequestException('Se alcanzó el número final de la numeración de presupuestos');
        }
        const puntoExp = await tx.empresas_puntos_expedicion.findUnique({
          where: { id: n.punto_expedicion_id },
          include: { empresas_sucursales: { select: { punto_establecimiento: true } } },
        });
        const est = puntoExp?.empresas_sucursales?.punto_establecimiento || '001';
        const pexp = puntoExp?.punto_expedicion || '001';
        const corr = String(n.numero_actual).padStart(7, '0');
        await tx.numeraciones_documento.update({
          where: { id: numeracionId },
          data: { numero_actual: n.numero_actual + 1 },
        });
        return `${prefijo}${est}-${pexp}-${corr}`;
      }
    }

    // Fallback (empresa sin numeración configurada): correlativo a nivel empresa.
    const last = await tx.presupuesto_cab.findFirst({
      where: { empresa_id, numero: { startsWith: prefijo } },
      orderBy: { numero: 'desc' },
      select: { numero: true },
    });
    let seq = 1;
    if (last?.numero) {
      const parsed = parseInt(last.numero.slice(prefijo.length).replace(/\D/g, ''), 10);
      if (Number.isFinite(parsed)) seq = parsed + 1;
    }
    return `${prefijo}${String(seq).padStart(4, '0')}`;
  }

  private async resolverMonedaId(empresa_id: string, monedaIdDto: string | undefined, monedaBaseCodigo: string) {
    if (monedaIdDto) return monedaIdDto;
    const moneda = await this.prisma.moneda.findFirst({
      where: { codigo: monedaBaseCodigo, active: true },
      select: { id: true },
    });
    return moneda?.id ?? null;
  }

  /**
   * Resuelve la condición de pago contra el catálogo único `condiciones_pago`
   * (pantalla Métodos de Pago, compartido con OV/facturación). Toma la `descripcion`
   * como snapshot (para el PDF).
   * Prioridad:
   * 1) condicion_pago_id válido
   * 2) condicion_pago (texto libre por compatibilidad API)
   * 3) condición marcada como `es_default`
   * 4) fallback a primera condición activa
   */
  private async resolverCondicion(
    empresa_id: string,
    condicionPagoId: string | undefined,
    condicionPagoTexto: string | undefined,
  ): Promise<{ condicion_pago_id: string | null; condicion_pago: string | null }> {
    if (condicionPagoId) {
      const c = await this.prisma.condiciones_pago.findFirst({
        where: { id: condicionPagoId, empresa_id },
        select: { id: true, descripcion: true },
      });
      if (!c) throw new BadRequestException('La condición de pago no existe o no pertenece a la empresa');
      return { condicion_pago_id: c.id, condicion_pago: c.descripcion };
    }
    // Texto libre explícito (compatibilidad con cargas manuales por API).
    if (condicionPagoTexto) {
      return { condicion_pago_id: null, condicion_pago: condicionPagoTexto };
    }
    // Sin selección: la condición predeterminada del catálogo único.
    const def = await this.prisma.condiciones_pago.findFirst({
      where: { empresa_id, es_default: true, activo: true },
      select: { id: true, descripcion: true },
    });
    if (def) return { condicion_pago_id: def.id, condicion_pago: def.descripcion };

    const firstActive = await this.prisma.condiciones_pago.findFirst({
      where: { empresa_id, activo: true },
      orderBy: [{ es_default: 'desc' }, { descripcion: 'asc' }],
      select: { id: true, descripcion: true },
    });
    return firstActive
      ? { condicion_pago_id: firstActive.id, condicion_pago: firstActive.descripcion }
      : { condicion_pago_id: null, condicion_pago: null };
  }

  private toCalcInput(item: PresupuestoItemDto): CalcItemInput {
    return {
      tipo: item.tipo,
      cantidad: Number(item.cantidad ?? 1),
      precio_unitario: Number(item.precio_unitario ?? 0),
      descuento_pct: Number(item.descuento_pct ?? 0),
      descuento_monto: Number(item.descuento_monto ?? 0),
      iva_categoria: item.iva_categoria ?? 'IVA_10',
    };
  }

  /** Mapea ítems del DTO a filas de creación con sus cálculos resueltos. */
  private buildItemsData(items: PresupuestoItemDto[]) {
    return items.map((item, idx) => {
      const calc = calcItem(this.toCalcInput(item));
      return {
        orden: item.orden ?? idx + 1,
        tipo: item.tipo,
        producto_id: item.producto_id ?? null,
        descripcion: item.descripcion,
        notas_item: item.notas_item ?? null,
        cantidad: Number(item.cantidad ?? 1),
        unidad_medida: item.unidad_medida ?? null,
        precio_unitario: Number(item.precio_unitario ?? 0),
        descuento_pct: Number(item.descuento_pct ?? 0),
        descuento_monto: Number(item.descuento_monto ?? 0),
        iva_categoria: item.iva_categoria ?? 'IVA_10',
        subtotal_con_iva: calc.subtotal_con_iva,
        monto_iva: calc.monto_iva,
      };
    });
  }

  private totalsFromItems(items: PresupuestoItemDto[], descGlobalPct: number, tipoCambio?: number | null) {
    const { totals } = calcTotals(items.map((i) => this.toCalcInput(i)), descGlobalPct, tipoCambio);
    return {
      subtotal_exento: totals.subtotal_exento,
      base_iva5: totals.base_iva5,
      iva5: totals.iva5,
      base_iva10: totals.base_iva10,
      iva10: totals.iva10,
      descuento_global_pct: totals.descuento_global_pct,
      descuento_global_monto: totals.descuento_global_monto,
      total: totals.total,
      total_equivalente: totals.total_equivalente,
    };
  }

  /** Recalcula y persiste los totales de la cabecera a partir de sus ítems actuales. */
  private async recalcularCabecera(presupuesto_id: string) {
    const cab = await this.prisma.presupuesto_cab.findUnique({
      where: { id: presupuesto_id },
      include: { items: true },
    });
    if (!cab) return;
    const itemsDto = cab.items.map((i) => ({
      tipo: i.tipo,
      cantidad: Number(i.cantidad),
      precio_unitario: Number(i.precio_unitario),
      descuento_pct: Number(i.descuento_pct),
      descuento_monto: Number(i.descuento_monto ?? 0),
      iva_categoria: i.iva_categoria,
    })) as PresupuestoItemDto[];
    const totals = this.totalsFromItems(itemsDto, Number(cab.descuento_global_pct), cab.tipo_cambio_usd ? Number(cab.tipo_cambio_usd) : null);
    await this.prisma.presupuesto_cab.update({
      where: { id: presupuesto_id },
      data: { ...totals, updated_at: new Date() },
    });
  }

  private async registrarHistorial(
    presupuesto_id: string,
    estado_nuevo: presupuesto_estado,
    estado_anterior: presupuesto_estado | null,
    user_id?: string,
    comentario?: string,
  ) {
    await this.prisma.presupuesto_historial.create({
      data: { presupuesto_id, estado_anterior, estado_nuevo, usuario_id: user_id ?? null, comentario: comentario ?? null },
    });
  }

  // ── CRUD ──────────────────────────────────────────────────────────────────

  async create(dto: CreatePresupuestoDto, empresa_id: string, user_id?: string, usuario?: string) {
    const config = await this.getConfig(empresa_id);

    // Validar que el cliente pertenezca a la empresa
    const cliente = await this.prisma.clientes.findFirst({
      where: { id: dto.cliente_id, deleted: false, personas: { empresa_id } },
      select: { id: true },
    });
    if (!cliente) throw new BadRequestException('El cliente no existe o no pertenece a la empresa');

    const moneda_id = await this.resolverMonedaId(empresa_id, dto.moneda_id, config.moneda_base);

    const fechaVigencia = dto.fecha_vigencia
      ? new Date(dto.fecha_vigencia)
      : new Date(Date.now() + config.dias_vigencia * 24 * 60 * 60 * 1000);

    const itemsData = this.buildItemsData(dto.items);
    const totals = this.totalsFromItems(dto.items, dto.descuento_global_pct ?? 0, dto.tipo_cambio_usd ?? null);
    const cond = await this.resolverCondicion(empresa_id, dto.condicion_pago_id, dto.condicion_pago);

    const created = await this.prisma.$transaction(async (tx) => {
      const numero = await this.reservarNumeroPresupuesto(tx, empresa_id, dto.sucursal_id ?? null, config.prefijo_numero);
      const cab = await tx.presupuesto_cab.create({
        data: {
          empresa_id,
          numero,
          version: 1,
          cliente_id: dto.cliente_id,
          contacto_cliente: dto.contacto_cliente ?? null,
          vendedor_id: dto.vendedor_id ?? null,
          titulo: dto.titulo,
          estado: presupuesto_estado.DRAFT,
          sucursal_id: dto.sucursal_id ?? null,
          moneda_id,
          tipo_cambio_usd: dto.tipo_cambio_usd ?? null,
          fecha_vigencia: fechaVigencia,
          condicion_pago: cond.condicion_pago,
          condicion_pago_id: cond.condicion_pago_id,
          notas_cliente: dto.notas_cliente ?? null,
          observaciones_internas: dto.observaciones_internas ?? null,
          ...totals,
          usuario: usuario ?? null,
          created_by: user_id ?? null,
          items: { create: itemsData },
        },
      });
      await tx.presupuesto_historial.create({
        data: { presupuesto_id: cab.id, estado_anterior: null, estado_nuevo: presupuesto_estado.DRAFT, usuario_id: user_id ?? null, comentario: 'Presupuesto creado' },
      });
      return cab;
    });

    return this.findOne(created.id, empresa_id);
  }

  async findAll(query: QueryPresupuestosDto, empresa_id: string, usuarioId?: string) {
    const page = Number(query.page) || 1;
    const limit = Number(query.limit) || 10;

    // El presupuesto guarda su sucursal, así que el alcance se aplica directo.
    const alcance = await alcanceSucursalUsuario(this.prisma, usuarioId, empresa_id);
    const filtroSucursal = filtroSucursalOperativo(alcance);

    const where: Prisma.presupuesto_cabWhereInput = {
      empresa_id,
      deleted: false,
      ...(query.estado ? { estado: query.estado } : {}),
      ...(query.cliente_id ? { cliente_id: query.cliente_id } : {}),
      ...(query.vendedor_id ? { vendedor_id: query.vendedor_id } : {}),
      ...(query.q
        ? { OR: [{ numero: { contains: query.q, mode: 'insensitive' } }, { titulo: { contains: query.q, mode: 'insensitive' } }] }
        : {}),
      ...(query.fecha_desde || query.fecha_hasta
        ? {
            fecha_emision: {
              ...(query.fecha_desde ? { gte: new Date(query.fecha_desde) } : {}),
              ...(query.fecha_hasta ? { lte: new Date(query.fecha_hasta) } : {}),
            },
          }
        : {}),
      ...(query.monto_min != null || query.monto_max != null
        ? { total: { ...(query.monto_min != null ? { gte: query.monto_min } : {}), ...(query.monto_max != null ? { lte: query.monto_max } : {}) } }
        : {}),
      ...(filtroSucursal !== undefined ? { sucursal_id: filtroSucursal } : {}),
    };

    const [items, total] = await Promise.all([
      this.prisma.presupuesto_cab.findMany({
        where,
        orderBy: { created_at: 'desc' },
        skip: (page - 1) * limit,
        take: limit,
        include: {
          clientes: { include: { personas: { select: { razon_social: true, ruc: true, email: true } } } },
          vendedor: { select: { id: true, nombre: true, apellido: true } },
          moneda: { select: { codigo: true, simbolo: true } },
          _count: { select: { items: true } },
        },
      }),
      this.prisma.presupuesto_cab.count({ where }),
    ]);

    // Agregados sobre TODO el filtro (no la página) para los KPIs de la pantalla:
    // conteo por estado y suma de monto por moneda. Usa el mismo `where`.
    const [estadoGroups, montoGroups] = await Promise.all([
      this.prisma.presupuesto_cab.groupBy({ by: ['estado'], where, _count: { _all: true } }),
      this.prisma.presupuesto_cab.groupBy({ by: ['moneda_id'], where, _sum: { total: true } }),
    ]);

    // stats: { total, <ESTADO>: count } — el frontend deriva borradores/activos/por-aprobar.
    const stats: Record<string, number> = { total };
    for (const g of estadoGroups) {
      if (g.estado) stats[g.estado] = g._count._all;
    }

    const monedaIds = montoGroups
      .map((g) => g.moneda_id)
      .filter((id): id is string => Boolean(id));
    const monedas = monedaIds.length
      ? await this.prisma.moneda.findMany({
          where: { id: { in: monedaIds } },
          select: { id: true, codigo: true },
        })
      : [];
    const codigoPorMonedaId = new Map(monedas.map((m) => [m.id, m.codigo]));
    const montosPorMoneda: Record<string, number> = {};
    for (const g of montoGroups) {
      const codigo = (g.moneda_id && codigoPorMonedaId.get(g.moneda_id)) || 'PYG';
      montosPorMoneda[codigo] = (montosPorMoneda[codigo] || 0) + Number(g._sum.total ?? 0);
    }

    return {
      items,
      total,
      page,
      limit,
      lastPage: Math.max(1, Math.ceil(total / limit)),
      stats,
      montosPorMoneda,
    };
  }

  async findOne(id: string, empresa_id: string) {
    const presupuesto = await this.prisma.presupuesto_cab.findFirst({
      where: { id, empresa_id, deleted: false },
      include: FULL_INCLUDE,
    });
    if (!presupuesto) throw new NotFoundException('Presupuesto no encontrado');
    return presupuesto;
  }

  /** Sólo editable en DRAFT. Si llegan `items`, reemplazan el detalle completo. */
  async update(id: string, dto: UpdatePresupuestoDto, empresa_id: string) {
    const actual = await this.findOne(id, empresa_id);
    if (actual.estado !== presupuesto_estado.DRAFT) {
      throw new ForbiddenException('Sólo se pueden editar presupuestos en estado Borrador');
    }

    const descGlobal = dto.descuento_global_pct ?? Number(actual.descuento_global_pct);
    const tipoCambio = dto.tipo_cambio_usd ?? (actual.tipo_cambio_usd ? Number(actual.tipo_cambio_usd) : null);

    // Condición de pago: sólo se toca si el DTO trae alguno de los dos campos.
    const condPatch =
      dto.condicion_pago_id !== undefined || dto.condicion_pago !== undefined
        ? await this.resolverCondicion(empresa_id, dto.condicion_pago_id, dto.condicion_pago)
        : null;

    await this.prisma.$transaction(async (tx) => {
      if (dto.items) {
        await tx.presupuesto_det.deleteMany({ where: { presupuesto_id: id } });
        await tx.presupuesto_det.createMany({
          data: this.buildItemsData(dto.items).map((d) => ({ ...d, presupuesto_id: id })),
        });
      }

      const itemsParaTotales: PresupuestoItemDto[] = (dto.items ??
        actual.items.map((i) => ({
          tipo: i.tipo,
          cantidad: Number(i.cantidad),
          precio_unitario: Number(i.precio_unitario),
          descuento_pct: Number(i.descuento_pct),
          descuento_monto: Number(i.descuento_monto ?? 0),
          iva_categoria: i.iva_categoria,
          descripcion: i.descripcion,
        }))) as PresupuestoItemDto[];
      const totals = this.totalsFromItems(itemsParaTotales, descGlobal, tipoCambio);

      await tx.presupuesto_cab.update({
        where: { id },
        data: {
          cliente_id: dto.cliente_id ?? undefined,
          contacto_cliente: dto.contacto_cliente ?? undefined,
          vendedor_id: dto.vendedor_id ?? undefined,
          titulo: dto.titulo ?? undefined,
          moneda_id: dto.moneda_id ?? undefined,
          tipo_cambio_usd: dto.tipo_cambio_usd ?? undefined,
          fecha_vigencia: dto.fecha_vigencia ? new Date(dto.fecha_vigencia) : undefined,
          ...(condPatch ? { condicion_pago: condPatch.condicion_pago, condicion_pago_id: condPatch.condicion_pago_id } : {}),
          notas_cliente: dto.notas_cliente ?? undefined,
          observaciones_internas: dto.observaciones_internas ?? undefined,
          ...totals,
          updated_at: new Date(),
        },
      });
    });

    return this.findOne(id, empresa_id);
  }

  /** Soft delete → CANCELLED. */
  async remove(id: string, empresa_id: string, user_id?: string) {
    const actual = await this.findOne(id, empresa_id);
    if (actual.estado === presupuesto_estado.CONVERTED || actual.estado === PRESUPUESTO_ESTADO_FACTURADO) {
      throw new ForbiddenException('No se puede cancelar un presupuesto ya convertido');
    }
    await this.prisma.presupuesto_cab.update({
      where: { id },
      data: { estado: presupuesto_estado.CANCELLED, deleted: true, deleted_at: new Date(), token_acceso_publico: null },
    });
    await this.registrarHistorial(id, presupuesto_estado.CANCELLED, actual.estado, user_id, 'Presupuesto cancelado');
    return { message: 'Presupuesto cancelado' };
  }

  // ── Ítems ───────────────────────────────────────────────────────────────────

  private async ensureDraft(id: string, empresa_id: string) {
    const cab = await this.findOne(id, empresa_id);
    if (cab.estado !== presupuesto_estado.DRAFT) {
      throw new ForbiddenException('Sólo se pueden modificar ítems de presupuestos en Borrador');
    }
    return cab;
  }

  async addItem(id: string, item: PresupuestoItemDto, empresa_id: string) {
    const cab = await this.ensureDraft(id, empresa_id);
    const orden = item.orden ?? (cab.items.length ? Math.max(...cab.items.map((i) => i.orden)) + 1 : 1);
    const [data] = this.buildItemsData([{ ...item, orden }]);
    await this.prisma.presupuesto_det.create({ data: { ...data, presupuesto_id: id } });
    await this.recalcularCabecera(id);
    return this.findOne(id, empresa_id);
  }

  async updateItem(id: string, itemId: string, item: PresupuestoItemDto, empresa_id: string) {
    await this.ensureDraft(id, empresa_id);
    const existing = await this.prisma.presupuesto_det.findFirst({ where: { id: itemId, presupuesto_id: id } });
    if (!existing) throw new NotFoundException('Ítem no encontrado');
    const [data] = this.buildItemsData([{ ...item, orden: item.orden ?? existing.orden }]);
    await this.prisma.presupuesto_det.update({ where: { id: itemId }, data });
    await this.recalcularCabecera(id);
    return this.findOne(id, empresa_id);
  }

  async deleteItem(id: string, itemId: string, empresa_id: string) {
    await this.ensureDraft(id, empresa_id);
    const existing = await this.prisma.presupuesto_det.findFirst({ where: { id: itemId, presupuesto_id: id } });
    if (!existing) throw new NotFoundException('Ítem no encontrado');
    await this.prisma.presupuesto_det.delete({ where: { id: itemId } });
    await this.recalcularCabecera(id);
    return this.findOne(id, empresa_id);
  }

  async reorderItems(id: string, orderedIds: string[], empresa_id: string) {
    await this.ensureDraft(id, empresa_id);
    await this.prisma.$transaction(
      orderedIds.map((itemId, idx) =>
        this.prisma.presupuesto_det.updateMany({ where: { id: itemId, presupuesto_id: id }, data: { orden: idx + 1 } }),
      ),
    );
    return this.findOne(id, empresa_id);
  }

  // ── Versionado ──────────────────────────────────────────────────────────────

  async newVersion(id: string, empresa_id: string, user_id?: string, usuario?: string) {
    const actual = await this.findOne(id, empresa_id);
    const origenId = actual.presupuesto_origen_id ?? actual.id;

    // Mayor versión existente para este número.
    const maxVersion = await this.prisma.presupuesto_cab.aggregate({
      where: { empresa_id, numero: actual.numero },
      _max: { version: true },
    });
    const nuevaVersion = (maxVersion._max.version ?? actual.version) + 1;

    const created = await this.prisma.$transaction(async (tx) => {
      const cab = await tx.presupuesto_cab.create({
        data: {
          empresa_id,
          numero: actual.numero,
          version: nuevaVersion,
          presupuesto_origen_id: origenId,
          cliente_id: actual.cliente_id,
          contacto_cliente: actual.contacto_cliente,
          vendedor_id: actual.vendedor_id,
          titulo: actual.titulo,
          estado: presupuesto_estado.DRAFT,
          sucursal_id: actual.sucursal_id,
          moneda_id: actual.moneda_id,
          tipo_cambio_usd: actual.tipo_cambio_usd,
          fecha_vigencia: actual.fecha_vigencia,
          condicion_pago: actual.condicion_pago,
          condicion_pago_id: actual.condicion_pago_id,
          notas_cliente: actual.notas_cliente,
          observaciones_internas: actual.observaciones_internas,
          subtotal_exento: actual.subtotal_exento,
          base_iva5: actual.base_iva5,
          iva5: actual.iva5,
          base_iva10: actual.base_iva10,
          iva10: actual.iva10,
          descuento_global_pct: actual.descuento_global_pct,
          descuento_global_monto: actual.descuento_global_monto,
          total: actual.total,
          total_equivalente: actual.total_equivalente,
          usuario: usuario ?? null,
          created_by: user_id ?? null,
          items: {
            create: actual.items.map((i) => ({
              orden: i.orden,
              tipo: i.tipo,
              producto_id: i.producto_id,
              descripcion: i.descripcion,
              notas_item: i.notas_item,
              cantidad: i.cantidad,
              unidad_medida: i.unidad_medida,
              precio_unitario: i.precio_unitario,
              descuento_pct: i.descuento_pct,
              descuento_monto: i.descuento_monto,
              iva_categoria: i.iva_categoria,
              subtotal_con_iva: i.subtotal_con_iva,
              monto_iva: i.monto_iva,
            })),
          },
        },
      });
      await tx.presupuesto_historial.create({
        data: { presupuesto_id: cab.id, estado_anterior: null, estado_nuevo: presupuesto_estado.DRAFT, usuario_id: user_id ?? null, comentario: `Nueva versión v${nuevaVersion} a partir de v${actual.version}` },
      });

      // Reemplazar las versiones anteriores activas del mismo número: pasan a
      // SUPERSEDED para que su enlace público deje de aceptar respuestas y no
      // cuenten como ofertas vigentes. No se tocan las terminales (convertidas,
      // canceladas) ni una eventual versión ya aceptada.
      const previas = await tx.presupuesto_cab.findMany({
        where: {
          empresa_id,
          numero: actual.numero,
          id: { not: cab.id },
          deleted: false,
          estado: {
            notIn: [
              presupuesto_estado.CONVERTED,
              PRESUPUESTO_ESTADO_FACTURADO,
              presupuesto_estado.CANCELLED,
              presupuesto_estado.ACCEPTED,
              presupuesto_estado.SUPERSEDED,
            ],
          },
        },
        select: { id: true, estado: true },
      });
      if (previas.length) {
        await tx.presupuesto_cab.updateMany({
          where: { id: { in: previas.map((v) => v.id) } },
          data: { estado: presupuesto_estado.SUPERSEDED, updated_at: new Date() },
        });
        await tx.presupuesto_historial.createMany({
          data: previas.map((v) => ({
            presupuesto_id: v.id,
            estado_anterior: v.estado,
            estado_nuevo: presupuesto_estado.SUPERSEDED,
            usuario_id: user_id ?? null,
            comentario: `Reemplazado por la versión v${nuevaVersion}`,
          })),
        });
      }

      return cab;
    });

    return this.findOne(created.id, empresa_id);
  }
}
