// LA PUERTA DEL FISCAL.
//
// Providencia 000121, articulo 3, numeral 8: hay que otorgarle al SENIAT una
// clave de consulta con acceso al sistema y a la interfaz de programacion. Y la
// Disposicion Final Primera lo deja entrar a verificar y pedir reportes.
//
// Reglas de esta puerta:
//   - SOLO LEE. Aca no hay forma de sellar, editar, anular ni configurar nada.
//     No es que este escondido: sencillamente no existe el metodo.
//   - TODO lo que el fiscal mira queda anotado en la bitacora (quien, cuando,
//     desde donde y que rango consulto). Eso protege a las dos partes.
//   - Exporta plano, para que se lo pueda llevar sin depender de programas
//     nuestros.
import { LibroFiscal } from './libro.js';
import type { Bitacora } from './bitacora.js';
import type { ColaTransmision } from './cola.js';
import type { Asiento, Evento } from './tipos.js';

export interface FilaReporte {
  fecha: string;
  tipo: string;
  serie: string;
  numero: number;
  control: number;
  numero_control_imprenta: string;
  /** Que imprenta digital le asigno ese numero de control. */
  imprenta: string;
  cliente: string;
  identificacion: string;
  canal: string;
  base_usd: number;
  iva_usd: number;
  total_usd: number;
  tasa_bs: number | null;
  total_bs: number | null;
  transmision: string;
}

/**
 * Como sale UNA celda del CSV. Una sola, para los dos reportes: si el escapado
 * vive en dos sitios, tarde o temprano se arregla en uno y se olvida el otro.
 *
 * Dos cosas hace:
 *  - comillas y punto y coma: lo de siempre, para que la celda no se parta.
 *  - el apostrofe delante de = + - @: Excel trata como FORMULA toda celda que
 *    empiece asi. El nombre del cliente lo escribe el comensal, o sea que es
 *    texto de afuera; sin esto, un nombre que empiece por `=` se ejecuta en la
 *    computadora de quien abra el archivo, que ahora tambien es el contador.
 *    Con el apostrofe Excel lo muestra igual, pero como texto.
 */
