// EL RELOJ DEL EQUIPO.
//
// Un documento fiscal lleva fecha y hora ciertas. El problema es que la hora la
// pone Windows, y a Windows le cambia la hora cualquiera que se siente en esa
// computadora: dos clics y la venta de hoy sale fechada la semana pasada, o el
// mes que viene. Nadie se entera, porque el libro sella lo que le den.
//
// Por eso la Estacion compara su reloj contra la hora de la nube y SE NIEGA A
// SELLAR si esta corrida. Es lo mismo que hace una maquina fiscal cuando pierde
// la hora: se planta.
//
// Dos comprobaciones, y son distintas a proposito:
//
//   1. CONTRA LA NUBE. Necesita internet. Si el desfase pasa de la tolerancia,
//      no se sella hasta que se corrija. Se confirma con una segunda lectura
//      antes de trancar, para no trancar el negocio por un tropiezo de la red.
//
//   2. CONTRA EL PROPIO LIBRO. Esta no necesita internet y no falla nunca: un
//      documento nuevo no puede tener fecha ANTERIOR al ultimo ya sellado. Si
//      alguien atrasa el reloj, se ve aunque el local este sin señal.
//
// Sin internet y sin haber podido revisar, la Estacion SIGUE TRABAJANDO: el
// negocio no puede quedarse sin vender porque se cayo el proveedor. Queda
// anotado desde cuando no se verifica, y la pantalla lo dice.
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeSync } from 'node:fs';
import { dirname } from 'node:path';

export const HORA_POR_DEFECTO = 'https://sello.gustitoxpress.app/api/hora';

/** Cuanto puede diferir el reloj antes de que se considere corrido. */
const TOLERANCIA_SEG = 300;
/** Margen para atras contra el libro: absorbe ajustes normales del sistema. */
const MARGEN_ATRAS_SEG = 60;
/**
 * Cuanto vale la ultima comprobacion. Pasado ese tiempo SIN poder comparar de
 * nuevo, un "reloj corrido" viejo deja de trancar: si no, un local al que se le
 * cayo el internet justo despues de una mala lectura se quedaria sin facturar
 * para siempre, sin manera de destrabarse. La comprobacion contra el libro (la
 * que no necesita señal) sigue trabajando igual.
 */
const VIGENCIA_HORAS = 6;

export interface EstadoReloj {
  /** Cuando se pudo comparar por ultima vez. */
  revisado_en: string | null;
  /** Diferencia en segundos: positivo = el equipo esta adelantado. */
  desfase_seg: number | null;
  /** Si la ultima revision dio corrido, aca queda por que. */
  corrido: boolean;
  /** Por que no se pudo revisar (sin internet, por ejemplo). */
  problema: string | null;
}

const VACIO: EstadoReloj = { revisado_en: null, desfase_seg: null, corrido: false, problema: null };

export class Reloj {
  private readonly ruta: string;
  private readonly url: string;
  private readonly tolerancia: number;

  constructor(op: { ruta: string; url?: string; toleranciaSeg?: number }) {
    this.ruta = op.ruta;
    this.url = op.url?.trim() || HORA_POR_DEFECTO;
    this.tolerancia = op.toleranciaSeg ?? TOLERANCIA_SEG;
    mkdirSync(dirname(this.ruta), { recursive: true });
  }

  estado(): EstadoReloj {
    if (!existsSync(this.ruta)) return { ...VACIO };
    try {
      return { ...VACIO, ...(JSON.parse(readFileSync(this.ruta, 'utf8')) as EstadoReloj) };
    } catch {
      return { ...VACIO };
    }
  }

  private guardar(e: EstadoReloj): void {
    const tmp = this.ruta + '.tmp';
    const fd = openSync(tmp, 'w');
    try {
      writeSync(fd, JSON.stringify(e, null, 2));
      fsyncSync(fd);
    } finally {
      closeSync(fd);
    }
    renameSync(tmp, this.ruta);
  }

