import {
  app,
  BrowserWindow,
  Notification,
  screen,
  Menu,
  Tray,
  ipcMain,
  dialog,
  shell,
} from "electron";
import pkg from "electron-updater";
const autoUpdater = pkg.autoUpdater;
import { iconsPath, publicPath, selectByOS } from "./utils.js";
import {
  toggleLaunchAtStartup,
  willLaunchAtStartup,
  refreshWillLaunchAtStartup,
} from "./auto-launch.js";
import {
  setNotificationLevel,
  getNotificationLevel,
  LevelDisabled,
  LevelWarningsAndErrors,
} from "./notifications.js";
import { serverForRepo } from "./server.js";
import {
  loadConfigs,
  allConfigs,
  deleteConfigIfDisconnected,
  addNewConfig,
  configDir,
  isFirstRun,
  isPortableConfig,
} from "./config.js";

import Store from "electron-store";
import log from "electron-log";
import path from "path";
import crypto from "crypto";

// Store to save parameters
const store = new Store();

app.name = "KopiaUI";

let tray = null;
let repositoryWindows = {};
let repoIDForWebContents = {};

if (process.env.KOPIA_CUSTOM_APPDATA) {
  app.setPath("appData", process.env.KOPIA_CUSTOM_APPDATA);
}

if (isPortableConfig()) {
  // in portable mode, write cache under 'repositories'
  app.setPath("userData", path.join(configDir(), "cache"));
}

/**
 * Stores the ids of the currently connected displays.
 * The ids are sorted to generate a hash that specifies the current display configuration
 * @returns A hash of the configuration
 */
function getDisplayConfiguration() {
  // Stores the IDs all all currently connected displays
  let config = [];
  let sha256 = crypto.createHash("sha256");
  // Get all displays
  let displays = screen.getAllDisplays();
  let isFactorEqual = false;
  // Stores the previous factor - initialized with the primary scaling factor
  let prevFactor = screen.getPrimaryDisplay().scaleFactor;
  //Workaround until https://github.com/electron/electron/issues/10862 is fixed
  for (let dsp in displays) {
    // Add the id to the config
    config.push(displays[dsp].id);
    isFactorEqual = prevFactor === displays[dsp].scaleFactor;
    // Update the previous factors
    prevFactor = displays[dsp].scaleFactor;
  }
  // Sort IDs to prevent different hashes through permutation
  config.sort();
  sha256.update(config.toString());
  return { hash: sha256.digest("hex"), factorsEqual: isFactorEqual };
}

/**
 * Creates a repository window with given options and parameters
 * @param {*} repositoryID
 * The id for that specific repository used as a reference for that window
 */
function showRepoWindow(repositoryID) {
  let primaryScreenBounds = screen.getPrimaryDisplay().bounds;
  if (repositoryWindows[repositoryID]) {
    repositoryWindows[repositoryID].focus();
    return;
  }

  let windowOptions = {
    title: "KopiaUI is Loading...",
    // default width
    width: 1000,
    // default height
    height: 700,
    // default x location
    x: (primaryScreenBounds.width - 1000) / 2,
    // default y location
    y: (primaryScreenBounds.height - 700) / 2,
    autoHideMenuBar: true,
    resizable: true,
    show: false,
    webPreferences: {
      preload: path.join(publicPath(), "preload.js"),
    },
  };

  // The bounds of the windows
  let configuration = getDisplayConfiguration();
  let winBounds = store.get(configuration.hash);
  let maximized = store.get("maximized");

  if (configuration.factorsEqual) {
    Object.assign(windowOptions, winBounds);
  }

  // Create the browser window
  let repositoryWindow = new BrowserWindow(windowOptions);
  // If the window was maximized, maximize it
  if (maximized) {
    repositoryWindow.maximize();
  }
  const webContentsID = repositoryWindow.webContents.id;
  repositoryWindows[repositoryID] = repositoryWindow;
  repoIDForWebContents[webContentsID] = repositoryID;

  // Failed to load the content, retry
  repositoryWindow.webContents.on("did-fail-load", () => {
    log.error("failed to load content");

    // schedule another attempt in 0.5s
    if (repositoryWindows[repositoryID]) {
      setTimeout(() => {
        log.info("reloading");
        repositoryWindows[repositoryID].loadURL(
          serverForRepo(repositoryID).getServerAddress() +
            "/?ts=" +
            new Date().valueOf(),
        );
      }, 500);
    }
  });

  repositoryWindow.loadURL(
    serverForRepo(repositoryID).getServerAddress() +
      "/?ts=" +
      new Date().valueOf(),
  );
  updateDockIcon();

  /**
   * Store the window size, height and position on close
   */
  repositoryWindow.on("close", function () {
    store.set(getDisplayConfiguration().hash, repositoryWindow.getBounds());
    store.set("maximized", repositoryWindow.isMaximized());
  });

  /**
   * Show the window once the content is ready
   */
  repositoryWindow.once("ready-to-show", function () {
    repositoryWindow.show();
  });

  /**
   * Delete references to the repository window
   */
  repositoryWindow.on("closed", function () {
    // Delete the reference to the window
    repositoryWindow = null;
    delete repositoryWindows[repositoryID];
    delete repoIDForWebContents[webContentsID];

    const s = serverForRepo(repositoryID);
    if (deleteConfigIfDisconnected(repositoryID)) {
      s.stopServer();
    }
    updateDockIcon();
  });
}

