// CONFIGURACION DE LA ESTACION FISCAL.
//
// Vive en el EQUIPO del negocio, junto al programa, nunca en la nube: una
// instalacion, un RIF. Es el molde que el SENIAT ya homologa (los sistemas
// autorizados se instalan; ninguno es un servicio en la nube sirviendo cien
// contribuyentes distintos desde el mismo sitio).
//
// Regla dura: si falta un dato obligatorio, el modulo NO sella nada. Es
// preferible que el negocio se entere al configurar y no que emita cien
// documentos con el RIF mal escrito.
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { rifValido, rifNormalizado } from '@gustito/shared/fiscal';

export interface ConfigImprenta {
  /** Que adaptador habla con ella. 'practica' no emite nada real. */
  adaptador: 'practica' | 'gustito' | 'hka';
  /** Nombre para mostrar (aparece en el estado y en la bitacora). */
  nombre: string;
  url?: string;
  usuario?: string;
  clave?: string;
  /** Token fijo, si el proveedor entrega uno en vez de usuario y clave. */
  token?: string;
}

export interface ConfigFiscal {
  emisor: {
    rif: string;
    razon_social: string;
    domicilio_fiscal: string;
    /** true = los precios de la carta ya traen el IVA adentro. */
    precios_con_iva: boolean;
    /** Alicuota general del negocio (16, 8 o 0). */
    alicuota_general: number;
    /**
     * IGTF (3% sobre pagos en divisas): APAGADO salvo que el SENIAT designe al
     * negocio sujeto pasivo especial (Providencia 000013). Programado para ese
     * dia; encenderlo sin designacion seria cobrar un impuesto que no toca.
     */
    igtf_activo?: boolean;
  };
  /** Serie del documento: una por instalacion, no se mezclan. */
  serie: string;
  sucursal: string;
  /** Nombre del equipo, para saber cual instalacion sello cada documento. */
  equipo: string;
  /** La imprenta con la que se emite. */
  imprenta: ConfigImprenta;
  /**
   * El salvavidas: una segunda imprenta configurada por si la primera falla.
   * NO se cambia sola: el cambio es manual y queda anotado, porque dos fuentes
   * de numeracion corriendo a la vez si es un problema fiscal.
   */
  respaldo?: ConfigImprenta | null;
  /** Cual de las dos esta en uso ahorita. */
  usando: 'imprenta' | 'respaldo';
  /**
   * El enlace con Gustito en la nube: por aca la Estacion HALA las ventas
   * cerradas (la nube no puede llamar a un 127.0.0.1) y manda su latido.
   * Es opcional: una Estacion puede trabajar sola, recibiendo por /api/venta.
   */
  nube?: {
    url: string;
    /** La misma llave que entrega la imprenta. */
    llave: string;
    /** Cada cuantos segundos se pregunta por ventas nuevas. */
    cada_segundos?: number;
  } | null;
  /**
   * Contra donde se compara el reloj del equipo. Vacio = la de la imprenta.
   * Se deja configurable porque un negocio puede querer su propia referencia.
   */
  hora_url?: string;
  /**
   * La cara de la factura, decidida por el negocio. Es la zona LIBRE del
   * documento: nada de aqui toca ni desplaza los datos que exige el art. 7 de
   * la 000102 (esos van siempre, trancados en el codigo). Todo es opcional.
   */
  modelo_factura?: {
    /** Mensaje del pie ("Gracias por su compra"). Maximo 240 caracteres. */
    mensaje_pie?: string | null;
    /** Logo para la version digital (direccion https). La termica no lo usa todavia. */
    logo_url?: string | null;
    /** Color de acento de la version digital, en hex (#RRGGBB). */
    color?: string | null;
  } | null;
}

export interface Falla {
  campo: string;
  problema: string;
}

const ALICUOTAS_VALIDAS = [0, 8, 16];

