#!/usr/bin/env -S gjs -m
/**
 * gjsify universal installer — bootstraps `@gjsify/cli` (or any GJS app
 * published to npm) on a system that has only `gjs` (and `curl`/`wget`)
 * available, without requiring Node.js or `npm`.
 *
 * Usage:
 *   gjs -m install.mjs                    # install / update @gjsify/cli
 *   gjs -m install.mjs --target @scope/x  # install any other GJS-runnable npm package
 *   gjs -m install.mjs --tag next         # pick an npm dist-tag or pinned version
 *   gjs -m install.mjs --force            # reinstall even if already present
 *   gjs -m install.mjs --help
 *
 * How it works (two-stage bootstrap):
 *   1. Download a small self-contained GJS bundle of the @gjsify/cli
 *      (`cli.gjs.mjs`) from this repo's GitHub releases. Verify SHA-256.
 *   2. Spawn that bundle: `gjs -m <bundle> install -g <target>@<tag>`. The
 *      bundle handles transitive dependency resolution, native prebuilds,
 *      lockfiles, and the `~/.local/bin` launchers — all the things this
 *      thin bootstrapper deliberately does NOT re-implement.
 *
 * Generated by `gjsify generate-installer` for end-user GJS apps: in that
 * mode the constants below (BOOTSTRAP_URL, DEFAULT_TARGET, DEFAULT_BIN_NAME)
 * are pre-substituted to the consumer's package + custom bootstrap URL.
 *
 * Test hooks (set by tests/e2e/install-script/run.mjs):
 *   GJSIFY_INSTALL_BOOTSTRAP_URL  override the cli.gjs.mjs download origin
 *                                 (accepts file:// for offline tests)
 *   GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL  override the .sha256 companion URL
 *                                 (set to empty string to skip SHA-256)
 *   GJSIFY_GLOBAL_PREFIX          override install prefix (forwarded to cli)
 *   GJSIFY_GLOBAL_BIN_DIR         override bin dir (forwarded to cli)
 *   GJSIFY_INSTALL_REGISTRY       override npm registry (forwarded as npm_config_registry)
 *   GJSIFY_INSTALL_BOOTSTRAP_CACHE  override the bootstrap cache dir
 */

import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Soup from 'gi://Soup?version=3.0';
import system, { exit } from 'system';

Gio._promisify(Soup.Session.prototype, 'send_and_read_async');
Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');

// Substituted by `gjsify generate-installer` for end-user apps.
const DEFAULT_TARGET = '@gjsify/cli';
const DEFAULT_BIN_NAME = 'gjsify';
const DEFAULT_BOOTSTRAP_URL = 'https://github.com/gjsify/gjsify/releases/latest/download/cli.gjs.mjs';
const DEFAULT_BOOTSTRAP_SHA256_URL = `${DEFAULT_BOOTSTRAP_URL}.sha256`;

const USER_AGENT = 'gjsify-installer/1.0';

function info(msg) {
    print(`[gjsify] ${msg}`);
}
function error(msg) {
    printerr(`[gjsify] ERROR: ${msg}`);
}

function parseArgs() {
    const argv = system?.programArgs ?? [];
    let target = DEFAULT_TARGET;
    let tag = 'latest';
    let force = false;
    let help = false;
    let bootstrapUrl = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_URL') || DEFAULT_BOOTSTRAP_URL;
    let bootstrapSha256Url = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_SHA256_URL');
    if (bootstrapSha256Url === null || bootstrapSha256Url === undefined) {
        bootstrapSha256Url =
            bootstrapUrl === DEFAULT_BOOTSTRAP_URL ? DEFAULT_BOOTSTRAP_SHA256_URL : `${bootstrapUrl}.sha256`;
    }
    for (let i = 0; i < argv.length; i++) {
        const a = argv[i];
        if (a === '--force' || a === '-f') force = true;
        else if (a === '--help' || a === '-h') help = true;
        else if (a === '--target') target = argv[++i];
        else if (a.startsWith('--target=')) target = a.slice('--target='.length);
        else if (a === '--tag') tag = argv[++i];
        else if (a.startsWith('--tag=')) tag = a.slice('--tag='.length);
        else if (a === '--bootstrap-url') bootstrapUrl = argv[++i];
        else if (a.startsWith('--bootstrap-url=')) bootstrapUrl = a.slice('--bootstrap-url='.length);
    }
    return { target, tag, force, help, bootstrapUrl, bootstrapSha256Url };
}

function printUsage() {
    print(`Usage: gjs -m install.mjs [options]

Installs (or updates) ${DEFAULT_TARGET} into the user-global XDG location,
using a self-contained GJS bundle of @gjsify/cli as a one-shot bootstrap.

Options:
  --target <pkg>     npm package to install   (default: ${DEFAULT_TARGET})
  --tag <tag>        npm dist-tag or version  (default: latest)
  --force, -f        Reinstall even when present.
  --bootstrap-url <url>  Override the cli.gjs.mjs download URL.
  --help, -h         Show this message.

Env vars:
  GJSIFY_INSTALL_BOOTSTRAP_URL   alternate bootstrap bundle URL (file:// OK)
  GJSIFY_GLOBAL_PREFIX           install prefix (default: ~/.local/share/gjsify/global)
  GJSIFY_GLOBAL_BIN_DIR          bin dir       (default: ~/.local/bin)
  GJSIFY_INSTALL_REGISTRY        npm registry override

Examples:
  # Install / update the gjsify CLI itself:
  gjs -m install.mjs

  # Install some other GJS-runnable package from npm:
  gjs -m install.mjs --target @ts-for-gir/cli

  # Pin a specific version:
  gjs -m install.mjs --tag 0.4.9
`);
}

