import streamDeck from "@elgato/streamdeck";
import { normalizeFilyxBaseUrl } from "./filyxClient";
import type { FilyxGlobalSettings } from "../types";

type PairStartResponse = { ok?: boolean; pairing_id?: string; expires_at?: string; connect_url?: string; error?: string };
type PairStatusResponse = { ok?: boolean; status?: string; error?: string };

const randomHex = (bytesLength: number): string => {
  const bytes = new Uint8Array(bytesLength);
  globalThis.crypto.getRandomValues(bytes);
  return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
};

/**
 * Secure, browser-confirmed first-run pairing.
 *
 * A fresh, revocable FilyX token is generated on the Stream Deck computer.
 * FilyX only receives its SHA-256 fingerprint until the already authenticated
 * browser approves it. The raw token therefore never travels through the
 * property inspector and is never rendered by the panel.
 */
export class FilyxAutomaticPairing {
  private static initializing: Promise<void> | null = null;
  private static polling = false;

  static install(): void {
    streamDeck.system.onDidReceiveDeepLink((event) => {
      const pairingId = event.url.queryParameters.get("pair") || "";
      if (event.url.path === "/pair" && pairingId) void this.complete(pairingId);
    });
  }

  static async ensure(): Promise<void> {
    if (!this.initializing) {
      this.initializing = this.begin().finally(() => { this.initializing = null; });
    }
    await this.initializing;
  }

  static async apiKey(): Promise<string> {
    const global = await streamDeck.settings.getGlobalSettings<FilyxGlobalSettings>();
    const key = String(global.apiKey || "").trim();
    if (key.startsWith("fxsd_")) return key;
    void this.ensure();
    return "";
  }

  private static async begin(): Promise<void> {
    const current = await streamDeck.settings.getGlobalSettings<FilyxGlobalSettings>();
    if (String(current.apiKey || "").trim().startsWith("fxsd_")) return;

    const baseUrl = normalizeFilyxBaseUrl(current.apiUrl);
    const now = Date.now();
    let pairingId = String(current.pairingId || "");
    let token = String(current.pairingToken || "");
    const expiresAt = Number(current.pairingExpiresAt || 0);

    if (!pairingId || !token.startsWith("fxsd_") || expiresAt <= now) {
      const deviceId = String(current.deviceId || `filyx-${globalThis.crypto.randomUUID()}`);
      token = `fxsd_${randomHex(36)}`;
      const response = await this.request<PairStartResponse>(baseUrl, "/api/streamdeck/plugin/pair/start", { device_id: deviceId, token });
      if (!response.ok || !response.pairing_id || !response.connect_url) throw new Error(response.error || "Impossible de démarrer la liaison FilyX.");
      pairingId = response.pairing_id;
      const parsedExpiry = Date.parse(String(response.expires_at || ""));
      await streamDeck.settings.setGlobalSettings<FilyxGlobalSettings>({
        ...current,
        apiUrl: baseUrl,
        deviceId,
        pairingId,
        pairingToken: token,
        pairingExpiresAt: Number.isFinite(parsedExpiry) ? parsedExpiry : now + 15 * 60 * 1000,
      });
      streamDeck.logger.info("[FilyX] Liaison automatique en attente de validation navigateur.");
      void streamDeck.system.openUrl(response.connect_url);
    }
    void this.poll(pairingId, token, baseUrl);
  }

  private static async poll(pairingId: string, token: string, baseUrl: string): Promise<void> {
    if (this.polling) return;
    this.polling = true;
    try {
      for (let attempt = 0; attempt < 180; attempt += 1) {
        const result = await this.status(pairingId, token, baseUrl);
        if (result.status === "approved") {
          await this.storeApproved(pairingId);
          return;
        }
        if (result.status !== "pending") return;
        await new Promise<void>((resolve) => setTimeout(resolve, 5000));
      }
    } catch (error) {
      streamDeck.logger.warn(`[FilyX] Liaison automatique indisponible : ${String(error)}`);
    } finally {
      this.polling = false;
    }
  }

  private static async complete(pairingId: string): Promise<void> {
    const global = await streamDeck.settings.getGlobalSettings<FilyxGlobalSettings>();
    if (String(global.pairingId || "") !== pairingId) return;
    const token = String(global.pairingToken || "");
    if (!token.startsWith("fxsd_")) return;
    try {
      const result = await this.status(pairingId, token, normalizeFilyxBaseUrl(global.apiUrl));
      if (result.status === "approved") await this.storeApproved(pairingId);
    } catch (error) {
      streamDeck.logger.warn(`[FilyX] Confirmation de liaison reçue mais non vérifiée : ${String(error)}`);
    }
  }

  private static async storeApproved(pairingId: string): Promise<void> {
    const global = await streamDeck.settings.getGlobalSettings<FilyxGlobalSettings>();
    if (String(global.pairingId || "") !== pairingId || !String(global.pairingToken || "").startsWith("fxsd_")) return;
    await streamDeck.settings.setGlobalSettings<FilyxGlobalSettings>({
      ...global,
      apiUrl: normalizeFilyxBaseUrl(global.apiUrl),
      apiKey: String(global.pairingToken),
      pairingId: "",
      pairingToken: "",
      pairingExpiresAt: 0,
    });
    streamDeck.logger.info("[FilyX] Stream Deck relié automatiquement au compte FilyX.");
  }

  private static async status(pairingId: string, token: string, baseUrl: string): Promise<PairStatusResponse> {
    const response = await this.request<PairStatusResponse>(baseUrl, "/api/streamdeck/plugin/pair/status", { pairing_id: pairingId, token });
    if (!response.ok) throw new Error(response.error || "État de liaison FilyX invalide.");
    return response;
  }

  private static async request<T>(baseUrl: string, path: string, body: Record<string, string>): Promise<T> {
    const response = await fetch(`${baseUrl}${path}`, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify(body),
    });
    const payload = await response.json().catch(() => ({ ok: false, error: "Réponse FilyX invalide." })) as T;
    if (!response.ok) return payload;
    return payload;
  }
}
