import log4js from "log4js";
import { WebSocketScanner } from "../network/websocketScanner.js";
import { io } from "socket.io-client";
// Load configuration
import { loadConfig, saveConfig } from "../config.js";
import { askOtp, getDeviceInfo, notPrompting } from "../utils.js";
import { sendIPC } from "../electron/ipc.js";
import { PrinterManager } from "../printer/printermanager.js";
import { HostManager } from "../host/hostmanager.js";
import { FileManager } from "../files/filemanager.js";
import { DocumentPrinterManager } from "../documentprinter/documentprintermanager.js";
import { startWaiting, stopWaiting } from "../spinner.js";

const config = loadConfig();

const logger = log4js.getLogger("Socket Client");
logger.level = config.logLevel;

export class SocketClient {
  constructor() {
    this.socket = null;
    this.authenticated = false;
    this.connected = false;
    this.loading = false;
    this.host = null;
    this.id = null;
    this.reconnectTimeout = null;
    this.hostManager = new HostManager(this);
    this.fileManager = new FileManager(this);
    this.printerManager = new PrinterManager(this);
    this.documentPrinterManager = new DocumentPrinterManager(this);
    this.scanner = new WebSocketScanner({ maxThreads: 50 });
    this.readLine = null;
    sendIPC("setOnline", false);
    sendIPC("setAuthenticated", false);
    sendIPC("setLoading", false);
  }

  setupSocketEventHandlers() {
    this.socket.on("connect", this.handleConnect.bind(this));
    this.socket.on("connect_error", this.handleError.bind(this));
    this.socket.on("objectUpdate", this.handleObjectUpdate.bind(this));
    this.socket.on("objectAction", this.handleObjectAction.bind(this));
    this.socket.on("objectEvent", this.handleObjectEvent.bind(this));
    this.socket.on("disconnect", this.handleDisconnect.bind(this));
  }

  scheduleReconnect() {
    // Clear any pending reconnect timeout
    if (this.reconnectTimeout) {
      clearTimeout(this.reconnectTimeout);
      this.reconnectTimeout = null;
    }

    startWaiting("Will attempt to reconnect in 3 seconds...", logger);
    this.reconnectTimeout = setTimeout(async () => {
      await stopWaiting();
      this.connect();
    }, 3000);
  }

  subscribeToObjectEvents() {
    this.subscribeToObjectEvent({
      objectType: "host",
      _id: this.id,
      eventType: "childUpdate",
    });
    this.subscribeToObjectEvent({
      objectType: "host",
      _id: this.id,
      eventType: "childDelete",
    });
    this.subscribeToObjectEvent({
      objectType: "host",
      _id: this.id,
      eventType: "childNew",
    });
  }

  unsubscribeFromObjectEvents() {
    this.unsubscribeFromObjectEvent({
      objectType: "host",
      _id: this.id,
      eventType: "childUpdate",
    });
    this.unsubscribeFromObjectEvent({
      objectType: "host",
      _id: this.id,
      eventType: "childDelete",
    });
    this.unsubscribeFromObjectEvent({
      objectType: "host",
      _id: this.id,
      eventType: "childNew",
    });
  }

  async connect() {
    try {
      await stopWaiting();
      startWaiting(`Connecting to Socket.IO server: ${config.url}`, logger);
      this.socket = io(config.url, {
        auth: { type: "host" },
        reconnection: false,
        timeout: 3000, // 3 second timeout
      });
      this.loading = true;
      sendIPC("setLoading", true);
      this.connected = false;
      sendIPC("setConnected", false);
      this.authenticated = false;
      sendIPC("setAuthenticated", false);
      this.setupSocketEventHandlers();
    } catch (error) {
      await stopWaiting();
      logger.error("Failed to create Socket.IO connection:", error);
    }
  }

  disconnect() {
    this.unsubscribeFromObjectEvents();
    this.socket.disconnect();
  }

