#!/usr/bin/env node
"use strict";

const fs = require("fs");
const path = require("path");
const childProcess = require("child_process");

const inventory = require("../package_inventory.json");

const TOOL_NAME = "apidev";
const ROOT_PACKAGE_NAME = "@ai-devtools/apidev";
const UPDATE_CHECK_TIMEOUT_MS = 1000;
const UPDATE_CHECK_COMMAND = "update-check";

function runtimeTargets() {
  return inventory.targets.filter((target) => target.runtime);
}

function supportedTargetList() {
  return runtimeTargets()
    .map((target) => `${target.nodePlatform}/${target.nodeArch} (${target.packageName})`)
    .join(", ");
}

function resolveTarget(platform = process.platform, arch = process.arch) {
  const target = runtimeTargets().find(
    (candidate) => candidate.nodePlatform === platform && candidate.nodeArch === arch,
  );
  if (!target) {
    throw new Error(
      `Unsupported platform or architecture: ${platform}/${arch}. ` +
        `Supported targets: ${supportedTargetList()}`,
    );
  }
  return target;
}

function resolvePackageName(platform = process.platform, arch = process.arch) {
  return resolveTarget(platform, arch).packageName;
}

function resolveBinaryPath(platform = process.platform, arch = process.arch) {
  const target = resolveTarget(platform, arch);
  const manifestPath = require.resolve(`${target.packageName}/package.json`);
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
  const binaryPath = manifest.apidev && manifest.apidev.binary;
  if (!binaryPath) {
    throw new Error(`Platform package ${target.packageName} does not declare apidev.binary`);
  }
  return path.join(path.dirname(manifestPath), binaryPath);
}

function rootPackageJsonPath() {
  return path.join(__dirname, "..", "package.json");
}

function rootPackageVersion() {
  const packageJsonPath = rootPackageJsonPath();
  if (!fs.existsSync(packageJsonPath)) {
    return null;
  }
  const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
  return typeof manifest.version === "string" ? manifest.version : null;
}

function parseVersion(version) {
  const match = String(version || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
  if (!match) {
    return null;
  }
  return match.slice(1, 4).map((part) => Number.parseInt(part, 10));
}

function compareVersions(left, right) {
  const leftParts = parseVersion(left);
  const rightParts = parseVersion(right);
  if (!leftParts || !rightParts) {
    return 0;
  }
  for (let index = 0; index < leftParts.length; index += 1) {
    if (leftParts[index] !== rightParts[index]) {
      return leftParts[index] - rightParts[index];
    }
  }
  return 0;
}

function latestPackageVersion(packageName, spawnSync = childProcess.spawnSync) {
  const result = spawnSync("npm", ["view", packageName, "version", "--silent"], {
    encoding: "utf8",
    timeout: UPDATE_CHECK_TIMEOUT_MS,
    stdio: ["ignore", "pipe", "ignore"],
  });
  if (result.error || result.status !== 0 || typeof result.stdout !== "string") {
    return null;
  }
  const latestVersion = result.stdout.trim();
  return parseVersion(latestVersion) ? latestVersion : null;
}

function runUpdateCheck(options = {}) {
  const toolName = options.toolName || TOOL_NAME;
  const packageName = options.packageName || ROOT_PACKAGE_NAME;
  const currentVersion = options.currentVersion || rootPackageVersion();
  const writeLine = options.writeLine || console.log;
  const writeError = options.writeError || console.error;
  if (!currentVersion) {
    writeError(`Could not resolve current ${toolName} version.`);
    return 1;
  }

  const resolveLatestVersion = options.latestPackageVersion || latestPackageVersion;
  const latestVersion = resolveLatestVersion(packageName, options.spawnSync || childProcess.spawnSync);
  if (!latestVersion) {
    writeError(`Could not check latest ${toolName} version from npm.`);
    return 1;
  }

  writeLine(`Current: ${currentVersion}`);
  writeLine(`Latest:  ${latestVersion}`);
  if (compareVersions(latestVersion, currentVersion) > 0) {
    writeLine(`Update: npm install -g ${packageName}@latest`);
  } else {
    writeLine(`${toolName} is up to date.`);
  }
  return 0;
}

function run(argv = process.argv.slice(2), options = {}) {
  if (argv[0] === UPDATE_CHECK_COMMAND) {
    return runUpdateCheck(options);
  }

  const resolveBinary = options.resolveBinaryPath || resolveBinaryPath;
  const spawnSync = options.spawnSync || childProcess.spawnSync;
  const binaryPath = resolveBinary();
  const result = spawnSync(binaryPath, argv, { stdio: "inherit" });
  if (result.error) {
    throw result.error;
  }
  if (result.signal) {
    process.kill(process.pid, result.signal);
  }
  return result.status === null ? 1 : result.status;
}

if (require.main === module) {
  try {
    process.exitCode = run();
  } catch (error) {
    console.error(error.message);
    process.exitCode = 1;
  }
}

module.exports = {
  compareVersions,
  latestPackageVersion,
  resolveBinaryPath,
  resolvePackageName,
  resolveTarget,
  run,
  runUpdateCheck,
  supportedTargetList,
};
