import OBSWebSocket from "obs-websocket-js/json";
import { FilyxObsCompanion } from "./filyxObsCompanion";
import type { FilyxSettings } from "../types";

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

/** FilyX Companion is the normal private local route; OBS WebSocket is only the recovery route. */
export class ObsAutoConnector {
  private obs: OBSWebSocket | null = null;
  private connectedHost = "";
  private connectionFingerprint = "";

  private async tryConnect(endpoint: string, password: string): Promise<OBSWebSocket> {
    const obs = new OBSWebSocket();
    let timeout: ReturnType<typeof setTimeout> | undefined;
    try {
      await Promise.race([
        obs.connect(endpoint, password || undefined, { rpcVersion: 1 }),
        new Promise<never>((_, reject) => {
          timeout = setTimeout(() => reject(new Error("Délai de connexion dépassé")), 1600);
        }),
      ]);
      return obs;
    } catch (error) {
      try { await obs.disconnect(); } catch (_) { /* Connexion non établie. */ }
      throw error;
    } finally {
      if (timeout) clearTimeout(timeout);
    }
  }

  public async connect(settings: FilyxSettings): Promise<OBSWebSocket> {
    const password = String(settings.obsPassword || "");
    const requestedPort = Number(settings.obsPort || 4455);
    const port = Number.isInteger(requestedPort) && requestedPort > 0 && requestedPort < 65536 ? requestedPort : 4455;
    const requestedHost = String(settings.obsHost || "127.0.0.1").trim() || "127.0.0.1";
    const explicit = requestedHost.startsWith("ws://") || requestedHost.startsWith("wss://")
      ? requestedHost
      : (/(:\d+|\]:\d+)$/.test(requestedHost) ? `ws://${requestedHost}` : `ws://${requestedHost}:${port}`);
    const fingerprint = `${explicit}\u0000${password}`;
    if (this.obs && this.connectionFingerprint === fingerprint) return this.obs;
    if (this.obs) {
      try { await this.obs.disconnect(); } catch (_) { /* Une ancienne connexion est déjà fermée. */ }
      this.obs = null;
      this.connectedHost = "";
      this.connectionFingerprint = "";
    }
    const isLocal = /^(?:localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(requestedHost.replace(/^wss?:\/\//, ""));
    const localCandidates = isLocal
      ? [`ws://127.0.0.1:${port}`, `ws://localhost:${port}`, "ws://127.0.0.1:4455", "ws://localhost:4455", "ws://127.0.0.1:4444", "ws://localhost:4444"]
      : [];
    const candidates = [...new Set([explicit, ...localCandidates])];
    let lastError: unknown;
    for (const candidate of candidates) {
      const endpoint = candidate.startsWith("ws") ? candidate : `ws://${candidate}`;
      try {
        const obs = await this.tryConnect(endpoint, password);
        this.obs = obs;
        this.connectedHost = endpoint;
        this.connectionFingerprint = fingerprint;
        obs.on("ConnectionClosed", () => {
          if (this.obs === obs) {
            this.obs = null;
            this.connectedHost = "";
            this.connectionFingerprint = "";
          }
        });
        return obs;
      } catch (error) { lastError = error; }
    }
    const detail = lastError instanceof Error ? ` (${lastError.message})` : "";
    throw new Error(`OBS ne répond pas sur ${requestedHost}:${port}${detail}. Vérifie l’IP, le port et le mot de passe dans la connexion OBS ci-dessus.`);
  }

  /** Connects locally then returns the names that can safely be selected in the inspector. */
  public async discover(settings: FilyxSettings): Promise<ObsDiscovery> {
    if (await FilyxObsCompanion.available()) return FilyxObsCompanion.discover();
    const obs = await this.connect(settings);
    const [versionResponse, sceneResponse, inputResponse] = await Promise.all([
      obs.call("GetVersion") as Promise<{ obsVersion?: string; obsWebSocketVersion?: string }>,
      obs.call("GetSceneList") as Promise<{ scenes?: Array<{ sceneName?: string }> }>,
      obs.call("GetInputList") as Promise<{ inputs?: Array<{ inputName?: string }> }>,
    ]);
    const scenes = (sceneResponse.scenes || [])
      .map((scene) => String(scene.sceneName || "").trim())
      .filter(Boolean);
    const sceneSources: Record<string, string[]> = {};
    await Promise.all(scenes.map(async (sceneName) => {
      try {
        const response = await obs.call("GetSceneItemList", { sceneName }) as { sceneItems?: Array<{ sourceName?: string }> };
        sceneSources[sceneName] = [...new Set((response.sceneItems || []).map((item) => String(item.sourceName || "").trim()).filter(Boolean))].sort((a, b) => a.localeCompare(b, "fr"));
      } catch (_) {
        sceneSources[sceneName] = [];
      }
    }));
    const inputs = [...new Set((inputResponse.inputs || []).map((input) => String(input.inputName || "").trim()).filter(Boolean))]
      .sort((a, b) => a.localeCompare(b, "fr"));
    return {
      transport: "obs-websocket",
      host: this.connectedHost || "ws://127.0.0.1:4455",
      version: String(versionResponse.obsVersion || versionResponse.obsWebSocketVersion || "OBS connecté"),
      scenes,
      sceneSources,
      inputs,
    };
  }

  public async setScene(settings: FilyxSettings): Promise<void> {
    const sceneName = String(settings.sceneName || "").trim();
    if (!sceneName) throw new Error("Indique le nom de la scène OBS.");
    if (await FilyxObsCompanion.available()) { await FilyxObsCompanion.setScene(sceneName); return; }
    const obs = await this.connect(settings);
    await obs.call("SetCurrentProgramScene", { sceneName });
  }

  public async setSource(settings: FilyxSettings): Promise<void> {
    const sceneName = String(settings.sceneName || "").trim();
    const sourceName = String(settings.sourceName || "").trim();
    if (!sceneName || !sourceName) throw new Error("Indique la scène et la source OBS.");
    const mode = String(settings.sourceVisible || "toggle");
    if (await FilyxObsCompanion.available()) { await FilyxObsCompanion.setSource(sceneName, sourceName, mode); return; }
    const obs = await this.connect(settings);
    const item = await obs.call("GetSceneItemId", { sceneName, sourceName }) as { sceneItemId: number };
    let enabled = mode === "on";
    if (mode === "toggle") {
      const state = await obs.call("GetSceneItemEnabled", { sceneName, sceneItemId: item.sceneItemId }) as { sceneItemEnabled: boolean };
      enabled = !state.sceneItemEnabled;
    }
    await obs.call("SetSceneItemEnabled", { sceneName, sceneItemId: item.sceneItemId, sceneItemEnabled: enabled });
  }

  public async refreshBrowser(settings: FilyxSettings): Promise<void> {
    const sourceName = String(settings.sourceName || "").trim();
    if (!sourceName) throw new Error("Indique le nom de la source navigateur OBS.");
    if (await FilyxObsCompanion.available()) { await FilyxObsCompanion.refreshBrowser(sourceName); return; }
    const obs = await this.connect(settings);
    await obs.call("PressInputPropertiesButton", { inputName: sourceName, propertyName: "refreshnocache" });
  }

  public async toggleRecord(settings: FilyxSettings): Promise<void> {
    if (await FilyxObsCompanion.available()) { await FilyxObsCompanion.toggleRecord(); return; }
    const obs = await this.connect(settings);
    const status = await obs.call("GetRecordStatus") as { outputActive: boolean };
    await obs.call(status.outputActive ? "StopRecord" : "StartRecord");
  }

  public async toggleStream(settings: FilyxSettings): Promise<void> {
    if (await FilyxObsCompanion.available()) { await FilyxObsCompanion.toggleStream(); return; }
    const obs = await this.connect(settings);
    const status = await obs.call("GetStreamStatus") as { outputActive: boolean };
    await obs.call(status.outputActive ? "StopStream" : "StartStream");
  }
}