  async waitForConnection(timeoutMs = 10000) {
    if (this.socket?.connected) {
      return;
    }

    if (!this.socket) {
      throw new Error("Socket client is not connected");
    }

    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.socket.off("connect", onConnect);
        reject(new Error("Socket connection timed out"));
      }, timeoutMs);

      const onConnect = () => {
        clearTimeout(timeout);
        resolve();
      };

      if (this.socket.connected) {
        clearTimeout(timeout);
        resolve();
        return;
      }

      this.socket.once("connect", onConnect);
    });
  }

  async authenticateWithOtp(otp = undefined, options = {}) {
    const { retryOnFailure = otp == undefined, logs = true } = options;

    if (otp == undefined) {
      otp = await askOtp();
    }

    await this.waitForConnection();
    return this.authenticate({ otp }, { retryOnFailure, logs });
  }

  async authenticate(authenticationData, options = {}) {
    const { retryOnFailure = true, logs = true } = options;

    await this.waitForConnection();

    return new Promise((resolve, reject) => {
      if (!this.socket?.connected) {
        reject(new Error("Socket client is not connected"));
        return;
      }

      if (logs) {
        logger.debug("Host authenticating...");
      }
      this.socket.emit(
        "authenticate",
        authenticationData,
        async (verifyResult) => {
          if (verifyResult.valid == false) {
            this.authenticated = false;
            sendIPC("setAuthenticated", false);
            if (logs) {
              logger.error("Host not authenticated:", verifyResult.error);
            }

            if (retryOnFailure) {
              const retryResult = await this.authenticateWithOtp();
              resolve(retryResult);
              return;
            }

            resolve({ valid: false, error: verifyResult.error });
            return;
          }

          if (logs) {
            logger.info("Host authenticated.");
          }
          this.authenticated = true;
          sendIPC("setAuthenticated", true);
          sendIPC("setLoading", false);
          this.loading = false;
          if (this.host) {
            sendIPC("setHost", {
              ...this.host,
              online: false,
              state: { type: "offline" },
            });
          }
          this.host = {
            ...verifyResult.host,
            state: { type: "online" },
            online: true,
          };
          sendIPC("setHost", this.host);
          this.id = this.host._id;

          config.host = { id: this.id, authCode: this.host.authCode };

          await saveConfig(config);

          this.sendDeviceInfo();
          this.subscribeToObjectEvents();
          this.subscribeToObjectUpdates({ objectType: "host", _id: this.id });
          this.documentPrinterManager.reloadDocumentPrinters();
          this.printerManager.reloadPrinters();
          await this.fileManager.updateFiles();
          resolve({ valid: true, host: this.host });
        },
      );
    });
  }

  handleHostAction(action, callback) {
    switch (action.type) {
      case "reloadPrinters":
        this.printerManager.updatePrinters().catch((error) => {
          logger.error("Failed to reload printers:", error);
        });
        return;
    }
    callback({ success: true });
  }

  async handleHostUpdate(data) {
    if (data._id != this.id) {
      return;
    }

    logger.debug("Handling host update for id:", data._id);
    const { online, state } = this.host;
    this.host = { ...this.host, ...data.object, online, state };
    sendIPC("setHost", this.host);

    if (data.object?.authCode) {
      config.host = { id: this.id, authCode: data.object.authCode };
      await saveConfig(config);
    }
  }

  handleObjectAction(data, callback) {
    logger.debug("Running object action...", data);
    const id = data._id;
    const objectType = data.objectType;
    const action = data.action;

    if (id == this.id && objectType == "host") {
      this.handleHostAction(action, callback);
    }

    if (objectType == "printer") {
      this.printerManager.handlePrinterAction(id, action, callback);
    }

    if (objectType == "documentPrinter") {
      this.documentPrinterManager.handleDocumentPrinterAction(
        id,
        action,
        callback,
      );
    }
  }

  handleObjectUpdate(data, callback) {
    logger.debug(
      "Got object update for type:",
      data.objectType,
      " id:",
      data._id,
    );
    if (data.objectType == "host") {
      this.handleHostUpdate(data);
    }
    if (data.objectType == "documentPrinter") {
      this.documentPrinterManager.handleDocumentPrinterUpdate(
        data._id,
        data,
        callback,
      );
    }
    if (data.objectType == "printer") {
      this.printerManager.handlePrinterUpdate(data._id, data, callback);
    }
  }

  async handleConnect() {
    await stopWaiting();
    logger.info("Connected to FarmControl Api.");
    this.connected = true;
    sendIPC("setConnected", true);
    this.loading = false;
    sendIPC("setLoading", false);

    // Clear any pending reconnect timeout
    if (this.reconnectTimeout) {
      clearTimeout(this.reconnectTimeout);
      this.reconnectTimeout = null;
    }

    const otpRequired =
      config.host?.id == undefined ||
      config.host?.id == "" ||
      config.host?.authCode == undefined ||
      config.host?.authCode == "";
    if (otpRequired) {
      logger.info("An OTP code is required to setup this host.");
      this.authenticated = false;
      sendIPC("setAuthenticated", false);
      this.authenticateWithOtp();
      return;
    }
    this.authenticate({ id: config.host.id, authCode: config.host.authCode });
  }

  async handleError(error) {
    await stopWaiting();
    logger.error("Connection error:", error.message);
    this.loading = false;
    sendIPC("setLoading", false);
    this.connected = false;
    sendIPC("setConnected", false);
    this.authenticated = false;
    sendIPC("setAuthenticated", false);
    this.socket.disconnect();
    this.scheduleReconnect();
  }

  sendDeviceInfo() {
    logger.debug("Sending device info...");
    const deviceInfo = getDeviceInfo();
    this.socket.emit("updateHost", {
      host: { deviceInfo: deviceInfo },
    });
  }

  async listObjects({
    objectType,
    populate,
    filter,
    sort,
    order,
    project,
    cached,
  }) {
    logger.trace("Listing objects...", {
      objectType,
      populate,
      filter,
      sort,
      order,
      project,
      cached,
    });
    return new Promise((resolve, reject) => {
      this.socket.emit(
        "listObjects",
        {
          objectType,
          populate,
          filter,
          sort,
          order,
          project,
          cached,
        },
        (result) => {
          if (result && result.error) {
            reject(new Error(result.error));
          } else {
            logger.trace("Listed objects.", {
              objectType,
              populate,
              filter,
              sort,
              order,
              project,
              cached,
              length: result.length,
            });
            resolve(result);
          }
        },
      );
    });
  }

  async newObject({ objectType, newData }) {
    logger.debug("Creating object...", {
      objectType,
      newData,
    });
    return new Promise((resolve, reject) => {
      this.socket.emit(
        "newObject",
        {
          objectType,
          newData,
        },
        (result) => {
          if (result && result.error) {
            reject(new Error(result.error));
          } else {
            logger.trace("Created object.", {
              objectType,
              newData,
            });
            resolve(result);
          }
        },
      );
    });
  }

  async editObject({ objectType, _id, populate, updateData, auditLog = true }) {
    logger.trace("Editing object...", {
      objectType,
      _id,
      populate,
    });
    return new Promise((resolve, reject) => {
      this.socket.emit(
        "editObject",
        {
          objectType,
          _id,
          populate,
          updateData,
          auditLog,
        },
        (result) => {
          if (result && result.error) {
            reject(new Error(result.error));
          } else {
            logger.trace("Edited object.", {
              objectType,
              _id,
              populate,
            });
            resolve(result);
          }
        },
      );
    });
  }

  async getObject({ objectType, _id, populate }) {
    logger.debug("Getting object...", {
      objectType,
      _id,
      populate,
    });
    return new Promise((resolve, reject) => {
      this.socket.emit(
        "getObject",
        {
          objectType,
          _id,
          populate,
        },
        (result) => {
          if (result && result.error) {
            reject(new Error(result.error));
          } else {
            logger.trace("Retreived object.", {
              objectType,
              _id,
              populate,
            });
            resolve(result);
          }
        },
      );
    });
  }

  async objectEvent({ objectType, _id, eventType, eventData }) {
    logger.trace("Sending object event...", {
      objectType,
      _id,
      eventType,
      eventData,
    });
    return new Promise((resolve, reject) => {
      this.socket.emit("objectEvent", {
        objectType,
        _id,
        event: {
          type: eventType,
          data: eventData,
        },
      });
    });
  }

  async renderTemplatePDF(templateData) {
    logger.debug("Rendering template PDF...", templateData);
    return new Promise((resolve, reject) => {
      this.socket.emit("renderTemplatePDF", templateData, (result) => {
        if (result && result.error) {
          logger.error("Failed to render template PDF:", result.error);
          reject(new Error(result.error));
        } else {
          logger.trace("Template PDF rendered successfully.");
          resolve(result);
        }
      });
    });
  }

  async subscribeToObjectActions({ objectType, _id }) {
    logger.debug("Suscribing to object actions...", {
      objectType,
      _id,
    });
    this.socket.emit("subscribeToObjectActions", {
      objectType,
      _id,
    });
  }

  async subscribeToObjectUpdates({ objectType, _id }) {
    logger.debug("Suscribing to object updates...", {
      objectType,
      _id,
    });
    this.socket.emit("subscribeToObjectUpdates", {
      objectType,
      _id,
    });
  }

  async unsubscribeFromObjectUpdates({ objectType, _id }) {
    logger.debug("Unsuscribing from object updates...", {
      objectType,
      _id,
    });
    this.socket.emit("unsubscribeFromObjectUpdates", {
      objectType,
      _id,
    });
  }

  async subscribeToObjectEvent({ objectType, _id, eventType }) {
    logger.debug("Suscribing to object event...", {
      objectType,
      _id,
      eventType,
    });
    this.socket.emit("subscribeToObjectEvent", {
      objectType,
      _id,
      eventType,
    });
  }

  async unsubscribeFromObjectEvent({ objectType, _id, eventType }) {
    logger.debug("Unsuscribing from object event...", {
      objectType,
      _id,
      eventType,
    });
    this.socket.emit("unsubscribeFromObjectEvent", {
      objectType,
      _id,
      eventType,
    });
  }

  async handleHostChildUpdate(data) {
    if (data.parentType == "printer") {
      this.printerManager.reloadPrinters();
    } else if (data.parentType == "documentPrinter") {
      this.documentPrinterManager.reloadDocumentPrinters();
    }
  }

  async handleObjectEvent({ objectType, _id, event }) {
    logger.debug("Received object event...", {
      objectType,
      _id,
      event,
    });
    const data = event.data;
    if (
      (event.type == "childUpdate" ||
        event.type == "childDelete" ||
        event.type == "childNew") &&
      objectType == "host" &&
      _id == this.id
    ) {
      await this.handleHostChildUpdate(data);
    }
  }

  //-------------------------------------- RE-WRITE ENDS HERE ---------------------------------------

  async handleScanNetworkStart(data, callback) {
    if (this.scanner.scanning == false) {
      try {
        this.scanner = new WebSocketScanner({ maxThreads: 50 });
        // Listen for found services
        this.scanner.on("serviceFound", (data) => {
          logger.info(
            `Found websocket service at ${data.hostname} (${data.ip})`,
          );
          this.socket.emit("notify_scan_network_found", data);
        });

        // Listen for scan progress
        this.scanner.on("scanProgress", ({ currentIP, progress }) => {
          logger.info(
            `Scanning ${currentIP} (${progress.toFixed(2)}% complete)`,
          );
          this.socket.emit("notify_scan_network_progress", {
            currentIP: currentIP,
            progress: progress,
          });
        });

        // Start scanning on port
        logger.info(
          "Scanning network for websocket services on port:",
          data?.port || 7125,
          "using protocol:",
          data?.protocol || "ws",
        );
        this.scanner
          .scanNetwork(data?.port || 7125, data?.protocol || "ws")
          .then((foundServices) => {
            logger.info("Scan complete. Found services:", foundServices);
            this.socket.emit("notify_scan_network_complete", foundServices);
          })
          .catch((error) => {
            logger.error("Scan error:", error);
            this.socket.emit("notify_scan_network_complete", false);
          });
      } catch (error) {
        logger.error("Scan error:", error);
        this.socket.emit("notify_scan_network_complete", false);
      }
    }
  }

  handleScanNetworkStop(callback) {
    if (this.scanner.scanning == true) {
      logger.info("Stopping network scan");
      this.scanner.removeAllListeners("serviceFound");
      this.scanner.removeAllListeners("scanProgress");
      this.scanner.removeAllListeners("scanComplete");
      this.scanner.stopScan();
      callback(true);
    } else {
      logger.info("Scan not in progress");
      callback(false);
    }
  }

  async handlePrinterObjectsQuery(data, callback) {
    logger.debug("Received printer.objects.query event:", data);
    try {
      const result = await this.printerManager.processPrinterCommand({
        method: "printer.objects.query",
        params: data,
      });

      if (callback) {
        callback(result);
      }
    } catch (e) {
      logger.error("Error processing printer objects query request:", e);
      if (callback) {
        callback({ error: e.message });
      }
    }
  }

  async handleEmergencyStop(data, callback) {
    logger.debug("Received printer.gcode.script event:", data);
    try {
      const result = await this.printerManager.processPrinterCommand({
        method: "printer.emergency_stop",
        params: data,
      });

      if (callback) {
        callback(result);
      }
    } catch (e) {
      logger.error("Error processing gcode script request:", e);
      if (callback) {
        callback({ error: e.message });
      }
    }
  }

  async handleFilamentStockLoad(data, callback) {
    logger.debug("Received printer.filamentstock.load event:", data);
    try {
      if (!data || !data.printerId) {
        throw new Error("Missing required printer ID");
      }
      if (!data || !data.filamentStockId) {
        throw new Error("Missing required filament stock ID");
      }

      // Get the printer client
      const printerClient = this.printerManager.getPrinterClient(
        data.printerId,
      );
      if (!printerClient) {
        throw new Error(`Printer with ID ${data.printerId} not found`);
      }

      // Load the filament stock
      const result = await printerClient.loadFilamentStock(
        data.filamentStockId,
      );

      if (callback) {
        callback(result);
      }
    } catch (e) {
      logger.error("Error processing filament load request:", e);
      if (callback) {
        callback({ error: e.message });
      }
    }
  }

  async handleDisconnect() {
    logger.info("Disconnected from FarmControl Api.");
    await this.printerManager.closeAllConnections();
    await this.documentPrinterManager.closeAllConnections();
    this.connected = false;
    sendIPC("setConnected", false);
    this.authenticated = false;
    sendIPC("setAuthenticated", false);
    notPrompting();
    this.scheduleReconnect();
  }
}