// Check if another instance of kopia is running
if (!app.requestSingleInstanceLock()) {
  app.quit();
} else {
  app.on("second-instance", (_event, _commandLine, _workingDirectory) => {
    // Someone tried to run a second instance, we should focus our window.
    for (let repositoryID in repositoryWindows) {
      let rw = repositoryWindows[repositoryID];
      if (rw.isMinimized()) {
        rw.restore();
      }
      rw.focus();
    }
  });
}

app.on("will-quit", function () {
  allConfigs().forEach((repositoryID) =>
    serverForRepo(repositoryID).stopServer(),
  );
});

app.on("login", (event, webContents, _request, _authInfo, callback) => {
  const repositoryID = repoIDForWebContents[webContents.id];

  // intercept password prompts and automatically enter password that the server has printed for us.
  const password = serverForRepo(repositoryID).getServerPassword();
  if (password) {
    event.preventDefault();
    log.info("automatically logging in...");
    callback("kopia", password);
  }
});

app.on(
  "certificate-error",
  (event, webContents, _url, _error, certificate, callback) => {
    const repositoryID = repoIDForWebContents[webContents.id];
    // intercept certificate errors and automatically trust the certificate the server has printed for us.
    const expected =
      "sha256/" +
      Buffer.from(
        serverForRepo(repositoryID).getServerCertSHA256(),
        "hex",
      ).toString("base64");
    if (certificate.fingerprint === expected) {
      log.debug("accepting server certificate.");

      // On certificate error we disable default behaviour (stop loading the page)
      // and we then say "it is all fine - true" to the callback
      event.preventDefault();
      callback(true);
      return;
    }

    log.warn("certificate error:", certificate.fingerprint, expected);
  },
);

/**
 * Ignore to let the application run, when all windows are closed
 */
app.on("window-all-closed", function () {});

ipcMain.handle("select-dir", async (_event, _arg) => {
  const result = await dialog.showOpenDialog({
    properties: ["openDirectory"],
  });

  if (result.filePaths) {
    return result.filePaths[0];
  } else {
    return null;
  }
});

ipcMain.handle("browse-dir", async (_event, path) => {
  shell.openPath(path);
});

ipcMain.on("server-status-updated", updateTrayContextMenu);
ipcMain.on("launch-at-startup-updated", updateTrayContextMenu);
ipcMain.on("notification-config-updated", updateTrayContextMenu);

let updateAvailableInfo = null;
let updateDownloadStatusInfo = "";
let updateFailed = false;
let checkForUpdatesTriggeredFromUI = false;

// set this environment variable when developing
// to allow offering downgrade to the latest released version.
autoUpdater.allowDowngrade = process.env["KOPIA_UI_ALLOW_DOWNGRADE"] == "1";

// we will be manually triggering download and quit&install.
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;

let lastNotifiedVersion = "";

autoUpdater.on("update-available", (a) => {
  log.info("update available " + a.version);

  updateAvailableInfo = a;
  updateDownloadStatusInfo = "";
  updateTrayContextMenu();

  // do not notify more than once for a particular version.
  if (checkForUpdatesTriggeredFromUI) {
    dialog
      .showMessageBox({
        buttons: ["Yes", "No"],
        message:
          "An updated KopiaUI v" +
          a.version +
          " is available.\n\nDo you want to install it now?",
      })
      .then((r) => {
        if (r.response == 0) {
          installUpdate();
        }
      });
    checkForUpdatesTriggeredFromUI = false;
  }

  if (lastNotifiedVersion != a.version) {
    lastNotifiedVersion = a.version;

    const notification = new Notification({
      title: "New version of KopiaUI",
      body:
        "Version v" +
        a.version +
        " is available.\n\nClick here to download and install it.",
    });

    notification.on("click", () => installUpdate());
    notification.show();
  }
});

autoUpdater.on("update-not-available", () => {
  updateAvailableInfo = null;
  updateDownloadStatusInfo = "";
  updateFailed = false;
  updateTrayContextMenu();
  if (checkForUpdatesTriggeredFromUI) {
    dialog.showMessageBox({
      buttons: ["OK"],
      message: "No updates available.",
    });
    checkForUpdatesTriggeredFromUI = false;
  }
});

