import { randomUUID } from "crypto";
import log4js from "log4js";
import NodeCache from "node-cache";
import { loadConfig } from "../config.js";
import { sendIPC } from "../electron/ipc.js";
import chalk from "chalk";
import { formatState } from "../commandlineutils.js";
const config = loadConfig();
const logger = log4js.getLogger("Printer Database");
logger.level = config.logLevel;

export class PrinterDatabase {
  constructor(socketClient, printer) {
    this.socketClient = socketClient;
    this.printer = printer;
    this.id = this.printer._id;
    // Initialize cache with 30 second TTL
    this.printerCache = new NodeCache({ stdTTL: 30 });
  }

  async getPrinter() {
    const populate = [
      "currentFilamentStock",
      "currentJob",
      "currentSubJob",
      "queue",
    ];
    const populateKey = populate.sort().join(",");
    const cacheKey = `printer_${this.printer._id}_${populateKey}`;

    // Check if printer is in cache
    const cachedPrinter = this.printerCache.get(cacheKey);
    if (cachedPrinter) {
      logger.debug(`Returning cached printer for ${this.id}`);
      this.printer = cachedPrinter;
      return cachedPrinter;
    }

    // Fetch from socketClient if not in cache
    const object = await this.socketClient.getObject({
      _id: this.printer._id,
      objectType: "printer",
      populate: populate,
    });

    this.printer = object;
    // Store in cache
    this.printerCache.set(cacheKey, object);
    logger.debug(`Cached printer for ${this.id}`);

    return object;
  }

  async editPrinter(updateData) {
    sendIPC("setPrinter", this.printer);
    const populate = [
      "currentFilamentStock",
      "currentJob",
      "currentSubJob",
      "queue",
    ];
    const object = await this.socketClient.editObject({
      _id: this.printer._id,
      objectType: "printer",
      updateData: updateData,
      populate: populate,
    });

    // Update cache with the edited printer (overwrite existing cache)
    const populateKey = populate.sort().join(",");
    const cacheKey = `printer_${this.printer._id}_${populateKey}`;
    this.printerCache.del(cacheKey); // Delete existing cache entry
    this.printerCache.set(cacheKey, { ...object, ...updateData }); // Set new cache entry
    this.printer = { ...object, ...updateData };
    logger.debug(`Updated printer cache for ${this.id}`);

    return object;
  }

  async setPrinterConnectedAt() {
    const updatedPrinter = await this.editPrinter({
      connectedAt: new Date(),
    });
    this.printer = updatedPrinter;
    return updatedPrinter;
  }

  async removePrinterConnectedAt() {
    const updatedPrinter = await this.editPrinter({
      connectedAt: null,
    });
    this.printer = updatedPrinter;
    return updatedPrinter;
  }

  async getPrinterConfig() {
    try {
      logger.debug(`Getting printer config for ${this.id}`);

      const printers = await this.socketClient.listObjects({
        objectType: "printer",
        filter: { _id: this.id },
        populate: ["moonraker"],
      });

      if (!printers || printers.length === 0) {
        logger.error(
          `Printer with ID ${this.id} not found when getting config`,
        );
        return null;
      }

      const printer = printers[0];
      logger.debug(
        `Retrieved printer config for ${this.id}:`,
        printer.moonraker,
      );
      return printer.moonraker;
    } catch (error) {
      logger.error(`Failed to get printer config for ${this.id}:`, error);
      throw error;
    }
  }

  async updatePrinterState(state, online, connectedAt) {
    try {
      logger.debug(`Updating printer state for ${this.printer.name}:`, {
        state,
        online,
      });

      if (state.type === "printing" && state.progress === undefined) {
        logger.debug(
          `Setting default progress for printing state on printer ${this.printer.name}`,
        );
        state.progress = 0;
      }

      this.printer.state = state;
      this.printer.online = online;
      this.printer.connectedAt = connectedAt;

      const updatedPrinter = await this.editPrinter({
        state,
        online,
        connectedAt,
      });

      logger.info(
        `Updated printer: ${chalk.bold.white(this.printer.name)}, State: ${chalk.bold.white(formatState(state.type))}${state?.progress ? ` (${state.progress})` : ""} (${online ? chalk.green("Online") : chalk.dim.white("Offline")})`,
      );

      return updatedPrinter;
    } catch (error) {
      logger.error(
        `Failed to update printer state for ${this.printer.name}:`,
        error,
      );
      throw error;
    }
  }

