/* ================================================================
   Warteschlange für Änderungen ohne Netz
   ================================================================
   Entscheidung aus COMPANION_APP_ARCHITECTURE.md §3: Offline gemachte
   Änderungen werden nicht abgewiesen, sondern gesammelt und nachgeliefert,
   sobald wieder eine Verbindung besteht. Weil es genau einen Benutzer gibt,
   reicht "der letzte Schreibvorgang gewinnt" - es gibt keinen zweiten
   Bearbeiter, gegen den abgeglichen werden müsste.

   Die Warteschlange liegt in derselben Ablage wie das Token, überlebt also
   einen App-Neustart: ein Beleg, der im Funkloch fotografiert wurde, geht
   nicht verloren, nur weil die App zwischendurch geschlossen wurde. */

import { ablage } from "./umgebung.ts";
import { ApiFehler, type HassRest } from "./rest.ts";

const SCHLUESSEL = "dm360.warteschlange";

export interface WartenderAuftrag {
  id: string;
  /** Zeitpunkt der Erstellung - die Oberfläche zeigt "wartet seit ...". */
  erstellt: string;
  bereich: string;
  dienst: string;
  daten: Record<string, unknown>;
  /** Kurzer Klartext für die Anzeige, z. B. "Beleg vom 3.8. hochladen". */
  beschreibung: string;
  fehlversuche: number;
  letzterFehler?: string;
}

type WarteschlangenHorcher = (auftraege: WartenderAuftrag[]) => void;

/** Nach so vielen vergeblichen Versuchen gilt ein Auftrag als kaputt und wird
    nicht weiter blind wiederholt - sonst blockiert ein einziger fehlerhafter
    Auftrag für immer alle nachfolgenden. */
const MAX_FEHLVERSUCHE = 5;

export class Warteschlange {
  #auftraege: WartenderAuftrag[] = [];
  #geladen = false;
  #laeuft = false;
  #horcher = new Set<WarteschlangenHorcher>();
  /* Bewusst als eigenes Feld statt als Parameter-Eigenschaft im Konstruktor:
     Node kann Parameter-Eigenschaften im Strip-Modus (--experimental-strip-types)
     nicht übersetzen, und genau darüber laufen die Rauchtests dieses Pakets. */
  readonly rest: HassRest;

  constructor(rest: HassRest) {
    this.rest = rest;
  }

  async laden(): Promise<void> {
    if (this.#geladen) return;
    const roh = await ablage().lesen(SCHLUESSEL);
    if (roh) {
      try {
        const gelesen: unknown = JSON.parse(roh);
        if (Array.isArray(gelesen)) this.#auftraege = gelesen as WartenderAuftrag[];
      } catch {
        /* Beschädigter Eintrag: lieber leer starten als beim Start abstürzen. */
        this.#auftraege = [];
      }
    }
    this.#geladen = true;
    this.#melden();
  }

  get auftraege(): readonly WartenderAuftrag[] {
    return this.#auftraege;
  }

  get anzahl(): number {
    return this.#auftraege.length;
  }

  aufAenderung(horcher: WarteschlangenHorcher): () => void {
    this.#horcher.add(horcher);
    horcher([...this.#auftraege]);
    return () => this.#horcher.delete(horcher);
  }

  #melden(): void {
    const kopie = [...this.#auftraege];
    for (const h of this.#horcher) h(kopie);
  }

  async #sichern(): Promise<void> {
    await ablage().schreiben(SCHLUESSEL, JSON.stringify(this.#auftraege));
    this.#melden();
  }

  /** Reiht einen Dienstaufruf ein. Wird direkt danach abgearbeitet, falls
      Netz da ist - im Normalfall merkt der Benutzer die Warteschlange also
      gar nicht. */
  async einreihen(
    bereich: string,
    dienst: string,
    daten: Record<string, unknown>,
    beschreibung: string,
  ): Promise<WartenderAuftrag> {
    await this.laden();
    const auftrag: WartenderAuftrag = {
      id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      erstellt: new Date().toISOString(),
      bereich,
      dienst,
      daten,
      beschreibung,
      fehlversuche: 0,
    };
    this.#auftraege.push(auftrag);
    await this.#sichern();
    void this.abarbeiten();
    return auftrag;
  }

  async entfernen(id: string): Promise<void> {
    await this.laden();
    this.#auftraege = this.#auftraege.filter((a) => a.id !== id);
    await this.#sichern();
  }

  /** Arbeitet die Warteschlange der Reihe nach ab. Reihenfolge ist wichtig:
      wer erst das Profil ändert und dann einen Beleg hochlädt, erwartet
      genau diese Abfolge auf dem Server. Deshalb wird beim ersten
      Netzproblem abgebrochen statt weitergesprungen. */
  async abarbeiten(): Promise<{ erledigt: number; verblieben: number }> {
    await this.laden();
    if (this.#laeuft) return { erledigt: 0, verblieben: this.#auftraege.length };
    this.#laeuft = true;

    let erledigt = 0;
    try {
      while (this.#auftraege.length > 0) {
        const auftrag = this.#auftraege[0];
        if (!auftrag) break;

        try {
          await this.rest.dienstAufrufen(auftrag.bereich, auftrag.dienst, auftrag.daten);
          this.#auftraege.shift();
          erledigt += 1;
          await this.#sichern();
        } catch (fehler) {
          if (fehler instanceof ApiFehler && fehler.istNetzproblem) {
            /* Weiterhin offline - später erneut versuchen, nichts verwerfen. */
            break;
          }

          auftrag.fehlversuche += 1;
          auftrag.letzterFehler = fehler instanceof Error ? fehler.message : String(fehler);

          if (auftrag.fehlversuche >= MAX_FEHLVERSUCHE) {
            /* Dauerhaft kaputt: aus dem Weg räumen, damit die übrigen
               Aufträge nicht ewig dahinter feststecken. Die Oberfläche kann
               den Fehler über aufAenderung() sichtbar machen. */
            this.#auftraege.shift();
          }
          await this.#sichern();
          break;
        }
      }
    } finally {
      this.#laeuft = false;
    }

    return { erledigt, verblieben: this.#auftraege.length };
  }
}
