// @ts-check
// Copyright 2018-2026 the Deno authors. MIT license.
"use strict";

const fs = require("fs");
const os = require("os");
const path = require("path");
/** @type {string | undefined} */
let cachedIsMusl = undefined;

module.exports = {
  replaceBinEntry,
  runInstall() {
    const denoFileName = os.platform() === "win32" ? "deno.exe" : "deno";
    const targetExecutablePath = path.join(
      __dirname,
      denoFileName,
    );

    if (fs.existsSync(targetExecutablePath)) {
      return targetExecutablePath;
    }

    const target = getTarget();
    const sourcePackagePath = path.dirname(
      require.resolve("@deno/" + target + "/package.json"),
    );
    const sourceExecutablePath = path.join(sourcePackagePath, denoFileName);

    if (!fs.existsSync(sourceExecutablePath)) {
      throw new Error(
        "Could not find executable for @deno/" + target + " at " +
          sourceExecutablePath,
      );
    }

    try {
      if (process.env.DENO_SIMULATED_READONLY_FILE_SYSTEM === "1") {
        console.warn("Simulating readonly file system for testing.");
        throw new Error("Throwing for testing purposes.");
      }

      // in order to make things faster the next time we run and to allow the
      // deno vscode extension to easily pick this up, copy the executable
      // into the deno package folder
      hardLinkOrCopy(sourceExecutablePath, targetExecutablePath);
      if (os.platform() !== "win32") {
        // chmod +x
        chmodX(targetExecutablePath);
      }
      return targetExecutablePath;
    } catch (err) {
      // this may fail on readonly file systems... in this case, fall
      // back to using the resolved package path
      if (process.env.DENO_DEBUG === "1") {
        console.warn(
          "Failed to copy executable from " +
            sourceExecutablePath + " to " + targetExecutablePath +
            ". Using resolved package path instead.",
          err,
        );
      }
      return sourceExecutablePath;
    }
  },
};

/** @filePath {string} */
function chmodX(filePath) {
  const fd = fs.openSync(filePath, "r");
  try {
    const perms = fs.fstatSync(fd).mode;
    fs.fchmodSync(fd, perms | 0o111);
  } finally {
    fs.closeSync(fd);
  }
}

function getTarget() {
  const platform = os.platform();
  if (platform === "linux") {
    return platform + "-" + getArch() + "-" + getLinuxFamily();
  } else {
    return platform + "-" + getArch();
  }
}

function getArch() {
  const arch = os.arch();
  if (arch !== "arm64" && arch !== "x64") {
    throw new Error(
      "Unsupported architecture " + os.arch() +
        ". Only x64 and aarch64 binaries are available.",
    );
  }
  return arch;
}

function getLinuxFamily() {
  if (getIsMusl()) {
    throw new Error(
      "Musl is not supported. It's one of our priorities. Please upvote this issue: https://github.com/denoland/deno/issues/3711",
    );
    // return "musl";
  }
  return "glibc";

  function getIsMusl() {
    // code adapted from https://github.com/lovell/detect-libc
    // Copyright Apache 2.0 license, the detect-libc maintainers
    if (cachedIsMusl == null) {
      cachedIsMusl = innerGet();
    }
    return cachedIsMusl;

    function innerGet() {
      try {
        if (os.platform() !== "linux") {
          return false;
        }
        return isProcessReportMusl() || isConfMusl();
      } catch (err) {
        // just in case
        console.warn("Error checking if musl.", err);
        return false;
      }
    }

    function isProcessReportMusl() {
      if (!process.report) {
        return false;
      }
      const rawReport = process.report.getReport();
      const report = typeof rawReport === "string"
        ? JSON.parse(rawReport)
        : rawReport;
      if (!report || !(report.sharedObjects instanceof Array)) {
        return false;
      }
      return report.sharedObjects.some((o) =>
        o.includes("libc.musl-") || o.includes("ld-musl-")
      );
    }

    function isConfMusl() {
      const output = getCommandOutput();
      const [_, ldd1] = output.split(/[\r\n]+/);
      return ldd1 && ldd1.includes("musl");
    }

    function getCommandOutput() {
      try {
        const command =
          "getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true";
        return require("child_process").execSync(command, { encoding: "utf8" });
      } catch (_err) {
        return "";
      }
    }
  }
}

