import path from 'node:path';

import debug from 'debug';
import findUp from 'find-up';
import fs from 'fs-extra';
import semver from 'semver';

const d = debug('electron-forge:electron-version');

const electronPackageNames = ['electron-nightly', 'electron'];

type PackageJSONWithDeps = {
  devDependencies?: Record<string, string>;
  dependencies?: Record<string, string>;
};

function findElectronDep(dep: string): boolean {
  return electronPackageNames.includes(dep);
}

async function findAncestorNodeModulesPath(
  dir: string,
  packageName: string,
): Promise<string | undefined> {
  d('Looking for a lock file to indicate the root of the repo');
  const lockPath = await findUp(
    ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'],
    { cwd: dir, type: 'file' },
  );
  if (lockPath) {
    d(`Found lock file: ${lockPath}`);
    const nodeModulesPath = path.join(
      path.dirname(lockPath),
      'node_modules',
      packageName,
    );
    if (await fs.pathExists(nodeModulesPath)) {
      return nodeModulesPath;
    }
  }

  return Promise.resolve(undefined);
}

async function determineNodeModulesPath(
  dir: string,
  packageName: string,
): Promise<string | undefined> {
  const nodeModulesPath: string | undefined = path.join(
    dir,
    'node_modules',
    packageName,
  );
  if (await fs.pathExists(nodeModulesPath)) {
    return nodeModulesPath;
  }
  return findAncestorNodeModulesPath(dir, packageName);
}

export class PackageNotFoundError extends Error {
  constructor(packageName: string, dir: string) {
    super(
      `Cannot find the package "${packageName}". Perhaps you need to run install it in "${dir}"?`,
    );
  }
}

function getElectronModuleName(packageJSON: PackageJSONWithDeps): string {
  if (!packageJSON.devDependencies) {
    throw new Error('package.json for app does not have any devDependencies');
  }

  // Why: checked above
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const packageName = electronPackageNames.find(
    (pkg) => packageJSON.devDependencies![pkg],
  );
  if (packageName === undefined) {
    throw new Error('Could not find any Electron packages in devDependencies');
  }

  return packageName;
}

async function getElectronPackageJSONPath(
  dir: string,
  packageName: string,
): Promise<string | undefined> {
  const nodeModulesPath = await determineNodeModulesPath(dir, packageName);
  if (!nodeModulesPath) {
    throw new PackageNotFoundError(packageName, dir);
  }

  const electronPackageJSONPath = path.join(nodeModulesPath, 'package.json');
  if (await fs.pathExists(electronPackageJSONPath)) {
    return electronPackageJSONPath;
  }

  return undefined;
}

export async function getElectronModulePath(
  dir: string,
  packageJSON: PackageJSONWithDeps,
): Promise<string | undefined> {
  const moduleName = getElectronModuleName(packageJSON);
  const packageJSONPath = await getElectronPackageJSONPath(dir, moduleName);
  if (packageJSONPath) {
    return path.dirname(packageJSONPath);
  }

  return undefined;
}

export async function getElectronVersion(
  dir: string,
  packageJSON: PackageJSONWithDeps,
): Promise<string> {
  const packageName = getElectronModuleName(packageJSON);

  // Why: checked in getElectronModuleName
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  let version = packageJSON.devDependencies![packageName];
  if (!semver.valid(version)) {
    // It's not an exact version, find it in the actual module
    const electronPackageJSONPath = await getElectronPackageJSONPath(
      dir,
      packageName,
    );
    if (electronPackageJSONPath) {
      const electronPackageJSON = await fs.readJson(electronPackageJSONPath);
      version = electronPackageJSON.version;
    } else {
      throw new PackageNotFoundError(packageName, dir);
    }
  }

  return version;
}

export function updateElectronDependency(
  packageJSON: PackageJSONWithDeps,
  dev: string[],
  exact: string[],
): [string[], string[]] {
  const alteredDev = ([] as string[]).concat(dev);
  let alteredExact = ([] as string[]).concat(exact);
  // Why: checked in getElectronModuleName
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  if (Object.keys(packageJSON.devDependencies!).find(findElectronDep)) {
    alteredExact = alteredExact.filter((dep) => dep !== 'electron');
  } else if (packageJSON.dependencies) {
    const electronKey = Object.keys(packageJSON.dependencies).find(
      findElectronDep,
    );
    if (electronKey) {
      alteredExact = alteredExact.filter((dep) => dep !== 'electron');
      d(`Moving ${electronKey} from dependencies to devDependencies`);
      alteredDev.push(
        `${electronKey}@${packageJSON.dependencies[electronKey]}`,
      );
      delete packageJSON.dependencies[electronKey];
    }
  }

  return [alteredDev, alteredExact];
}
