#!/usr/bin/env node

/**
 * Pre-installation script that checks for and uninstalls previous versions of the Superblocks CLI
 * to prevent conflicts between different versions.
 */

import * as child_process from "node:child_process";
import { constants } from "node:fs";
import { access, readdir } from "node:fs/promises";
import { createInterface } from "node:readline";
import { promisify } from "node:util";

const exec = promisify(child_process.exec);

// get all superblocks binary paths from PATH and all Node environments
async function getAllSuperblocksPaths() {
  const allPaths = new Set();

  try {
    const command = process.platform === "win32" ? "where" : "which -a";
    const { stdout } = await exec(`${command} superblocks`);
    const pathResults = stdout
      .trim()
      .split("\n")
      .filter((path) => path && path.trim());
    pathResults.forEach((path) => allPaths.add(path));
  } catch {
    // No superblocks in PATH - ignore
  }

  await scanNvmVersions(allPaths);

  return Array.from(allPaths);
}

// scan nvm Node versions for superblocks installations
async function scanNvmVersions(allPaths) {
  try {
    const homeDir = process.env.HOME || process.env.USERPROFILE;
    const nvmDir = process.env.NVM_DIR || `${homeDir}/.nvm`;
    const versionsDir = `${nvmDir}/versions/node`;

    // If it doesn't exist user might not have nvm
    if (!(await fileExists(versionsDir))) return;

    // List all Node versions
    const versions = await readdir(versionsDir).catch(() => []);

    // Check each Node version for a superblocks installation
    for (const version of versions) {
      const binaryPath =
        process.platform === "win32"
          ? `${versionsDir}/${version}/superblocks.cmd`
          : `${versionsDir}/${version}/bin/superblocks`;

      if (await fileExists(binaryPath)) {
        allPaths.add(binaryPath);
      }
    }
  } catch {
    // nvm not installed or we can't read versions dir - ignore
  }
}