autoUpdater.on("download-progress", (progress) => {
  if (updateAvailableInfo) {
    updateDownloadStatusInfo =
      "Downloading Update: v" +
      updateAvailableInfo.version +
      " (" +
      Math.round(progress.percent * 10) / 10.0 +
      "%) ...";
    updateTrayContextMenu();
  }
});

autoUpdater.on("update-downloaded", (_info) => {
  updateDownloadStatusInfo =
    "Installing Update: v" + updateAvailableInfo.version + " ...";
  updateTrayContextMenu();

  setTimeout(() => {
    try {
      autoUpdater.quitAndInstall();
    } catch (e) {
      log.info("update error", e);
    }

    updateDownloadStatusInfo = null;
    updateFailed = true;
    updateTrayContextMenu();
  }, 500);
});

autoUpdater.on("error", (a) => {
  updateAvailableInfo = null;
  updateDownloadStatusInfo = "Error checking for updates.";
  log.info("error checking for updates", a);
  updateTrayContextMenu();
  if (checkForUpdatesTriggeredFromUI) {
    dialog.showErrorBox(
      "Error checking for updates.",
      "There was an error checking for updates, try again later.",
    );
    checkForUpdatesTriggeredFromUI = false;
  }
});

function checkForUpdates() {
  updateDownloadStatusInfo = "Checking for update...";
  updateAvailableInfo = null;
  updateTrayContextMenu();

  autoUpdater.checkForUpdates();
}

function checkForUpdatesNow() {
  checkForUpdatesTriggeredFromUI = true;
  checkForUpdates();
}

function installUpdate() {
  updateDownloadStatusInfo = "Downloading and installing update...";
  autoUpdater.downloadUpdate();
}

function viewReleaseNotes() {
  const ver = updateAvailableInfo.version + "";
  if (ver.match(/^\d{8}\./)) {
    // kopia-test builds are named yyyymmdd.0.hhmmss
    shell.openExternal(
      "https://github.com/kopia/kopia-test-builds/releases/v" + ver,
    );
  } else {
    shell.openExternal("https://github.com/kopia/kopia/releases/v" + ver);
  }
}

function isOutsideOfApplicationsFolderOnMac() {
  if (!app.isPackaged || isPortableConfig()) {
    return false;
  }

  // this method is only available on Mac.
  if (!app.isInApplicationsFolder) {
    return false;
  }

  return !app.isInApplicationsFolder();
}

function maybeMoveToApplicationsFolder() {
  if (process.env["KOPIA_UI_TESTING"]) {
    return;
  }

  dialog
    .showMessageBox({
      buttons: ["Yes", "No"],
      message:
        "For best experience, Kopia needs to be installed in Applications folder.\n\nDo you want to move it now?",
    })
    .then((r) => {
      if (r.response == 0) {
        app.moveToApplicationsFolder();
      } else {
        checkForUpdates();
      }
    })
    .catch((e) => {
      log.info(e);
    });
}

function updateDockIcon() {
  if (process.platform === "darwin") {
    let any = false;
    for (const _k in repositoryWindows) {
      any = true;
    }
    if (any) {
      app.dock.show();
    } else {
      app.dock.hide();
    }
  }
}

/**
 * Show all repository windows at once
 */
function showAllRepoWindows() {
  allConfigs().forEach(showRepoWindow);
}

function safeTrayHandler(ev, h) {
  tray.on(ev, () => {
    try {
      h();
    } catch (e) {}
  });
}

app.on("ready", () => {
  loadConfigs();

  if (isPortableConfig()) {
    const logDir = path.join(configDir(), "logs");

    log.transports.file.resolvePath = (variables) =>
      path.join(logDir, variables.fileName);
  }

  log.transports.console.level = "warn";
  log.transports.file.level = "debug";
  autoUpdater.logger = log;

  // re-check for updates every 24 hours
  setInterval(checkForUpdates, 86400000);

  tray = new Tray(
    path.join(
      iconsPath(),
      selectByOS({
        mac: "kopiaTrayTemplate.png",
        win: "kopia-tray.ico",
        linux: "kopia-tray.png",
      }),
    ),
  );

  tray.setToolTip("Kopia");

  // hooks exposed to tests
  if (process.env["KOPIA_UI_TESTING"]) {
    app.testHooks = {
      tray: tray,
      showRepoWindow: showRepoWindow,
      allConfigs: allConfigs,
    };
  }

  safeTrayHandler("click", () => tray.popUpContextMenu());
  safeTrayHandler("right-click", () => tray.popUpContextMenu());
  safeTrayHandler("double-click", () => showAllRepoWindows());

  updateTrayContextMenu();
  refreshWillLaunchAtStartup();
  updateDockIcon();

  allConfigs().forEach((repoID) => serverForRepo(repoID).actuateServer());

  if (isFirstRun()) {
    // open all repo windows on first run.
    showAllRepoWindows();

    // on Windows, also show the notification.
    if (process.platform === "win32") {
      tray.displayBalloon({
        title: "Kopia is running in the background",
        content: "Click on the system tray icon to open the menu",
      });
    }
  }

  if (isOutsideOfApplicationsFolderOnMac()) {
    setTimeout(maybeMoveToApplicationsFolder, 1000);
  } else {
    checkForUpdates();
  }
});

