// moonraker-connection.js - Handles connection to a single Moonraker instance
import { JsonRPC } from "./jsonrpc.js";
import { WebSocket } from "ws";
import { loadConfig } from "../config.js";
import { PrinterDatabase } from "./database.js";
import { PrinterFileManager } from "./printerfilemanager.js";
import log4js from "log4js";
import { startWaiting, stopWaiting } from "../spinner.js";
import axios from "axios";
import FormData from "form-data";
import _ from "lodash";
// Load configuration
const config = loadConfig();

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

export class PrinterClient {
  constructor(printer, printerManager) {
    this.id = printer._id;
    this.printer = printer;
    this.name = printer.name;
    this.printerManager = printerManager;
    this.socketClient = printerManager.socketClient;
    this.database = new PrinterDatabase(this.socketClient, this.printer);
    this.printerFileManager = new PrinterFileManager(this);
    this.active = printer.active !== false;
    this.state =
      this.active == true
        ? { type: "offline", message: "Moonraker disconnected." }
        : { type: "inactive" };
    this.klippyState = { type: "offline", message: "Klippy disconnected." };
    this.config = printer.moonraker;
    this.version = printer.version;
    this.jsonRpc = new JsonRPC();
    this.shouldReconnect = true;
    this.socket = null;
    this.connectionId = null;
    this.currentJob = printer.currentJob;
    this.currentSubJob = printer.currentSubJob;
    this.temperatureObject = {};
    this.motionObject = {};
    this.miscObject = {};
    this.currentFilamentStock = printer.currentFilamentStock;
    this.currentFilament = null;
    this.currentFilamentSku = null;
    this.currentFilamentUsed = 0;
    this.registerEventHandlers();
    this.subscribeToActions();
    this.subscribeToObjectUpdates();
    this.baseSubscription = {
      print_stats: null,
      display_status: null,
      "filament_switch_sensor fsensor": null,
      output_pin: null,
      extruder: null,
      heater_bed: null,
      gcode_move: null,
      toolhead: null,
      motion_report: null,
      "heater_fan nozzle_cooling_fan": null,
      fan: null,
      "output_pin BEEPER_pin": null,
      "output_pin LCD_backlight_pin": null,
    };
    this.subscriptions = new Map();
    this.queuedJobIds = [];
    this.queue = printer.queue;
    this.isOnline = printer.online;
    this.subJobIsCancelling = false;
    this.subJobCancelId = null;
    this.filamentDetected = null;
    this.eventUpdateInterval = null;
    this.deploySubJobQueue = [];
    this.deploySubJobTimer = null;
    this.deploySubJobTargetTime = null;
    this.printrFileIds = [];
    this.queueMutationChain = Promise.resolve();
  }

  registerEventHandlers() {
    // Register event handlers for Moonraker notifications
    this.jsonRpc.registerMethod(
      "notify_gcode_response",
      //this.handleGcodeResponse.bind(this),
    );
    this.jsonRpc.registerMethod(
      "notify_status_update",
      this.handleStatusUpdate.bind(this),
    );
    this.jsonRpc.registerMethod(
      "notify_klippy_disconnected",
      this.handleKlippyDisconnected.bind(this),
    );
    this.jsonRpc.registerMethod(
      "notify_klippy_ready",
      this.handleKlippyReady.bind(this),
    );
    this.jsonRpc.registerMethod(
      "notify_filelist_changed",
      this.handleFileListChanged.bind(this),
    );
    this.jsonRpc.registerMethod(
      "notify_metadata_update",
      this.handleMetadataUpdate.bind(this),
    );
    this.jsonRpc.registerMethod(
      "notify_power_changed",
      this.handlePowerChanged.bind(this),
    );
  }

  subscribeToActions() {
    this.socketClient.subscribeToObjectActions({
      objectType: "printer",
      _id: this.id,
    });
  }

  subscribeToObjectUpdates() {
    this.socketClient.subscribeToObjectUpdates({
      objectType: "printer",
      _id: this.id,
    });
  }

  async updatePrinter(data) {
    logger.debug(`Updating printer ${this.id} with data...`);
    this.printer = { ...this.printer, ...data };
    this.database.printer = this.printer;

    if (data?.moonraker) {
      this.config = data.moonraker;
    }

    if (Object.hasOwn(data || {}, "active") && data.active != this.active) {
      if (data.active == true) {
        await this.setActive();
      } else {
        await this.setInactive();
      }
    }
  }

  async _runQueueMutation(task) {
    const executeTask = async () => task();
    this.queueMutationChain = this.queueMutationChain
      .catch(() => {})
      .then(executeTask);
    return this.queueMutationChain;
  }

  async connect() {
    if (this.active == false) {
      this.shouldReconnect = false;
      this.isOnline = false;
      this.state = { type: "inactive" };
      await this.updatePrinterState();
      return false;
    }

    const { protocol, host, port } = this.config;
    const wsUrl = `${protocol}://${host}:${port}/websocket`;

    logger.info(`Connecting to Moonraker at ${wsUrl} (${this.id})`);

    this.state = { type: "connecting", message: "" };
    await this.updatePrinterState();

    this.socket = new WebSocket(wsUrl);
    this.jsonRpc.setSocket(this.socket);

    this.socket.addEventListener("open", () => {
      logger.info(`Connected to Moonraker (${this.printer.name})`);
      this.isOnline = true;
      this.identifyConnection();
    });

    this.socket.addEventListener("message", (event) => {
      this.jsonRpc.processMessage(JSON.parse(event.data));
    });

    this.socket.addEventListener("close", () => {
      logger.info(`Disconnected from Moonraker (${this.printer.name})`);
      this.isOnline = false;
      this.state =
        this.active == true
          ? { type: "offline", message: "Moonraker disconnected." }
          : { type: "inactive" };
      this.connectedAt = null;
      this.updatePrinterState();
      this.connectionId = null;
      // Always stop the event update interval when the socket closes
      clearInterval(this.eventUpdateInterval);
      this.eventUpdateInterval = null;
      if (this.shouldReconnect) {
        // Attempt to reconnect after delay
        setTimeout(() => this.connect(), 10000);
      }
    });

    this.socket.addEventListener("error", (error) => {
      logger.error(`Moonraker connection error (${this.printer.name}):`, error);
    });

    await this.database.clearAlerts();
  }