  async updateSubJobState(subJobId, state) {
    try {
      const updatedSubJob = await this.socketClient.editObject({
        _id: subJobId,
        objectType: "subJob",
        updateData: { state },
      });

      logger.info(`Updated subjob ${subJobId} state:`, {
        type: state.type,
        progress: state.progress,
      });
      return updatedSubJob;
    } catch (error) {
      logger.error(`Failed to update sub job state for ${subJobId}:`, error);
      throw error;
    }
  }

  async updateJobState(jobId) {
    try {
      logger.debug(`Updating job state for ${jobId}`);

      const subJobs = await this.socketClient.listObjects({
        objectType: "subJob",
        filter: { job: { _id: jobId } },
        cached: false,
      });

      const subJobStates = subJobs.map((subJob) => subJob.state);
      const stateCounts = {
        printing: 0,
        paused: 0,
        complete: 0,
        failed: 0,
        queued: 0,
        cancelled: 0,
        deploying: 0,
      };

      subJobStates.forEach((state) => {
        stateCounts[state.type]++;
      });

      logger.debug(`Job ${jobId} state counts:`, stateCounts);

      const jobStateType = this.determineJobState(stateCounts);

      let jobProgress = 0;
      subJobStates.forEach((state) => {
        let subJobProgress = Number(state.progress) || 0;
        if (state.type === "complete") {
          subJobProgress = 1;
        } else if (jobStateType === "deploying" && state.type === "queued") {
          // Subjobs transition to queued once deployment finishes; count them as done.
          subJobProgress = 1;
        }
        jobProgress += subJobProgress;
      });

      const jobState = {
        type: jobStateType,
        progress: jobProgress / subJobs.length,
      };

      logger.debug(`Calculated job state for ${jobId}:`, {
        type: jobState.type,
        progress: jobState.progress,
        subJobCount: subJobs.length,
      });

      if (jobState.type == "complete") {
        await this.setJobFinishedAt(jobId, new Date());
      }

      await this.socketClient.editObject({
        _id: jobId,
        objectType: "job",
        updateData: { state: jobState },
      });

      logger.info(`Updated job ${jobId} state:`, {
        _id: jobId,
        state: jobState,
        subJobStats: stateCounts,
      });

      return jobState;
    } catch (error) {
      logger.error(`Failed to update job state for ${jobId}:`, error);
      throw error;
    }
  }

  async setSubJobMoonrakerJobId(subJobId, moonrakerJobId) {
    try {
      logger.debug(`Setting subjob ${subJobId} moonraker job id:`, {
        moonrakerJobId,
      });
      const updatedSubJob = await this.socketClient.editObject({
        _id: subJobId,
        objectType: "subJob",
        updateData: { moonrakerJobId: moonrakerJobId },
      });
      logger.info(`Set subjob ${subJobId} moonraker job id:`, {
        moonrakerJobId,
      });
      return updatedSubJob;
    } catch (error) {
      logger.error(`Failed to set subjob ${subJobId} moonraker job id:`, error);
      throw error;
    }
  }

  determineJobState(stateCounts) {
    logger.debug("Determining job state from counts:", stateCounts);

    // If any subjob is printing, the overall state should be printing
    if (stateCounts.deploying > 0) {
      logger.debug(
        "Job state determined as 'deploying' due to active deploying subjobs",
      );
      return "deploying";
    }

    // If any subjob is printing, the overall state should be printing
    if (stateCounts.printing > 0) {
      logger.debug(
        "Job state determined as 'printing' due to active printing subjobs",
      );
      return "printing";
    }

    if (stateCounts.failed > 0 || stateCounts.cancelled > 0) {
      logger.debug(
        "Job state determined as 'failed' due to failed or cancelled subjobs",
      );
      return "failed";
    }
    if (stateCounts.paused > 0) {
      logger.debug("Job state determined as 'paused'");
      return "paused";
    }
    if (
      stateCounts.complete ===
      Object.values(stateCounts).reduce((a, b) => a + b, 0)
    ) {
      logger.debug("Job state determined as 'complete'");
      return "complete";
    }
    logger.debug("Job state determined as 'queued'");
    return "queued";
  }

