// @ts-nocheck

const fs = require('fs');
const path = require('path');
const process = require('process');
const cp = require('child_process');
const log = require('electron-log');
const { autoUpdater } = require('electron-updater');
const { ipcMain } = require('electron');
const { updaterGeneric } = require('./updaterGeneric');
const { UpdaterStatus } = require('./updaterUtils');
const { isSnap, isDebOrRpm } = require('../common/utils');
const { displayDialogMain } = require('../common/utilsMain');

/**
 * Main process updater service
 */
class UpdaterService {
  /**
   * Creates updater service object
   *
   * @param {import('electron').BrowserWindow} rendererWindow
   * Renderer window to communicate update status.
   *
   * @param {import('electron').App} app
   * App instance to quit before updating.
   *
   * @param {boolean} shouldAutoDownload
   * Specify if any new update should be downloaded
   * automatically.
   */
  constructor(rendererWindow, app, shouldAutoDownload) {
    this.rendererWindow = rendererWindow;
    this.app = app;
    this.shouldAutoDownload = shouldAutoDownload;
    this.postUpdateDownloadInfo = null;
    this._isDownloadCached = false;

    autoUpdater.autoDownload = false;
    autoUpdater.autoInstallOnAppQuit = false;
  }

  /**
   * Checks if a generic updater should be used over electron
   * auto-updater based on platform, package format and environment.
   */
  static shouldUseGenericUpdater() {
    return isDebOrRpm() || isSnap() || process.env.DEV_MODE;
  }

  /**
   * Initializes updater with event listeners
   *
   * @param {() => void} onUpdateReady
   * Callback function called when the update is downloaded
   * and is ready to be installed.
   */
  initializeUpdater(onUpdateReady) {
    log.info('Starting Updater Service...');
    const loggerPath = path.join(global.userDataPath, 'updater-debug.log');

    if (!UpdaterService.shouldUseGenericUpdater()) {
      autoUpdater.logger = log;
      autoUpdater.logger.transports.file.level = 'info';
      autoUpdater.logger.transports.file.resolve = () => loggerPath;

      autoUpdater.logger._info = autoUpdater.logger.info;

      // Monkey patch logger's info function
      autoUpdater.logger.info = (message) => {
        // Check if the downloaded update is loaded from
        // cache and set the `_isDownloadCached` property

        if (
          typeof message === 'string'
          && message.includes('Update has already been downloaded')
        ) {
          this._isDownloadCached = true;
        }

        autoUpdater.logger._info(message);
      };

      /** @type {string?} */
      this.currentStatus = null;

      /** @type {object?} */
      this.currentInfo = null;

      autoUpdater.on('checking-for-update', () => {
        this.sendStatusToWindow(UpdaterStatus.CheckingForUpdates);
      });

      autoUpdater.on('update-available', (info) => {
        this.sendStatusToWindow(UpdaterStatus.UpdateAvailable, info);

        if (this.shouldAutoDownload) {
          autoUpdater.downloadUpdate();
        }
      });

      autoUpdater.on('update-not-available', (info) => {
        this.sendStatusToWindow(UpdaterStatus.UpdateNotAvailable, info);
      });

      autoUpdater.on('error', (err) => {
        this.sendStatusToWindow(UpdaterStatus.Error, JSON.stringify({ errorMessage: err }));
      });

      autoUpdater.on('download-progress', (progressObj) => {
        let logMessage = `Download speed: ${progressObj.bytesPerSecond}`;
        logMessage = `${logMessage} - Downloaded ${progressObj.percent}%`;
        logMessage = `${logMessage} (${progressObj.transferred} / ${progressObj.total})`;
        console.log(logMessage);

        this.sendStatusToWindow(UpdaterStatus.DownloadProgress, progressObj);
      });

      autoUpdater.on('update-downloaded', (info) => {
        this.postUpdateDownloadInfo = info;
        this.sendStatusToWindow(UpdaterStatus.UpdateDownloaded, info);
        onUpdateReady();
      });

      // Check for updates
      autoUpdater.checkForUpdates();
    }
    else {
      updaterGeneric.logger = log;
      updaterGeneric.logger.transports.file.resolve = () => loggerPath;

      updaterGeneric.on('checking-for-update', () => {
        this.sendStatusToWindow(UpdaterStatus.CheckingForUpdates);
      });

      updaterGeneric.on('update-available', (info) => {
        this.sendStatusToWindow(UpdaterStatus.UpdateAvailable, info);
      });

      updaterGeneric.on('update-not-available', (info) => {
        this.sendStatusToWindow(UpdaterStatus.UpdateNotAvailable, info);
      });

      updaterGeneric.on('error', (err) => {
        this.sendStatusToWindow(UpdaterStatus.Error, JSON.stringify({ errorMessage: err }));
      });

      // Check for updates
      updaterGeneric.checkForUpdates();
    }

    // Handle incoming IPC messages

    ipcMain.on('update:checkForUpdates', () => {
      UpdaterService.checkForUpdates();
    });

    ipcMain.on('update:installUpdateAndRestart', () => {
      this.installUpdateAndRestart(this.postUpdateDownloadInfo);
    });

    ipcMain.on('update:downloadUpdate', () => {
      if (!UpdaterService.shouldUseGenericUpdater()) {
        autoUpdater.downloadUpdate();
      }
      else {
        updaterGeneric.downloadUpdate();
      }
    });

    ipcMain.on('update:getChangelog', (event) => {
      // eslint-disable-next-line no-param-reassign
      event.returnValue = this.getChangelog();
    });

    ipcMain.on('update:doesUseGenericUpdater', (event) => {
      // eslint-disable-next-line no-param-reassign
      event.returnValue = UpdaterService.shouldUseGenericUpdater();
    });

    ipcMain.on('update:syncUpdaterStatus', (event) => {
      // eslint-disable-next-line no-param-reassign
      event.returnValue = undefined;
      this.sendStatusToWindow(this.currentStatus, this.currentInfo);
    });
  }

