#!/usr/bin/env node

import { execFile } from 'node:child_process';
import { constants } from 'node:fs';
import { access as fsAccess, chmod as fsChmod, writeFile as fsWriteFile } from 'node:fs/promises';
import { delimiter, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { promisify } from 'node:util';
import { ensureCliBinExecutable, ktxRootDir } from './prepare-cli-bin.mjs';

const execFileAsync = promisify(execFile);

function hasFlag(flag) {
  return process.argv.includes(flag);
}

function optionValue(flag, fallback) {
  const index = process.argv.indexOf(flag);
  if (index === -1) {
    return fallback;
  }
  const value = process.argv[index + 1];
  if (!value || value.startsWith('-')) {
    throw new Error(`${flag} requires a value`);
  }
  return value;
}

function commandEnv(extraPath) {
  if (!extraPath) {
    return process.env;
  }

  return {
    ...process.env,
    PATH: `${extraPath}${delimiter}${process.env.PATH ?? ''}`,
  };
}

async function execText(command, args, options = {}) {
  const result = await execFileAsync(command, args, {
    cwd: options.cwd,
    env: options.env,
    maxBuffer: 1024 * 1024,
  });
  return `${result.stdout}${result.stderr}`.trim();
}

async function optionalText(command, args, options = {}) {
  try {
    return await execText(command, args, options);
  } catch {
    return '';
  }
}

async function findPnpmGlobalBin() {
  const output = await optionalText('pnpm', ['bin', '--global']);
  return output.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim() ?? '';
}

function shellDoubleQuote(value) {
  return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('$', '\\$').replaceAll('`', '\\`')}"`;
}

function assertBinaryName(binaryName) {
  if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(binaryName)) {
    throw new Error(`Invalid binary name: ${binaryName}`);
  }
}

async function writePinnedPosixLauncher(globalBin, binPath, binaryName, writeFile, chmod) {
  const launcherPath = join(globalBin, binaryName);
  const script = [
    '#!/bin/sh',
    '# Generated by `pnpm run link:dev` in the KTX workspace.',
    '# Keep this launcher pinned to the Node binary that built native dependencies.',
    `exec ${shellDoubleQuote(process.execPath)} ${shellDoubleQuote(binPath)} "$@"`,
    '',
  ].join('\n');

  await writeFile(launcherPath, script, 'utf-8');
  await chmod(launcherPath, 0o755);
  return launcherPath;
}

async function writePinnedWindowsLauncher(globalBin, binPath, binaryName, writeFile) {
  const launcherPath = join(globalBin, `${binaryName}.cmd`);
  const script = [
    '@echo off',
    'REM Generated by `pnpm run link:dev` in the KTX workspace.',
    `"${process.execPath}" "${binPath}" %*`,
    '',
  ].join('\r\n');

  await writeFile(launcherPath, script, 'utf-8');
  return launcherPath;
}

async function writePinnedLauncher(globalBin, binPath, binaryName, deps) {
  if (!globalBin) {
    throw new Error('Could not find pnpm global bin directory. Run `pnpm setup`, restart your shell, then retry.');
  }

  if (process.platform === 'win32') {
    return writePinnedWindowsLauncher(globalBin, binPath, binaryName, deps.writeFile);
  }

  return writePinnedPosixLauncher(globalBin, binPath, binaryName, deps.writeFile, deps.chmod);
}

async function verifyBinaryOnPath(binaryName, globalBin, execTextFn) {
  try {
    const output = await execTextFn(binaryName, ['--version']);
    return { ok: true, output };
  } catch (error) {
    if (!globalBin) {
      return { ok: false, output: '', error };
    }

    try {
      const output = await execTextFn(binaryName, ['--version'], { env: commandEnv(globalBin) });
      return { ok: false, output, globalBin, error };
    } catch {
      return { ok: false, output: '', error };
    }
  }
}

async function assertBuiltCli(rootDir, access, binPathOverride) {
  const binPath = binPathOverride ?? (await ensureCliBinExecutable(rootDir));
  await access(binPath, constants.X_OK);
  return binPath;
}

export async function linkDevCli(options = {}) {
  const rootDir = options.rootDir ?? ktxRootDir();
  const binaryName = options.binaryName ?? 'ktx-dev';
  const access = options.access ?? fsAccess;
  const chmod = options.chmod ?? fsChmod;
  const writeFile = options.writeFile ?? fsWriteFile;
  const execTextFn = options.execText ?? execText;
  assertBinaryName(binaryName);

  const binPath = await assertBuiltCli(rootDir, access, options.binPath);
  const globalBin = options.globalBin ?? (await findPnpmGlobalBin());

  if (options.checkOnly) {
    return {
      binaryName,
      binPath,
      linked: false,
      verification: await verifyBinaryOnPath(binaryName, globalBin, execTextFn),
    };
  }

  const launcherPath = await writePinnedLauncher(globalBin, binPath, binaryName, { writeFile, chmod });
  const verification = await verifyBinaryOnPath(binaryName, globalBin, execTextFn);
  if (!verification.ok) {
    const pathHint = verification.globalBin
      ? `\nAdd pnpm's global bin directory to PATH, then retry:\n\n  export PATH="${verification.globalBin}:$PATH"\n\n`
      : '\nRun `pnpm setup`, restart your shell, then rerun `pnpm run link:dev`.\n\n';

    throw new Error(`${binaryName} was linked at ${launcherPath}, but it is not available on PATH.${pathHint}`);
  }

  return {
    binaryName,
    binPath,
    launcherPath,
    linked: true,
    verification,
  };
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  try {
    const result = await linkDevCli({
      checkOnly: hasFlag('--check-only'),
      binaryName: optionValue('--name', 'ktx-dev'),
    });
    process.stdout.write(`KTX CLI bin: ${result.binPath}\n`);
    if (result.linked) {
      process.stdout.write(`Linked binary: ${result.binaryName}\n`);
      process.stdout.write(`Verified: ${result.verification.output}\n`);
      process.stdout.write(`Pinned Node: ${process.execPath} ${process.version} ABI ${process.versions.modules}\n`);
      process.stdout.write(`You can now run \`${result.binaryName} --help\` from any directory.\n`);
    } else if (result.verification.ok) {
      process.stdout.write(`Already available: ${result.verification.output}\n`);
    } else {
      process.stdout.write(`${result.binaryName} is not linked on PATH yet.\n`);
    }
  } catch (error) {
    process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
    process.exitCode = 1;
  }
}
