/* eslint-disable @typescript-eslint/no-var-requires */
const { app, ipcMain } = require("electron");
const zlib = require("zlib");
const path = require("path");
const fs = require("fs/promises");
const { promisify } = require("util");
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);

const greenworks = require("./greenworks");
const log = require("electron-log");
const flatten = require("lodash/flatten");
const Config = require("electron-config");
const config = new Config();

// https://stackoverflow.com/a/69418940
const dirSize = async (directory) => {
  const files = await fs.readdir(directory);
  const stats = files.map((file) => fs.stat(path.join(directory, file)));
  return (await Promise.all(stats)).reduce((accumulator, { size }) => accumulator + size, 0);
};

const getDirFileStats = async (directory) => {
  const files = await fs.readdir(directory);
  const stats = files.map((f) => {
    const file = path.join(directory, f);
    return fs.stat(file).then((stat) => ({ file, stat }));
  });
  const data = await Promise.all(stats);
  return data;
};

const getNewestFile = async (directory) => {
  const data = await getDirFileStats(directory);
  return data.sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime())[0];
};

const getAllSaves = async (window) => {
  const rootDirectory = await getSaveFolder(window, true);
  const data = await fs.readdir(rootDirectory, { withFileTypes: true });
  const savesPromises = data
    .filter((e) => e.isDirectory())
    .map((dir) => path.join(rootDirectory, dir.name))
    .map((dir) => getDirFileStats(dir));
  const saves = await Promise.all(savesPromises);
  const flat = flatten(saves);
  return flat;
};

async function prepareSaveFolders(window) {
  const rootFolder = await getSaveFolder(window, true);
  const currentFolder = await getSaveFolder(window);
  const backupsFolder = path.join(rootFolder, "/_backups");
  await prepareFolders(rootFolder, currentFolder, backupsFolder);
}

async function prepareFolders(...folders) {
  for (const folder of folders) {
    try {
      // Making sure the folder exists
      // eslint-disable-next-line no-await-in-loop
      await fs.stat(folder);
    } catch (error) {
      if (error.code === "ENOENT") {
        log.warn(`'${folder}' not found, creating it...`);
        // eslint-disable-next-line no-await-in-loop
        await fs.mkdir(folder);
      } else {
        log.error(error);
      }
    }
  }
}

async function getFolderSizeInBytes(saveFolder) {
  try {
    return await dirSize(saveFolder);
  } catch (error) {
    log.error(error);
  }
}

function setAutosaveConfig(value) {
  config.set("autosave-enabled", value);
}

function isAutosaveEnabled() {
  return config.get("autosave-enabled", true);
}

function setSaveCompressionConfig(value) {
  config.set("save-compression-enabled", value);
}

function isSaveCompressionEnabled() {
  return config.get("save-compression-enabled", true);
}

function setCloudEnabledConfig(value) {
  config.set("cloud-enabled", value);
}

async function getSaveFolder(window, root = false) {
  if (root) return path.join(app.getPath("userData"), "/saves");
  const identifier = window.gameInfo?.player?.identifier ?? "";
  return path.join(app.getPath("userData"), "/saves", `/${identifier}`);
}

function isCloudEnabled() {
  // If the Steam API could not be initialized on game start, we'll abort this.
  if (global.greenworksError) return false;

  // If the user disables it in Steam there's nothing we can do
  if (!greenworks.isCloudEnabledForUser()) return false;

  // Let's check the config file to see if it's been overriden
  const enabledInConf = config.get("cloud-enabled", true);
  if (!enabledInConf) return false;

  const isAppEnabled = greenworks.isCloudEnabled();
  if (!isAppEnabled) greenworks.enableCloud(true);

  return true;
}

function saveCloudFile(name, content) {
  return new Promise((resolve, reject) => {
    greenworks.saveTextToFile(name, content, resolve, reject);
  });
}

function getFirstCloudFile() {
  const nbFiles = greenworks.getFileCount();
  if (nbFiles === 0) throw new Error("No files in cloud");
  const file = greenworks.getFileNameAndSize(0);
  log.silly(`Found ${nbFiles} files.`);
  log.silly(`First File: ${file.name} (${file.size} bytes)`);
  return file.name;
}

function getCloudFile() {
  const file = getFirstCloudFile();
  return new Promise((resolve, reject) => {
    greenworks.readTextFromFile(file, resolve, reject);
  });
}

function deleteCloudFile() {
  const file = getFirstCloudFile();
  return new Promise((resolve, reject) => {
    greenworks.deleteFile(file, resolve, reject);
  });
}

