const isDev = require('electron-is-dev');
require('module').globalPaths.push(__dirname + '/..');

const {
  app,
  ipcMain,
  BrowserWindow,
  Menu,
  dialog,
  shell,
  protocol
} = require('electron');

// This allows relative URLs to be resolved to app:// which makes
// local assets load correctly
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { standard: true } }
]);

global.fetch = require('node-fetch');

const SentryClient = require('@sentry/electron');
const findOpenSocket = require('./findOpenSocket');
const updater = require('./updater');
const about = require('./about');
const { SentryMetricIntegration } = require('@jlongster/sentry-metrics-actual');

require('./security');

if (!isDev) {
  // Install sentry
  SentryClient.init({
    dsn:
      'https://f2fa901455894dc8bf28210ef1247e2d:b9e69eb21d9740539b3ff593f7346396@sentry.io/261029',
    release: app.getVersion(),
    enableUnresponsive: false,
    ignoreErrors: ['PostError', 'HTTPError', 'ResizeObserver loop'],
    integrations: [
      new SentryMetricIntegration({
        url: 'https://sync.actualbudget.com/metrics',
        metric: 'app-errors',
        dimensions: { platform: 'desktop' },
        headers: { Origin: 'app://actual' }
      })
    ]
  });
}

const { fork } = require('child_process');
const path = require('path');
const getMenu = require('./menu');

require('./setRequireHook');

if (!isDev || !process.env.ACTUAL_DOCUMENT_DIR) {
  process.env.ACTUAL_DOCUMENT_DIR = app.getPath('documents');
}

if (!isDev || !process.env.ACTUAL_DATA_DIR) {
  process.env.ACTUAL_DATA_DIR = app.getPath('userData');
}

const WindowState = require('./window-state.js');

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let clientWin;
let serverWin;
let serverProcess;
let serverSocket;
let IS_QUITTING = false;

updater.onEvent((type, data) => {
  // Notify both the app and the about window
  if (clientWin) {
    clientWin.webContents.send(type, data);
  }

  if (about.getWindow()) {
    about.getWindow().webContents.send(type, data);
  }
});

if (isDev) {
  process.traceProcessWarnings = true;
}

function createBackgroundProcess(socketName) {
  const SentryClient = require('@sentry/electron');

  serverProcess = fork(__dirname + '/server.js', [
    '--subprocess',
    app.getVersion(),
    socketName
  ]);

  serverProcess.on('message', msg => {
    switch (msg.type) {
      case 'captureEvent':
        let event = msg.event;
        SentryClient.captureEvent(event);
        break;
      case 'captureBreadcrumb':
        SentryClient.addBreadcrumb(msg.breadcrumb);
        break;
      case 'shouldAutoUpdate':
        if (msg.flag) {
          updater.start();
        } else {
          updater.stop();
        }
        break;
      default:
        console.log('Unknown server message: ' + msg.type);
    }
  });
}

function createBackgroundWindow(socketName) {
  const win = new BrowserWindow({
    show: true,
    title: 'Actual Server',
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });
  win.loadURL(`file://${__dirname}/server.html`);

  win.webContents.on('did-finish-load', () => {
    win.webContents.send('set-socket', { name: socketName });
  });

  win.on('closed', () => {
    serverWin = null;
  });

  serverWin = win;
}

async function createWindow() {
  const windowState = await WindowState.get();

  // Create the browser window.
  const win = new BrowserWindow({
    x: windowState.x,
    y: windowState.y,
    width: windowState.width,
    height: windowState.height,
    title: 'Actual',
    titleBarStyle: 'hiddenInset',
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      preload: __dirname + '/preload.js'
    }
  });
  win.setBackgroundColor('#E8ECF0');

  const unlistenToState = WindowState.listen(win, windowState);

  if (isDev) {
    win.loadURL(`file://${__dirname}/loading.html`);
    // Wait for the development server to start
    setTimeout(() => {
      win.loadURL('http://localhost:3001/');
    }, 3000);
  } else {
    win.loadURL(`app://actual/`);
  }

  win.on('close', () => {
    // We don't want to close the budget on exit because that will
    // clear the state which re-opens the last budget automatically on
    // startup
    if (!IS_QUITTING) {
      clientWin.webContents.executeJavaScript('__actionsForMenu.closeBudget()');
    }
  });

  win.on('closed', () => {
    clientWin = null;
    updateMenu(false);
    unlistenToState();
  });

  win.on('unresponsive', () => {
    console.log(
      'browser window went unresponsive (maybe because of a modal though)'
    );
  });

  win.on('focus', async () => {
    let url = clientWin.webContents.getURL();
    if (url.includes('app://') || url.includes('localhost:')) {
      clientWin.webContents.executeJavaScript('__actionsForMenu.focused()');
    }
  });

  win.webContents.on('did-finish-load', () => {
    win.webContents.send('set-socket', { name: serverSocket });
  });

  if (process.platform === 'win32') {
    Menu.setApplicationMenu(null);
    win.setMenu(getMenu(isDev, createWindow));
  } else {
    Menu.setApplicationMenu(getMenu(isDev, createWindow));
  }

  clientWin = win;
}

