'use strict';

const fs = require('../fs');
const path = require('path');
const util = require('util');
const atLeastNode = require('at-least-node');

const nodeSupportsBigInt = atLeastNode('10.5.0');
const stat = (file) =>
  nodeSupportsBigInt ? fs.stat(file, { bigint: true }) : fs.stat(file);
const statSync = (file) =>
  nodeSupportsBigInt ? fs.statSync(file, { bigint: true }) : fs.statSync(file);

function getStats(src, dest) {
  return Promise.all([
    stat(src),
    stat(dest).catch((err) => {
      if (err.code === 'ENOENT') return null;
      throw err;
    }),
  ]).then(([srcStat, destStat]) => ({ srcStat, destStat }));
}

function getStatsSync(src, dest) {
  let destStat;
  const srcStat = statSync(src);
  try {
    destStat = statSync(dest);
  } catch (err) {
    if (err.code === 'ENOENT') return { srcStat, destStat: null };
    throw err;
  }
  return { srcStat, destStat };
}

function checkPaths(src, dest, funcName, cb) {
  util.callbackify(getStats)(src, dest, (err, stats) => {
    if (err) return cb(err);
    const { srcStat, destStat } = stats;
    if (destStat && areIdentical(srcStat, destStat)) {
      return cb(new Error('Source and destination must not be the same.'));
    }
    if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
      return cb(new Error(errMsg(src, dest, funcName)));
    }
    return cb(null, { srcStat, destStat });
  });
}

function checkPathsSync(src, dest, funcName) {
  const { srcStat, destStat } = getStatsSync(src, dest);
  if (destStat && areIdentical(srcStat, destStat)) {
    throw new Error('Source and destination must not be the same.');
  }
  if (srcStat.isDirectory() && isSrcSubdir(src, dest)) {
    throw new Error(errMsg(src, dest, funcName));
  }
  return { srcStat, destStat };
}

// recursively check if dest parent is a subdirectory of src.
// It works for all file types including symlinks since it
// checks the src and dest inodes. It starts from the deepest
// parent and stops once it reaches the src parent or the root path.
function checkParentPaths(src, srcStat, dest, funcName, cb) {
  const srcParent = path.resolve(path.dirname(src));
  const destParent = path.resolve(path.dirname(dest));
  if (destParent === srcParent || destParent === path.parse(destParent).root)
    return cb();
  const callback = (err, destStat) => {
    if (err) {
      if (err.code === 'ENOENT') return cb();
      return cb(err);
    }
    if (areIdentical(srcStat, destStat)) {
      return cb(new Error(errMsg(src, dest, funcName)));
    }
    return checkParentPaths(src, srcStat, destParent, funcName, cb);
  };
  if (nodeSupportsBigInt) fs.stat(destParent, { bigint: true }, callback);
  else fs.stat(destParent, callback);
}

function checkParentPathsSync(src, srcStat, dest, funcName) {
  const srcParent = path.resolve(path.dirname(src));
  const destParent = path.resolve(path.dirname(dest));
  if (destParent === srcParent || destParent === path.parse(destParent).root)
    return;
  let destStat;
  try {
    destStat = statSync(destParent);
  } catch (err) {
    if (err.code === 'ENOENT') return;
    throw err;
  }
  if (areIdentical(srcStat, destStat)) {
    throw new Error(errMsg(src, dest, funcName));
  }
  return checkParentPathsSync(src, srcStat, destParent, funcName);
}

function areIdentical(srcStat, destStat) {
  if (
    destStat.ino &&
    destStat.dev &&
    destStat.ino === srcStat.ino &&
    destStat.dev === srcStat.dev
  ) {
    if (nodeSupportsBigInt || destStat.ino < Number.MAX_SAFE_INTEGER) {
      // definitive answer
      return true;
    }
    // Use additional heuristics if we can't use 'bigint'.
    // Different 'ino' could be represented the same if they are >= Number.MAX_SAFE_INTEGER
    // See issue 657
    if (
      destStat.size === srcStat.size &&
      destStat.mode === srcStat.mode &&
      destStat.nlink === srcStat.nlink &&
      destStat.atimeMs === srcStat.atimeMs &&
      destStat.mtimeMs === srcStat.mtimeMs &&
      destStat.ctimeMs === srcStat.ctimeMs &&
      destStat.birthtimeMs === srcStat.birthtimeMs
    ) {
      // heuristic answer
      return true;
    }
  }
  return false;
}

// return true if dest is a subdir of src, otherwise false.
// It only checks the path strings.
function isSrcSubdir(src, dest) {
  const srcArr = path
    .resolve(src)
    .split(path.sep)
    .filter((i) => i);
  const destArr = path
    .resolve(dest)
    .split(path.sep)
    .filter((i) => i);
  return srcArr.reduce((acc, cur, i) => acc && destArr[i] === cur, true);
}

function errMsg(src, dest, funcName) {
  return `Cannot ${funcName} '${src}' to a subdirectory of itself, '${dest}'.`;
}

module.exports = {
  checkPaths,
  checkPathsSync,
  checkParentPaths,
  checkParentPathsSync,
  isSrcSubdir,
};