// filemanager.js - Manages file downloads and caching
import fs from "fs";
import path from "path";
import axios from "axios";
import log4js from "log4js";
import _ from "lodash";
import { loadConfig, ensureDataDir } from "../config.js";
import { sendIPC } from "../electron/ipc.js";

const config = loadConfig();

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

const FILES_DIR = ensureDataDir("files");

export class FileManager {
  constructor(socketClient) {
    this.socketClient = socketClient;
    this.files = [];
    this.downloadingFiles = new Map(); // Track ongoing downloads by fileId
    this.ensureFilesDirectory();
    this.progressCallbacks = new Map();
  }

  /**
   * Ensure the files directory exists within the data directory
   */
  ensureFilesDirectory() {
    try {
      ensureDataDir("files");
    } catch (error) {
      logger.error(`Failed to create files directory: ${error.message}`);
    }
  }

  /**
   * Update the files list from the host's files property
   */
  async updateFiles() {
    const host = await this.socketClient.getObject({
      objectType: "host",
      _id: this.socketClient.host._id,
      populate: ["files"],
    });
    this.files = (host.files || []).map((file) => {
      return { ...file, state: { type: "downloaded" } };
    });
    sendIPC("setFiles", this.files);
    logger.debug(`Updated files list: ${this.files.length} files`);
  }

  /**
   * Get the API URL for downloading files
   * Converts the socket URL to HTTP/HTTPS URL
   */
  getApiUrl() {
    let apiUrl = config.apiUrl;
    if (!apiUrl) {
      throw new Error("API URL not configured");
    }
    return apiUrl;
  }

  /**
   * Get the file path in the files directory
   */
  getFilePath(fileId) {
    return path.join(FILES_DIR, fileId);
  }

  /**
   * Check if file exists in the files directory
   */
  fileExistsInCache(fileId) {
    const filePath = this.getFilePath(fileId);
    return fs.existsSync(filePath);
  }

  /**
   * Get file from cache
   */
  getFileFromCache(fileId) {
    const filePath = this.getFilePath(fileId);
    try {
      const fileBuffer = fs.readFileSync(filePath);
      logger.debug(`Retrieved file ${fileId} from cache`);
      return fileBuffer;
    } catch (error) {
      logger.error(`Failed to read file from cache: ${error.message}`);
      throw error;
    }
  }

  /**
   * Save file to cache
   */
  saveFileToCache(fileId, fileBuffer) {
    const filePath = this.getFilePath(fileId);
    try {
      fs.writeFileSync(filePath, fileBuffer);
      logger.debug(`Saved file ${fileId} to cache`);
    } catch (error) {
      logger.error(`Failed to save file to cache: ${error.message}`);
      throw error;
    }
  }

  /**
   * Download file from API
   */
  async downloadFileFromApi(fileId) {
    const apiUrl = this.getApiUrl();
    const downloadUrl = `${apiUrl}/files/${fileId}/content/`;

    const fileObject = await this.socketClient.getObject({
      objectType: "file",
      _id: fileId,
    });

    this.files = [
      ...this.files,
      { ...fileObject, state: { type: "downloading", percent: 0 } },
    ];
    sendIPC("setFiles", this.files);
    logger.info(`Downloading file ${fileId} from ${downloadUrl}`);

    try {
      const response = await axios.get(downloadUrl, {
        responseType: "arraybuffer", // Get binary data
        headers: {
          "X-Auth-Code": config.host.authCode,
          "X-Host-Id": config.host.id,
        },
        onDownloadProgress: (progressEvent) => {
          console.log(progressEvent);
          const percent = Math.round(
            (progressEvent.loaded * 100) / (progressEvent.total || 1),
          );
          logger.debug(`Downloading file ${fileId}: ${percent}%`);
          this.files = _.unionBy(
            [{ ...fileObject, state: { type: "downloading", percent } }],
            this.files,
            "_id",
          );
          sendIPC("setFiles", this.files);
          if (this.progressCallbacks.has(fileId)) {
            const progressCallbacks = this.progressCallbacks.get(fileId);
            for (const callback of progressCallbacks) {
              callback(percent);
            }
          }
        },
      });

      const fileBuffer = Buffer.from(response.data);
      logger.info(`Successfully downloaded file ${fileId}`);
      this.saveFileToCache(fileId, fileBuffer);
      this.files = _.unionBy(
        [{ ...fileObject, state: { type: "downloaded" } }],
        this.files,
        "_id",
      );
      if (this.progressCallbacks.has(fileId)) {
        const progressCallbacks = this.progressCallbacks.get(fileId);
        for (const callback of progressCallbacks) {
          callback(100);
        }
        this.progressCallbacks.delete(fileId);
      }
      this.socketClient.editObject({
        objectType: "host",
        _id: this.socketClient.host._id,
        updateData: {
          files: this.files.map((file) => file._id),
        },
      });
      sendIPC("setFiles", this.files);
      return fileBuffer;
    } catch (error) {
      logger.error(`Failed to download file ${fileId}: ${error.message}`);
      if (error.response) {
        logger.error(`Response status: ${error.response.status}`);
        logger.error(`Response data: ${error.response.data}`);
      }
      this.files = _.unionBy(
        [{ ...fileObject, state: { type: "error" } }],
        this.files,
        "_id",
      );
      sendIPC("setFiles", this.files);
    }
  }

  /**
   * Get file by ID
   * Checks the files directory first, then downloads from API if not found
   * If a download is already in progress for the same fileId, waits for that download
   */
  async getFile(fileId, onProgress) {
    if (!fileId) {
      throw new Error("File ID is required");
    }

    // Update files list from host
    this.updateFiles();

    // Check if file exists in cache
    if (this.fileExistsInCache(fileId)) {
      logger.debug(`File ${fileId} found in cache`);
      if (onProgress) {
        onProgress(100);
      }
      return this.getFileFromCache(fileId);
    }

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

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

    // File not in cache and no download in progress, start new download
    logger.debug(`File ${fileId} not in cache, downloading from API`);
    const downloadPromise = this.downloadFileFromApi(fileId)
      .then((fileBuffer) => {
        // Remove from downloading map on success
        this.downloadingFiles.delete(fileId);
        return fileBuffer;
      })
      .catch((error) => {
        // Remove from downloading map on error
        this.downloadingFiles.delete(fileId);
        throw error;
      });

    // Store the promise in the map
    this.downloadingFiles.set(fileId, downloadPromise);

    return await downloadPromise;
  }
}