async function getSteamCloudQuota() {
  return new Promise((resolve, reject) => {
    greenworks.getCloudQuota(resolve, reject);
  });
}

async function backupSteamDataToDisk(currentPlayerId) {
  const nbFiles = greenworks.getFileCount();
  if (nbFiles === 0) return;

  const file = greenworks.getFileNameAndSize(0);
  const previousPlayerId = file.name.replace(".json.gz", "");
  if (previousPlayerId !== currentPlayerId) {
    const backupSave = await getSteamCloudSaveString();
    const backupFile = path.join(app.getPath("userData"), "/saves/_backups", `${previousPlayerId}.json.gz`);
    const buffer = Buffer.from(backupSave, "base64").toString("utf8");
    saveContent = await gzip(buffer);
    await fs.writeFile(backupFile, saveContent, "utf8");
    log.debug(`Saved backup game to '${backupFile}`);
  }
}

async function pushGameSaveToSteamCloud(base64save, currentPlayerId) {
  if (!isCloudEnabled) return Promise.reject("Steam Cloud is not Enabled");

  try {
    backupSteamDataToDisk(currentPlayerId);
  } catch (error) {
    log.error(error);
  }

  const steamSaveName = `${currentPlayerId}.json.gz`;

  // Let's decode the base64 string so GZIP is more efficient.
  const buffer = Buffer.from(base64save, "base64");
  const compressedBuffer = await gzip(buffer);
  // We can't use utf8 for some reason, steamworks is unhappy.
  const content = compressedBuffer.toString("base64");
  log.debug(`Uncompressed: ${base64save.length} bytes`);
  log.debug(`Compressed: ${content.length} bytes`);
  log.debug(`Saving to Steam Cloud as ${steamSaveName}`);

  try {
    await saveCloudFile(steamSaveName, content);
  } catch (error) {
    log.error(error);
  }
}

async function getSteamCloudSaveString() {
  if (!isCloudEnabled()) return Promise.reject("Steam Cloud is not Enabled");
  log.debug(`Fetching Save in Steam Cloud`);
  const cloudString = await getCloudFile();
  const gzippedBase64Buffer = Buffer.from(cloudString, "base64");
  const uncompressedBuffer = await gunzip(gzippedBase64Buffer);
  const content = uncompressedBuffer.toString("base64");
  log.debug(`Compressed: ${cloudString.length} bytes`);
  log.debug(`Uncompressed: ${content.length} bytes`);
  return content;
}

async function saveGameToDisk(window, saveData) {
  const currentFolder = await getSaveFolder(window);
  let saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
  const maxFolderSizeBytes = config.get("autosave-quota", 1e8); // 100Mb per playerIndentifier
  const remainingSpaceBytes = maxFolderSizeBytes - saveFolderSizeBytes;
  log.debug(`Folder Usage: ${saveFolderSizeBytes} bytes`);
  log.debug(`Folder Capacity: ${maxFolderSizeBytes} bytes`);
  log.debug(
    `Remaining: ${remainingSpaceBytes} bytes (${((saveFolderSizeBytes / maxFolderSizeBytes) * 100).toFixed(2)}% used)`,
  );
  const shouldCompress = isSaveCompressionEnabled();
  const fileName = saveData.fileName;
  const file = path.join(currentFolder, fileName + (shouldCompress ? ".gz" : ""));
  try {
    let saveContent = saveData.save;
    if (shouldCompress) {
      // Let's decode the base64 string so GZIP is more efficient.
      const buffer = Buffer.from(saveContent, "base64").toString("utf8");
      saveContent = await gzip(buffer);
    }
    await fs.writeFile(file, saveContent, "utf8");
    log.debug(`Saved Game to '${file}'`);
    log.debug(`Save Size: ${saveContent.length} bytes`);
  } catch (error) {
    log.error(error);
  }

  const fileStats = await getDirFileStats(currentFolder);
  const oldestFiles = fileStats
    .sort((a, b) => a.stat.mtime.getTime() - b.stat.mtime.getTime())
    .map((f) => f.file)
    .filter((f) => f !== file);

  while (saveFolderSizeBytes > maxFolderSizeBytes && oldestFiles.length > 0) {
    const fileToRemove = oldestFiles.shift();
    log.debug(`Over Quota -> Removing "${fileToRemove}"`);
    try {
      // eslint-disable-next-line no-await-in-loop
      await fs.unlink(fileToRemove);
    } catch (error) {
      log.error(error);
    }

    // eslint-disable-next-line no-await-in-loop
    saveFolderSizeBytes = await getFolderSizeInBytes(currentFolder);
    log.debug(`Save Folder: ${saveFolderSizeBytes} bytes`);
    log.debug(
      `Remaining: ${maxFolderSizeBytes - saveFolderSizeBytes} bytes (${(
        (saveFolderSizeBytes / maxFolderSizeBytes) *
        100
      ).toFixed(2)}% used)`,
    );
  }

  return file;
}

