import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { MAPEO_DEFAULT } from '../seeds/plan-cuentas-paraguay.seed';
import { AvisosMapeoService } from './avisos-mapeo.service';
import { FaltaMapeoError } from './falta-mapeo.error';
import { cargarNombresDeValores, destinoRegla } from './frase-regla';
import { MapeoCuentasService, TrazaMapeo } from './mapeo-cuentas.service';
import { mapeoUsable } from './mapeo-usable';
import { CONCEPTOS_CON_MODO } from './recomendaciones-mapeo';
import {
  CONCEPTOS_CON_APERTURA_ITEM,
  CondicionRegla,
  DIMENSIONES_ITEM,
  DIMENSIONES_POR_ORIGEN,
  Dimension,
  DimensionesDocumento,
  ETIQUETA_DIMENSION,
  ReglaMapeo,
  fraseRegla,
  hoyParaguay,
  validarCondiciones,
} from './reglas-mapeo';

/**
 * Reglas de cuenta por dimensión (spec 2026-10-13 §3, §6.1, §8): el contador
 * crea excepciones al mapeo por defecto ("si la moneda es USD → Ventas
 * exterior"). Nada se decide en silencio:
 *  - toda regla nace en BORRADOR y solo se aplica en asientos reales cuando
 *    alguien la activa (queda quién y cuándo);
 *  - solo se edita un borrador: una activa se retira y se crea otra;
 *  - retirar no borra, cierra la vigencia (los documentos de antes la siguen
 *    usando al rehacer asientos); borrar solo si nunca estuvo activa;
 *  - cada cambio va a audit_logs con la regla completa.
 */

export interface GuardarRegla {
  concepto: string;
  condiciones: CondicionRegla[];
  modo?: 'CUENTA' | 'SUMAR_A_PRINCIPAL';
  cuenta_id?: string | null;
  vigente_desde: string;
  descripcion?: string | null;
}

const ENTIDAD = 'cont_mapeo_reglas';
const DIMENSIONES_VALIDAS = new Set(Object.keys(ETIQUETA_DIMENSION));

const INCLUDE_REGLA = {
  cuenta: { select: { codigo: true, descripcion: true, active: true, acepta_movimientos: true } },
  creador: { select: { nombres: true, apellidos: true } },
  activador: { select: { nombres: true, apellidos: true } },
  retirador: { select: { nombres: true, apellidos: true } },
} as const;

type Persona = { nombres?: string | null; apellidos?: string | null } | null | undefined;
type FilaRegla = {
  id: string;
  empresa_id: string;
  concepto: string;
  orden: number;
  condiciones: unknown;
  modo: string;
  cuenta_id: string | null;
  estado: string;
  vigente_desde: Date;
  vigente_hasta: Date | null;
  descripcion: string | null;
  creado_por: string | null;
  activado_por: string | null;
  activado_at: Date | null;
  retirado_por: string | null;
  retirado_at: Date | null;
  created_at: Date | null;
  cuenta?: { codigo: string; descripcion: string; active?: boolean | null; acepta_movimientos?: boolean | null } | null;
  creador?: Persona;
  activador?: Persona;
  retirador?: Persona;
};

const soloFecha = (d: Date | null | undefined) => (d ? d.toISOString().slice(0, 10) : null);
const nombreDe = (p: Persona) => (p ? [p.nombres, p.apellidos].filter(Boolean).join(' ') || null : null);
const hoy = hoyParaguay;

/** La regla completa, como se guarda en audit_logs (spec §8). */
const fotoRegla = (r: FilaRegla) => ({
  id: r.id,
  concepto: r.concepto,
  condiciones: r.condiciones as Prisma.InputJsonValue,
  modo: r.modo,
  cuenta_id: r.cuenta_id,
  cuenta_codigo: r.cuenta?.codigo ?? null,
  orden: r.orden,
  estado: r.estado,
  vigente_desde: soloFecha(r.vigente_desde),
  vigente_hasta: soloFecha(r.vigente_hasta),
  descripcion: r.descripcion,
});