/**
 * Replaces the bin entry in node_modules/.bin to point directly at the
 * native binary, avoiding Node.js startup overhead on each invocation.
 * @param exePath {string}
 */
function replaceBinEntry(exePath) {
  const binDir = findBinDir();
  if (binDir === undefined) return;

  const relative = path.relative(binDir, exePath);
  if (os.platform() === "win32") {
    // Rewrite .cmd and .ps1 wrappers to invoke the native binary directly
    fs.writeFileSync(
      path.join(binDir, "deno.cmd"),
      '@"%~dp0' + relative + '" %*\r\n',
    );
    fs.writeFileSync(
      path.join(binDir, "deno.ps1"),
      '& "$PSScriptRoot/' + relative.replace(/\\/g, "/") +
        '" $args\r\nexit $LASTEXITCODE\r\n',
    );
  } else {
    // Replace symlink to point directly at the native binary
    const binDeno = path.join(binDir, "deno");
    fs.unlinkSync(binDeno);
    fs.symlinkSync(relative, binDeno);
  }
}

function findBinDir() {
  // For global installs, npm sets npm_config_global=true and npm_config_prefix
  // to the install prefix. The bin dir is {prefix}/bin on Linux/Mac or
  // {prefix} on Windows (e.g. %APPDATA%\npm).
  if (process.env.npm_config_global === "true") {
    const prefix = process.env.npm_config_prefix;
    if (prefix) {
      const binDir = os.platform() === "win32"
        ? prefix
        : path.join(prefix, "bin");
      if (isBinDirForThisPackage(binDir)) {
        return binDir;
      }
    }
  }

  // For local installs, walk up looking for node_modules/.bin
  let dir = __dirname;
  for (let i = 0; i < 64; i++) {
    const parent = path.dirname(dir);
    if (parent === dir) {
      break;
    }
    if (path.basename(parent) === "node_modules") {
      const binDir = path.join(parent, ".bin");
      if (isBinDirForThisPackage(binDir)) {
        return binDir;
      }
    }
    dir = parent;
  }
  return undefined;
}

function isBinDirForThisPackage(binDir) {
  try {
    if (os.platform() === "win32") {
      // Verify the .cmd wrapper references our bin.cjs
      const content = fs.readFileSync(
        path.join(binDir, "deno.cmd"),
        "utf8",
      );
      return content.includes("bin.cjs");
    } else {
      // Verify the symlink points into our package directory
      const linkTarget = fs.readlinkSync(path.join(binDir, "deno"));
      const resolved = path.resolve(binDir, linkTarget);
      return resolved.endsWith("bin.cjs");
    }
  } catch (_err) {
    return false;
  }
}

/**
 * @param sourcePath {string}
 * @param destinationPath {string}
 */
function hardLinkOrCopy(sourcePath, destinationPath) {
  try {
    fs.linkSync(sourcePath, destinationPath);
  } catch {
    atomicCopyFile(sourcePath, destinationPath);
  }
}

/**
 * @param sourcePath {string}
 * @param destinationPath {string}
 */
function atomicCopyFile(sourcePath, destinationPath) {
  const crypto = require("crypto");
  const rand = crypto.randomBytes(4).toString("hex");
  const tempFilePath = destinationPath + "." + rand;
  fs.copyFileSync(sourcePath, tempFilePath);
  try {
    fs.renameSync(tempFilePath, destinationPath);
  } catch (err) {
    // will maybe throw when another process had already done this
    // so just ignore and delete the created temporary file
    try {
      fs.unlinkSync(tempFilePath);
    } catch (_err2) {
      // ignore
    }
    throw err;
  }
}
