/**
 * CLI launcher generation and PATH plumbing — pure helpers, no Electron imports, so
 * they unit-test under plain vitest AND load in plain node: scripts/stage.mjs imports
 * the built dist/launcher.js at stage time to write the launcher scripts into the
 * packaged app directory's bin/.
 *
 * The launchers run the bundled @prismshadow/penguin-cli on the app's own Electron
 * runtime as Node (ELECTRON_RUN_AS_NODE), so an installed desktop app provides a
 * working `penguin` command without any system Node installation. How bin/ reaches
 * PATH is per platform (see cli-install.ts and build/linux/*.tpl for the deb variant).
 */

/** CLI entry inside the staged app directory (pnpm-deploy layout, asar off). */
export const CLI_ENTRY_RELPATH = "node_modules/@prismshadow/penguin-cli/dist/index.js";

/**
 * Platform executable names, fixed by electron-builder.yml: productName PenguinHarness
 * (macOS bundle executable and Windows .exe) and linux.executableName penguin-harness.
 */
export const MAC_EXECUTABLE = "PenguinHarness";
export const WIN_EXECUTABLE = "PenguinHarness.exe";
export const LINUX_EXECUTABLE = "penguin-harness";

/**
 * POSIX launcher, staged as `<app>/bin/penguin` (chmod 755). Resolves its own real
 * location first (PATH exposure is a symlink on macOS/deb), then finds the Electron
 * executable at the platform's fixed position relative to the app directory:
 * - macOS: app dir is `<bundle>/Contents/Resources/app` → `../../MacOS/PenguinHarness`
 * - Linux: app dir is `<install>/resources/app` → `../../penguin-harness`
 */
export function posixLauncherScript(): string {
  return `#!/bin/sh
# penguin CLI launcher (generated by packages/desktop/scripts/stage.mjs).
# Runs the bundled penguin CLI on this app's Electron runtime as Node
# (ELECTRON_RUN_AS_NODE=1) - no system Node installation required.

# Resolve this script's real location; PATH exposure installs a symlink to it
# (readlink -f is not portable to macOS, hence the loop).
SOURCE=$0
while [ -h "$SOURCE" ]; do
  DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)
  SOURCE=$(readlink "$SOURCE")
  case $SOURCE in
    /*) ;;
    *) SOURCE="$DIR/$SOURCE" ;;
  esac
done
BIN_DIR=$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)
APP_DIR=$(dirname "$BIN_DIR")

CLI_ENTRY="$APP_DIR/${CLI_ENTRY_RELPATH}"
# The Electron executable sits at a fixed location relative to the app directory:
# macOS <bundle>/Contents/Resources/app -> ../../MacOS/${MAC_EXECUTABLE};
# Linux <install>/resources/app -> ../../${LINUX_EXECUTABLE}.
for CANDIDATE in "$APP_DIR/../../MacOS/${MAC_EXECUTABLE}" "$APP_DIR/../../${LINUX_EXECUTABLE}"; do
  if [ -x "$CANDIDATE" ]; then
    export ELECTRON_RUN_AS_NODE=1
    exec "$CANDIDATE" "$CLI_ENTRY" "$@"
  fi
done
echo "penguin: could not find the PenguinHarness runtime next to $APP_DIR" >&2
exit 1
`;
}

/**
 * Windows launcher, staged as `<app>\\bin\\penguin.cmd`. `%~dp0` is this script's
 * directory (with a trailing backslash): the exe sits three levels up
 * (bin -> app -> resources -> install root), the CLI entry one level up in the app dir.
 * CRLF line endings on purpose - cmd.exe is unreliable with bare LF scripts.
 */
export function windowsLauncherScript(): string {
  return [
    "@echo off",
    "rem penguin CLI launcher (generated by packages/desktop/scripts/stage.mjs).",
    "rem Runs the bundled penguin CLI on this app's Electron runtime as Node.",
    "setlocal",
    'set "ELECTRON_RUN_AS_NODE=1"',
    `"%~dp0..\\..\\..\\${WIN_EXECUTABLE}" "%~dp0..\\${CLI_ENTRY_RELPATH.replaceAll("/", "\\")}" %*`,
    "exit /b %errorlevel%",
    "",
  ].join("\r\n");
}

