// COMPROBANTES DE RETENCION RECIBIDOS.
//
// Cuando el negocio le vende a un sujeto pasivo especial, el COMPRADOR retiene
// el 75% o el 100% del IVA de la factura y entrega un comprobante (Providencia
// SNAT/2025/000054). Ese comprobante es PLATA: credito contra la cuota del mes
// en la declaracion de IVA. El sistema no lo emite (eso lo hace el agente que
// retiene): lo REGISTRA, para que el mes cuadre y el credito no se pierda en
// una gaveta.
//
// Mismo espiritu del libro: se agrega, no se edita. Un comprobante mal cargado
// no se borra: se anula con una contramarca que dice quien y por que, y el
// registro completo queda para quien audite.
import { appendFileSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { rifValido, rifNormalizado } from '@gustito/shared/fiscal';

export interface RetencionRecibida {
  tipo: 'retencion';
  /** El numero del comprobante tal como lo entrego el agente (normalizado). */
  numero: string;
  /** Fecha del comprobante (la que trae el papel), AAAA-MM-DD. */
  fecha: string;
  /** El periodo fiscal al que aporta el credito, AAAA-MM (sale de la fecha). */
  periodo: string;
  agente: { rif: string; razon_social: string };
  /** A que factura(s) nuestras corresponde, tal como diga el comprobante. */
  facturas: string;
  /** El IVA que nos retuvieron, en bolivares (los comprobantes vienen en Bs). */
  iva_retenido_bs: number;
  porcentaje: 75 | 100;
  registrado_en: string;
  quien: string;
  nota?: string | null;
}

interface Anulacion {
  tipo: 'anulacion';
  numero: string;
  motivo: string;
  registrado_en: string;
  quien: string;
}

type Linea = RetencionRecibida | Anulacion;

export interface DatosRetencion {
  numero: string;
  fecha: string;
  agente_rif: string;
  agente_razon_social: string;
  facturas: string;
  iva_retenido_bs: number;
  porcentaje: number;
  nota?: string | null;
}

const normalizarNumero = (n: string): string => n.trim().toUpperCase().replace(/\s+/g, '');

export class Retenciones {
  private readonly ruta: string;
  private lineas: Linea[] = [];

  constructor(base: string) {
    this.ruta = join(base, 'estado', 'retenciones.jsonl');
    if (existsSync(this.ruta)) {
      this.lineas = readFileSync(this.ruta, 'utf8')
        .split('\n')
        .filter((l) => l.trim())
        .map((l) => JSON.parse(l) as Linea);
    }
  }

  private escribir(linea: Linea): void {
    mkdirSync(dirname(this.ruta), { recursive: true });
    appendFileSync(this.ruta, JSON.stringify(linea) + '\n');
    // fsync: un comprobante registrado es credito fiscal; un apagon no lo borra.
    const fd = openSync(this.ruta, 'r+');
    try {
      fsyncSync(fd);
    } finally {
      closeSync(fd);
    }
    this.lineas.push(linea);
  }

  private anulados(): Set<string> {
    return new Set(this.lineas.filter((l): l is Anulacion => l.tipo === 'anulacion').map((l) => l.numero));
  }

  registrar(d: DatosRetencion, quien: string): RetencionRecibida {
    const numero = normalizarNumero(d.numero ?? '');
    if (!numero) throw new Error('Falta el numero del comprobante');
    if (!/^\d{4}-\d{2}-\d{2}$/.test(d.fecha ?? '')) {
      throw new Error('La fecha del comprobante va como AAAA-MM-DD');
    }
    if (!rifValido(d.agente_rif ?? '')) {
      throw new Error('El RIF del agente de retencion falta o el ultimo digito no cuadra');
    }
    if (!d.agente_razon_social?.trim()) throw new Error('Falta la razon social del agente de retencion');
    if (!d.facturas?.trim()) throw new Error('Falta a que factura(s) corresponde el comprobante');
    const monto = Number(d.iva_retenido_bs);
    if (!Number.isFinite(monto) || monto <= 0) throw new Error('El IVA retenido debe ser mayor que cero');
    if (d.porcentaje !== 75 && d.porcentaje !== 100) {
      throw new Error('El porcentaje de retencion es 75 o 100');
    }
    const yaEsta = this.lineas.some(
      (l) => l.tipo === 'retencion' && l.numero === numero && !this.anulados().has(l.numero),
    );
    if (yaEsta) throw new Error(`El comprobante ${numero} ya esta registrado`);

    const registro: RetencionRecibida = {
      tipo: 'retencion',
      numero,
      fecha: d.fecha,
      periodo: d.fecha.slice(0, 7),
      agente: {
        rif: rifNormalizado(d.agente_rif) ?? d.agente_rif.trim().toUpperCase(),
        razon_social: d.agente_razon_social.trim(),
      },
      facturas: d.facturas.trim(),
      iva_retenido_bs: Math.round(monto * 100) / 100,
      porcentaje: d.porcentaje,
      registrado_en: new Date().toISOString(),
      quien,
      nota: d.nota?.trim() || null,
    };
    this.escribir(registro);
    return registro;
  }

  /** Anula por contramarca: el registro original queda, con su porque al lado. */
  anular(numero: string, motivo: string, quien: string): void {
    const n = normalizarNumero(numero);
    const existe = this.lineas.some((l) => l.tipo === 'retencion' && l.numero === n);
    if (!existe) throw new Error(`No hay ningun comprobante ${n} registrado`);
    if (this.anulados().has(n)) throw new Error(`El comprobante ${n} ya esta anulado`);
    if (!motivo?.trim()) throw new Error('La anulacion lleva su motivo, siempre');
    this.escribir({ tipo: 'anulacion', numero: n, motivo: motivo.trim(), registrado_en: new Date().toISOString(), quien });
  }

  /** Los comprobantes vigentes (sin los anulados), del mas nuevo al mas viejo. */
  listar(periodo?: string): (RetencionRecibida & { anulado: false })[] {
    const fuera = this.anulados();
    return this.lineas
      .filter((l): l is RetencionRecibida => l.tipo === 'retencion' && !fuera.has(l.numero))
      .filter((l) => !periodo || l.periodo === periodo)
      .map((l) => ({ ...l, anulado: false as const }))
      .reverse();
  }

  /** El credito del mes: lo que va contra la cuota en la declaracion. */
  resumen(periodo: string): { periodo: string; comprobantes: number; iva_retenido_bs: number } {
    const del = this.listar(periodo);
    return {
      periodo,
      comprobantes: del.length,
      iva_retenido_bs: Math.round(del.reduce((s, l) => s + l.iva_retenido_bs, 0) * 100) / 100,
    };
  }
}