  identifyConnection() {
    const args = {
      client_name: "farmcontrol-server",
      version: "0.1.0",
      type: "web",
      url: "https://github.com/printer-bridge",
    };

    if (this.config.apiKey) {
      args.api_key = this.config.apiKey;
    }

    logger.debug(`Identifying connection... (${this.printer.name})`);

    this.jsonRpc
      .callMethodWithKwargs("server.connection.identify", args)
      .then(async (result) => {
        this.connectionId = result.connection_id;
        logger.info(
          `Connection identified with ID: ${this.connectionId} (${this.printer.name})`,
        );
        await this.initialize();
      })
      .catch((error) => {
        logger.error(
          `Error identifying connection (${this.printer.name}):`,
          error,
        );
      });
  }

  async initialize() {
    startWaiting("Running printer initialization...", logger);
    try {
      this.state = { type: "initializing", message: null };
      this.isOnline = true;
      this.connectedAt = new Date();
      await this.getInfo();
      await this.getFiles();
      await this.getCurrentFilament();
      await this.loadCurrentFilament();
      await this.updateSubscriptions();
      await this.getPrinterState();
      await this.syncSubJobs();
      this.eventUpdateInterval = setInterval(
        this.handleEventUpdate.bind(this),
        500,
      );
      await stopWaiting();
    } catch (error) {
      await stopWaiting();
      throw error;
    }
  }

  async getInfo() {
    startWaiting("Getting printer info...", logger);
    try {
      try {
        // Get server info
        const serverResult = await this.jsonRpc.callMethod("server.info");
        this.isOnline = true;
        this.klippyState = { type: serverResult.klippy_state };
        logger.info(
          "Server:",
          `Moonraker ${serverResult.moonraker_version} (${this.printer.name})`,
          `State: ${this.klippyState.type}`,
        );

        try {
          const klippyResult = await this.jsonRpc.callMethod("printer.info");
          logger.info(
            `Klippy info for ${this.printer.name}: ${klippyResult.hostname}, ${klippyResult.software_version}`,
          );
          // Update firmware version in database
          await this.database.updatePrinterFirmware(
            klippyResult.software_version,
          );
          logger.info(
            `Updated firmware version for ${this.printer.name} to ${klippyResult.software_version}`,
          );

          if (klippyResult.state === "error" && klippyResult.state_message) {
            logger.error(
              `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`,
              this.database.addAlert({
                code: "klippyError",
                type: "error",
                message: klippyResult.state_message,
                actions: ["restart", "restartFirmware"],
              }),
            );
          }

          if (klippyResult.state === "shutdown" && klippyResult.state_message) {
            logger.error(
              `Klippy error for ${this.printer.name}: ${klippyResult.state_message}`,
              this.database.addAlert({
                code: "klippyError",
                type: "error",
                message: klippyResult.state_message,
                actions: ["restart", "restartFirmware"],
              }),
            );
          }

          if (klippyResult.state === "startup" && klippyResult.state_message) {
            logger.warn(
              `Klippy startup message for ${this.printer.name}: ${klippyResult.state_message}`,
              this.database.addAlert({
                type: "info",
                code: "klippyStartup",
                message: klippyResult.state_message,
                priority: 8,
                timestamp: new Date(),
              }),
            );
          }
        } catch (error) {
          logger.error(
            `Error getting Klippy info for ${this.printer.name}:`,
            error,
          );
        }
      } catch (error) {
        logger.error(
          `Error getting server info for ${this.printer.name}:`,
          error,
        );
      }
      await stopWaiting();
    } catch (error) {
      await stopWaiting();
      throw error;
    }
  }

  async getFiles() {
    const result = await this.sendPrinterCommand({
      method: "server.files.list",
    });
    this.printrFileIds = [];
    for (const file of result) {
      if (file.path.startsWith("farmcontrol/")) {
        this.printrFileIds.push(
          file.path.replace("farmcontrol/", "").replace(".gcode", ""),
        );
      }
    }
    logger.debug(`Printr file IDs: ${this.printrFileIds}`);
  }

  async getCurrentFilament() {
    logger.info(`Getting current filament for printer: ${this.printer.name}`);
    if (!this.currentFilamentStock || !this.currentFilamentStock?._id) {
      logger.warn(
        `No current filament stock found for printer: ${this.printer.name}`,
      );
      return null;
    }
    const result = await this.socketClient.getObject({
      objectType: "filamentStock",
      _id: this.currentFilamentStock._id,
      populate: ["filamentSku"],
    });
    this.currentFilamentSku = result.filamentSku;
    return this.currentFilamentSku;
  }

  async loadCurrentFilament() {
    if (!this.currentFilamentStock?.filament?._id) {
      this.currentFilament = null;
      return null;
    }
    this.currentFilament = await this.socketClient.getObject({
      objectType: "filament",
      _id: this.currentFilamentStock.filament._id,
    });
    return this.currentFilament;
  }

  async setCurrentFilamentStock(filamentStock) {
    this.currentFilamentStock = filamentStock;
    await this.loadCurrentFilament();
  }

  async getQueuedJobIds() {
    logger.info(`Getting queued jobs info for (${this.printer.name})`);
    const result = await this.sendPrinterCommand({
      method: "server.job_queue.status",
    });
    const idList = result.queued_jobs.map((job) => job.job_id);
    logger.debug(`Queued job IDs: ${idList}`);
    return idList;
  }

