// printerfilemanager.js - Manages file uploads from FileManager to Moonraker
import fs from "fs";
import axios from "axios";
import FormData from "form-data";
import log4js from "log4js";
import { loadConfig } from "../config.js";

const config = loadConfig();

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

const UPLOAD_MAX_RETRIES = 3;
const UPLOAD_RETRY_DELAY_MS = 1000;

function isRetryableUploadError(error) {
  const code = error?.code || error?.cause?.code;
  return (
    code === "ERR_STREAM_PREMATURE_CLOSE" ||
    code === "ECONNRESET" ||
    code === "EPIPE" ||
    code === "ETIMEDOUT"
  );
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export class PrinterFileManager {
  constructor(printerClient) {
    this.printerClient = printerClient;
    this.socketClient = printerClient.socketClient;
    this.uploadingFiles = new Map(); // Track ongoing uploads by fileId
    this.progressCallbacks = new Map();
  }

  /**
   * Upload a file from FileManager to Moonraker
   * @param {string} fileId - The ID of the file to upload (from FileManager)
   * @param {string} [fileName] - Optional name to use for the file on Moonraker (will use file object name if not provided)
   * @param {Object} moonrakerConfig - Moonraker configuration object with protocol, host, port, and optional apiKey
   * @param {Function} [onProgress] - Optional progress callback (percent) => void
   * @returns {Promise<boolean>} - True if upload succeeded, false otherwise
   */
  async uploadFile(fileId, file, onProgress = null) {
    if (!fileId) {
      throw new Error("File ID is required");
    }

    if (onProgress) {
      const progressCallbacks = this.progressCallbacks.get(fileId) || [];
      progressCallbacks.push(onProgress);
      this.progressCallbacks.set(fileId, progressCallbacks);
    }

    // Check if upload is already in progress for this file
    if (this.uploadingFiles.has(fileId)) {
      logger.debug(`File ${fileId} upload already in progress, waiting...`);
      try {
        return await this.uploadingFiles.get(fileId);
      } catch (error) {
        // If the upload failed, remove it from the map so it can be retried
        this.uploadingFiles.delete(fileId);
        throw error;
      }
    }

    if (
      !this.printerClient.config ||
      !this.printerClient.config.host ||
      !this.printerClient.config.port
    ) {
      throw new Error("Printer configuration is required");
    }

    // Create upload promise and store it
    const uploadPromise = this._performUpload(fileId, file)
      .then((result) => {
        this.uploadingFiles.delete(fileId);
        this.progressCallbacks.delete(fileId);
        return result;
      })
      .catch((error) => {
        this.uploadingFiles.delete(fileId);
        this.progressCallbacks.delete(fileId);
        throw error;
      });

    // Store the promise in the map
    this.uploadingFiles.set(fileId, uploadPromise);

    return await uploadPromise;
  }

  /**
   * Internal method to perform the actual upload
   * @private
   */
  async _performUpload(fileId, file) {
    let lastError;

    for (let attempt = 1; attempt <= UPLOAD_MAX_RETRIES; attempt++) {
      try {
        return await this._attemptUpload(fileId, file);
      } catch (error) {
        lastError = error;
        if (attempt >= UPLOAD_MAX_RETRIES || !isRetryableUploadError(error)) {
          throw error;
        }

        logger.warn(
          `Upload attempt ${attempt} failed for file ${fileId}, retrying... (${error.message})`,
        );
        await sleep(UPLOAD_RETRY_DELAY_MS * attempt);
      }
    }

    throw lastError;
  }

  /**
   * @private
   */
  async _attemptUpload(fileId, file) {
    try {
      const uploadFileName = `${fileId}.gcode`;

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

      logger.info(
        `Uploading file ${uploadFileName} to printer ${this.printerClient.id} at ${httpUrl}`,
      );

      const filePath = this.socketClient.fileManager.getFilePath(fileId);
      let filePart;
      let knownLength;

      if (fs.existsSync(filePath)) {
        const stats = fs.statSync(filePath);
        knownLength = stats.size;
        filePart = fs.createReadStream(filePath);
      } else if (Buffer.isBuffer(file)) {
        knownLength = file.length;
        filePart = file;
      } else {
        throw new Error(`File ${fileId} not found for upload`);
      }

      const formData = new FormData({ maxDataSize: Infinity });
      formData.append("file", filePart, {
        filename: "farmcontrol/" + uploadFileName,
        contentType: "text/plain",
        knownLength,
      });

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

      // Add API key if provided
      if (this.printerClient.config.apiKey) {
        headers["X-Api-Key"] = this.printerClient.config.apiKey;
      }

      // Upload to Moonraker
      const response = await axios.post(httpUrl, formData, {
        headers,
        maxBodyLength: Infinity,
        maxContentLength: Infinity,
        onUploadProgress: (progressEvent) => {
          if (progressEvent.total) {
            const percentCompleted = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total,
            );
            logger.debug(
              `Uploading file to printer ${this.printerClient.id}: ${uploadFileName} ${percentCompleted}%`,
            );
            if (this.progressCallbacks.has(fileId)) {
              const progressCallbacks = this.progressCallbacks.get(fileId);
              for (const callback of progressCallbacks) {
                callback(percentCompleted);
              }
            }
          }
        },
      });
      // Check response
      if (response.data && response.data.action) {
        logger.info(
          `Successfully uploaded file ${uploadFileName} to printer ${this.printerClient.id}`,
        );
        return true;
      } else {
        logger.error(
          `Failed to upload file ${uploadFileName} to printer ${this.printerClient.id}: Invalid response`,
        );
        return false;
      }
    } catch (error) {
      logger.error(
        `Error uploading file ${fileId} to printer ${this.printerClient.id}: ${error.message}`,
      );
      if (error.response) {
        logger.error(`Response status: ${error.response.status}`);
        logger.error(`Response data: ${JSON.stringify(error.response.data)}`);
      }
      throw error;
    }
  }
}
