'use strict';

const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');

const ELECTRON_SKIP_ENV_KEYS = [
  'ELECTRON_SKIP_BINARY_DOWNLOAD',
  'npm_config_electron_skip_binary_download',
  'NPM_CONFIG_ELECTRON_SKIP_BINARY_DOWNLOAD',
];

const NPM_IGNORE_SCRIPTS_ENV_KEYS = [
  'npm_config_ignore_scripts',
  'NPM_CONFIG_IGNORE_SCRIPTS',
];

function resolveElectronPackageRoot() {
  try {
    return path.dirname(require.resolve('electron/package.json'));
  } catch {
    return null;
  }
}

function readElectronPathHint(packageRoot) {
  const pathFile = path.join(packageRoot, 'path.txt');
  if (!fs.existsSync(pathFile)) return null;

  const hint = fs.readFileSync(pathFile, 'utf8').trim();
  return hint || null;
}

function platformBinaryCandidates(platform) {
  switch (platform) {
    case 'win32':
      return ['electron.exe'];
    case 'darwin':
      return ['Electron.app/Contents/MacOS/Electron'];
    case 'linux':
    case 'freebsd':
    case 'openbsd':
      return ['electron'];
    default:
      return ['electron'];
  }
}

function sanitizeElectronEnv(baseEnv = process.env) {
  const env = { ...baseEnv };

  delete env.ELECTRON_RUN_AS_NODE;
  for (const key of ELECTRON_SKIP_ENV_KEYS) {
    delete env[key];
  }
  for (const key of NPM_IGNORE_SCRIPTS_ENV_KEYS) {
    delete env[key];
  }

  return env;
}

function electronBinaryCandidates({ packageRoot, distDir, platform }) {
  const candidatePaths = [];
  const pathHint = packageRoot ? readElectronPathHint(packageRoot) : null;

  if (pathHint) {
    candidatePaths.push(path.join(distDir, pathHint));
  }

  for (const relativePath of platformBinaryCandidates(platform)) {
    candidatePaths.push(path.join(distDir, relativePath));
  }

  return candidatePaths;
}

function runNpmCommand({
  packageRoot,
  npmArgs,
  errorLabel,
  npmExecPath = process.env.npm_execpath || null,
  npmNodeExecPath = process.env.npm_node_execpath || process.execPath,
}) {
  if (!npmExecPath) {
    return false;
  }

  const result = spawnSync(npmNodeExecPath, [npmExecPath, ...npmArgs], {
    cwd: packageRoot,
    env: sanitizeElectronEnv(),
    stdio: 'inherit',
  });

  if (result.error) {
    throw result.error;
  }

  if (result.signal) {
    throw new Error(`${errorLabel} was interrupted by ${result.signal}`);
  }

  if (result.status !== 0) {
    throw new Error(`${errorLabel} failed with exit code ${result.status ?? 1}`);
  }

  return true;
}

function runNpmRebuild({
  packageRoot,
  npmExecPath = process.env.npm_execpath || null,
  npmNodeExecPath = process.env.npm_node_execpath || process.execPath,
}) {
  return runNpmCommand({
    packageRoot,
    npmArgs: ['rebuild', 'electron', '--force', '--foreground-scripts'],
    errorLabel: 'Electron rebuild',
    npmExecPath,
    npmNodeExecPath,
  });
}

function runNpmInstall({
  packageRoot,
  npmExecPath = process.env.npm_execpath || null,
  npmNodeExecPath = process.env.npm_node_execpath || process.execPath,
}) {
  return runNpmCommand({
    packageRoot,
    npmArgs: [
      'install',
      '--include=dev',
      '--ignore-scripts=false',
      '--foreground-scripts',
      '--no-audit',
      '--no-fund',
      '--package-lock=false',
    ],
    errorLabel: 'Electron dependency install',
    npmExecPath,
    npmNodeExecPath,
  });
}

function repairElectronInstall({
  packageRoot,
}) {
  const installScript = path.join(packageRoot, 'install.js');
  if (!fs.existsSync(installScript)) {
    return false;
  }

  const result = spawnSync(process.execPath, [installScript], {
    cwd: packageRoot,
    env: sanitizeElectronEnv(),
    stdio: 'inherit',
  });

  if (result.error) {
    throw result.error;
  }

  if (result.signal) {
    throw new Error(`Electron repair was interrupted by ${result.signal}`);
  }

  if (result.status !== 0) {
    throw new Error(`Electron repair failed with exit code ${result.status ?? 1}`);
  }

  return true;
}