  async setCurrentJobAndSubJob(subJob, job) {
    try {
      logger.debug(`Setting current job and subjob for printer ${this.id}:`, {
        subJobId: subJob?._id || null,
        jobId: job?._id || null,
      });
      const updatedPrinter = await this.editPrinter({
        currentSubJob: subJob,
        currentJob: job,
      });
      return updatedPrinter;
    } catch (error) {
      logger.error(
        `Failed to set current job and subjob for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async updateDisplayStatus(message) {
    try {
      logger.debug(`Updating display status for printer ${this.id}:`, {
        message,
      });

      logger.info(`Updated display status for printer ${this.id}:`, {
        message,
      });
    } catch (error) {
      logger.error(
        `Failed to update display status for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async updatePrinterFirmware(firmwareVersion) {
    try {
      logger.debug(
        `Updating firmware version for printer ${this.id}:`,
        firmwareVersion,
      );
      this.printer.firmware = firmwareVersion;
      await this.editPrinter({ firmware: firmwareVersion });
      logger.info(
        `Updated firmware version for printer ${this.id}:`,
        firmwareVersion,
      );
    } catch (error) {
      logger.error(
        `Failed to update firmware version for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async addAlert(alert) {
    try {
      const alertWithId = {
        ...alert,
        _id: alert._id ?? randomUUID(),
      };

      logger.debug(`Adding alert to printer ${this.id}:`, alertWithId);

      const printer = await this.getPrinter();
      const updatedAlerts = [...printer.alerts, alertWithId];
      const updatedPrinter = await this.editPrinter({ alerts: updatedAlerts });

      logger.info(`Added new alert to printer ${this.id}:`, {
        type: alert.type,
        code: alert.code,
        priority: alert.priority,
        hasMessage: !!alert.message,
      });

      return updatedPrinter;
    } catch (error) {
      logger.error(`Failed to add alert to printer ${this.id}:`, error);
      throw error;
    }
  }

  async removeAlertById(alertId) {
    try {
      logger.debug(`Removing alert by id ${alertId} for printer ${this.id}`);

      const printer = await this.getPrinter();
      const filteredAlerts = printer.alerts.filter(
        (alert) => alert._id !== alertId,
      );

      const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts });

      logger.info(`Removed alert ${alertId} for printer ${this.id}:`, {
        alertId,
        alerts: updatedPrinter.alerts,
      });

      return updatedPrinter;
    } catch (error) {
      logger.error(
        `Failed to remove alert ${alertId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async removeAlertsByCode(code) {
    try {
      logger.debug(`Removing alerts by code ${code} for printer ${this.id}`);

      const printer = await this.getPrinter();
      const filteredAlerts = printer.alerts.filter(
        (alert) => alert.code !== code,
      );

      const updatedPrinter = await this.editPrinter({ alerts: filteredAlerts });

      logger.info(`Removed alerts with code ${code} for printer ${this.id}:`, {
        code,
        alerts: updatedPrinter.alerts,
      });

      return updatedPrinter;
    } catch (error) {
      logger.error(
        `Failed to remove alerts with code ${code} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async getAlerts(options = {}) {
    try {
      logger.debug(`Getting alerts for printer ${this.id}:`, options);

      const printer = await this.getPrinter();
      let alerts = printer.alerts;

      // Filter alerts based on options
      if (options.action) {
        alerts = alerts.filter((alert) => alert.action === options.action);
      }

      // Sort alerts by priority
      alerts.sort((a, b) =>
        String(a.priority ?? "").localeCompare(String(b.priority ?? "")),
      );

      logger.info(`Retrieved ${alerts.length} alerts for printer ${this.id}`);
      return alerts;
    } catch (error) {
      logger.error(`Failed to get alerts for printer ${this.id}:`, error);
      throw error;
    }
  }

  async clearAlerts() {
    try {
      logger.debug(`Clearing all alerts for printer ${this.id}`);

      const updatedPrinter = await this.editPrinter({ alerts: [] });

      if (!updatedPrinter) {
        logger.error(
          `Printer with ID ${this.id} not found when clearing alerts`,
        );
        return null;
      }

      logger.info(`Cleared all alerts for printer ${this.id}`);

      return updatedPrinter;
    } catch (error) {
      logger.error(`Failed to clear alerts for printer ${this.id}:`, error);
      throw error;
    }
  }

  async setCurrentFilamentStock(filamentStock) {
    try {
      logger.debug(`Setting current filament stock for printer ${this.id}:`, {
        filamentStock,
      });

      const updatedPrinter = await this.editPrinter({
        currentFilamentStock: filamentStock,
      });

      logger.info(
        `Updated current filament stock for printer ${this.id}:`,
        filamentStock,
      );

      return updatedPrinter.currentFilamentStock;
    } catch (error) {
      logger.error(
        `Failed to set current filament stock for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async updateFilamentStockWeight(
    filamentStock,
    weight,
    subJob = null,
    job = null,
  ) {
    const subJobId = subJob._id || subJob.id;
    const jobId = job._id || job.id;
    try {
      const filamentStockStockEvents = await this.socketClient.listObjects({
        objectType: "stockEvent",
        filter: {
          "parent._id": filamentStock._id,
          parentType: "filamentStock",
        },
        populate: ["owner"],
      });

      const stockEvents = filamentStockStockEvents.filter(
        (event) =>
          event.owner._id.toString() === subJobId.toString() &&
          event.ownerType === "subJob",
      );

      let stockEvent;
      if (stockEvents && stockEvents.length > 0) {
        const existingEvent = stockEvents[0];
        logger.trace(
          `Updating existing stock event for subJobId ${subJobId} and jobId ${jobId}`,
        );
        stockEvent = await this.socketClient.editObject({
          _id: existingEvent._id,
          objectType: "stockEvent",
          updateData: {
            value: weight,
            updatedAt: new Date(),
          },
          auditLog: false,
        });
      } else {
        logger.debug(
          `Creating new stock event for subJobId ${subJobId} and jobId ${jobId}`,
        );
        stockEvent = await this.socketClient.newObject({
          objectType: "stockEvent",
          newData: {
            filamentStock: filamentStock._id,
            value: weight,
            unit: "g",
            parentType: "filamentStock",
            parent: { _id: filamentStock._id },
            owner: this.printer.currentSubJob,
            ownerType: "subJob",
          },
        });
      }

      logger.trace(
        `Updated stock event for filament stock ${filamentStock._id}:`,
        {
          value: weight,
          updatedExistingEvent: stockEvents.length > 0,
        },
      );

      return stockEvent;
    } catch (error) {
      logger.error(
        `Failed to update stock event for filament stock ${filamentStock._id}:`,
        error,
      );
      throw error;
    }
  }

  async queueSubJob(subJob, moonrakerJobId) {
    try {
      logger.info(
        `Queueing subjob ${subJob._id} for printer ${this.id} with Moonraker ID ${moonrakerJobId}`,
      );

      // 1. Update SubJob with Moonraker ID and State
      await this.socketClient.editObject({
        _id: subJob._id,
        objectType: "subJob",
        updateData: {
          moonrakerJobId: moonrakerJobId,
          state: { type: "queued" },
        },
      });

      // 2. Add to Printer's Queue
      const newQueue = [...this.printer.queue, subJob];

      const updatedPrinter = await this.editPrinter({
        queue: newQueue,
      });

      this.printer.queue = newQueue;

      return updatedPrinter;
    } catch (error) {
      logger.error(`Failed to queue subjob for printer ${this.id}:`, error);
      throw error;
    }
  }

  async setQueuedSubJobs(subJobs) {
    try {
      logger.debug(
        `Setting queued subjobs for printer ${this.id}:`,
        subJobs.map((subJob) => subJob._id).join(", "),
      );
      const updatedPrinter = await this.editPrinter({
        queue: subJobs,
      });
      return updatedPrinter;
    } catch (error) {
      logger.error(
        `Failed to set queued subjobs for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async getQueuedSubJobs() {
    try {
      logger.debug(`Getting queued subjobs for printer ${this.id}`);
      const printer = await this.getPrinter();
      return printer.queue;
    } catch (error) {
      logger.error(
        `Failed to get queued subjobs for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async getJobById(jobId) {
    try {
      logger.debug(`Getting job by ID ${jobId} for printer ${this.id}`);
      const job = await this.socketClient.getObject({
        objectType: "job",
        _id: jobId,
      });
      return job;
    } catch (error) {
      logger.error(
        `Failed to get job by ID ${jobId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async postSubJobPartStockItems(subJobId) {
    try {
      logger.info(
        `Posting subjob part stock items for subjob ${subJobId} for printer ${this.id}`,
      );
      logger.debug(`Getting subjob ${subJobId} for printer ${this.id}`);
      const subJob = await this.socketClient.getObject({
        objectType: "subJob",
        _id: subJobId,
      });
      logger.debug(`Subjob ${subJobId} for printer ${this.id}:`, subJob);
      logger.debug(
        `Getting gcode file for subjob ${subJobId} for printer ${this.id}`,
      );
      const gcodeFile = await this.socketClient.getObject({
        objectType: "gcodeFile",
        _id: subJob.gcodeFile._id,
        populate: ["parts.part", "parts.partSku"],
      });
      const partItems = gcodeFile.parts;
      for (const partItem of partItems) {
        const partSkuId = partItem.partSku?._id || partItem.partSku;
        logger.info(
          `Posting part stock item for part SKU ${
            partItem.partSku?.name || partSkuId
          } for subjob ${subJobId} for printer ${this.id}`,
        );

        const quantity = partItem.quantity;

        const partStock = await this.socketClient.newObject({
          objectType: "partStock",
          newData: {
            sourceType: "subJob",
            source: subJobId,
            partSku: partSkuId,
            currentQuantity: quantity,
            state: {
              type: "new",
              progress: 1,
            },
          },
        });

        logger.info(
          `Creating stock event for part stock ${partStock._id} with quantity ${quantity} for subjob ${subJobId} for printer ${this.id}`,
        );

        await this.socketClient.newObject({
          objectType: "stockEvent",
          newData: {
            value: quantity,
            unit: "",
            parentType: "partStock",
            parent: partStock._id,
            owner: subJobId,
            ownerType: "subJob",
          },
        });
      }
    } catch (error) {
      logger.error(
        `Failed to post subjob part stock items for subjob ${subJobId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async setSubJobStartedAt(subJobId, date) {
    try {
      logger.debug(
        `Setting started at for subjob ${subJobId} for printer ${this.id}`,
      );
      const updatedSubJob = await this.socketClient.editObject({
        _id: subJobId,
        objectType: "subJob",
        updateData: { startedAt: date },
      });
      return updatedSubJob;
    } catch (error) {
      logger.error(
        `Failed to set started at for subjob ${subJobId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async setSubJobFinishedAt(subJobId, date) {
    try {
      logger.debug(
        `Setting finished at for subjob ${subJobId} for printer ${this.id}`,
      );
      const updatedSubJob = await this.socketClient.editObject({
        _id: subJobId,
        objectType: "subJob",
        updateData: { finishedAt: date },
      });
      return updatedSubJob;
    } catch (error) {
      logger.error(
        `Failed to set finished at for subjob ${subJobId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async setJobStartedAt(jobId, date) {
    try {
      logger.debug(
        `Setting started at for job ${jobId} for printer ${this.id}`,
      );
      const updatedJob = await this.socketClient.editObject({
        _id: jobId,
        objectType: "job",
        updateData: { startedAt: date },
      });
      return updatedJob;
    } catch (error) {
      logger.error(
        `Failed to set started at for job ${jobId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }

  async setJobFinishedAt(jobId, date) {
    try {
      logger.debug(
        `Setting finished at for job ${jobId} for printer ${this.id}`,
      );
      const updatedJob = await this.socketClient.editObject({
        _id: jobId,
        objectType: "job",
        updateData: { finishedAt: date },
      });
      return updatedJob;
    } catch (error) {
      logger.error(
        `Failed to set finished at for job ${jobId} for printer ${this.id}:`,
        error,
      );
      throw error;
    }
  }
}