// check if a file or directory exists
async function fileExists(path) {
  try {
    await access(path, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

function askConfirmation(question) {
  const rl = createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  return new Promise((resolve) => {
    rl.question(question, (answer) => {
      rl.close();
      // treats hitting enter as yes
      resolve(answer.toLowerCase().startsWith("y") || answer.trim() === "");
    });
  });
}

/**
 * Get the package directory path from a binary path
 * For nvm/npm installations, this returns the node_modules/@superblocksteam/cli directory
 */
function getPackageDirectoryFromBinary(binaryPath) {
  try {
    // For nvm installations: ~/.nvm/versions/node/v20/bin/superblocks -> ~/.nvm/versions/node/v20/lib/node_modules/@superblocksteam/cli
    if (
      binaryPath.includes("/.nvm/versions/node/") ||
      binaryPath.includes("\\.nvm\\versions\\node\\")
    ) {
      const versionPath = binaryPath
        .replace(/\/bin\/superblocks.*$/, "")
        .replace(/\\bin\\superblocks.*$/, "");
      return process.platform === "win32"
        ? `${versionPath}\\lib\\node_modules\\@superblocksteam\\cli`
        : `${versionPath}/lib/node_modules/@superblocksteam/cli`;
    }

    // For global npm installations: look for node_modules directory structure
    if (
      binaryPath.includes("/bin/superblocks") ||
      binaryPath.includes("\\bin\\superblocks")
    ) {
      const prefixPath = binaryPath
        .replace(/\/bin\/superblocks.*$/, "")
        .replace(/\\bin\\superblocks.*$/, "");
      return process.platform === "win32"
        ? `${prefixPath}\\lib\\node_modules\\@superblocksteam\\cli`
        : `${prefixPath}/lib/node_modules/@superblocksteam/cli`;
    }

    return null;
  } catch {
    return null;
  }
}

// get the command(s) that would be used to remove a CLI installation (can be more than one for oclif installations -- one for the symlink and one for the library)
async function getRemovalCommands(binaryPath) {
  const commands = [];

  try {
    // Check if it's a symlink to an OCLIF installation (Unix only)
    let targetPath = binaryPath;
    let isOclifInstall = false;

    if (process.platform !== "win32") {
      try {
        // readlink shows where a symlink points to
        const { stdout } = await exec(`readlink "${binaryPath}"`);
        if (stdout.trim()) {
          targetPath = stdout.trim();
          if (targetPath.includes("/usr/local/lib/superblocks")) {
            isOclifInstall = true;
          }
        }
      } catch {
        // Not a symlink or readlink failed
      }
    }

    if (isOclifInstall && process.platform !== "win32") {
      // OCLIF removal commands (unix)
      commands.push(`sudo rm -f "${binaryPath}"`);
      commands.push(`sudo rm -rf "/usr/local/lib/superblocks"`);
      return commands;
    }
  } catch {
    // fall through on error
  }

  // For npm/nvm installations, remove both the package directory and binary for complete cleanup
  const packageDir = getPackageDirectoryFromBinary(binaryPath);
  if (packageDir) {
    // Remove the entire package directory
    if (process.platform === "win32") {
      commands.push(`rmdir /s /q "${packageDir}"`);
      commands.push(`del /f "${binaryPath}"`);
    } else {
      commands.push(`rm -rf "${packageDir}"`);
      commands.push(`rm -f "${binaryPath}"`);
    }
  } else {
    // Fallback to binary removal if we can't determine package directory
    if (process.platform === "win32") {
      commands.push(`del /f "${binaryPath}"`);
    } else {
      commands.push(`rm -f "${binaryPath}"`);
    }
  }

  return commands;
}

async function attemptRemoval(binaryPath) {
  try {
    // get removal commands
    const commands = await getRemovalCommands(binaryPath);

    for (const command of commands) {
      await exec(command);
    }

    return true;
  } catch {
    return false;
  }
}

// get default nvm Node environment path
async function getDefaultNodePath() {
  // Skip nvm default detection on Windows (nvm-windows works differently)
  if (process.platform === "win32") {
    return null;
  }

  try {
    // Try to use nvm to resolve the default version properly
    const { stdout } = await exec(
      'bash -c "source ~/.nvm/nvm.sh && nvm version default 2>/dev/null"',
    );
    const defaultVersion = stdout.trim();

    if (
      defaultVersion &&
      defaultVersion !== "N/A" &&
      defaultVersion.startsWith("v")
    ) {
      const homeDir = process.env.HOME || process.env.USERPROFILE;
      const nvmDir = process.env.NVM_DIR || `${homeDir}/.nvm`;
      return `${nvmDir}/versions/node/${defaultVersion}`;
    }
  } catch {
    // Fallback: try reading alias file directly
    try {
      const homeDir = process.env.HOME || process.env.USERPROFILE;
      const nvmDir = process.env.NVM_DIR || `${homeDir}/.nvm`;

      const { readFile } = await import("fs/promises");
      const defaultAliasFile = `${nvmDir}/alias/default`;

      const defaultVersion = await readFile(defaultAliasFile, "utf8");
      const version = defaultVersion.trim();

      if (version && version !== "N/A") {
        // For partial versions like "20", we need to find the actual directory
        const versionPrefix = version.startsWith("v") ? version : `v${version}`;
        return `${nvmDir}/versions/node/${versionPrefix}`;
      }
    } catch {
      // nvm not available, no default set, or alias file doesn't exist
    }
  }
  return null;
}

async function isCurrentNodeDefault() {
  try {
    const currentNodePath = await getCurrentNodeInstallPath();
    const defaultNodePath = await getDefaultNodePath();

    if (!currentNodePath || !defaultNodePath) {
      return true; // we don't want to block installation if we can't determine the default
    }

    return currentNodePath.includes(defaultNodePath);
  } catch {
    return true;
  }
}

async function getCurrentNodeInstallPath() {
  try {
    const { stdout } = await exec("npm config get prefix");
    const npmPrefix = stdout.trim();
    return process.platform === "win32"
      ? `${npmPrefix}\\superblocks.cmd`
      : `${npmPrefix}/bin/superblocks`;
  } catch {
    return null;
  }
}

async function main() {
  try {
    // skip conflict detection during development/local installs
    // unless running in cleanup mode
    if (
      process.env.npm_config_global !== "true" &&
      !process.env.SUPERBLOCKS_CLEANUP_MODE
    ) {
      return;
    }

    // Check if user is in default Node environment, prompt if not (skip in cleanup mode)
    if (!process.env.SUPERBLOCKS_CLEANUP_MODE) {
      const isDefault = await isCurrentNodeDefault();
      if (!isDefault) {
        console.log("⚠️  You are not in your default Node environment.");
        console.log(
          "   Installing here means the CLI may not be available in new terminals.",
        );
        console.log();

        const proceed = await askConfirmation(
          "Would you like to proceed with installation in the current environment? [Y/n] ",
        );
        if (!proceed) {
          console.log(
            "Installation cancelled. Consider switching to your default Node version:",
          );
          console.log("   nvm use default");
          return;
        }
        console.log();
      }
    }

    const allInstallations = await getAllSuperblocksPaths();

    const currentNodePath = await getCurrentNodeInstallPath();
    const defaultNodePath = await getDefaultNodePath();

    const conflictingInstallations = [];
    const defaultInstallations = [];

    allInstallations.forEach((path) => {
      // For npm install mode: ignore current Node environment since npm will overwrite it
      // For cleanup mode: include current environment but protect it from automatic removal
      if (
        currentNodePath &&
        path === currentNodePath &&
        !process.env.SUPERBLOCKS_CLEANUP_MODE
      ) {
        return;
      }

      // track default installation separately since we don't want to remove it
      if (defaultNodePath && path.includes(defaultNodePath)) {
        defaultInstallations.push(path);
      } else {
        // In cleanup mode, don't auto-remove current installation even if it's not default
        if (
          process.env.SUPERBLOCKS_CLEANUP_MODE &&
          currentNodePath &&
          path === currentNodePath
        ) {
          // Treat current installation as "protected" like defaults
          defaultInstallations.push(path);
        } else {
          conflictingInstallations.push(path);
        }
      }
    });

    if (
      conflictingInstallations.length === 0 &&
      defaultInstallations.length === 0
    ) {
      // no conflicts or defaults to show
      if (process.env.SUPERBLOCKS_CLEANUP_MODE) {
        console.log(
          "✅ No Superblocks CLI installations found that need cleanup.",
        );
      }
      return;
    }

    // show how many installations were found
    const totalInstallations =
      conflictingInstallations.length + defaultInstallations.length;
    const instanceText = totalInstallations === 1 ? "instance" : "instances";
    const verbText = totalInstallations === 1 ? "was" : "were";
    console.log(
      `⚠️  ${totalInstallations > 1 ? "Multiple" : "An"} ${instanceText} of the Superblocks CLI ${verbText} found:`,
    );

    // show conflicting installations (will be removed)
    conflictingInstallations.forEach((path) => {
      console.log(`    - ${path}`);
    });

    // show default installations (protected)
    defaultInstallations.forEach((path) => {
      let label = "";
      if (defaultNodePath && path.includes(defaultNodePath)) {
        label = "(default - will not be removed)";
      } else if (currentNodePath && path === currentNodePath) {
        label = "(current - will not be removed)";
      }
      console.log(`    - ${path} ${label}`);
    });

    console.log();
    // If there are no conflicting installations (only defaults), proceed without asking
    if (conflictingInstallations.length === 0) {
      if (process.env.SUPERBLOCKS_CLEANUP_MODE) {
        console.log("✅ No conflicting installations to remove.");

        // Show manual removal commands for default installations in cleanup mode
        if (defaultInstallations.length > 0) {
          console.log();
          const defaultText =
            defaultInstallations.length === 1
              ? "installation"
              : "installations";
          console.log(`To remove the default ${defaultText} manually, run:`);
          console.log();

          for (const installation of defaultInstallations) {
            const commands = await getRemovalCommands(installation);
            commands.forEach((command) => {
              console.log(`    ${command}`);
            });
          }
        }
      } else {
        console.log(
          "No conflicting installations to remove. Proceeding with installation.",
        );
      }
      console.log();
      return;
    }

    console.log("Installing a new instance may cause PATH conflicts.");
    console.log();

    console.log("Commands that will be executed:");
    const allCommands = [];
    for (const installation of conflictingInstallations) {
      const commands = await getRemovalCommands(installation);
      allCommands.push(...commands);
    }

    allCommands.forEach((command) => {
      console.log(`    ${command}`);
    });
    console.log();

    const conflictInstanceText =
      conflictingInstallations.length === 1 ? "instance" : "instances";
    const shouldRemove = await askConfirmation(
      `Would you like us to attempt to remove the conflicting ${conflictInstanceText}? (Recommended) [Y/n] `,
    );

    if (shouldRemove) {
      console.log();
      console.log("Attempting to remove:");
      conflictingInstallations.forEach((path) => {
        console.log(` - ${path}`);
      });
      console.log();

      const successfulRemovals = [];
      const failedRemovals = [];

      for (const installation of conflictingInstallations) {
        // attemptRemoval returns true if successful, false if any command failed
        const success = await attemptRemoval(installation);
        if (success) {
          successfulRemovals.push(installation);
        } else {
          failedRemovals.push(installation);
        }
      }

      if (successfulRemovals.length > 0) {
        const successText =
          successfulRemovals.length === 1 ? "instance" : "instances";
        console.log(
          `✅ Successfully removed ${successfulRemovals.length} ${successText}:`,
        );
        successfulRemovals.forEach((path) => {
          console.log(` - ${path}`);
        });
        console.log();
      }

      // if some removals failed, provide manual instructions
      if (failedRemovals.length > 0) {
        const failText = failedRemovals.length === 1 ? "instance" : "instances";
        console.log(
          `❌ The following ${failText} could not be removed automatically:`,
        );
        failedRemovals.forEach((path) => {
          console.log(` - ${path}`);
        });
        console.log();
        console.log("Please remove them manually:");

        // print instructions for manual removal
        for (const path of failedRemovals) {
          const commands = await getRemovalCommands(path);
          commands.forEach((command) => {
            console.log(`  ${command}`);
          });
        }
        console.log();
      }

      // proceed with installation even if some removals failed
      if (!process.env.SUPERBLOCKS_CLEANUP_MODE) {
        console.log(`Proceeding with NPM installation of Superblocks CLI...`);
      } else {
        console.log("✅ Cleanup completed!");
      }
    } else {
      // user selects no to removal -- print instructions for manual removal, and proceed with installation
      console.log();
      console.log(
        `You chose not to remove conflicting ${instanceText} of the Superblocks CLI.`,
      );
      console.log();

      // tell user which version will be active by default -- first match in PATH
      const pathResults = await getAllSuperblocksPaths();
      const activePath = pathResults.length > 0 ? pathResults[0] : "unknown";
      console.log("The version that will be active by default is:");
      console.log(`    ${activePath}`);
      console.log();

      // provide manual removal commands
      if (conflictingInstallations.length > 0) {
        const versionText =
          conflictingInstallations.length === 1 ? "version" : "versions";
        console.log(`To remove conflicting ${versionText} manually, run:`);

        for (const path of conflictingInstallations) {
          const commands = await getRemovalCommands(path);
          commands.forEach((command) => {
            console.log(`    ${command}`);
          });
        }
      }
    }
  } catch (error) {
    // if anything goes wrong during detection or removal, don't fail the installation
    console.warn(
      `⚠️  Error: ${error.message}. Still proceeding with CLI installation.`,
    );
  }
}

main().catch((error) => {
  // if main() throws an unhandled error, log it but don't fail the installation
  console.error(`❌ Preinstall script failed: ${error.message}`);
});
