/* ================================================================
   REST-Zugriff auf Home Assistant
   ================================================================
   Ersetzt exakt das, was im alten panel_custom-Dashboard das eingespritzte
   hass-Objekt geleistet hat (SPECIFICATION.md §3 "Data flow"):
     hass.states[id].attributes.daten   ->  zustandLesen()/datenLesen()
     hass.callService(...)              ->  dienstAufrufen()
   Dieselben Entitäten, dieselben Dienste der Integration - nur über HTTP statt
   über ein Objekt, das nur innerhalb des HA-Frontends existiert. */

import type { DatenEntity, HassState } from "./types.ts";
import { zugangLesen, type Zugang } from "./umgebung.ts";

export class ApiFehler extends Error {
  /* Eigene Felder statt Parameter-Eigenschaften: Node kann letztere im
     Strip-Modus (--experimental-strip-types) nicht übersetzen, und darüber
     laufen die Rauchtests dieses Pakets. */
  readonly status: number | null;
  readonly ursache: unknown;

  constructor(message: string, status: number | null, ursache?: unknown) {
    super(message);
    this.name = "ApiFehler";
    this.status = status;
    this.ursache = ursache;
  }

  /** Fehlt oder taugt das Token nicht mehr - dann muss die Oberfläche zurück
      in die Ersteinrichtung, statt es endlos weiterzuversuchen. */
  get istAnmeldeproblem(): boolean {
    return this.status === 401 || this.status === 403;
  }

  /** Kein Netz erreichbar (status===null) - Offline-Fall, keine echte
      Fehlermeldung: die Oberfläche zeigt zwischengespeicherte Daten. */
  get istNetzproblem(): boolean {
    return this.status === null;
  }
}

export interface AnfrageOptionen {
  /** Bricht die Anfrage ab, statt im Funkloch minutenlang zu hängen. */
  zeitlimitMs?: number;
  signal?: AbortSignal;
}

const ZEITLIMIT_STANDARD_MS = 15_000;

export class HassRest {
  #zugang: Zugang | null = null;

  constructor(zugang?: Zugang) {
    this.#zugang = zugang ?? null;
  }

  /** Holt den Zugang beim ersten Bedarf aus der Ablage nach. */
  async #zugangHolen(): Promise<Zugang> {
    if (this.#zugang) return this.#zugang;
    const gelesen = await zugangLesen();
    if (!gelesen) throw new ApiFehler("Nicht eingerichtet: Server-Adresse oder Token fehlt.", 401);
    this.#zugang = gelesen;
    return gelesen;
  }

  zugangSetzen(zugang: Zugang | null): void {
    this.#zugang = zugang;
  }

  async #anfrage<T>(pfad: string, init: RequestInit, opt: AnfrageOptionen = {}): Promise<T> {
    const zugang = await this.#zugangHolen();

    /* Eigener Abbruch-Zeitgeber, zusätzlich zu einem evtl. übergebenen
       Signal - beide sollen abbrechen können. */
    const uhr = new AbortController();
    const frist = setTimeout(() => uhr.abort(), opt.zeitlimitMs ?? ZEITLIMIT_STANDARD_MS);
    const abbruch = () => uhr.abort();
    opt.signal?.addEventListener("abort", abbruch);

    try {
      const antwort = await fetch(`${zugang.basisUrl}${pfad}`, {
        ...init,
        signal: uhr.signal,
        headers: {
          Authorization: `Bearer ${zugang.token}`,
          "Content-Type": "application/json",
          ...(init.headers ?? {}),
        },
      });

      if (!antwort.ok) {
        throw new ApiFehler(
          `Home Assistant antwortete mit ${antwort.status} ${antwort.statusText} auf ${pfad}`,
          antwort.status,
        );
      }

      /* Manche Dienste antworten mit leerem Rumpf - dann kein JSON erzwingen. */
      const text = await antwort.text();
      return (text ? JSON.parse(text) : null) as T;
    } catch (fehler) {
      if (fehler instanceof ApiFehler) throw fehler;
      throw new ApiFehler(`Home Assistant nicht erreichbar (${pfad})`, null, fehler);
    } finally {
      clearTimeout(frist);
      opt.signal?.removeEventListener("abort", abbruch);
    }
  }

  /** Prüft Adresse und Token - für den letzten Schritt der Ersteinrichtung. */
  async verbindungPruefen(opt?: AnfrageOptionen): Promise<{ message: string }> {
    return this.#anfrage<{ message: string }>("/api/", { method: "GET" }, opt);
  }

  async zustandLesen<A extends Record<string, unknown>>(
    entityId: string,
    opt?: AnfrageOptionen,
  ): Promise<HassState<A>> {
    return this.#anfrage<HassState<A>>(`/api/states/${encodeURIComponent(entityId)}`, { method: "GET" }, opt);
  }

  /** Liest eine pyscript-Entität und packt gleich die Nutzlast aus. */
  async datenLesen<T>(entityId: string, opt?: AnfrageOptionen): Promise<T> {
    const zustand = await this.zustandLesen<{ daten: T }>(entityId, opt);
    return (zustand as DatenEntity<T>).attributes.daten;
  }

  /** Ruft einen Dienst auf, z. B. dienstAufrufen("audi_dashboard",
      "profil_schreiben", { profil_json }). */
  async dienstAufrufen(
    bereich: string,
    dienst: string,
    daten: Record<string, unknown> = {},
    opt?: AnfrageOptionen,
  ): Promise<unknown> {
    return this.#anfrage<unknown>(
      `/api/services/${encodeURIComponent(bereich)}/${encodeURIComponent(dienst)}`,
      { method: "POST", body: JSON.stringify(daten) },
      opt,
    );
  }
}