const aRegla = (r: FilaRegla): ReglaMapeo => ({
  id: r.id,
  concepto: r.concepto,
  orden: r.orden,
  condiciones: r.condiciones as CondicionRegla[],
  modo: r.modo as ReglaMapeo['modo'],
  cuenta_id: r.cuenta_id,
  estado: r.estado as ReglaMapeo['estado'],
  vigente_desde: r.vigente_desde,
  vigente_hasta: r.vigente_hasta,
});

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly avisos: AvisosMapeoService,
    private readonly mapeo: MapeoCuentasService,
  ) {}

  async listar(empresaId: string, concepto?: string) {
    const filas = (await this.prisma.cont_mapeo_reglas.findMany({
      where: { empresa_id: empresaId, ...(concepto ? { concepto } : {}) },
      include: INCLUDE_REGLA,
      orderBy: [{ concepto: 'asc' }, { orden: 'asc' }, { id: 'asc' }],
    })) as unknown as FilaRegla[];
    const nombres = await this.nombresDeValores(empresaId, filas);
    return filas.map((r) => this.describir(r, nombres));
  }

  /** Orden, estado y frase de cada regla pedida (para la traza de la vista previa). */
  async describirPorId(empresaId: string, ids: string[]) {
    const pedidas = [...new Set(ids.filter(Boolean))];
    if (!pedidas.length) return new Map<string, ReturnType<ReglasMapeoService['describir']>>();
    const filas = (await this.prisma.cont_mapeo_reglas.findMany({
      where: { empresa_id: empresaId, id: { in: pedidas } },
      include: INCLUDE_REGLA,
    })) as unknown as FilaRegla[];
    const nombres = await this.nombresDeValores(empresaId, filas);
    return new Map(filas.map((r) => [r.id, this.describir(r, nombres)]));
  }

  async crear(empresaId: string, usuarioId: string, dto: GuardarRegla) {
    const datos = await this.validar(empresaId, dto);
    const max = await this.prisma.cont_mapeo_reglas.aggregate({
      where: { empresa_id: empresaId, concepto: datos.concepto },
      _max: { orden: true },
    });
    const creada = (await this.prisma.cont_mapeo_reglas.create({
      data: {
        empresa_id: empresaId,
        ...datos,
        orden: (max._max.orden ?? 0) + 1,
        estado: 'BORRADOR',
        creado_por: usuarioId,
      },
      include: INCLUDE_REGLA,
    })) as unknown as FilaRegla;
    await this.auditar(empresaId, usuarioId, 'CONT_REGLA_CREAR', creada, null);
    return creada;
  }

  /**
   * Solo BORRADOR. Una ACTIVA (o RETIRADA) no se edita: se retira y se crea otra,
   * así la regla que valió para los documentos de su vigencia queda como estaba
   * para rehacer asientos.
   */
  async editar(empresaId: string, usuarioId: string, id: string, dto: GuardarRegla) {
    const anterior = await this.buscar(empresaId, id);
    if (anterior.estado === 'ACTIVA') {
      throw new BadRequestException('La regla está activa: retirala y creá una nueva (podés usar Duplicar).');
    }
    if (anterior.estado !== 'BORRADOR') throw new BadRequestException('Una regla retirada no se edita: creá una nueva.');
    const datos = await this.validar(empresaId, dto);
    if (datos.concepto !== anterior.concepto) {
      throw new BadRequestException('Una regla no cambia de concepto: creá una nueva en el otro concepto.');
    }
    const editada = (await this.prisma.cont_mapeo_reglas.update({
      where: { id },
      data: { ...datos, updated_at: new Date() },
      include: INCLUDE_REGLA,
    })) as unknown as FilaRegla;
    await this.auditar(empresaId, usuarioId, 'CONT_REGLA_EDITAR', editada, anterior);
    return editada;
  }

  async activar(empresaId: string, usuarioId: string, id: string) {
    const anterior = await this.buscar(empresaId, id);
    if (anterior.estado !== 'BORRADOR') throw new BadRequestException('Solo se activa una regla en borrador.');
    // Un borrador anterior a esta validación puede tener un dato de ítem en un concepto que no se abre por ítem.
    validarCondiciones(anterior.condiciones, anterior.concepto);
    if (anterior.modo === 'SUMAR_A_PRINCIPAL') {
      if (!CONCEPTOS_CON_MODO.has(anterior.concepto)) {
        throw new BadRequestException(`El concepto "${anterior.concepto}" no admite "sumar a la cuenta principal".`);
      }
    } else {
      // Misma condición que usa el asiento: una regla que no se podría usar no se activa.
      const cuenta = anterior.cuenta_id ? await this.cuentaDeEmpresa(empresaId, anterior.cuenta_id) : null;
      if (!cuenta) throw new BadRequestException('La regla no tiene una cuenta válida: elegí una antes de activarla.');
      if (!cuenta.active) throw new BadRequestException(`La cuenta ${cuenta.codigo} está desactivada: elegí otra antes de activar la regla.`);
      if (!cuenta.acepta_movimientos) {
        throw new BadRequestException(`La cuenta ${cuenta.codigo} es de agrupación: elegí una imputable antes de activar la regla.`);
      }
    }
    const ahora = new Date();
    const activa = (await this.prisma.cont_mapeo_reglas.update({
      where: { id },
      data: { estado: 'ACTIVA', activado_por: usuarioId, activado_at: ahora, updated_at: ahora },
      include: INCLUDE_REGLA,
    })) as unknown as FilaRegla;
    await this.auditar(empresaId, usuarioId, 'CONT_REGLA_ACTIVAR', activa, anterior);
    await this.sincronizarAvisos(empresaId);
    return activa;
  }

  /**
   * Cierra la vigencia en `vigenteHasta` (exclusivo; por defecto hoy). Siempre
   * fija `vigente_hasta`: la resolución sigue aplicando una retirada a los
   * documentos con fecha dentro de su vigencia. Fin igual al inicio = "nunca
   * aplicó" (vigencia vacía); anterior al inicio no se acepta.
   * Devuelve además cuántos asientos con esta regla tienen fecha ≥ fin: si se
   * rehacen, ya no la van a usar.
   */
  async retirar(empresaId: string, usuarioId: string, id: string, opciones: { vigenteHasta?: string } = {}) {
    const anterior = await this.buscar(empresaId, id);
    if (anterior.estado === 'RETIRADA') throw new BadRequestException('La regla ya está retirada.');
    if (anterior.estado !== 'ACTIVA') throw new BadRequestException('La regla nunca estuvo activa: borrala en vez de retirarla.');
    let fin = hoy();
    if (opciones.vigenteHasta !== undefined && opciones.vigenteHasta !== null && opciones.vigenteHasta !== '') {
      if (!/^\d{4}-\d{2}-\d{2}/.test(opciones.vigenteHasta)) throw new BadRequestException('La fecha de fin no es válida (AAAA-MM-DD).');
      fin = new Date(opciones.vigenteHasta.slice(0, 10));
      if (Number.isNaN(fin.getTime())) throw new BadRequestException('La fecha de fin no es válida (AAAA-MM-DD).');
    }
    if (soloFecha(fin)! < soloFecha(anterior.vigente_desde)!) {
      throw new BadRequestException(
        `La fecha de fin no puede ser anterior al inicio de la regla (${soloFecha(anterior.vigente_desde)}). Elegí esa misma fecha si nunca tuvo que aplicar.`,
      );
    }
    const ahora = new Date();
    const retirada = (await this.prisma.cont_mapeo_reglas.update({
      where: { id },
      data: { estado: 'RETIRADA', vigente_hasta: fin, retirado_por: usuarioId, retirado_at: ahora, updated_at: ahora },
      include: INCLUDE_REGLA,
    })) as unknown as FilaRegla;
    await this.auditar(empresaId, usuarioId, 'CONT_REGLA_RETIRAR', retirada, anterior);
    await this.sincronizarAvisos(empresaId);
    const asientosDesdeFin = await this.prisma.cont_asientos.count({
      where: { empresa_id: empresaId, fecha: { gte: fin }, estado: { not: 'REVERTIDO' }, lineas: { some: { mapeo_regla_id: id } } },
    });
    return { ...retirada, asientos_desde_fin: asientosDesdeFin };
  }

  /** Solo si nunca estuvo activa: una que se usó en asientos se retira (queda la constancia). */
  async borrar(empresaId: string, usuarioId: string, id: string) {
    const anterior = await this.buscar(empresaId, id);
    if (anterior.activado_at) {
      throw new BadRequestException('La regla estuvo activa y puede haberse usado en asientos: retirala en vez de borrarla.');
    }
    await this.prisma.cont_mapeo_reglas.delete({ where: { id } });
    await this.auditar(empresaId, usuarioId, 'CONT_REGLA_BORRAR', null, anterior);
    return { ok: true };
  }

  /**
   * Reasigna el orden 1..n en el orden pedido; las reglas del concepto que no
   * vinieron en la lista quedan después, en el orden que tenían (el orden sigue
   * siendo único por concepto).
   */
  async reordenar(empresaId: string, usuarioId: string, dto: { concepto: string; ids: string[] }) {
    if (new Set(dto.ids).size !== dto.ids.length) throw new BadRequestException('La lista de reglas tiene repetidos.');
    const actuales = (await this.prisma.cont_mapeo_reglas.findMany({
      where: { empresa_id: empresaId, concepto: dto.concepto },
      include: INCLUDE_REGLA,
      orderBy: [{ orden: 'asc' }, { id: 'asc' }],
    })) as unknown as FilaRegla[];
    const porId = new Map(actuales.map((r) => [r.id, r]));
    if (dto.ids.some((id) => !porId.has(id))) throw new NotFoundException(`Alguna regla no existe en el concepto ${dto.concepto}.`);
    const nuevoOrden = [...dto.ids.map((id) => porId.get(id)!), ...actuales.filter((r) => !dto.ids.includes(r.id))];
    const antes = actuales.map((r) => fotoRegla(r));
    const ahora = new Date();
    await this.prisma.$transaction(async (tx) => {
      for (const [i, r] of nuevoOrden.entries()) {
        if (r.orden === i + 1) continue;
        await tx.cont_mapeo_reglas.update({ where: { id: r.id }, data: { orden: i + 1, updated_at: ahora } });
      }
      await tx.audit_logs.create({
        data: {
          empresa_id: empresaId,
          user_id: usuarioId,
          action: 'CONT_REGLA_ORDEN',
          entity_type: ENTIDAD,
          old_value: { concepto: dto.concepto, reglas: antes },
          new_value: { concepto: dto.concepto, reglas: nuevoOrden.map((r, i) => fotoRegla({ ...r, orden: i + 1 })) },
          descripcion: `Orden de reglas ${dto.concepto}`.slice(0, 255),
        },
      });
    });
    await this.sincronizarAvisos(empresaId);
    return this.listar(empresaId, dto.concepto);
  }

  /**
   * "¿Qué cuenta usaría?": resuelve el concepto con los valores elegidos, igual
   * que el integrador (reglas, categorías con sus padres, vigencia y mapeo por
   * defecto). La falta de mapeo es una respuesta, no un error.
   */
  async probador(
    empresaId: string,
    dto: { concepto: string; dimensiones: DimensionesDocumento; fecha?: string; incluirBorradores?: boolean },
  ) {
    await this.validarConcepto(empresaId, dto.concepto);
    const dimensiones: DimensionesDocumento = {};
    for (const [k, v] of Object.entries(dto.dimensiones ?? {})) {
      if (!DIMENSIONES_VALIDAS.has(k)) throw new BadRequestException(`Dimensión desconocida: ${k}.`);
      if (v === undefined || v === null || v === '') continue;
      if (typeof v !== 'string') throw new BadRequestException(`El valor de ${k} tiene que ser un texto.`);
      dimensiones[k as Dimension] = v;
    }
    // Mismo criterio que al guardar una regla: los datos de ítem no se miran en un concepto de una sola línea.
    let advertencia: string | undefined;
    const deItem = Object.keys(dimensiones).filter((d) => DIMENSIONES_ITEM.has(d as Dimension));
    if (deItem.length && !CONCEPTOS_CON_APERTURA_ITEM.has(dto.concepto)) {
      for (const d of deItem) delete dimensiones[d as Dimension];
      advertencia =
        `${dto.concepto} se asienta en una sola línea por documento: ${deItem.map((d) => ETIQUETA_DIMENSION[d as Dimension]).join(', ')} ` +
        'no se mira (solo vale en ventas, inventario e importación de compras).';
    }
    const fecha = dto.fecha ? new Date(dto.fecha.slice(0, 10)) : hoy();
    const resolvedor = await this.mapeo.crearResolvedor(empresaId, { fecha, dimensiones, incluirBorradores: dto.incluirBorradores });
    if (dimensiones.CATEGORIA_PRODUCTO) await resolvedor.prepararCategorias([dimensiones.CATEGORIA_PRODUCTO]);
    try {
      const r = await resolvedor.resolver(dto.concepto);
      const traza = await this.trazaConRegla(empresaId, r.traza);
      const extra = advertencia ? { advertencia } : {};
      if (r.modo === 'SUMAR_A_PRINCIPAL') return { modo: r.modo, cuentaId: null, cuenta: null, traza, ...extra };
      const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
        where: { id: r.cuentaId, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
        select: { codigo: true, descripcion: true },
      });
      return { modo: r.modo, cuentaId: r.cuentaId, cuenta: cuenta ? { codigo: cuenta.codigo, descripcion: cuenta.descripcion } : null, traza, ...extra };
    } catch (e) {
      if (e instanceof FaltaMapeoError) return { falta: { motivo: e.motivo, mensaje: e.message }, ...(advertencia ? { advertencia } : {}) };
      throw e;
    }
  }

  /** Qué dimensiones publica cada documento y los valores que se pueden elegir. */
  async dimensiones(empresaId: string) {
    const [sucursales, monedas, proveedores, clientes, categorias, rubros, tipos] = await Promise.all([
      this.prisma.empresas_sucursales.findMany({
        where: { empresa_id: empresaId },
        select: { id: true, descripcion: true },
        orderBy: { descripcion: 'asc' },
      }),
      this.prisma.moneda.findMany({
        where: { active: true, codigo: { not: null } },
        select: { codigo: true, descripcion: true },
        orderBy: { codigo: 'asc' },
      }),
      this.prisma.proveedores.findMany({
        where: { empresa_id: empresaId },
        distinct: ['tipo_entidad'],
        select: { tipo_entidad: true },
      }),
      // `clientes` no tiene empresa: se llega por su persona.
      this.prisma.clientes.findMany({
        where: { personas: { empresa_id: empresaId }, tipo_cliente: { not: null } },
        distinct: ['tipo_cliente'],
        select: { tipo_cliente: true },
      }),
      this.prisma.categorias.findMany({
        where: { empresa_id: empresaId, deleted_at: { not: true } },
        select: { id: true, descripcion: true, padre_id: true },
        orderBy: { descripcion: 'asc' },
      }),
      this.prisma.rubros.findMany({
        where: { empresa_id: empresaId },
        select: { id: true, descripcion: true },
        orderBy: { descripcion: 'asc' },
      }),
      this.prisma.productos.findMany({
        where: { empresa_id: empresaId, tipo: { not: null } },
        distinct: ['tipo'],
        select: { tipo: true },
      }),
    ]);
    const distintos = (xs: Array<string | null>) => [...new Set(xs.filter((x): x is string => !!x))].sort();
    return {
      porOrigen: DIMENSIONES_POR_ORIGEN,
      // Solo en estos conceptos se ofrecen los datos de ítem (categoría, rubro, tipo).
      conceptosConItem: [...CONCEPTOS_CON_APERTURA_ITEM],
      valores: {
        SUCURSAL: sucursales.map((s) => ({ id: s.id, nombre: s.descripcion })),
        MONEDA: monedas.map((m) => ({ id: m.codigo as string, nombre: m.descripcion ?? (m.codigo as string) })),
        TIPO_PROVEEDOR: distintos(proveedores.map((p) => p.tipo_entidad)),
        TIPO_CLIENTE: distintos(clientes.map((c) => c.tipo_cliente)),
        CANAL: ['POS', 'FACTURA'],
        CATEGORIA_PRODUCTO: categorias.map((c) => ({ id: c.id, nombre: c.descripcion ?? '', padre_id: c.padre_id })),
        RUBRO: rubros.map((r) => ({ id: r.id, nombre: r.descripcion })),
        TIPO_ITEM: distintos(tipos.map((t) => t.tipo)),
      },
    };
  }

  // ── internos ─────────────────────────────────────────────────────────

  private async buscar(empresaId: string, id: string): Promise<FilaRegla> {
    const r = await this.prisma.cont_mapeo_reglas.findFirst({ where: { id, empresa_id: empresaId }, include: INCLUDE_REGLA });
    if (!r) throw new NotFoundException('La regla no existe.');
    return r as unknown as FilaRegla;
  }

  /** Conceptos que existen: los del mapeo por defecto o los que la empresa tiene mapeados. */
  private async validarConcepto(empresaId: string, concepto: string) {
    if (typeof concepto !== 'string' || !concepto.trim()) throw new BadRequestException('Indicá el concepto de la regla.');
    if (concepto in MAPEO_DEFAULT) return;
    const propio = await this.prisma.cont_mapeo_cuentas.findFirst({ where: { empresa_id: empresaId, concepto }, select: { concepto: true } });
    if (!propio) throw new BadRequestException(`El concepto "${concepto}" no existe en el mapeo de cuentas.`);
  }

  private cuentaDeEmpresa(empresaId: string, cuentaId: string) {
    return this.prisma.cont_plan_cuentas.findFirst({
      where: { id: cuentaId, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
      select: { id: true, codigo: true, descripcion: true, active: true, acepta_movimientos: true },
    });
  }

  private async validar(empresaId: string, dto: GuardarRegla) {
    await this.validarConcepto(empresaId, dto.concepto);
    const condiciones = validarCondiciones(dto.condiciones, dto.concepto);
    const modo = dto.modo ?? 'CUENTA';
    if (modo !== 'CUENTA' && modo !== 'SUMAR_A_PRINCIPAL') throw new BadRequestException('Modo inválido: usá CUENTA o SUMAR_A_PRINCIPAL.');
    if (modo === 'SUMAR_A_PRINCIPAL' && !CONCEPTOS_CON_MODO.has(dto.concepto)) {
      throw new BadRequestException(`El concepto "${dto.concepto}" no admite "sumar a la cuenta principal".`);
    }
    let cuentaId: string | null = null;
    if (modo === 'CUENTA') {
      if (!dto.cuenta_id) throw new BadRequestException('Elegí una cuenta para la regla.');
      const cuenta = await this.cuentaDeEmpresa(empresaId, dto.cuenta_id);
      if (!cuenta) throw new NotFoundException('La cuenta contable seleccionada no existe o fue eliminada');
      cuentaId = cuenta.id;
    }
    if (!/^\d{4}-\d{2}-\d{2}/.test(dto.vigente_desde ?? '')) throw new BadRequestException('Indicá desde qué fecha aplica la regla.');
    const descripcion = dto.descripcion?.trim() ? dto.descripcion.trim().slice(0, 200) : null;
    return {
      concepto: dto.concepto,
      // Prisma pide un JSON: las condiciones ya validadas son datos planos.
      condiciones: condiciones as unknown as object[],
      modo,
      cuenta_id: cuentaId,
      vigente_desde: new Date(dto.vigente_desde.slice(0, 10)),
      descripcion,
    };
  }

  private async auditar(empresaId: string, usuarioId: string, action: string, nueva: FilaRegla | null, anterior: FilaRegla | null) {
    const r = (nueva ?? anterior)!;
    await this.prisma.audit_logs.create({
      data: {
        empresa_id: empresaId,
        user_id: usuarioId,
        action,
        entity_type: ENTIDAD,
        entity_id: r.id,
        old_value: anterior ? fotoRegla(anterior) : undefined,
        new_value: nueva ? fotoRegla(nueva) : undefined,
        descripcion: `Regla n.º ${r.orden} de ${r.concepto}`.slice(0, 255),
      },
    });
  }

  /** Como en `actualizarBulk`: si falla, no tapa lo que ya quedó guardado. */
  private async sincronizarAvisos(empresaId: string) {
    try {
      await this.avisos.sincronizar(empresaId);
    } catch (e) {
      this.logger.error(`No se pudieron sincronizar los avisos de mapeo de ${empresaId}: ${e instanceof Error ? e.message : e}`);
    }
  }

  private async trazaConRegla(empresaId: string, traza: TrazaMapeo) {
    if (!traza.reglaId) return traza;
    const regla = (await this.describirPorId(empresaId, [traza.reglaId])).get(traza.reglaId);
    return { ...traza, regla: regla ? { id: regla.id, orden: regla.orden, estado: regla.estado, frase: regla.frase } : null };
  }

  /** Nombres legibles de los valores que usan las condiciones (sucursal, categoría, rubro). */
  private nombresDeValores(empresaId: string, filas: FilaRegla[]) {
    return cargarNombresDeValores(this.prisma, empresaId, filas.map((r) => r.condiciones));
  }

  private describir(r: FilaRegla, nombres: (d: Dimension, valor: string) => string) {
    return {
      id: r.id,
      concepto: r.concepto,
      orden: r.orden,
      condiciones: r.condiciones as CondicionRegla[],
      modo: r.modo,
      cuenta_id: r.cuenta_id,
      cuenta: r.cuenta ? { codigo: r.cuenta.codigo, descripcion: r.cuenta.descripcion } : null,
      // Para que la pantalla avise antes de que el asiento falle.
      cuenta_usable: mapeoUsable({ modo: r.modo, cuenta_id: r.cuenta_id, cuenta: r.cuenta }),
      estado: r.estado,
      vigente_desde: soloFecha(r.vigente_desde),
      vigente_hasta: soloFecha(r.vigente_hasta),
      descripcion: r.descripcion,
      frase: fraseRegla(aRegla(r), nombres, destinoRegla(r)),
      creado_por: r.creado_por,
      creado_por_nombre: nombreDe(r.creador),
      created_at: r.created_at,
      activado_por: r.activado_por,
      activado_por_nombre: nombreDe(r.activador),
      activado_at: r.activado_at,
      retirado_por: r.retirado_por,
      retirado_por_nombre: nombreDe(r.retirador),
      retirado_at: r.retirado_at,
    };
  }
}
