import WebSocket from "ws";
import streamDeck from "@elgato/streamdeck";

const DEFAULT_URL = "ws://127.0.0.1:9876";

class WebSocketManager {
  constructor() {
    this.ws = null;
    this.url = DEFAULT_URL;
    this.reconnectTimeout = null;
    this.listeners = new Map();
    this.isConnected = false;
    this.decks = []; // Store deck configuration received from app
    this.reconnectAttempts = 0;
    this.maxReconnectDelay = 30000; // Max 30 seconds between attempts
    this.baseReconnectDelay = 1000; // Start at 1 second
  }

  connect(url = DEFAULT_URL) {
    this.url = url;
    this._connect();
  }

  _connect() {
    if (this.reconnectTimeout) {
      clearTimeout(this.reconnectTimeout);
      this.reconnectTimeout = null;
    }

    try {
      this.ws = new WebSocket(this.url);

      this.ws.on("open", () => {
        streamDeck.logger.info("Connected to DJ Live Playlist");
        this.isConnected = true;
        this.reconnectAttempts = 0; // Reset on successful connection
        this._emit("connected");

        // Request decks configuration from the app
        this.send("GET_DECKS_CONFIG", {});
      });

      this.ws.on("message", (data) => {
        try {
          const message = JSON.parse(data.toString());
          streamDeck.logger.debug("Received:", message);
          this._emit("message", message);

          // Handle DECKS_CONFIG message - store deck configuration
          if (message.type === "DECKS_CONFIG" && message.payload?.decks) {
            this.decks = message.payload.decks;
            streamDeck.logger.info(`Received ${this.decks.length} decks from app`);
          }

          // Emit specific event based on message type
          if (message.type) {
            this._emit(message.type, message.payload);
          }
        } catch (e) {
          streamDeck.logger.error("Failed to parse message:", e);
        }
      });

      this.ws.on("close", () => {
        streamDeck.logger.info("Disconnected from DJ Live Playlist");
        this.isConnected = false;
        this._emit("disconnected");
        this._scheduleReconnect();
      });

      this.ws.on("error", (error) => {
        streamDeck.logger.error("WebSocket error:", error.message);
        this.isConnected = false;
      });
    } catch (error) {
      streamDeck.logger.error("Failed to connect:", error);
      this._scheduleReconnect();
    }
  }

  _scheduleReconnect() {
    if (!this.reconnectTimeout) {
      // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max)
      const delay = Math.min(
        this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts),
        this.maxReconnectDelay
      );
      this.reconnectAttempts++;

      streamDeck.logger.info(`Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts})...`);

      this.reconnectTimeout = setTimeout(() => {
        this._connect();
      }, delay);
    }
  }

  send(type, payload) {
    if (this.ws && this.isConnected) {
      const message = { type, payload };
      this.ws.send(JSON.stringify(message));
      streamDeck.logger.debug("Sent:", message);
    } else {
      streamDeck.logger.warn("Cannot send message, not connected");
    }
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event).push(callback);
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      const callbacks = this.listeners.get(event);
      const index = callbacks.indexOf(callback);
      if (index !== -1) {
        callbacks.splice(index, 1);
      }
    }
  }

  _emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach((callback) => {
        try {
          callback(data);
        } catch (e) {
          streamDeck.logger.error(`Error in listener for ${event}:`, e);
        }
      });
    }
  }

  disconnect() {
    if (this.reconnectTimeout) {
      clearTimeout(this.reconnectTimeout);
      this.reconnectTimeout = null;
    }
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }

  /**
   * Get deck info by ID
   * @param {string} deckId - The deck ID (e.g., "deck-1")
   * @returns {object|null} - Deck info with id, name, deckType, image (base64)
   */
  getDeckById(deckId) {
    return this.decks.find((d) => d.id === deckId) || null;
  }

  /**
   * Get all available decks
   * @returns {Array} - Array of deck configurations
   */
  getDecks() {
    return this.decks;
  }

  /**
   * Request updated decks configuration from the app
   */
  requestDecksConfig() {
    this.send("GET_DECKS_CONFIG", {});
  }
}

// Singleton instance
export const wsManager = new WebSocketManager();