  async getPrinterState() {
    logger.info(`Getting state of (${this.printer.name})`);
    if (!this.isOnline) {
      logger.error(
        `Cannot send command: Not connected to Moonraker. (${this.printer.name})`,
      );
      return false;
    }

    if (this.klippyState.type === "error") {
      logger.error(`Klippy is reporting error for ${this.printer.name}`);
      this.state = this.klippyState;
      this.updatePrinterState();
      return;
    }

    if (this.klippyState.type === "shutdown") {
      logger.error(`Klippy is reporting shutdown for ${this.printer.name}.`);
      this.state = this.klippyState;
      this.updatePrinterState();
      return;
    }

    try {
      const result = await this.jsonRpc.callMethodWithKwargs(
        "printer.objects.query",
        { objects: this.baseSubscription },
      );
      logger.debug(`Command sent to ${this.printer.name}`);
      if (result.status != undefined) {
        this.handleStatusUpdate([result.status]);
      }
      return result;
    } catch (error) {
      logger.error(`Error sending command to ${this.printer.name}:`, error);
      return false;
    }
  }

  async updateSubscriptions() {
    logger.info(`Updating subscriptions for (${this.printer.name})`);

    // Start with base subscription content
    const allSubscriptions = { ...this.baseSubscription };

    // Add all subscription content from the Map
    for (const [_, value] of this.subscriptions) {
      Object.assign(allSubscriptions, value);
    }

    logger.debug(
      "Combined subscriptions:",
      Object.keys(allSubscriptions).join(", "),
    );

    if (!this.isOnline) {
      logger.error(
        `Cannot send command: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }

    try {
      await this.jsonRpc.callMethodWithKwargs("printer.objects.subscribe", {
        objects: allSubscriptions,
      });
      logger.debug(`Command sent to ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(`Error sending command to (${this.printer.name}):`, error);
      return false;
    }
  }

  async sendPrinterCommand(command) {
    logger.info(`Sending ${command.method} command to (${this.printer.name})`);
    if (!this.isOnline) {
      logger.error(
        `Cannot send command: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }

    try {
      const result = await this.jsonRpc.callMethodWithKwargs(
        command.method,
        command.params,
      );
      logger.debug(`Command sent to ${this.printer.name}`);
      if (result.status != undefined) {
        if (command.method == "printer.objects.query") {
          this.handleStatusUpdate([result.status]);
        }
      }
      return result;
    } catch (error) {
      logger.error(`Error sending command to (${this.printer.name}):`, error);
      return false;
    }
  }

  printerActionResult(success, error = null, code = null) {
    if (success) {
      return { success: true };
    }

    const result = { success: false, error };
    if (code !== undefined && code !== null) {
      result.code = code;
    }
    return result;
  }

  async addMotionErrorAlert(error) {
    const code = error?.code;
    if (code === undefined || code === null) {
      return;
    }

    try {
      await this.database.addAlert({
        code: `motionError${code}`,
        type: "error",
        priority: "9",
        message: error?.message || "Failed to execute printer movement command",
        canDismiss: true,
      });
    } catch (alertError) {
      logger.error(
        `Failed to add motion error alert for ${this.printer.name}:`,
        alertError,
      );
    }
  }

  async runGcodeScript(script) {
    if (!this.isOnline) {
      return this.printerActionResult(
        false,
        "Printer is not connected to Moonraker",
      );
    }

    try {
      await this.jsonRpc.callMethodWithKwargs("printer.gcode.script", {
        script,
      });
      return this.printerActionResult(true);
    } catch (error) {
      logger.error(`G-code script failed for ${this.printer.name}:`, error);
      await this.addMotionErrorAlert(error);
      return this.printerActionResult(
        false,
        error?.message || "Failed to send command to printer",
        error?.code,
      );
    }
  }

  async handleStatusUpdate(status) {
    logger.trace("Status update:", status);
    status = status[0];

    let stateChanged = false;
    let progressChanged = false;
    let printerFinished = false;
    let printedStarted = false;

    if (status.print_stats?.state) {
      const newState = status.print_stats.state;
      const oldState = this.state.type;
      if (newState !== oldState) {
        logger.info(
          `Printer ${this.printer.name} state changed from ${this.state.type} to ${newState}`,
        );
        if (oldState == "printing" && newState == "complete") {
          printerFinished = true;
        }
        if (
          (oldState == "standby" || oldState == "complete") &&
          newState == "printing"
        ) {
          printedStarted = true;
        }
        this.state.type = newState;
        stateChanged = true;
      }
    }

    if (status.print_stats?.filament_used) {
      // Convert mm to cm and calculate volume
      const filamentLengthCm = status.print_stats.filament_used / 10;
      const filamentDiameterCm = this.currentFilament?.diameter / 10 || 0; // 1.75mm in cm
      const filamentRadiusCm = filamentDiameterCm / 2;
      const filamentVolumeCm3 =
        Math.PI * Math.pow(filamentRadiusCm, 2) * filamentLengthCm;
      // Calculate weight in grams
      const filamentWeightG =
        filamentVolumeCm3 * this.currentFilament?.density || 0;
      this.currentFilamentUsed = -1 * filamentWeightG;
    }

    if (status.display_status?.progress !== undefined) {
      const newProgress = status.display_status.progress;
      if (newProgress !== this.state.progress) {
        logger.info(
          `Printer ${this.printer.name} progress changed from ${this.state.progress} to ${newProgress}`,
        );
        this.state.progress = newProgress;
        progressChanged = true;
      }
    }

    if (status.display_status?.message) {
      this.state.message = status.display_status.message;
      progressChanged = true;
    }

    if (status.extruder?.temperature !== undefined) {
      _.merge(this.temperatureObject, {
        extruder: { current: status.extruder.temperature },
      });
    }

    if (status.extruder?.power !== undefined) {
      _.merge(this.temperatureObject, {
        extruder: { power: status.extruder.power },
      });
    }

    if (status.extruder?.target !== undefined) {
      _.merge(this.temperatureObject, {
        extruder: { target: status.extruder.target },
      });
    }

    if (status.heater_bed?.temperature !== undefined) {
      _.merge(this.temperatureObject, {
        bed: { current: status.heater_bed.temperature },
      });
    }

    if (status.heater_bed?.power !== undefined) {
      _.merge(this.temperatureObject, {
        bed: { power: status.heater_bed.power },
      });
    }

    if (status.heater_bed?.target !== undefined) {
      _.merge(this.temperatureObject, {
        bed: { target: status.heater_bed.target },
      });
    }

    if (status.motion_report?.live_position !== undefined) {
      _.merge(this.motionObject, {
        livePosition: status.motion_report.live_position,
      });
    }

    if (status.gcode_move?.position !== undefined) {
      _.merge(this.motionObject, {
        gcodePosition: status.gcode_move.position,
      });
    }

    if (status.gcode_move?.speed !== undefined) {
      _.merge(this.motionObject, {
        speed: status.gcode_move.speed,
      });
    }

    if (status.gcode_move?.extrude_factor !== undefined) {
      _.merge(this.motionObject, {
        extrudeFactor: status.gcode_move.extrude_factor,
      });
    }

    if (status.gcode_move?.speed_factor !== undefined) {
      _.merge(this.motionObject, {
        speedFactor: status.gcode_move.speed_factor,
      });
    }

    if (status.gcode_move?.absolute_coordinates !== undefined) {
      _.merge(this.motionObject, {
        absoluteCoordinates: status.gcode_move.absolute_coordinates,
      });
    }

    if (status.gcode_move?.absolute_extrude !== undefined) {
      _.merge(this.motionObject, {
        absoluteExtrude: status.gcode_move.absolute_extrude,
      });
    }

    if (status.toolhead?.position !== undefined) {
      _.merge(this.motionObject, {
        toolheadPosition: status.toolhead.position,
      });
    }

    if (status.toolhead?.max_velocity !== undefined) {
      _.merge(this.motionObject, {
        maxVelocity: status.toolhead.max_velocity,
      });
    }

    if (status.toolhead?.max_accel !== undefined) {
      _.merge(this.motionObject, {
        maxAcceleration: status.toolhead.max_accel,
      });
    }

    if (status.toolhead?.square_corner_velocity !== undefined) {
      _.merge(this.motionObject, {
        squareCornerVelocity: status.toolhead.square_corner_velocity,
      });
    }

    if (status.toolhead?.minimum_cruise_ratio !== undefined) {
      _.merge(this.motionObject, {
        minCruiseRatio: status.toolhead.minimum_cruise_ratio,
      });
    }

    if (status["heater_fan nozzle_cooling_fan"] !== undefined) {
      _.merge(this.miscObject, {
        coolingFan: status["heater_fan nozzle_cooling_fan"],
      });
    }

    if (status.fan !== undefined) {
      _.merge(this.miscObject, {
        fan: status.fan,
      });
    }

    if (status["output_pin BEEPER_pin"] !== undefined) {
      _.merge(this.miscObject, {
        beeper: status["output_pin BEEPER_pin"],
      });
    }

    if (status["output_pin LCD_backlight_pin"] !== undefined) {
      const newLcdBacklight = status["output_pin LCD_backlight_pin"];
      _.merge(this.miscObject, {
        lcdBacklight: {
          brightness: status["output_pin LCD_backlight_pin"].value,
        },
      });
    }

    // Handle filament switch sensor
    if (
      status["filament_switch_sensor fsensor"]?.filament_detected !== undefined
    ) {
      const newFilamentDetected =
        status["filament_switch_sensor fsensor"].filament_detected;

      _.merge(this.miscObject, {
        filamentSensor: {
          filamentDetected: newFilamentDetected,
          enabled: status["filament_switch_sensor fsensor"].enabled,
        },
      });
      if (newFilamentDetected !== this.filamentDetected) {
        logger.info(
          `Printer ${this.printer.name} filament detection changed from ${this.filamentDetected} to ${newFilamentDetected}.`,
        );
        this.filamentDetected = newFilamentDetected;

        console.log(this.currentFilamentStock);

        if (newFilamentDetected == false && this.currentFilamentStock == null) {
          await this.database.removeAlertsByCode("noFilamentLoaded");
          await this.database.addAlert({
            code: "noFilamentSelected",
            type: "info",
            actions: ["loadFilamentStock"],
            canDismiss: false,
            message:
              "No filament loaded. Please load filament to continue printing.",
          });
        } else if (
          newFilamentDetected == true &&
          this.currentFilamentStock == null
        ) {
          await this.setCurrentFilamentStock(null);
          await this.database.setCurrentFilamentStock(null);
          await this.database.removeAlertsByCode("noFilamentSelected");
          await this.database.addAlert({
            code: "noFilamentLoaded",
            type: "info",
            actions: ["loadFilamentStock"],
            canDismiss: false,
            message:
              "No filament loaded. Please load filament to continue printing.",
          });
        } else if (
          newFilamentDetected == true &&
          this.currentFilamentStock != null
        ) {
          await this.database.removeAlertsByCode("noFilamentSelected");
          await this.database.removeAlertsByCode("noFilamentLoaded");
        } else if (
          newFilamentDetected == false &&
          this.currentFilamentStock != null
        ) {
          await this.database.removeAlertsByCode("noFilamentLoaded");
          await this.database.addAlert({
            code: "noFilamentLoaded",
            type: "info",
            message:
              "No filament loaded. Please load filament to continue printing.",
            actions: ["loadFilamentStock"],
            canDismiss: false,
          });
          await this.setCurrentFilamentStock(null);
          await this.database.setCurrentFilamentStock(null);
        }
      }
    }

    var isCurrentJobSubJob =
      this.currentSubJob != null && this.currentJob != null;

    if (stateChanged == true) {
      if (printerFinished == true && isCurrentJobSubJob == true) {
        logger.info(
          `Subjob ${this.currentSubJob._id} completed, posting part stock items for printer ${this.id}`,
        );
        await this.database.setSubJobFinishedAt(
          this.currentSubJob._id,
          new Date(),
        );
        await this.database.postSubJobPartStockItems(this.currentSubJob._id);
      }
      // Update printer state first
      await this.updatePrinterState();
      await this.syncSubJobs();

      isCurrentJobSubJob =
        this.currentSubJob != null && this.currentJob != null;
      if (isCurrentJobSubJob == true && printedStarted == true) {
        if (this.currentSubJob.startedAt == null) {
          this.currentSubJob.startedAt = new Date();
          await this.database.setSubJobStartedAt(
            this.currentSubJob._id,
            new Date(),
          );
        }
        if (this.currentJob.startedAt == null) {
          this.currentJob.startedAt = new Date();
          await this.database.setJobStartedAt(this.currentJob._id, new Date());
        }
        this.currentFilamentUsed = 0;
      }
    }

    if (progressChanged && isCurrentJobSubJob == true) {
      const subJobState = {
        type: this.currentSubJob.state.type,
        progress: this.state.progress,
      };
      await this.database.updateSubJobState(
        this.currentSubJob._id,
        subJobState,
      );
      await this.database.updateJobState(this.currentJob._id);
    }

    if (progressChanged) {
      await this.updatePrinterState();
    }
  }

  async handleEventUpdate() {
    this.socketClient.objectEvent({
      objectType: "printer",
      _id: this.id,
      eventType: "temperature",
      eventData: this.temperatureObject,
    });
    this.socketClient.objectEvent({
      objectType: "printer",
      _id: this.id,
      eventType: "filamentSensor",
      eventData: { detected: this.filamentDetected },
    });
    this.socketClient.objectEvent({
      objectType: "printer",
      _id: this.id,
      eventType: "motion",
      eventData: this.motionObject,
    });
    this.socketClient.objectEvent({
      objectType: "printer",
      _id: this.id,
      eventType: "misc",
      eventData: this.miscObject,
    });
    if (
      this.currentSubJob != null &&
      this.currentJob != null &&
      this.currentFilamentStock != null
    ) {
      logger.debug(
        "Updating stock event value:",
        this.currentFilamentUsed.toFixed(8),
      );
      this.database.updateFilamentStockWeight(
        this.currentFilamentStock,
        this.currentFilamentUsed,
        this.currentSubJob,
        this.currentJob,
      );
    }
  }

  async updatePrinterState() {
    try {
      const state =
        this.active == false
          ? { type: "inactive" }
          : this.klippyState.type !== "ready"
            ? this.klippyState
            : this.state;
      await this.database.updatePrinterState(
        state,
        this.isOnline,
        this.connectedAt,
      );
    } catch (error) {
      logger.error(`Failed to update printer state:`, error);
    }
  }

  async syncSubJobs() {
    try {
      logger.debug(`Syncing printer subjobs for ${this.name}...`, {
        queuedJobIds: this.queuedJobIds,
        currentSubJobId: this.currentSubJob,
        currentJob: this.currentJob,
        printerState: this.state.type,
      });

      if (this.isOnline == true) {
        this.queuedJobIds = await this.getQueuedJobIds();
      }

      logger.info(`Getting queued subjobs for ${this.name}`);
      const serverSubJobs = await this.database.getQueuedSubJobs();

      const queuedSubJobs = serverSubJobs.filter((subJob) =>
        this.queuedJobIds.includes(subJob.moonrakerJobId),
      );

      const unqueuedSubJobs = serverSubJobs.filter(
        (subJob) => !this.queuedJobIds.includes(subJob.moonrakerJobId),
      );

      this.queue = queuedSubJobs;

      const stateType = this.state.type;

      const syncedTypes = ["printing", "paused", "complete", "cancelled"];
      const completedTypes = ["complete"];
      const printingTypes = ["printing", "paused"];
      const failedTypes = ["error", "shutdown", "standby"];

      var isCurrentSubJob = this.currentSubJob != null;
      var isCurrentJob = this.currentJob != null;

      var isCurrentJobSubJob = isCurrentJob && isCurrentSubJob;

      // if printing, paused, or complete
      if (
        isCurrentJobSubJob == false &&
        unqueuedSubJobs.length > 0 &&
        printingTypes.includes(stateType)
      ) {
        logger.info(
          `No current subjob or job, setting first unqueued subjob as current...`,
        );
        const targetSubJob = unqueuedSubJobs[0];
        const targetJob = await this.database.getJobById(targetSubJob.job);
        this.currentSubJob = targetSubJob;
        this.currentJob = targetJob;
        await this.database.setCurrentJobAndSubJob(targetSubJob, targetJob);
        isCurrentSubJob = this.currentSubJob != null;
        isCurrentJob = this.currentJob != null;
        isCurrentJobSubJob = isCurrentJob && isCurrentSubJob;
      }

      if (syncedTypes.includes(stateType)) {
        if (isCurrentJobSubJob == true) {
          await this.database.updateSubJobState(this.currentSubJob._id, {
            type: stateType,
          });
          this.currentSubJob.state.type = stateType;
          this.currentJob.state = await this.database.updateJobState(
            this.currentJob._id,
          );
        }
      }

      if (failedTypes.includes(stateType)) {
        for (const subJob of unqueuedSubJobs) {
          await this.database.updateSubJobState(subJob._id, {
            type: "failed",
          });
          await this.database.updateJobState(subJob.job._id);
        }
        if (isCurrentJobSubJob == true) {
          await this.database.updateSubJobState(this.currentSubJob._id, {
            type: "failed",
          });
          await this.database.updateJobState(this.currentJob._id);
          this.currentSubJob = null;
          this.currentJob = null;
          await this.database.setCurrentJobAndSubJob(null, null);
        }
      }

      if (completedTypes.includes(stateType)) {
        const otherSubJobs = unqueuedSubJobs
          .filter((subJob) => subJob._id != this.currentSubJob?._id)
          .filter((subJob) => subJob.state.type == "queued");
        if (otherSubJobs.length > 0) {
          for (const subJob of otherSubJobs) {
            await this.database.updateSubJobState(subJob._id, {
              type: "failed",
            });
            await this.database.updateJobState(subJob.job._id);
          }
        }

        this.currentSubJob = null;
        this.currentJob = null;
        await await this.database.setCurrentJobAndSubJob(null, null);
      }
      await this.database.setQueuedSubJobs(queuedSubJobs);
    } catch (error) {
      logger.error(`Failed to sync printer subjobs for ${this.id}:`, error);
    }
  }

  async handleKlippyDisconnected() {
    logger.info(`Klippy disconnected (${this.printer.name})`);
    this.state = { type: "offline", message: "Klippy disconnected." };
    this.klippyState = { type: "offline", message: "Klippy disconnected." };
    this.isOnline = false;
    this.isPrinting = false;
    this.isError = false;
    this.isReady = false;
    await this.database.clearAlerts();
    await this.updatePrinterState();
    clearInterval(this.eventUpdateInterval);
    this.eventUpdateInterval = null;
  }

  async handleKlippyReady() {
    logger.info(`Klippy ready (${this.printer.name})`);
    await this.initialize();
  }

  handleFileListChanged(fileInfo) {
    logger.debug(`File list changed for ${this.printer.name}:`, fileInfo);
  }

  handleMetadataUpdate(metadata) {
    logger.info(
      `Metadata updated for ${this.printer.name}:`,
      metadata.filename,
    );
  }

  handlePowerChanged(powerStatus) {
    logger.info(`Power status changed for ${this.printer.name}:`, powerStatus);
  }

  async uploadGcodeFile(fileBlob, fileName) {
    logger.info(`Uploading G-code file ${fileName} to ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot upload file: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }

    try {
      const { protocol, host, port } = this.config;
      const httpUrl = `${
        protocol === "ws" ? "http" : "https"
      }://${host}:${port}/server/files/upload`;

      // Convert Blob to Buffer
      const arrayBuffer = await fileBlob.arrayBuffer();
      const buffer = Buffer.from(arrayBuffer);

      const formData = new FormData({ maxDataSize: Infinity });
      formData.append("file", buffer, {
        filename: fileName,
        contentType: fileBlob.type || "text/plain",
        knownLength: buffer.length,
      });

      const headers = {
        ...formData.getHeaders(),
      };

      if (this.config.apiKey) {
        headers["X-Api-Key"] = this.config.apiKey;
      }

      const response = await axios.post(httpUrl, formData, {
        headers,
        maxBodyLength: Infinity,
        maxContentLength: Infinity,
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total,
          );
          logger.debug(
            `Uploading file to ${this.printer.name}: ` +
              fileName +
              " " +
              percentCompleted +
              "%",
          );
          this.socketManager.broadcast("notify_printer_update", {
            printerId: this.id,
            state: { type: "deploying", progress: percentCompleted },
          });
        },
      });

      if (!response.result) {
        this.socketManager.broadcast("notify_printer_update", {
          printerId: this.id,
          state: { type: "error" },
        });
        throw new Error("Failed to upload G-code file to printer");
      }

      logger.info(
        `Successfully uploaded file ${fileName} to ${this.printer.name}`,
      );
      return true;
    } catch (error) {
      logger.error(`Error uploading file to ${this.printer.name}:`, error);
      return false;
    }
  }

