import { promises as fs } from "node:fs";
import path from "node:path";

export type FilyxObsCompanionDiscovery = {
  transport: "filyx-companion";
  host: "filyx://obs-companion";
  version: string;
  scenes: string[];
  sceneSources: Record<string, string[]>;
  inputs: string[];
};

type CompanionResponse = { ok: boolean; payload: string };

const delay = (milliseconds: number) => new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
const toBase64Url = (value: string) => Buffer.from(value, "utf8").toString("base64url");
const fromBase64Url = (value: string) => Buffer.from(value || "", "base64url").toString("utf8");

/**
 * The companion never opens a network listener. Stream Deck and OBS exchange
 * one short command at a time in the local user's AppData folder instead.
 */
export class FilyxObsCompanion {
  private static sequence = 0;
  private static queue: Promise<unknown> = Promise.resolve();

  private static get directory(): string {
    const appData = process.env.APPDATA || process.env.LOCALAPPDATA;
    return path.join(appData || process.cwd(), "FilyX", "obs-companion");
  }

  private static get requestPath(): string { return path.join(this.directory, "request.txt"); }
  private static get responsePath(): string { return path.join(this.directory, "response.txt"); }

  private static async requestNow(command: string, values: string[] = [], timeout = 1800): Promise<string> {
    await fs.mkdir(this.directory, { recursive: true });
    const requestId = `${process.pid}-${Date.now()}-${++this.sequence}`;
    const request = [requestId, command, ...values.map(toBase64Url)].join("|");
    const temporaryPath = `${this.requestPath}.${requestId}.tmp`;
    await fs.rm(this.responsePath, { force: true });
    await fs.rm(this.requestPath, { force: true });
    await fs.writeFile(temporaryPath, request, "utf8");
    await fs.rename(temporaryPath, this.requestPath);

    const deadline = Date.now() + timeout;
    while (Date.now() < deadline) {
      try {
        const raw = (await fs.readFile(this.responsePath, "utf8")).trim();
        const [responseId, state, payload = ""] = raw.split("|");
        if (responseId === requestId) {
          await fs.rm(this.responsePath, { force: true });
          const decoded = fromBase64Url(payload);
          if (state === "OK") return decoded;
          throw new Error(decoded || "Le FilyX OBS Companion a refusé la commande.");
        }
      } catch (error) {
        const code = error && typeof error === "object" && "code" in error ? String((error as { code?: unknown }).code || "") : "";
        if (error instanceof Error && code !== "ENOENT") throw error;
      }
      await delay(30);
    }
    throw new Error("FilyX OBS Companion introuvable. Dans OBS : Outils → Scripts → +, ajoute FilyX_OBS_Companion.lua une seule fois.");
  }

  private static async request(command: string, values: string[] = [], timeout = 1800): Promise<string> {
    const work = this.queue.then(() => this.requestNow(command, values, timeout));
    this.queue = work.catch(() => undefined);
    return work;
  }

  public static async available(): Promise<boolean> {
    try { await this.request("ping", [], 280); return true; } catch (_) { return false; }
  }

  public static async discover(): Promise<FilyxObsCompanionDiscovery> {
    const raw = await this.request("discover");
    const scenes: string[] = [];
    const sceneSources: Record<string, string[]> = {};
    const inputs: string[] = [];
    let version = "FilyX OBS Companion";
    for (const line of raw.split("\n")) {
      const [type, ...fields] = line.split("\t");
      if (type === "version") version = fromBase64Url(fields[0] || "") || version;
      if (type === "scene") {
        const name = fromBase64Url(fields[0] || "");
        if (!name) continue;
        scenes.push(name);
        sceneSources[name] = fields.slice(1).map(fromBase64Url).filter(Boolean);
      }
      if (type === "input") {
        const name = fromBase64Url(fields[0] || "");
        if (name) inputs.push(name);
      }
    }
    return { transport: "filyx-companion", host: "filyx://obs-companion", version, scenes, sceneSources, inputs };
  }

  public static setScene(sceneName: string): Promise<string> { return this.request("set_scene", [sceneName]); }
  public static setSource(sceneName: string, sourceName: string, mode: string): Promise<string> { return this.request("set_source", [sceneName, sourceName, mode]); }
  public static refreshBrowser(sourceName: string): Promise<string> { return this.request("refresh_browser", [sourceName]); }
  public static toggleRecord(): Promise<string> { return this.request("toggle_record"); }
  public static toggleStream(): Promise<string> { return this.request("toggle_stream"); }
}
