import { BadRequestException } from '@nestjs/common';
import { comprobante_compra_tipo, tipo_timbrado_comprobante } from '@prisma/client';
import { parsearCdc } from './cdc.util';

/**
 * Datos del comprobante de compra o gasto que pide el Registro de Comprobantes de
 * Marangatu (RG 90): el tipo de comprobante y la imputación (IVA/IRE/IRP-RSP/no imputa).
 * Compras y Gastos los validan y guardan igual; ver docs/plan-compras-gastos.md.
 */
export interface DatosFiscalesCompraInput {
  tipo_comprobante?: comprobante_compra_tipo | null;
  imputa_iva?: boolean | null;
  imputa_ire?: boolean | null;
  imputa_irp_rsp?: boolean | null;
  no_imputa?: boolean | null;
  extracto_cuenta?: string | null;
  extracto_entidad?: string | null;
  tipo_timbrado?: tipo_timbrado_comprobante | null;
  cdc?: string | null;
  /** Sólo para quien todavía manda la marca vieja (rendiciones, sincronización). */
  es_preimpreso?: boolean | null;
}

export interface DatosFiscalesCompra {
  tipo_comprobante: comprobante_compra_tipo;
  imputa_iva: boolean | null;
  imputa_ire: boolean | null;
  imputa_irp_rsp: boolean | null;
  no_imputa: boolean;
  extracto_cuenta: string | null;
  extracto_entidad: string | null;
  tipo_timbrado: tipo_timbrado_comprobante;
  cdc: string | null;
  /** Derivado del tipo de timbrado: sólo preimpreso y autoimpreso se declaran en Marangatu. */
  es_preimpreso: boolean;
}

/** Los que se cargan a mano y se declaran; el resto ya está en el sistema de la SET. */
export const TIMBRADOS_PREIMPRESOS: ReadonlySet<tipo_timbrado_comprobante> = new Set([
  tipo_timbrado_comprobante.PREIMPRESO,
  tipo_timbrado_comprobante.AUTOIMPRESO,
]);

/** Número del comprobante con el que se compara el CDC (en una edición, el que queda guardado). */
export interface NumeroComprobante {
  establecimiento?: string | null;
  punto_expedicion?: string | null;
  numero_factura?: string | null;
}

const pad = (v: string | null | undefined, n: number) => String(v ?? '').replace(/\D/g, '').padStart(n, '0');

/**
 * Tipo de timbrado y CDC:
 * - lo elegido manda; si no se eligió, un CDC válido lo hace electrónico; si tampoco,
 *   la marca vieja es_preimpreso (true → preimpreso, false → electrónico); si nada, lo guardado.
 * - el CDC tiene que tener su dígito verificador bien y corresponder al número de factura.
 */
function resolverTimbrado(
  input: DatosFiscalesCompraInput,
  actual: Partial<DatosFiscalesCompra> | null,
  numero: NumeroComprobante | null,
): Pick<DatosFiscalesCompra, 'tipo_timbrado' | 'cdc' | 'es_preimpreso'> {
  let cdc: string | null = input.cdc !== undefined ? input.cdc || null : actual?.cdc ?? null;
  if (cdc) {
    const datos = parsearCdc(cdc);
    if (!datos) throw new BadRequestException('El CDC no es válido: tiene que tener 44 dígitos y su dígito verificador.');
    cdc = datos.cdc;
    if (numero?.numero_factura && /^\d+$/.test(String(numero.numero_factura).trim())) {
      const mismoNumero =
        pad(numero.establecimiento, 3) === datos.establecimiento &&
        pad(numero.punto_expedicion, 3) === datos.punto &&
        pad(numero.numero_factura, 7) === datos.numero;
      if (!mismoNumero) {
        throw new BadRequestException(
          `El CDC corresponde al comprobante ${datos.establecimiento}-${datos.punto}-${datos.numero}, no al cargado.`,
        );
      }
    }
  }

  let tipo: tipo_timbrado_comprobante;
  if (input.tipo_timbrado) tipo = input.tipo_timbrado;
  else if (input.cdc) tipo = tipo_timbrado_comprobante.ELECTRONICO;
  else if (input.es_preimpreso === true)
    tipo = actual?.tipo_timbrado && TIMBRADOS_PREIMPRESOS.has(actual.tipo_timbrado) ? actual.tipo_timbrado : tipo_timbrado_comprobante.PREIMPRESO;
  else if (input.es_preimpreso === false)
    tipo = actual?.tipo_timbrado && !TIMBRADOS_PREIMPRESOS.has(actual.tipo_timbrado) ? actual.tipo_timbrado : tipo_timbrado_comprobante.ELECTRONICO;
  else tipo = actual?.tipo_timbrado ?? tipo_timbrado_comprobante.PREIMPRESO;

  return { tipo_timbrado: tipo, cdc, es_preimpreso: TIMBRADOS_PREIMPRESOS.has(tipo) };
}