  async deploySubJob(subJob) {
    // Add subJob to queue
    this.deploySubJobQueue.push(subJob);
    logger.debug(
      `Queued sub job ${subJob._id} for deployment. Queue size: ${this.deploySubJobQueue.length}`,
    );

    const now = Date.now();

    if (this.deploySubJobTargetTime == null) {
      this.deploySubJobTargetTime = now + 500;
    } else {
      this.deploySubJobTargetTime += 500;
    }

    const remainingTime = this.deploySubJobTargetTime - now;
    clearTimeout(this.deploySubJobTimer);
    this.deploySubJobTimer = setTimeout(() => {
      this.processDeploySubJobQueue();
    }, Math.max(remainingTime, 0));

    await this.database.updateSubJobState(subJob._id, {
      type: "deploying",
      progress: 0,
    });
    await this.database.updateJobState(subJob.job._id);
  }

  async processDeploySubJobQueue() {
    // Clear the timer and target time
    if (this.deploySubJobTimer) {
      clearTimeout(this.deploySubJobTimer);
      this.deploySubJobTimer = null;
    }
    this.deploySubJobTargetTime = null;

    // Get all queued subJobs and clear the queue
    const subJobsToDeploy = [...this.deploySubJobQueue];
    this.deploySubJobQueue = [];

    logger.info(
      `Processing ${subJobsToDeploy.length} queued sub job(s) for printer ${this.id}`,
    );

    for (const subJob of subJobsToDeploy) {
      try {
        await this._deploySubJobInternal(subJob);
      } catch (error) {
        logger.error(
          `Error deploying sub job ${subJob._id} to printer ${this.id}:`,
          error,
        );
        await this.database.updateSubJobState(subJob._id, {
          type: "failed",
        });
        await this.database.updateJobState(subJob.job._id);
      }
    }
  }