  /** Una lectura contra la nube. Devuelve el desfase en segundos. */
  private async medir(): Promise<number> {
    const antes = Date.now();
    const r = await fetch(this.url, { cache: 'no-store' });
    if (!r.ok) throw new Error(`la hora de referencia respondio ${r.status}`);
    const d = (await r.json()) as { epoch_ms?: number; hora?: string };
    const nube = d.epoch_ms ?? (d.hora ? Date.parse(d.hora) : NaN);
    if (!Number.isFinite(nube)) throw new Error('la hora de referencia vino vacia');
    // El punto medio de la llamada: descuenta la mitad del viaje de ida y vuelta,
    // que si no un internet lento se veria como reloj atrasado.
    const despues = Date.now();
    const local = antes + (despues - antes) / 2;
    return Math.round((local - nube) / 1000);
  }

  /**
   * Compara el reloj con la nube y guarda el resultado.
   * Nunca lanza: si no hay internet, lo anota y la estacion sigue.
   */
  async revisar(): Promise<EstadoReloj> {
    const previo = this.estado();
    try {
      let desfase = await this.medir();
      if (Math.abs(desfase) > this.tolerancia) {
        // Segunda lectura antes de trancarle el negocio a nadie: una respuesta
        // lenta o una cache intermedia no pueden costar una tarde de ventas.
        try {
          const otra = await this.medir();
          if (Math.abs(otra) < Math.abs(desfase)) desfase = otra;
        } catch {
          /* con la primera basta para decidir */
        }
      }
      const e: EstadoReloj = {
        revisado_en: new Date().toISOString(),
        desfase_seg: desfase,
        corrido: Math.abs(desfase) > this.tolerancia,
        problema: null,
      };
      this.guardar(e);
      return e;
    } catch (err) {
      // No se pudo comparar: se conserva lo que se sabia y se anota por que.
      const e: EstadoReloj = { ...previo, problema: (err as Error).message };
      this.guardar(e);
      return e;
    }
  }

  /**
   * ¿Se puede sellar? Se le pasa la fecha del ultimo documento del libro para
   * la comprobacion que no necesita internet.
   */
  aprueba(ultimoSelladoEn?: string | null): { ok: boolean; problema?: string } {
    const e = this.estado();
    if (e.corrido && e.desfase_seg != null && this.vigente(e)) {
      const min = Math.round(Math.abs(e.desfase_seg) / 60);
      const lado = e.desfase_seg > 0 ? 'adelantada' : 'atrasada';
      return {
        ok: false,
        problema:
          `La hora de esta computadora esta ${lado} ${min} minuto(s) y no se puede sellar asi. ` +
          'Corrige la fecha y la hora de Windows (o dejala en automatico) y vuelve a revisar.',
      };
    }
    if (ultimoSelladoEn) {
      const atraso = Math.round((Date.parse(ultimoSelladoEn) - Date.now()) / 1000);
      if (atraso > MARGEN_ATRAS_SEG) {
        const min = Math.round(atraso / 60);
        return {
          ok: false,
          problema:
            `El reloj de esta computadora quedo ${min} minuto(s) ATRAS del ultimo documento ya sellado. ` +
            'Un documento no puede tener fecha anterior al anterior: corrige la hora antes de seguir.',
        };
      }
    }
    return { ok: true };
  }

  /** ¿La ultima comprobacion todavia vale? */
  private vigente(e: EstadoReloj): boolean {
    if (!e.revisado_en) return false;
    return Date.now() - Date.parse(e.revisado_en) < VIGENCIA_HORAS * 3600_000;
  }

  /** Para la pantalla: en cristiano. */
  resumen(): { texto: string; bien: boolean } {
    const e = this.estado();
    if (e.corrido && e.desfase_seg != null && this.vigente(e)) {
      return { texto: `CORRIDA ${Math.round(Math.abs(e.desfase_seg) / 60)} min, no se puede sellar`, bien: false };
    }
    if (e.corrido) {
      return { texto: 'estaba corrida y no se ha podido volver a comparar', bien: false };
    }
    if (!e.revisado_en) return { texto: e.problema ? 'sin poder verificar' : 'sin verificar todavia', bien: true };
    const horas = (Date.now() - Date.parse(e.revisado_en)) / 3600_000;
    const cuando = horas < 1 ? 'hace menos de una hora' : horas < 48 ? `hace ${Math.round(horas)} hora(s)` : `hace ${Math.round(horas / 24)} dia(s)`;
    return { texto: `verificada ${cuando}${e.problema ? ' (ahorita sin señal)' : ''}`, bien: true };
  }
}