/** Revisa la configuracion completa. Devuelve la lista de lo que falta o esta malo. */
export function revisar(c: Partial<ConfigFiscal> | null): Falla[] {
  const fallas: Falla[] = [];
  if (!c) return [{ campo: 'todo', problema: 'Todavia no hay configuracion' }];

  const e = c.emisor;
  if (!e?.rif || !rifValido(e.rif)) {
    fallas.push({ campo: 'rif', problema: 'El RIF del negocio falta o el ultimo digito no cuadra' });
  }
  if (!e?.razon_social?.trim()) {
    fallas.push({ campo: 'razon_social', problema: 'Falta la razon social, como aparece en el RIF' });
  }
  if (!e?.domicilio_fiscal?.trim()) {
    fallas.push({ campo: 'domicilio_fiscal', problema: 'Falta el domicilio fiscal' });
  }
  if (e && !ALICUOTAS_VALIDAS.includes(Number(e.alicuota_general))) {
    fallas.push({ campo: 'alicuota_general', problema: 'La alicuota debe ser 16, 8 o 0' });
  }
  // Se acepta en minuscula y se sube al guardar, igual que el RIF: rechazar a
  // alguien por escribir "a" en vez de "A" es fricción tonta.
  if (!c.serie || !/^[A-Za-z]{1,4}$/.test(c.serie)) {
    fallas.push({ campo: 'serie', problema: 'La serie va de 1 a 4 letras (ejemplo: A)' });
  }
  if (!c.sucursal?.trim()) {
    fallas.push({ campo: 'sucursal', problema: 'Falta identificar la sucursal (si es una sola, pon 1)' });
  }
  if (!c.equipo?.trim()) {
    fallas.push({ campo: 'equipo', problema: 'Falta el nombre del equipo donde corre la estacion' });
  }
  if (!c.imprenta?.adaptador) {
    fallas.push({ campo: 'imprenta', problema: 'Falta decir con cual imprenta se emite' });
  } else if (c.imprenta.adaptador === 'gustito') {
    // La direccion tiene valor por defecto; lo unico imprescindible es la llave
    // que entrega la imprenta al dar de alta al negocio.
    if (!c.imprenta.token?.trim()) {
      fallas.push({ campo: 'imprenta.acceso', problema: 'Falta la llave que te entrego la imprenta' });
    }
  } else if (c.imprenta.adaptador !== 'practica') {
    if (!c.imprenta.url?.trim()) fallas.push({ campo: 'imprenta.url', problema: 'Falta la direccion de la imprenta' });
    if (!c.imprenta.token?.trim() && !(c.imprenta.usuario?.trim() && c.imprenta.clave?.trim())) {
      fallas.push({ campo: 'imprenta.acceso', problema: 'Falta el usuario y la clave (o el token) de la imprenta' });
    }
  }

  // El enlace con la nube es opcional (una Estacion puede trabajar sola), pero
  // si esta puesto tiene que estar completo: media configuracion es peor que
  // ninguna, porque el negocio cree que las ventas estan entrando y no.
  if (c.nube) {
    if (!c.nube.url?.trim()) fallas.push({ campo: 'nube.url', problema: 'Falta la direccion de Gustito' });
    if (!c.nube.llave?.trim()) fallas.push({ campo: 'nube.llave', problema: 'Falta la llave para conectarse con Gustito' });
  }

  // El modelo de factura es opcional, pero lo que traiga debe estar sano: es
  // la cara del documento ante el cliente del negocio.
  const m = c.modelo_factura;
  if (m) {
    if (m.mensaje_pie && m.mensaje_pie.length > 240) {
      fallas.push({ campo: 'modelo_factura.mensaje_pie', problema: 'El mensaje del pie no puede pasar de 240 caracteres' });
    }
    if (m.color && !/^#[0-9a-fA-F]{6}$/.test(m.color)) {
      fallas.push({ campo: 'modelo_factura.color', problema: 'El color va en formato #RRGGBB, por ejemplo #C9A227' });
    }
    if (m.logo_url && !/^https:\/\/\S+$/.test(m.logo_url)) {
      fallas.push({ campo: 'modelo_factura.logo_url', problema: 'La direccion del logo debe empezar con https://' });
    }
  }
  return fallas;
}

/** ¿Se puede sellar? Solo si no hay ni una falla. */
export function puedeOperar(c: Partial<ConfigFiscal> | null): boolean {
  return revisar(c).length === 0;
}

/** La imprenta que esta en uso segun la configuracion. */
export function imprentaActiva(c: ConfigFiscal): ConfigImprenta {
  if (c.usando === 'respaldo' && c.respaldo) return c.respaldo;
  return c.imprenta;
}

export function rutaConfig(base: string): string {
  return join(base, 'fiscal.config.json');
}

export function leerConfig(base: string): ConfigFiscal | null {
  const ruta = rutaConfig(base);
  if (!existsSync(ruta)) return null;
  try {
    return JSON.parse(readFileSync(ruta, 'utf8')) as ConfigFiscal;
  } catch {
    throw new Error('El archivo de configuracion esta dañado. Revisalo o vuelve a configurar la estacion.');
  }
}

/**
 * Guarda la configuracion. Normaliza el RIF y escribe con archivo temporal
 * primero: si se va la luz a mitad, la configuracion vieja queda intacta.
 */
export function guardarConfig(base: string, c: ConfigFiscal): ConfigFiscal {
  const fallas = revisar(c);
  if (fallas.length) {
    throw new Error('No se puede guardar: ' + fallas.map((f) => f.problema).join('; '));
  }
  const limpia: ConfigFiscal = {
    ...c,
    emisor: {
      ...c.emisor,
      rif: rifNormalizado(c.emisor.rif) ?? c.emisor.rif,
      razon_social: c.emisor.razon_social.trim(),
      domicilio_fiscal: c.emisor.domicilio_fiscal.trim(),
    },
    serie: c.serie.toUpperCase(),
    sucursal: c.sucursal.trim(),
    equipo: c.equipo.trim(),
  };
  const ruta = rutaConfig(base);
  mkdirSync(dirname(ruta), { recursive: true });
  const tmp = ruta + '.tmp';
  const fd = openSync(tmp, 'w');
  try {
    writeSync(fd, JSON.stringify(limpia, null, 2));
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
  renameSync(tmp, ruta);
  return limpia;
}

// ===== EL SERIAL DE LA ESTACION =====
// Lo asigna LA IMPRENTA al registrar al cliente (EF-00001-9) y la estacion lo
// aprende sola por el latido. No es configuracion del negocio (nadie lo
// teclea ni lo cambia aca): por eso vive aparte, en estado/serial.json.

export function rutaSerial(base: string): string {
  return join(base, 'estado', 'serial.json');
}

export function leerSerial(base: string): string | null {
  try {
    const d = JSON.parse(readFileSync(rutaSerial(base), 'utf8')) as { serial?: string };
    return d.serial?.trim() || null;
  } catch {
    return null;
  }
}

export function guardarSerial(base: string, serial: string): void {
  const ruta = rutaSerial(base);
  mkdirSync(dirname(ruta), { recursive: true });
  const tmp = ruta + '.tmp';
  const fd = openSync(tmp, 'w');
  try {
    writeSync(fd, JSON.stringify({ serial, recibido_en: new Date().toISOString() }, null, 2));
    fsyncSync(fd);
  } finally {
    closeSync(fd);
  }
  renameSync(tmp, ruta);
}
