import { BadRequestException } from '@nestjs/common';

export type Dimension = 'SUCURSAL' | 'MONEDA' | 'TIPO_PROVEEDOR' | 'TIPO_CLIENTE' | 'CANAL' | 'CATEGORIA_PRODUCTO' | 'RUBRO' | 'TIPO_ITEM';
const DIMENSIONES: readonly Dimension[] = ['SUCURSAL', 'MONEDA', 'TIPO_PROVEEDOR', 'TIPO_CLIENTE', 'CANAL', 'CATEGORIA_PRODUCTO', 'RUBRO', 'TIPO_ITEM'];
export const DIMENSIONES_ITEM: ReadonlySet<Dimension> = new Set(['CATEGORIA_PRODUCTO', 'RUBRO', 'TIPO_ITEM']);

export const ETIQUETA_DIMENSION: Record<Dimension, string> = {
  SUCURSAL: 'sucursal', MONEDA: 'moneda', TIPO_PROVEEDOR: 'tipo de proveedor', TIPO_CLIENTE: 'tipo de cliente',
  CANAL: 'canal', CATEGORIA_PRODUCTO: 'categoría', RUBRO: 'rubro', TIPO_ITEM: 'tipo de ítem',
};

export type CondicionRegla = { dimension: Dimension; operador: 'EN' | 'NO_EN'; valores: string[] };
export type ReglaMapeo = {
  id: string; concepto: string; orden: number; condiciones: CondicionRegla[];
  modo: 'CUENTA' | 'SUMAR_A_PRINCIPAL'; cuenta_id: string | null;
  estado: 'BORRADOR' | 'ACTIVA' | 'RETIRADA'; vigente_desde: Date; vigente_hasta: Date | null;
};
export type DimensionesDocumento = Partial<Record<Dimension, string | null>>;
export type AncestrosCategoria = Map<string, string[]>;

const CABECERA_VENTA: Dimension[] = ['SUCURSAL', 'MONEDA', 'TIPO_CLIENTE', 'CANAL'];
const ITEM: Dimension[] = ['CATEGORIA_PRODUCTO', 'RUBRO', 'TIPO_ITEM'];

/**
 * Qué dimensiones publica cada documento al contabilizarse. La pantalla lo usa
 * para avisar que una condición nunca va a aplicar a un concepto; los tests,
 * para que el integrador y esta tabla no se separen.
 */
export const DIMENSIONES_POR_ORIGEN: Record<string, Dimension[]> = {
  factura_cab: [...CABECERA_VENTA, ...ITEM],
  // La NC de venta no tiene formas de pago: no publica CANAL.
  nota_credito_cab: ['SUCURSAL', 'MONEDA', 'TIPO_CLIENTE', ...ITEM],
  compra_cab: ['SUCURSAL', 'MONEDA', 'TIPO_PROVEEDOR', ...ITEM],
  // La NC de compra no tiene sucursal propia: publica la de su compra (sin compra, no).
  nota_credito_compra_cab: ['SUCURSAL', 'MONEDA', 'TIPO_PROVEEDOR', ...ITEM],
  gasto_cab: ['SUCURSAL', 'MONEDA', 'TIPO_PROVEEDOR'],
  imp_despacho: ['MONEDA', 'TIPO_PROVEEDOR'],
  imp_cierre: ['MONEDA'],
  recibos_cobro: ['SUCURSAL', 'MONEDA', 'TIPO_CLIENTE'],
  recibos_multi: ['SUCURSAL', 'MONEDA', 'TIPO_CLIENTE'],
  // El pago a proveedor no tiene sucursal propia: no publica SUCURSAL.
  pagos_proveedor: ['MONEDA', 'TIPO_PROVEEDOR'],
  orden_pago_proveedor_cab: ['SUCURSAL', 'MONEDA', 'TIPO_PROVEEDOR'],
  tes_movimientos: ['MONEDA'],
  tes_transferencias: ['MONEDA'],
  pago_bancard: ['MONEDA'],
  rrhh_liquidacion: ['MONEDA'],
  liquidacion_comisiones_deveng: ['MONEDA'],
  liquidacion_comisiones_pago: ['MONEDA'],
  comision_supervisor: ['MONEDA'],
  rendicion_adelanto: ['MONEDA'],
  rendicion_devolucion: ['MONEDA'],
  // Pago de la CxP del reintegro diferido de viáticos (RendicionViaticosContabilidadService).
  cxp_empleado_pago: ['MONEDA'],
};

/**
 * Conceptos cuya línea se abre por ítem (spec §5.2): ventas por tasa (también las
 * de la venta inmobiliaria, que se resuelven en su concepto INM_*), inventario de
 * la compra y la compra cargada a una importación. Solo en estos una condición de
 * ítem (categoría, rubro, tipo) puede aplicar; en los demás el asiento lleva una
 * sola línea por documento y la condición no se cumpliría nunca.
 */
export const CONCEPTOS_CON_APERTURA_ITEM: ReadonlySet<string> = new Set([
  'VENTAS_10',
  'VENTAS_5',
  'VENTAS_EXENTAS',
  'INM_VENTAS_5',
  'INM_VENTAS_EXENTAS',
  'INVENTARIO',
  'IMPORTACION_COMPRA',
]);