async function loadLastFromDisk(window) {
  const folder = await getSaveFolder(window);
  const last = await getNewestFile(folder);
  log.debug(`Last modified file: "${last.file}" (${last.stat.mtime.toLocaleString()})`);
  return loadFileFromDisk(last.file);
}

async function loadFileFromDisk(path) {
  const buffer = await fs.readFile(path);
  let content;
  if (path.endsWith(".gz")) {
    const uncompressedBuffer = await gunzip(buffer);
    content = uncompressedBuffer.toString("base64");
    log.debug(`Uncompressed file content (new size: ${content.length} bytes)`);
  } else {
    content = buffer.toString("utf8");
    log.debug(`Loaded file with ${content.length} bytes`);
  }
  return content;
}

function getSaveInformation(window, save) {
  return new Promise((resolve) => {
    ipcMain.once("get-save-info-response", async (event, data) => {
      resolve(data);
    });
    window.webContents.send("get-save-info-request", save);
  });
}

function getCurrentSave(window) {
  return new Promise((resolve) => {
    ipcMain.once("get-save-data-response", (event, data) => {
      resolve(data);
    });
    window.webContents.send("get-save-data-request");
  });
}

function pushSaveGameForImport(window, save, automatic) {
  ipcMain.once("push-import-result", async (event, arg) => {
    log.debug(`Was save imported? ${arg.wasImported ? "Yes" : "No"}`);
  });
  window.webContents.send("push-save-request", { save, automatic });
}

async function restoreIfNewerExists(window) {
  const currentSave = await getCurrentSave(window);
  const currentData = await getSaveInformation(window, currentSave.save);
  const steam = {};
  const disk = {};

  try {
    steam.save = await getSteamCloudSaveString();
    steam.data = await getSaveInformation(window, steam.save);
  } catch (error) {
    log.error("Could not retrieve steam file");
    log.debug(error);
  }

  try {
    const saves = (await getAllSaves()).sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime());
    if (saves.length > 0) {
      disk.save = await loadFileFromDisk(saves[0].file);
      disk.data = await getSaveInformation(window, disk.save);
    }
  } catch (error) {
    log.error("Could not retrieve disk file");
    log.debug(error);
  }

  const lowPlaytime = 1000 * 60 * 15;
  let bestMatch;
  if (!steam.data && !disk.data) {
    log.info("No data to import");
  } else if (!steam.data) {
    // We'll just compare using the lastSave field for now.
    log.debug("Best potential save match: Disk");
    bestMatch = disk;
  } else if (!disk.data) {
    log.debug("Best potential save match: Steam Cloud");
    bestMatch = steam;
  } else if (steam.data.lastSave >= disk.data.lastSave || steam.data.playtime + lowPlaytime > disk.data.playtime) {
    // We want to prioritze steam data if the playtime is very close
    log.debug("Best potential save match: Steam Cloud");
    bestMatch = steam;
  } else {
    log.debug("Best potential save match: disk");
    bestMatch = disk;
  }
  if (bestMatch) {
    if (bestMatch.data.lastSave > currentData.lastSave + 5000) {
      // We add a few seconds to the currentSave's lastSave to prioritize it
      log.info("Found newer data than the current's save file");
      log.silly(bestMatch.data);
      await pushSaveGameForImport(window, bestMatch.save, true);
      return true;
    } else if (bestMatch.data.playtime > currentData.playtime && currentData.playtime < lowPlaytime) {
      log.info("Found older save, but with more playtime, and current less than 15 mins played");
      log.silly(bestMatch.data);
      await pushSaveGameForImport(window, bestMatch.save, true);
      return true;
    } else {
      log.debug("Current save data is the freshest");
      return false;
    }
  }
}

module.exports = {
  getCurrentSave,
  getSaveInformation,
  restoreIfNewerExists,
  pushSaveGameForImport,
  pushGameSaveToSteamCloud,
  getSteamCloudSaveString,
  getSteamCloudQuota,
  deleteCloudFile,
  saveGameToDisk,
  loadLastFromDisk,
  loadFileFromDisk,
  getSaveFolder,
  prepareSaveFolders,
  getAllSaves,
  isCloudEnabled,
  setCloudEnabledConfig,
  isAutosaveEnabled,
  setAutosaveConfig,
  isSaveCompressionEnabled,
  setSaveCompressionConfig,
};