  async _deploySubJobInternal(subJob) {
    logger.info(
      "Deploying sub job:",
      subJob._id,
      "to printer:",
      this.id,
      "with gcode file:",
      `${subJob.gcodeFile._id}`,
    );
    console.log("subJob", subJob);
    const gcodeFile = await this.socketClient.getObject({
      objectType: "gcodeFile",
      _id: subJob.gcodeFile._id,
    });

    if (!gcodeFile) {
      throw new Error("G-code file not found");
    }

    console.log("gcodeFile", gcodeFile);

    const fileId = (gcodeFile.file?._id || gcodeFile.file).toString();
    let deploymentActive = true;

    const updateDeployProgress = async (progress) => {
      if (!deploymentActive) {
        return;
      }
      await this.database.updateSubJobState(subJob._id, {
        type: "deploying",
        progress,
      });
      await this.database.updateJobState(subJob.job._id);
    };

    const file = await this.socketClient.fileManager.getFile(
      fileId,
      async (progress) => {
        await updateDeployProgress(progress / 200);
      },
    );
    if (!file) {
      throw new Error("Error getting file");
    }

    if (!this.printrFileIds.includes(fileId)) {
      logger.debug(`Uploading file ${fileId} to printer`);
      const uploadResult = await this.printerFileManager.uploadFile(
        fileId,
        file,
        async (progress) => {
          await updateDeployProgress(progress / 200 + 0.5);
        },
      );
      if (!uploadResult) {
        throw new Error("Failed to upload file");
      }
      this.printrFileIds.push(fileId);
    } else {
      logger.debug(`File ${fileId} already uploaded to printer`);
      await updateDeployProgress(1);
    }

    deploymentActive = false;

    await this._runQueueMutation(async () => {
      const result = await this.sendPrinterCommand({
        method: "server.job_queue.post_job",
        params: {
          filenames: [`${fileId}.gcode`],
          reset: false,
        },
      });

      if (!result || !result?.queued_jobs) {
        throw new Error("Failed to deploy sub job to printer");
      }

      const queuedJobs = result.queued_jobs;
      const mostRecentQueuedJob = queuedJobs[queuedJobs.length - 1];
      const updatedSubJob = await this.database.setSubJobMoonrakerJobId(
        subJob._id,
        mostRecentQueuedJob.job_id,
      );
      await this.database.updateSubJobState(subJob._id, {
        type: "queued",
      });
      await this.database.updateJobState(subJob.job._id);

      this.queue.push({
        ...updatedSubJob,
        moonrakerJobId: mostRecentQueuedJob.job_id,
        state: { type: "queued" },
      });

      await this.database.setQueuedSubJobs(this.queue);
    });

    logger.info("Sub job deployed to printer:", this.id);
  }