function buildMissingElectronError({ packageRoot, distDir, candidatePaths }) {
  const tried = candidatePaths.map((candidate) => `  - ${candidate}`).join('\n');
  return [
    'Electron could not be started because the desktop runtime is missing.',
    '',
    `Looked under: ${packageRoot}`,
    `Expected the binary in: ${distDir}`,
    '',
    'Try reinstalling dependencies from the repo root:',
    '',
    '  npm install',
    '  npm rebuild electron --force --foreground-scripts',
    '  make sure ELECTRON_SKIP_BINARY_DOWNLOAD is not set',
    '',
    'If that does not help, delete node_modules/electron and install again.',
    '',
    'Searched:',
    tried,
  ].join('\n');
}

function resolveElectronBinary({
  packageRoot = resolveElectronPackageRoot(),
  projectRoot = process.cwd(),
  platform = process.platform,
  overrideDistPath = process.env.ELECTRON_OVERRIDE_DIST_PATH || null,
} = {}) {
  const repairErrors = [];

  function resolveCurrentPackageRoot() {
    if (packageRoot) return packageRoot;
    const conventionalRoot = path.join(projectRoot, 'node_modules', 'electron');
    if (fs.existsSync(path.join(conventionalRoot, 'package.json'))) {
      packageRoot = conventionalRoot;
      return packageRoot;
    }
    packageRoot = resolveElectronPackageRoot();
    return packageRoot;
  }

  function tryRepair(label, repairFn) {
    try {
      if (!repairFn()) {
        return null;
      }
    } catch (error) {
      repairErrors.push(`${label}: ${error && error.message ? error.message : String(error)}`);
      return null;
    }

    const currentPackageRoot = resolveCurrentPackageRoot();
    if (!currentPackageRoot && !overrideDistPath) {
      return null;
    }

    const distDir = overrideDistPath || path.join(currentPackageRoot, 'dist');
    return electronBinaryCandidates({ packageRoot: currentPackageRoot, distDir, platform }).find((candidate) =>
      fs.existsSync(candidate)
    );
  }

  let currentPackageRoot = resolveCurrentPackageRoot();
  if (!currentPackageRoot && !overrideDistPath) {
    const installed = tryRepair('Electron dependency install', () =>
      runNpmInstall({ packageRoot: projectRoot })
    );
    if (installed) {
      return installed;
    }

    currentPackageRoot = resolveCurrentPackageRoot();
  }

  if (!currentPackageRoot && !overrideDistPath) {
    throw new Error(
      'Electron could not be started because node_modules/electron is not installed.\n\n' +
        'Run `npm install` from the repo root, then try `npm start` again.'
    );
  }

  const distDir = overrideDistPath || path.join(currentPackageRoot, 'dist');
  let candidatePaths = electronBinaryCandidates({ packageRoot: currentPackageRoot, distDir, platform });
  let resolved = candidatePaths.find((candidate) => fs.existsSync(candidate));
  if (resolved) {
    return resolved;
  }

  const repairAttempts = [
    ['Electron rebuild', () => runNpmRebuild({ packageRoot: currentPackageRoot })],
    ['Electron install repair', () => repairElectronInstall({ packageRoot: currentPackageRoot })],
    ['Electron dependency install', () => runNpmInstall({ packageRoot: projectRoot })],
  ];

  for (const [label, repairFn] of repairAttempts) {
    const repaired = tryRepair(label, repairFn);
    if (repaired) {
      return repaired;
    }
  }

  throw new Error(
    buildMissingElectronError({
      packageRoot: currentPackageRoot,
      distDir,
      candidatePaths,
    }) +
      (repairErrors.length
        ? `\n\nAutomatic repair attempts failed:\n${repairErrors.map((error) => `  - ${error}`).join('\n')}`
        : '')
  );
}

module.exports = {
  buildMissingElectronError,
  electronBinaryCandidates,
  readElectronPathHint,
  repairElectronInstall,
  runNpmRebuild,
  runNpmInstall,
  sanitizeElectronEnv,
  resolveElectronBinary,
  resolveElectronPackageRoot,
  platformBinaryCandidates,
};
