// compares the inventory of package items in the tree
// that is about to be installed (idealTree) with the inventory
// of items stored in the package-lock file (virtualTree)
//
// Returns empty array if no errors found or an array populated
// with an entry for each validation error found.
function validateLockfile (virtualTree, idealTree) {
  const errors = []

  // loops through the inventory of packages resulted by ideal tree,
  // for each package compares the versions with the version stored in the
  // package-lock and adds an error to the list in case of mismatches
  for (const [key, entry] of idealTree.entries()) {
    const lock = virtualTree.get(key)

    if (!lock) {
      errors.push(`Missing: ${entry.name}@${entry.version} from lock file`)
      continue
    }

    if (entry.version !== lock.version) {
      errors.push(`Invalid: lock file's ${lock.name}@${lock.version} does ` +
      `not satisfy ${entry.name}@${entry.version}`)
    }

    // a patch whose on-disk hash or path diverges from the lockfile is out of sync
    if ((lock.patched?.integrity || null) !== (entry.patched?.integrity || null) ||
      (lock.patched?.path || null) !== (entry.patched?.path || null)) {
      if (entry.patched && !lock.patched) {
        // package.json declares a patch the lockfile lacks: newly added, or skipped via --ignore-patch-failures
        errors.push(`Invalid: package.json declares a patch for ${entry.name}@${entry.version} ` +
        `that the lock file does not record (it may have been skipped with --ignore-patch-failures). ` +
        `Fix the patch and reinstall, or remove its patchedDependencies entry`)
      } else if (lock.patched && !entry.patched) {
        // describe the lock file's own version, which can differ from the ideal tree's when the version also drifted
        errors.push(`Invalid: lock file records a patch for ${lock.name}@${lock.version} ` +
        `that package.json no longer declares`)
      } else {
        errors.push(`Invalid: patch for ${entry.name}@${entry.version} does not ` +
        `match the patch recorded in the lock file`)
      }
    }
  }
  return errors
}

// validates that the root packageExtensions state matches what the lockfile recorded, and that the locked tree is still consistent with the rule set.
// Returns an array of human-readable error strings, empty when valid.
function validatePackageExtensions (virtualTree, idealTree) {
  const errors = []
  const lockHash = virtualTree.meta?.packageExtensionsHash || null
  const idealHash = idealTree.meta?.packageExtensionsHash || null

  if (idealHash !== lockHash) {
    if (idealHash && !lockHash) {
      errors.push('Missing: packageExtensions state from lock file')
    } else if (!idealHash && lockHash) {
      errors.push('Invalid: lock file records packageExtensions state but package.json has none')
    } else {
      errors.push('Invalid: packageExtensions in package.json do not match the lock file')
    }
    // once the canonical hashes diverge, the deeper per-node checks are moot
    return errors
  }

  // the hashes match, so validate the locked tree's own consistency against the rules
  const { PackageExtensions } = require('@npmcli/arborist')
  const root = idealTree.target || idealTree
  let pe
  try {
    pe = new PackageExtensions(root.package?.packageExtensions)
  } catch (err) {
    return [`Invalid: ${err.message}`]
  }

  for (const node of virtualTree.inventory.values()) {
    if (node.isProjectRoot || node.isWorkspace) {
      continue
    }
    // selectors match the underlying package identity, which is the alias target for aliased installs
    const name = node.packageName || node.name
    // a locked package identity must not match more than one selector
    try {
      pe.match(name, node.version)
    } catch (err) {
      errors.push(`Invalid: ${err.message}`)
    }
    // recorded provenance must still correspond to a selector that matches the node
    const applied = node.packageExtensionsApplied
    if (applied) {
      const sel = pe.selectors.find(s => s.key === applied.selector)
      if (!sel || !pe.wouldMatch(name, node.version)) {
        errors.push(
          `Invalid: stale packageExtensions provenance for ${node.name}@${node.version} (selector "${applied.selector}")`)
      }
    }
  }
  return errors
}

// validates that the .npm-extension state recorded in the lockfile still matches the selected extension file.
// Validation is hash-based: arbitrary code has no selector to re-check, so a matching hash is trusted and a mismatch fails.
// fileHash is computed from the on-disk file (discovery only, no execution), so this holds even under ignore-extension/ignore-scripts.
// The lockfile carries extension state if it records a root hash or any per-package npmExtensionApplied provenance.
// Returns an array of human-readable error strings, empty when valid.
function validateNpmExtension (virtualTree, fileHash) {
  const lockHash = virtualTree?.meta?.npmExtensionHash || null
  const hasProvenance = !!virtualTree &&
    [...virtualTree.inventory.values()].some(node => node.npmExtensionApplied)
  fileHash = fileHash || null

  if (fileHash) {
    if (!lockHash) {
      return ['Missing: .npm-extension state from lock file']
    }
    if (lockHash !== fileHash) {
      return ['Invalid: .npm-extension file does not match the lock file']
    }
    return []
  }
  // no extension file present
  if (lockHash || hasProvenance) {
    return ['Invalid: lock file records .npm-extension state but no .npm-extension file is present']
  }
  return []
}

module.exports = validateLockfile
module.exports.validatePackageExtensions = validatePackageExtensions
module.exports.validateNpmExtension = validateNpmExtension