function showRepoNotification(e) {
  const nl = getNotificationLevel();
  if (nl === LevelDisabled) {
    // notifications disabled
    return;
  }

  const severity = e.notification.severity;
  if (severity < 10 && nl === LevelWarningsAndErrors) {
    log.info(
      "showRepoNotification",
      "skipping notification",
      e.notification.subject,
    );
    return;
  }

  let urgency = "normal";

  if (severity < 0) {
    urgency = "low";
  } else if (severity >= 10) {
    // warnings and errors
    urgency = "critical";
  } else {
    urgency = "normal";
  }

  const notification = new Notification({
    title: e.notification.subject,
    body: e.notification.body,
    urgency: urgency,
  });

  notification.on("click", () => showRepoWindow(e.repositoryID));
  notification.show();
}

ipcMain.addListener("config-list-updated-event", () => updateTrayContextMenu());
ipcMain.addListener("status-updated-event", () => updateTrayContextMenu());
ipcMain.addListener("repo-notification-event", showRepoNotification);

function addAnotherRepository() {
  const repoID = addNewConfig();
  serverForRepo(repoID).actuateServer();
  showRepoWindow(repoID);
}

function updateTrayContextMenu() {
  if (!tray) {
    return;
  }

  let defaultReposTemplates = [];
  let additionalReposTemplates = [];

  allConfigs().forEach((repoID) => {
    const sd = serverForRepo(repoID).getServerStatusDetails();
    let desc = "";

    if (sd.startingUp) {
      desc = "<starting up>";
    } else if (!sd.connected) {
      if (sd.initTaskID) {
        desc = "<initializing>";
      } else {
        desc = "<not connected>";
      }
    } else {
      desc = sd.description;
    }

    // put primary repository first.
    const collection =
      repoID === "repository"
        ? defaultReposTemplates
        : additionalReposTemplates;

    collection.push({
      label: desc,
      click: () => showRepoWindow(repoID),
      toolTip: desc + " (" + repoID + ")",
    });
  });

  if (additionalReposTemplates.length > 0) {
    additionalReposTemplates.sort((a, b) => a.label.localeCompare(b.label));
  }

  let autoUpdateMenuItems = [];

  if (updateDownloadStatusInfo) {
    autoUpdateMenuItems.push({
      label: updateDownloadStatusInfo,
      enabled: false,
    });
  } else if (updateAvailableInfo) {
    if (updateFailed) {
      autoUpdateMenuItems.push({
        label:
          "Update Failed, click to manually download and install v" +
          updateAvailableInfo.version,
        click: viewReleaseNotes,
      });
    } else {
      autoUpdateMenuItems.push({
        label: "Update Available: v" + updateAvailableInfo.version,
        click: viewReleaseNotes,
      });
      autoUpdateMenuItems.push({
        label: "Download And Install...",
        click: installUpdate,
      });
    }
  } else {
    autoUpdateMenuItems.push({
      label: "KopiaUI is up-to-date: " + app.getVersion(),
      enabled: false,
    });
  }

  const nl = getNotificationLevel();

  let template = defaultReposTemplates
    .concat(additionalReposTemplates)
    .concat([
      { type: "separator" },
      {
        label: "Connect To Another Repository...",
        click: addAnotherRepository,
      },
      { type: "separator" },
      { label: "Check For Updates Now", click: checkForUpdatesNow },
    ])
    .concat(autoUpdateMenuItems)
    .concat([
      { type: "separator" },
      {
        label: "Launch At Startup",
        type: "checkbox",
        click: toggleLaunchAtStartup,
        checked: willLaunchAtStartup(),
      },
      {
        label: "Notifications",
        type: "submenu",
        submenu: [
          {
            label: "Enabled",
            type: "radio",
            click: () => setNotificationLevel(2),
            checked: nl === 2,
          },
          {
            label: "Warnings And Errors",
            type: "radio",
            click: () => setNotificationLevel(1),
            checked: nl === 1,
          },
          {
            label: "Disabled",
            type: "radio",
            click: () => setNotificationLevel(0),
            checked: nl === 0,
          },
        ],
      },
      { label: "Quit", role: "quit" },
    ]);
  tray.setContextMenu(Menu.buildFromTemplate(template));
}