/** Sin timbrado ni número paraguayo: el número se acepta tal cual viene (o vacío). */
export const TIPOS_SIN_FORMATO_SET: ReadonlySet<comprobante_compra_tipo> = new Set([
  comprobante_compra_tipo.DESPACHO_IMPORTACION,
  comprobante_compra_tipo.COMPROBANTE_EXTERIOR,
  comprobante_compra_tipo.EXTRACTO_TARJETA,
]);

/** Lo que se carga por Compras: el despacho y el extracto de tarjeta no son mercadería. */
export const TIPOS_PARA_COMPRAS: ReadonlySet<comprobante_compra_tipo> = new Set([
  comprobante_compra_tipo.FACTURA,
  comprobante_compra_tipo.AUTOFACTURA,
  comprobante_compra_tipo.COMPROBANTE_EXTERIOR,
]);

const texto = (v?: string | null) => {
  const t = String(v ?? '').trim();
  return t || null;
};

/**
 * Combina lo que llegó con lo guardado (en una edición, `actual`) y valida:
 * - al menos una obligación cuando se eligen las tres (la SET rechaza una fila sin imputación);
 * - "no imputa" sólo junto con otra;
 * - el extracto de tarjeta necesita cuenta y entidad (las pide el registro 207).
 */
export function resolverDatosFiscalesCompra(
  input: DatosFiscalesCompraInput,
  actual: Partial<DatosFiscalesCompra> | null = null,
  opciones: { origen: 'compra' | 'gasto'; numero?: NumeroComprobante | null } = { origen: 'gasto' },
): DatosFiscalesCompra {
  const elegir = <K extends keyof DatosFiscalesCompraInput>(k: K) =>
    input[k] !== undefined ? input[k] : (actual?.[k as keyof DatosFiscalesCompra] as DatosFiscalesCompraInput[K]);

  const tipo = (elegir('tipo_comprobante') ?? comprobante_compra_tipo.FACTURA) as comprobante_compra_tipo;
  if (opciones.origen === 'compra' && !TIPOS_PARA_COMPRAS.has(tipo)) {
    throw new BadRequestException(
      'Un despacho de importación o un extracto de tarjeta no es una compra de mercadería: cargalo como gasto.',
    );
  }

  const datos: DatosFiscalesCompra = {
    tipo_comprobante: tipo,
    imputa_iva: elegir('imputa_iva') ?? null,
    imputa_ire: elegir('imputa_ire') ?? null,
    imputa_irp_rsp: elegir('imputa_irp_rsp') ?? null,
    no_imputa: Boolean(elegir('no_imputa')),
    extracto_cuenta: tipo === comprobante_compra_tipo.EXTRACTO_TARJETA ? texto(elegir('extracto_cuenta')) : null,
    extracto_entidad: tipo === comprobante_compra_tipo.EXTRACTO_TARJETA ? texto(elegir('extracto_entidad')) : null,
    ...resolverTimbrado(input, actual, opciones.numero ?? null),
  };

  if (datos.imputa_iva === false && datos.imputa_ire === false && datos.imputa_irp_rsp === false) {
    throw new BadRequestException('Elegí al menos un impuesto al que imputa el comprobante (IVA, IRE o IRP).');
  }
  if (tipo === comprobante_compra_tipo.EXTRACTO_TARJETA && (!datos.extracto_cuenta || !datos.extracto_entidad)) {
    throw new BadRequestException('Para un extracto de tarjeta indicá el número de cuenta o tarjeta y el banco.');
  }
  return datos;
}

/** Los campos de `DatosFiscalesCompra` que trae una compra o un gasto ya guardado. */
export function datosFiscalesGuardados(fila: Partial<DatosFiscalesCompra> | null | undefined): Partial<DatosFiscalesCompra> | null {
  if (!fila) return null;
  return {
    tipo_comprobante: fila.tipo_comprobante,
    imputa_iva: fila.imputa_iva ?? null,
    imputa_ire: fila.imputa_ire ?? null,
    imputa_irp_rsp: fila.imputa_irp_rsp ?? null,
    no_imputa: fila.no_imputa ?? false,
    extracto_cuenta: fila.extracto_cuenta ?? null,
    extracto_entidad: fila.extracto_entidad ?? null,
    tipo_timbrado: fila.tipo_timbrado,
    cdc: fila.cdc ?? null,
    es_preimpreso: fila.es_preimpreso,
  };
}

/** compra_cab no tiene los datos del extracto de tarjeta (es sólo para gastos). */
export function sinExtracto(datos: DatosFiscalesCompra): Omit<DatosFiscalesCompra, 'extracto_cuenta' | 'extracto_entidad'> {
  const { extracto_cuenta: _c, extracto_entidad: _e, ...resto } = datos;
  return resto;
}