function updateMenu(isBudgetOpen) {
  const menu = getMenu(isDev, createWindow);
  const file = menu.items.filter(item => item.label === 'File')[0];
  const fileItems = file.submenu.items;
  fileItems
    .filter(
      item =>
        item.label === 'Start Tutorial' ||
        item.label === 'Manage Payees...' ||
        item.label === 'Manage Rules...' ||
        item.label === 'Load Backup...'
    )

    .map(item => (item.enabled = isBudgetOpen));

  let tools = menu.items.filter(item => item.label === 'Tools')[0];
  tools.submenu.items.forEach(item => {
    item.enabled = isBudgetOpen;
  });

  const edit = menu.items.filter(item => item.label === 'Edit')[0];
  const editItems = edit.submenu.items;
  editItems
    .filter(item => item.label === 'Undo' || item.label === 'Redo')
    .map(item => (item.enabled = isBudgetOpen));

  if (process.platform === 'win32') {
    if (clientWin) {
      clientWin.setMenu(menu);
    }
  } else {
    Menu.setApplicationMenu(menu);
  }
}

app.setAppUserModelId('com.shiftreset.actual');

app.on('ready', async () => {
  serverSocket = await findOpenSocket();

  // Install an `app://` protocol that always returns the base HTML
  // file no matter what URL it is. This allows us to use react-router
  // on the frontend
  protocol.registerFileProtocol('app', (request, callback) => {
    if (request.method !== 'GET') {
      callback({ error: -322 }); // METHOD_NOT_SUPPORTED from chromium/src/net/base/net_error_list.h
      return null;
    }

    const parsedUrl = new URL(request.url);
    if (parsedUrl.protocol !== 'app:') {
      callback({ error: -302 }); // UNKNOWN_URL_SCHEME
      return;
    }

    if (parsedUrl.host !== 'actual') {
      callback({ error: -105 }); // NAME_NOT_RESOLVED
      return;
    }

    const pathname = parsedUrl.pathname;

    if (pathname.startsWith('/static') || pathname.startsWith('/Inter')) {
      callback({
        path: path.normalize(`${__dirname}/client-build${pathname}`)
      });
    } else {
      callback({
        path: path.normalize(`${__dirname}/client-build/index.html`)
      });
    }
  });

  if (process.argv[1] !== '--server') {
    await createWindow();
  }

  // This is mainly to aid debugging Sentry errors - it will add a
  // breadcrumb
  require('electron').powerMonitor.on('suspend', () => {
    console.log('Suspending', new Date());
  });

  if (isDev) {
    createBackgroundWindow(serverSocket);
  } else {
    createBackgroundProcess(serverSocket);
  }
});

app.on('window-all-closed', () => {
  // On macOS, closing all windows shouldn't exit the process
  if (process.platform !== 'darwin') {
    app.quit();
  }
});

app.on('before-quit', () => {
  IS_QUITTING = true;
  if (serverProcess) {
    serverProcess.kill();
    serverProcess = null;
  }
});

app.on('activate', () => {
  if (clientWin === null) {
    createWindow();
  }
});

ipcMain.on('get-bootstrap-data', event => {
  event.returnValue = {
    version: app.getVersion(),
    isDev
  };
});

ipcMain.handle('get-version', () => {
  return app.getVersion();
});

ipcMain.handle('relaunch', () => {
  app.relaunch();
  app.exit();
});

ipcMain.handle('open-file-dialog', (event, { filters, properties }) => {
  return dialog.showOpenDialogSync({
    properties: properties || ['openFile'],
    filters
  });
});

ipcMain.handle('save-file-dialog', (event, { title, defaultPath }) => {
  return dialog.showSaveDialogSync({ title, defaultPath });
});

ipcMain.handle('open-external-url', (event, url) => {
  shell.openExternal(url);
});

ipcMain.on('show-about', () => {
  about.openAboutWindow();
});

ipcMain.on('screenshot', () => {
  if (isDev) {
    let width = 1100;

    // This is for the main screenshot inside the frame
    clientWin.setSize(width, (width * (427 / 623)) | 0);
    // clientWin.setSize(width, (width * (495 / 700)) | 0);
  }
});

ipcMain.on('check-for-update', () => {
  // If the updater is in the middle of an update already, send the
  // about window the current status
  if (updater.isChecking()) {
    // This should always come from the about window so we can
    // guarantee that it exists. If we ever see an error here
    // something is wrong
    about.getWindow().webContents.send(updater.getLastEvent());
  } else {
    updater.check();
  }
});

ipcMain.on('apply-update', () => {
  updater.apply();
});

ipcMain.on('update-menu', (event, isBudgetOpen) => {
  updateMenu(isBudgetOpen);
});