  async cancelSubJob(subJobId) {
    try {
      this.subJobCancelId = subJobId;
      const result = await this.sendPrinterCommand({
        method: "server.job_queue.delete_job",
        params: {
          job_ids: [subJobId],
        },
      });

      if (!result) {
        throw new Error("Failed to cancel sub job");
      }
      this.subJobCancelId = null;

      logger.info("Sub job canceled:", subJobId);
    } catch (error) {
      logger.error(`Error canceling sub job ${subJobId}:`, error);
    }
  }

  async setTemperature(temperature) {
    logger.info(`Setting temperature for ${this.printer.name}:`, temperature);

    if (!this.isOnline) {
      logger.error(
        `Cannot set temperature: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }

    try {
      let gcodeCommands = [];

      // Handle extruder temperature
      if (temperature.extruder?.target !== undefined) {
        gcodeCommands.push(
          `SET_HEATER_TEMPERATURE HEATER=extruder TARGET=${temperature.extruder.target}`,
        );
      }

      // Handle bed temperature
      if (temperature.bed?.target !== undefined) {
        gcodeCommands.push(
          `SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=${temperature.bed.target}`,
        );
      }

      if (gcodeCommands.length === 0) {
        logger.warn(
          `No valid temperature targets provided for ${this.printer.name}`,
        );
        return false;
      }
      // Send each temperature command
      for (const gcodeCommand of gcodeCommands) {
        const result = await this.sendPrinterCommand({
          method: "printer.gcode.script",
          params: {
            script: gcodeCommand,
          },
        });

        if (!result) {
          logger.error(
            `Failed to set temperature with command: ${gcodeCommand}`,
          );
          return false;
        }
      }

      _.merge(this.temperatureObject, temperature);

      logger.info(`Successfully set temperature for ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(
        `Error setting temperature for ${this.printer.name}:`,
        error,
      );
      return false;
    }
  }

  async homeAxis({ axis }) {
    logger.info(`Homing axis for ${this.printer.name}:`, axis);

    if (!this.isOnline) {
      logger.error(
        `Cannot home axis: Not connected to Moonraker (${this.printer.name})`,
      );
      return this.printerActionResult(
        false,
        "Printer is not connected to Moonraker",
      );
    }

    try {
      if (!axis) {
        logger.warn(`No axis provided for homing on ${this.printer.name}`);
        return this.printerActionResult(false, "No axis provided for homing");
      }

      const gcodeCommand = axis === "ALL" ? "G28" : `G28 ${axis}`;
      const result = await this.runGcodeScript(gcodeCommand);

      if (!result.success) {
        logger.error(
          `Failed to home axis with command: ${gcodeCommand}`,
          result.error,
        );
        return result;
      }

      logger.info(`Successfully homed axis for ${this.printer.name}`);
      return result;
    } catch (error) {
      logger.error(`Error homing axis for ${this.printer.name}:`, error);
      await this.addMotionErrorAlert(error);
      return this.printerActionResult(
        false,
        error?.message || "Failed to home axis",
        error?.code,
      );
    }
  }

  async moveAxis({ axis, distance, rate }) {
    logger.info(`Moving axis for ${this.printer.name}:`, {
      axis,
      distance,
      rate,
    });

    if (!this.isOnline) {
      logger.error(
        `Cannot move axis: Not connected to Moonraker (${this.printer.name})`,
      );
      return this.printerActionResult(
        false,
        "Printer is not connected to Moonraker",
      );
    }

    try {
      const validAxes = ["X", "Y", "Z", "E"];
      if (!axis || !validAxes.includes(axis)) {
        logger.warn(
          `Invalid axis provided for move on ${this.printer.name}: ${axis}`,
        );
        return this.printerActionResult(false, `Invalid axis: ${axis}`);
      }

      if (distance === undefined || distance === null) {
        logger.warn(`No distance provided for move on ${this.printer.name}`);
        return this.printerActionResult(false, "No distance provided for move");
      }

      const feedRate = rate ?? 1000;
      const gcodeCommand = `G91\nG1 ${axis}${distance} F${feedRate}\nG90`;
      const result = await this.runGcodeScript(gcodeCommand);

      if (!result.success) {
        logger.error(
          `Failed to move axis with command: ${gcodeCommand}`,
          result.error,
        );
        return result;
      }

      logger.info(`Successfully moved axis for ${this.printer.name}`);
      return result;
    } catch (error) {
      logger.error(`Error moving axis for ${this.printer.name}:`, error);
      await this.addMotionErrorAlert(error);
      return this.printerActionResult(
        false,
        error?.message || "Failed to move axis",
        error?.code,
      );
    }
  }

  async restartPrinterFirmware() {
    logger.info(`Restarting printer firmware for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot restart printer firmware: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }

    try {
      const result = await this.sendPrinterCommand({
        method: "printer.firmware_restart",
      });
      if (!result) {
        throw new Error("Failed to restart printer firmware");
      }
      logger.info(
        `Successfully restarted printer firmware for ${this.printer.name}`,
      );
      return true;
    } catch (error) {
      logger.error(
        `Error restarting printer firmware for ${this.printer.name}:`,
        error,
      );
      return false;
    }
  }

  async restartPrinter() {
    logger.info(`Restarting printer for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot restart printer: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }
    try {
      const result = await this.sendPrinterCommand({
        method: "printer.restart",
      });
      if (!result) {
        throw new Error("Failed to restart printer");
      }
      logger.info(`Successfully restarted printer for ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(`Error restarting printer for ${this.printer.name}:`, error);
      return false;
    }
  }

  async restartMoonraker() {
    logger.info(`Restarting moonraker server for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot restart moonraker server: Not connected to moonraker server (${this.printer.name})`,
      );
      return false;
    }

    try {
      const result = await this.sendPrinterCommand({
        method: "server.restart",
      });
      if (!result) {
        throw new Error("Failed to restart moonraker server");
      }
      logger.info(
        `Successfully restarted moonraker server for ${this.printer.name}`,
      );
      return true;
    } catch (error) {
      logger.error(
        `Error restarting moonraker server for ${this.printer.name}:`,
        error,
      );
      return false;
    }
  }

  async startQueue() {
    logger.info(`Starting queue for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot start queue: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }
    try {
      const result = await this.sendPrinterCommand({
        method: "server.job_queue.start",
      });
      if (!result) {
        throw new Error("Failed to start queue");
      }
      logger.info(`Successfully started queue for ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(`Error starting queue for ${this.printer.name}:`, error);
      return false;
    }
  }

  async pauseJob() {
    logger.info(`Pausing job for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot pause job: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }
    try {
      const result = await this.sendPrinterCommand({
        method: "printer.print.pause",
      });
      if (!result) {
        throw new Error("Failed to pause job");
      }
      logger.info(`Successfully paused job for ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(`Error pausing job for ${this.printer.name}:`, error);
      return false;
    }
  }

  async resumeJob() {
    logger.info(`Resuming job for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot resume job: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }
    try {
      const result = await this.sendPrinterCommand({
        method: "printer.print.resume",
      });
      if (!result) {
        throw new Error("Failed to resume job");
      }
      logger.info(`Successfully resumed job for ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(`Error resuming job for ${this.printer.name}:`, error);
      return false;
    }
  }

  async cancelJob() {
    logger.info(`Cancelling job for ${this.printer.name}`);
    if (!this.isOnline) {
      logger.error(
        `Cannot cancel job: Not connected to Moonraker (${this.printer.name})`,
      );
      return false;
    }
    try {
      const result = await this.sendPrinterCommand({
        method: "printer.print.cancel",
      });
      if (!result) {
        throw new Error("Failed to cancel job");
      }
      logger.info(`Successfully cancelled job for ${this.printer.name}`);
      return true;
    } catch (error) {
      logger.error(`Error cancelling job for ${this.printer.name}:`, error);
      return false;
    }
  }

  async unloadFilamentStock() {
    await this.sendPrinterCommand({
      method: "printer.gcode.script",
      params: {
        script: `_CLIENT_LINEAR_MOVE E=-200 F=1000`,
      },
    });
  }

  async loadFilamentStock(filamentStock) {
    if (filamentStock == null) {
      await this.database.setCurrentFilamentStock(null);
      await this.setCurrentFilamentStock(null);
      return true;
    }
    await this.database.setCurrentFilamentStock(filamentStock);
    await this.setCurrentFilamentStock(filamentStock);
    await this.getCurrentFilament();
    await this.database.removeAlertsByCode("noFilamentSelected");
    await this.database.removeAlertsByCode("noFilamentLoaded");
  }

  async disconnect() {
    logger.info(`Disconnecting from ${this.printer.name}`);
    this.shouldReconnect = false;
    if (this.socket) {
      await this.jsonRpc.callMethodWithKwargs("printer.objects.subscribe", {
        objects: {},
      });
      this.socket.close();
      this.socket = null;
    }

    this.socket = null;
    if (this.eventUpdateInterval) {
      clearInterval(this.eventUpdateInterval);
      this.eventUpdateInterval = null;
    }
    logger.info(`Successfully disconnected from ${this.printer.name}`);
    return true;
  }

  async setInactive() {
    this.active = false;
    this.printer.active = false;
    this.database.printer = this.printer;
    this.isOnline = false;
    await this.disconnect();
    this.state = { type: "inactive" };
    this.klippyState = { type: "inactive" };
    this.connectedAt = null;
    await this.updatePrinterState();
  }

  async setActive() {
    this.active = true;
    this.printer.active = true;
    this.database.printer = this.printer;
    this.klippyState = { type: "offline", message: "Klippy disconnected." };
    this.shouldReconnect = true;
    this.isOnline = false;
    await this.connect();
  }
}