function checkGjsVersion() {
    // imports.system.version is packed: major*10000 + minor*100 + micro
    const v = system?.version;
    if (typeof v !== 'number') return;
    const major = Math.floor(v / 10000);
    const minor = Math.floor((v - major * 10000) / 100);
    if (major < 1 || (major === 1 && minor < 86)) {
        error(`gjs ${major}.${minor} is too old — gjsify requires gjs 1.86 or newer.`);
        error('Install hints:');
        error('  Fedora 43+:  sudo dnf install gjs');
        error('  Debian 13+:  sudo apt install gjs');
        error('  Arch:        sudo pacman -S gjs');
        exit(1);
    }
}

async function fetchBytes(session, url) {
    if (url.startsWith('file://')) {
        const path = url.slice('file://'.length);
        const file = Gio.File.new_for_path(path);
        const [, bytes] = file.load_contents(null);
        return bytes;
    }
    const message = Soup.Message.new('GET', url);
    message.request_headers.append('User-Agent', USER_AGENT);
    const bytes = await session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null);
    const status = message.get_status();
    if (status !== Soup.Status.OK) {
        throw new Error(`HTTP ${status} from ${url}`);
    }
    return bytes.get_data();
}

function sha256Hex(bytes) {
    const checksum = GLib.Checksum.new(GLib.ChecksumType.SHA256);
    checksum.update(bytes);
    return checksum.get_string();
}

function cacheDir() {
    const override = GLib.getenv('GJSIFY_INSTALL_BOOTSTRAP_CACHE');
    if (override) return override;
    const xdg = GLib.getenv('XDG_CACHE_HOME') || GLib.build_filenamev([GLib.get_home_dir(), '.cache']);
    return GLib.build_filenamev([xdg, 'gjsify', 'bootstrap']);
}

function ensureDir(dir) {
    Gio.File.new_for_path(dir).make_directory_with_parents(null);
}

function writeBytes(path, bytes) {
    Gio.File.new_for_path(path).replace_contents(bytes, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null);
}

async function downloadBootstrap(session, bootstrapUrl, sha256Url) {
    info(`Downloading bootstrap from ${bootstrapUrl} ...`);
    const bundleBytes = await fetchBytes(session, bootstrapUrl);
    if (sha256Url && sha256Url !== '') {
        info('Verifying SHA-256 ...');
        let sumExpected;
        try {
            const sumBytes = await fetchBytes(session, sha256Url);
            sumExpected = new TextDecoder().decode(sumBytes).trim().split(/\s+/)[0];
        } catch (err) {
            error(`Could not fetch ${sha256Url} — skipping verification: ${err.message}`);
        }
        if (sumExpected) {
            const sumActual = sha256Hex(bundleBytes);
            if (sumExpected.toLowerCase() !== sumActual.toLowerCase()) {
                error(`SHA-256 mismatch: expected ${sumExpected}, got ${sumActual}`);
                exit(1);
            }
        }
    }
    const dir = cacheDir();
    try {
        ensureDir(dir);
    } catch {
        /* exists */
    }
    const bundlePath = GLib.build_filenamev([dir, 'cli.gjs.mjs']);
    writeBytes(bundlePath, bundleBytes);
    info(`Bootstrap cached at ${bundlePath} (${bundleBytes.length} bytes)`);
    return bundlePath;
}

function buildSpec(target, tag) {
    if (!tag || tag === 'latest') return target;
    return `${target}@${tag}`;
}

async function runInstall(bundlePath, spec) {
    // `gjsify install -g <spec>` is always idempotent — it rewrites the tree
    // unconditionally. There is no separate "force" flag to forward; the
    // installer's own `--force` is satisfied by the fact that we always
    // re-download the bootstrap and re-invoke the CLI.
    info(`Running: gjs -m <bootstrap> install -g ${spec}`);
    const argv = ['gjs', '-m', bundlePath, 'install', '-g', spec];
    // Forward env vars verbatim so override paths set by tests / power-users
    // reach the spawned CLI.
    const launcher = new Gio.SubprocessLauncher({
        flags: Gio.SubprocessFlags.NONE,
    });
    const proc = launcher.spawnv(argv);
    await proc.wait_check_async(null);
}

async function main() {
    const opts = parseArgs();
    if (opts.help) {
        printUsage();
        exit(0);
    }
    checkGjsVersion();

    const session = new Soup.Session();
    let bundlePath;
    try {
        bundlePath = await downloadBootstrap(session, opts.bootstrapUrl, opts.bootstrapSha256Url);
    } catch (err) {
        error(`Bootstrap download failed: ${err.message}`);
        exit(1);
    }

    const spec = buildSpec(opts.target, opts.tag);
    try {
        await runInstall(bundlePath, spec);
    } catch (err) {
        error(`Install failed: ${err.message}`);
        exit(1);
    }

    info('');
    info(`Installed ${spec}`);
    info(`Run: ${DEFAULT_BIN_NAME} --help`);
}

await main();