/**
 * Single-line CJS bootstrap for the AppImage wrapper (`electron -e <this>`): resolves
 * the CLI entry relative to the mounted application (process.execPath is the Electron
 * binary inside the AppImage mount, resources/app sits next to it), splices it into
 * argv[1] so `node -e` argv ([execPath, ...args]) becomes the [execPath, entry, ...args]
 * shape the CLI's argv parsing expects, then imports the (ESM) entry. Must not contain
 * single quotes: the wrapper embeds it in a single-quoted shell string.
 */
export function appImageBootstrapJs(): string {
  return (
    'const path=require("path");' +
    'const cli=path.join(path.dirname(process.execPath),"resources","app",' +
    '"node_modules","@prismshadow","penguin-cli","dist","index.js");' +
    "process.argv.splice(1,0,cli);" +
    "import(cli).catch((err)=>{console.error(err);process.exit(1);});"
  );
}

/**
 * Wrapper script installed to ~/.local/bin/penguin for the AppImage form, which has no
 * stable install directory to symlink into: it invokes the AppImage itself as Node.
 * The AppImage path is baked in at install time; the guard turns a moved/deleted
 * AppImage into a clear message instead of a shell "not found".
 */
export function appImageWrapperScript(appImagePath: string): string {
  if (appImagePath.includes("'")) {
    // A quote would break out of the single-quoted assignment below; refuse rather
    // than generate a broken (or injectable) script.
    throw new Error(`AppImage path must not contain single quotes: ${appImagePath}`);
  }
  return `#!/bin/sh
# penguin CLI launcher (generated by the PenguinHarness desktop app).
# Runs the CLI bundled inside the AppImage by starting the AppImage's Electron
# runtime as Node (ELECTRON_RUN_AS_NODE=1). If the AppImage moves, re-run
# "Install 'penguin' Command" from the PenguinHarness application menu.
APPIMAGE_PATH='${appImagePath}'
if [ ! -x "$APPIMAGE_PATH" ]; then
  echo "penguin: AppImage not found at $APPIMAGE_PATH (moved or deleted?)." >&2
  echo "penguin: re-run \\"Install 'penguin' Command\\" from the PenguinHarness menu." >&2
  exit 1
fi
export ELECTRON_RUN_AS_NODE=1
# The -- ends Node option parsing: without it a leading-dash argument (penguin --help)
# would be swallowed as a Node flag instead of reaching the CLI.
exec "$APPIMAGE_PATH" -e '${appImageBootstrapJs()}' -- "$@"
`;
}

/**
 * Idempotent HKCU\\Environment PATH append: returns the new value to write, or null
 * when `binDir` is already present (comparison ignores case, surrounding quotes and
 * trailing slashes). The existing value is preserved verbatim — only `;binDir` is
 * appended — so other software's entries are never rewritten.
 */
export function mergeWindowsUserPath(
  current: string | null | undefined,
  binDir: string,
): string | null {
  const normalize = (entry: string): string =>
    entry
      .trim()
      .replace(/^"|"$/g, "")
      .replace(/[\\/]+$/, "")
      .toLowerCase();
  const target = binDir.trim().replace(/[\\/]+$/, "");
  const base = (current ?? "").trim();
  const present = base
    .split(";")
    .map(normalize)
    .some((entry) => entry !== "" && entry === normalize(target));
  if (present) return null;
  if (base === "") return target;
  return base.endsWith(";") ? `${base}${target}` : `${base};${target}`;
}

export type CliInstallKind = "darwin" | "windows" | "appimage";

/**
 * Whether (and how) this run can install the `penguin` command onto PATH. Dev runs have
 * no staged bin/; deb installs already ship a /usr/bin symlink from their postinst
 * script (build/linux/after-install.tpl), so only the AppImage form qualifies on Linux
 * (recognized by the APPIMAGE env var its runtime sets).
 */
export function cliInstallKind(opts: {
  packaged: boolean;
  platform: NodeJS.Platform;
  appImagePath: string | null;
}): CliInstallKind | null {
  if (!opts.packaged) return null;
  if (opts.platform === "darwin") return "darwin";
  if (opts.platform === "win32") return "windows";
  if (opts.platform === "linux" && opts.appImagePath !== null && opts.appImagePath !== "") {
    return "appimage";
  }
  return null;
}