export function celdaCsv(v: unknown): string {
  let s = v == null ? '' : String(v);
  if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
  return /[";\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
}

export class Auditor {
  private readonly libro: LibroFiscal;
  private readonly bitacora: Bitacora;
  private readonly cola: ColaTransmision | null;
  private readonly quien: string;
  private readonly desde_donde: string;

  constructor(op: {
    libro: LibroFiscal;
    bitacora: Bitacora;
    cola?: ColaTransmision | null;
    quien?: string;
    desde?: string;
  }) {
    this.libro = op.libro;
    this.bitacora = op.bitacora;
    this.cola = op.cola ?? null;
    this.quien = op.quien ?? 'auditor';
    this.desde_donde = op.desde ?? 'local';
  }

  /** Documentos de un rango de fechas. Queda anotado que se consultaron. */
  consultar(desde: Date, hasta: Date): Asiento[] {
    const docs = this.libro.entre(desde, hasta);
    this.bitacora.anotar(this.quien, this.desde_donde, 'consulta_fiscal', {
      desde: desde.toISOString(),
      hasta: hasta.toISOString(),
      documentos: docs.length,
    });
    return docs;
  }

  /** Un documento puntual, por tipo y numero. */
  verDocumento(tipo: Asiento['tipo'], numero: number): Asiento | null {
    const a = this.libro.buscar(tipo, numero);
    this.bitacora.anotar(this.quien, this.desde_donde, 'consulta_documento', { tipo, numero, encontrado: !!a });
    return a;
  }

  /** El reporte de ventas del rango, listo para exportar. */
  reporte(desde: Date, hasta: Date): FilaReporte[] {
    return this.consultar(desde, hasta).map((a) => ({
      // En una factura del talonario manual vale la fecha del papel, no la del
      // dia en que se cargo.
      fecha: LibroFiscal.fechaDe(a),
      tipo: a.tipo,
      serie: a.serie,
      numero: a.numero,
      control: a.control,
      // La del talonario ya venia con su numero impreso: ese es el que manda.
      numero_control_imprenta:
        a.documento.talonario?.numero ?? this.cola?.ver(a.seq)?.numeroControl ?? '',
      // Con proveedor de respaldo configurado, de cual salio ESTE numero.
      imprenta: a.documento.talonario ? 'talonario manual' : (this.cola?.ver(a.seq)?.imprenta ?? ''),
      cliente: a.documento.cliente.nombre,
      identificacion: a.documento.cliente.rif ?? a.documento.cliente.cedula ?? '',
      canal: a.documento.canal,
      base_usd: a.documento.base_usd,
      iva_usd: a.documento.iva_usd,
      total_usd: a.documento.total_usd,
      tasa_bs: a.documento.tasa_bs,
      total_bs: a.documento.total_bs,
      transmision: this.cola?.ver(a.seq)?.estado ?? 'sin registro',
    }));
  }

  /** El mismo reporte en texto plano (CSV), que es lo que se lleva un fiscal. */
  exportarCsv(desde: Date, hasta: Date): string {
    const filas = this.reporte(desde, hasta);
    const columnas: (keyof FilaReporte)[] = [
      'fecha', 'tipo', 'serie', 'numero', 'control', 'numero_control_imprenta', 'imprenta',
      'cliente', 'identificacion', 'canal', 'base_usd', 'iva_usd', 'total_usd',
      'tasa_bs', 'total_bs', 'transmision',
    ];
    // Separador punto y coma: en Venezuela la coma es el decimal y Excel en
    // español abre bien el punto y coma.
    const lineas = [columnas.join(';')];
    for (const f of filas) lineas.push(columnas.map((c) => celdaCsv(f[c])).join(';'));
    this.bitacora.anotar(this.quien, this.desde_donde, 'exportacion', {
      desde: desde.toISOString(),
      hasta: hasta.toISOString(),
      filas: filas.length,
    });
    return lineas.join('\n');
  }

  /** Totales del periodo, con el desglose por alicuota. */
  totales(desde: Date, hasta: Date): {
    documentos: number;
    base_usd: number;
    iva_usd: number;
    total_usd: number;
    por_alicuota: { alicuota: number; base_usd: number; iva_usd: number }[];
  } {
    const docs = this.consultar(desde, hasta);
    const mapa = new Map<number, { base_usd: number; iva_usd: number }>();
    let base = 0;
    let iva = 0;
    let total = 0;
    for (const a of docs) {
      base += a.documento.base_usd;
      iva += a.documento.iva_usd;
      total += a.documento.total_usd;
      for (const x of a.documento.por_alicuota) {
        const acc = mapa.get(x.alicuota) ?? { base_usd: 0, iva_usd: 0 };
        acc.base_usd += x.base_usd;
        acc.iva_usd += x.iva_usd;
        mapa.set(x.alicuota, acc);
      }
    }
    const dos = (n: number) => Number(n.toFixed(2));
    return {
      documentos: docs.length,
      base_usd: dos(base),
      iva_usd: dos(iva),
      total_usd: dos(total),
      por_alicuota: [...mapa.entries()]
        .map(([alicuota, v]) => ({ alicuota, base_usd: dos(v.base_usd), iva_usd: dos(v.iva_usd) }))
        .sort((x, y) => x.alicuota - y.alicuota),
    };
  }

  /**
   * Deja que el propio fiscal compruebe que el libro no fue alterado: recorre
   * la cadena completa y dice si esta sana o en cual linea se rompio.
   */
  comprobarIntegridad(): { integro: boolean; problema?: string; linea?: number } {
    const r = this.libro.verificar();
    this.bitacora.anotar(this.quien, this.desde_donde, 'comprobacion_integridad', { integro: r.integro, problema: r.problema });
    return r;
  }

  /**
   * El registro de EVENTOS del rango (art. 3, num. 8: la clave de consulta da
   * acceso a los registros fiscales Y de eventos). Se filtra ANTES de anotar la
   * consulta, asi el evento de "me consultaron" no aparece en su propia
   * respuesta pero si queda para la proxima.
   */
  eventos(desde: Date, hasta: Date): Evento[] {
    const filtrados = this.bitacora.todos().filter((e) => {
      const t = new Date(e.en).getTime();
      return t >= desde.getTime() && t <= hasta.getTime();
    });
    this.bitacora.anotar(this.quien, this.desde_donde, 'consulta_eventos', {
      desde: desde.toISOString(),
      hasta: hasta.toISOString(),
      eventos: filtrados.length,
    });
    return filtrados;
  }

  /** Los eventos en texto plano, mismo separador que el reporte de documentos. */
  exportarEventosCsv(desde: Date, hasta: Date): string {
    const filas = this.eventos(desde, hasta);
    const lineas = ['seq;fecha;quien;desde;accion;detalle;hash'];
    for (const e of filas) {
      lineas.push([e.seq, e.en, e.quien, e.desde, e.accion, JSON.stringify(e.detalle ?? {}), e.hash].map(celdaCsv).join(';'));
    }
    this.bitacora.anotar(this.quien, this.desde_donde, 'exportacion_eventos', {
      desde: desde.toISOString(),
      hasta: hasta.toISOString(),
      filas: filas.length,
    });
    return lineas.join('\n');
  }

  /** La cadena de eventos tambien se puede comprobar sola, igual que el libro. */
  comprobarIntegridadEventos(): { integro: boolean; problema?: string; linea?: number } {
    const r = this.bitacora.verificar();
    this.bitacora.anotar(this.quien, this.desde_donde, 'comprobacion_integridad_eventos', { integro: r.integro, problema: r.problema });
    return r;
  }
}