/**
 * Valida y normaliza las condiciones. Con `concepto`, además rechaza las
 * dimensiones de ítem en un concepto que no se abre por ítem.
 */
export function validarCondiciones(c: unknown, concepto?: string): CondicionRegla[] {
  if (!Array.isArray(c) || c.length === 0) throw new BadRequestException('La regla necesita al menos una condición.');
  return c.map((x, i) => {
    const n = i + 1;
    if (!x || !DIMENSIONES.includes(x.dimension)) throw new BadRequestException(`Condición ${n}: dimensión desconocida.`);
    if (concepto && DIMENSIONES_ITEM.has(x.dimension) && !CONCEPTOS_CON_APERTURA_ITEM.has(concepto)) {
      throw new BadRequestException(
        `Condición ${n}: ${ETIQUETA_DIMENSION[x.dimension as Dimension]} es un dato de cada ítem y solo vale en ventas, inventario e importación de compras. ` +
          `${concepto} se asienta en una sola línea por documento: usá un dato de la cabecera (sucursal, moneda, tipo de cliente o proveedor, canal).`,
      );
    }
    if (x.operador !== 'EN' && x.operador !== 'NO_EN') throw new BadRequestException(`Condición ${n}: operador inválido.`);
    if (!Array.isArray(x.valores) || x.valores.length === 0 || x.valores.some((v: unknown) => typeof v !== 'string' || !v)) {
      throw new BadRequestException(`Condición ${n}: elegí al menos un valor.`);
    }
    return { dimension: x.dimension, operador: x.operador, valores: [...new Set<string>(x.valores)] };
  });
}

export function cumpleCondiciones(c: CondicionRegla[], dims: DimensionesDocumento, ancestros?: AncestrosCategoria): boolean {
  return c.every((cond) => {
    const valor = dims[cond.dimension];
    // El documento no publica esa dimensión (o la tiene vacía): la regla no aplica.
    if (valor === undefined || valor === null || valor === '') return false;
    const candidatos = cond.dimension === 'CATEGORIA_PRODUCTO' ? ancestros?.get(valor) ?? [valor] : [valor];
    const coincide = candidatos.some((v) => cond.valores.includes(v));
    return cond.operador === 'EN' ? coincide : !coincide;
  });
}

const soloFecha = (d: Date) => d.toISOString().slice(0, 10);
/** Hoy en Paraguay, como fecha (lo que el contador entiende por "hoy"). */
export const hoyParaguay = () => new Date(new Date().toLocaleDateString('en-CA', { timeZone: 'America/Asuncion' }));

/** Reglas que pueden aplicar a un documento de esa fecha, en orden de evaluación. */
export function reglasVigentes(reglas: ReglaMapeo[], fecha: Date, opciones: { incluirBorradores?: boolean } = {}): ReglaMapeo[] {
  const f = soloFecha(fecha);
  // Una retirada sigue valiendo para documentos con fecha dentro de su vigencia
  // (al retirarla se le pone vigente_hasta; sin él no hay nada que conservar).
  return reglas
    .filter((r) => r.estado === 'ACTIVA' || (opciones.incluirBorradores && r.estado === 'BORRADOR') || (r.estado === 'RETIRADA' && !!r.vigente_hasta))
    // Un borrador solo entra en la vista previa (incluirBorradores, que los asientos
    // reales nunca pasan) y se está probando: aplica sin mirar su vigencia, para
    // poder verlo sobre documentos viejos. Activas y retiradas respetan la fecha.
    .filter((r) => r.estado === 'BORRADOR' || (soloFecha(r.vigente_desde) <= f && (!r.vigente_hasta || f < soloFecha(r.vigente_hasta))))
    .sort((a, b) => a.orden - b.orden || a.id.localeCompare(b.id));
}

export function elegirRegla(
  reglas: ReglaMapeo[],
  fecha: Date,
  dims: DimensionesDocumento,
  opciones: { incluirBorradores?: boolean; ancestros?: AncestrosCategoria } = {},
): ReglaMapeo | null {
  return reglasVigentes(reglas, fecha, opciones).find((r) => cumpleCondiciones(r.condiciones, dims, opciones.ancestros)) ?? null;
}

export const usaDimensionesDeItem = (r: Pick<ReglaMapeo, 'condiciones'>) => r.condiciones.some((c) => DIMENSIONES_ITEM.has(c.dimension));

/** Una regla de ítem abre el asiento solo en un concepto que se abre por ítem. */
export const abreConceptoPorItem = (r: Pick<ReglaMapeo, 'concepto' | 'condiciones'>) =>
  CONCEPTOS_CON_APERTURA_ITEM.has(r.concepto) && usaDimensionesDeItem(r);

export function fraseRegla(r: ReglaMapeo, nombres: (d: Dimension, valor: string) => string, cuenta: string): string {
  const partes = r.condiciones.map((c) => {
    const nombrados = c.valores.map((v) => nombres(c.dimension, v));
    const valores = nombrados.join(c.operador === 'NO_EN' ? ' ni ' : ' o ');
    return `${ETIQUETA_DIMENSION[c.dimension]} ${c.operador === 'EN' ? 'es' : 'no es'} ${valores}`;
  });
  return `Si ${partes.join(' y ')} → ${cuenta}`;
}