  /**
   * Sends the updater status and args to renderer process.
   * Also updates the current status in the updater service.
   *
   * @param {string?} status
   * @param {any?} arg
   */
  sendStatusToWindow(status, arg) {
    if (status === undefined) return;

    const currentStatus = status ?? this.currentStatus;
    const currentInfo = arg ?? this.currentInfo;

    this.currentStatus = currentStatus;
    this.currentInfo = currentInfo;

    if (this.rendererWindow.isDestroyed()) return;
    this.rendererWindow.webContents.send(currentStatus, currentInfo);
  }

  /**
   * Returns changelog of either the current version or
   * the new version available as a string of HTML
   *
   * @returns {string?}
   */
  getChangelog() {
    const releaseNotes = this.currentInfo?.releaseNotes;
    return releaseNotes;
  }

  /**
   * Installs update on MacOS
   *
   * @param {() => void} onUpdateApplied
   * Callback function called after update is installed
   */
  installMacUpdate(onUpdateApplied) {
    const { downloadedFile } = this.postUpdateDownloadInfo;
    const cacheFolder = path.dirname(downloadedFile);

    // Path to the `.app` folder
    const appPath = path.resolve(this.app.getAppPath(), '../../..');
    const appPathParent = path.dirname(appPath);

    this.sendStatusToWindow(UpdaterStatus.InstallingUpdate, {
      downloadedFile,
      cacheFolder,
      appPath,
      appPathParent,
    });

    if (fs.existsSync(`${cacheFolder}/Google Assistant.app`)) {
      cp.execSync(`rm -rf "${cacheFolder}/Google Assistant.app"`);
    }

    // Extract the downloaded archive
    const appExtractionCmd = `ditto -x -k "${downloadedFile}" "${cacheFolder}"`;

    cp.exec(appExtractionCmd, (err, stdout, stderr) => {
      if (err) {
        displayDialogMain({
          type: 'error',
          message: 'Error occurred while extracting archive',
          detail: err.message,
        });

        return;
      }

      // Delete existing `.app` in application directory
      // to avoid problems with moving the updated version
      // to the destination.
      const removeExistingApp = `rm -rf "${appPath}"`;

      // Copy the extracted `.app` to the application directory
      const moveUpdateToApplications = [
        'mv',
        `"${cacheFolder}/Google Assistant.app"`,
        `"${appPathParent}"`,
      ].join(' ');

      // Apply update (remove and move update)
      cp.execSync(`${removeExistingApp} && ${moveUpdateToApplications}`);

      this.sendStatusToWindow(UpdaterStatus.UpdateApplied, null);
      onUpdateApplied();
    });
  }

  /**
   * Restarts the application after applying update
   */
  installUpdateAndRestart() {
    // Prevent the app from applying the same update
    // multiple times in a row
    if (this.app.isUpdating) return;

    this.app.isQuitting = true;
    this.app.isUpdating = true;

    if (process.platform !== 'darwin') {
      autoUpdater.quitAndInstall(true, true);
    }
    else {
      this.installMacUpdate(() => {
        this.app.relaunch();
        this.app.quit();
      });
    }
  }

  /**
   * Quits the application after applying update
   */
  installUpdateAndQuit() {
    // Prevent the app from applying the same update
    // multiple times in a row
    if (this.app.isUpdating) return;

    this.app.isQuitting = true;
    this.app.isUpdating = true;

    if (process.platform !== 'darwin') {
      autoUpdater.quitAndInstall(true);
    }
    else {
      this.installMacUpdate(() => {
        this.app.quit();
      });
    }
  }

  /**
   * Checks for an update and notifies the user when available
   */
  static checkForUpdates() {
    if (!UpdaterService.shouldUseGenericUpdater()) {
      autoUpdater.checkForUpdates();
    }
    else {
      updaterGeneric.checkForUpdates();
    }
  }
}

module.exports = UpdaterService;
