#!/usr/bin/env node

/**
 * Compare Node.js HTTP/1.1 over loopback TCP and a filesystem Unix socket.
 *
 * The controller and server run in separate Node.js processes. Each trial
 * alternates transport order, warms persistent connections, measures
 * sequential round-trip latency, and then measures fixed-work concurrency.
 *
 * @module scripts/dev/benchmark-http-transports
 */

import { fork } from "node:child_process";
import fs from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { performance } from "node:perf_hooks";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseArgs } from "node:util";

const SCRIPT_PATH = fileURLToPath(import.meta.url);
const LOOPBACK_HOST = "127.0.0.1";
const REQUEST_TIMEOUT_MS = 5_000;
const CHILD_START_TIMEOUT_MS = 10_000;
const CHILD_STOP_TIMEOUT_MS = 5_000;
const RESPONSE_CONTENT_TYPE = "application/octet-stream";
const TRANSPORTS = Object.freeze(["tcp", "uds"]);

export const DEFAULT_BENCHMARK_OPTIONS = Object.freeze({
    trials: 7,
    warmupRequests: 1_000,
    latencyRequests: 10_000,
    throughputRequests: 100_000,
    concurrency: 64,
    payloadBytes: 1_024,
    output: null,
});

const OPTION_LIMITS = Object.freeze({
    trials: Object.freeze({ min: 1, max: 25 }),
    warmupRequests: Object.freeze({ min: 1, max: 100_000 }),
    latencyRequests: Object.freeze({ min: 1, max: 1_000_000 }),
    throughputRequests: Object.freeze({ min: 1, max: 10_000_000 }),
    concurrency: Object.freeze({ min: 1, max: 1_024 }),
    payloadBytes: Object.freeze({ min: 0, max: 16 * 1024 * 1024 }),
});

/**
 * Parse and validate one bounded integer option.
 * @param {string} name - Stable option name used in errors.
 * @param {string | undefined} value - Command-line value.
 * @param {number} fallback - Default value.
 * @returns {number} Validated integer.
 */
function boundedInteger(name, value, fallback) {
    const parsed = value === undefined ? fallback : Number(value);
    const { min, max } = OPTION_LIMITS[name];
    if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) {
        throw new RangeError(
            `--${name.replaceAll(/[A-Z]/g, (character) => `-${character.toLowerCase()}`)} must be an integer from ${min} to ${max}`,
        );
    }
    return parsed;
}

/**
 * Parse the public benchmark command line.
 * @param {string[]} args - Arguments excluding the executable and script.
 * @returns {typeof DEFAULT_BENCHMARK_OPTIONS} Validated immutable options.
 */
export function parseBenchmarkOptions(args) {
    const { values } = parseArgs({
        args,
        allowPositionals: false,
        strict: true,
        options: {
            trials: { type: "string" },
            "warmup-requests": { type: "string" },
            "latency-requests": { type: "string" },
            "throughput-requests": { type: "string" },
            concurrency: { type: "string" },
            "payload-bytes": { type: "string" },
            output: { type: "string" },
        },
    });
    const output = values.output === undefined ? null : path.resolve(values.output);
    return Object.freeze({
        trials: boundedInteger("trials", values.trials, DEFAULT_BENCHMARK_OPTIONS.trials),
        warmupRequests: boundedInteger(
            "warmupRequests",
            values["warmup-requests"],
            DEFAULT_BENCHMARK_OPTIONS.warmupRequests,
        ),
        latencyRequests: boundedInteger(
            "latencyRequests",
            values["latency-requests"],
            DEFAULT_BENCHMARK_OPTIONS.latencyRequests,
        ),
        throughputRequests: boundedInteger(
            "throughputRequests",
            values["throughput-requests"],
            DEFAULT_BENCHMARK_OPTIONS.throughputRequests,
        ),
        concurrency: boundedInteger("concurrency", values.concurrency, DEFAULT_BENCHMARK_OPTIONS.concurrency),
        payloadBytes: boundedInteger("payloadBytes", values["payload-bytes"], DEFAULT_BENCHMARK_OPTIONS.payloadBytes),
        output,
    });
}

/**
 * Return one nearest-rank percentile from a non-empty numeric sample.
 * @param {number[]} values - Finite observations.
 * @param {number} fraction - Percentile as a value greater than 0 and at most 1.
 * @returns {number} Selected observation.
 */
export function percentile(values, fraction) {
    if (values.length === 0 || !values.every(Number.isFinite)) {
        throw new TypeError("percentile requires a non-empty finite sample");
    }
    if (!Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {
        throw new RangeError("percentile fraction must be greater than 0 and at most 1");
    }
    const sorted = values.toSorted((left, right) => left - right);
    return sorted[Math.ceil(fraction * sorted.length) - 1];
}

/** Round a measurement without changing its unit. */
function rounded(value, digits = 3) {
    const scale = 10 ** digits;
    return Math.round(value * scale) / scale;
}

/** Return a fixed-shape endpoint suitable for node:http. */
function requestEndpoint(transport, address) {
    if (transport === "tcp") {
        return Object.freeze({ host: LOOPBACK_HOST, port: address.port });
    }
    return Object.freeze({ host: "localhost", socketPath: address.socketPath });
}

/**
 * Make one request and consume its bounded response body.
 * @param {object} endpoint - Loopback TCP or UDS request options.
 * @param {http.Agent} agent - Owned persistent-connection pool.
 * @param {number} expectedBytes - Exact response size.
 * @returns {Promise<void>} Fulfilled after the response is consumed.
 */
function requestOnce(endpoint, agent, expectedBytes) {
    return new Promise((resolve, reject) => {
        const request = http.get({ ...endpoint, agent, path: "/" }, (response) => {
            let receivedBytes = 0;
            response.on("data", (chunk) => {
                receivedBytes += chunk.length;
            });
            response.once("aborted", () => reject(new Error("benchmark response was aborted")));
            response.once("error", reject);
            response.once("end", () => {
                if (response.statusCode !== 200) {
                    reject(new Error(`benchmark server returned HTTP ${response.statusCode}`));
                    return;
                }
                if (receivedBytes !== expectedBytes) {
                    reject(new Error(`benchmark response was ${receivedBytes} bytes; expected ${expectedBytes}`));
                    return;
                }
                resolve();
            });
        });
        request.setTimeout(REQUEST_TIMEOUT_MS, () => request.destroy(new Error("benchmark request timed out")));
        request.once("error", reject);
    });
}

/**
 * Complete a fixed number of requests through bounded concurrent workers.
 * @returns {Promise<void>}
 */
async function runConcurrent(endpoint, agent, expectedBytes, requestCount, concurrency) {
    let nextRequest = 0;
    async function worker() {
        while (nextRequest < requestCount) {
            ++nextRequest;
            await requestOnce(endpoint, agent, expectedBytes);
        }
    }
    const workerCount = Math.min(concurrency, requestCount);
    await Promise.all(Array.from({ length: workerCount }, () => worker()));
}

/** Measure sequential persistent-connection round trips in microseconds. */
async function measureLatency(endpoint, expectedBytes, warmupRequests, requestCount) {
    const agent = new http.Agent({ keepAlive: true, maxSockets: 1 });
    try {
        for (let requestIndex = 0; requestIndex < warmupRequests; ++requestIndex) {
            await requestOnce(endpoint, agent, expectedBytes);
        }
        const samples = [];
        let totalMicroseconds = 0;
        for (let requestIndex = 0; requestIndex < requestCount; ++requestIndex) {
            const started = performance.now();
            await requestOnce(endpoint, agent, expectedBytes);
            const elapsedMicroseconds = (performance.now() - started) * 1_000;
            samples.push(elapsedMicroseconds);
            totalMicroseconds += elapsedMicroseconds;
        }
        return Object.freeze({
            medianMicroseconds: rounded(percentile(samples, 0.5)),
            p95Microseconds: rounded(percentile(samples, 0.95)),
            meanMicroseconds: rounded(totalMicroseconds / samples.length),
        });
    } finally {
        agent.destroy();
    }
}

/** Measure fixed-work HTTP throughput with a bounded connection pool. */
async function measureThroughput(endpoint, expectedBytes, warmupRequests, requestCount, concurrency) {
    const agent = new http.Agent({ keepAlive: true, maxSockets: concurrency });
    try {
        await runConcurrent(endpoint, agent, expectedBytes, warmupRequests, concurrency);
        const started = performance.now();
        await runConcurrent(endpoint, agent, expectedBytes, requestCount, concurrency);
        const elapsedSeconds = (performance.now() - started) / 1_000;
        return Object.freeze({
            requestsPerSecond: rounded(requestCount / elapsedSeconds),
            elapsedSeconds: rounded(elapsedSeconds, 6),
        });
    } finally {
        agent.destroy();
    }
}

/** Wait for an owned child server to report its bound endpoint. */
function waitForChildReady(child) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(
            () => reject(new Error("benchmark server did not start in time")),
            CHILD_START_TIMEOUT_MS,
        );
        const finish = (callback) => (value) => {
            clearTimeout(timer);
            child.off("error", onError);
            child.off("exit", onExit);
            child.off("message", onMessage);
            callback(value);
        };
        const onError = finish(reject);
        const onExit = finish((code) => reject(new Error(`benchmark server exited before ready with code ${code}`)));
        const onMessage = finish((message) => {
            if (!message || message.type !== "ready" || !message.address) {
                reject(new Error("benchmark server sent an invalid ready message"));
                return;
            }
            resolve(message.address);
        });
        child.once("error", onError);
        child.once("exit", onExit);
        child.once("message", onMessage);
    });
}

/** Stop an owned child server, escalating only after a bounded grace period. */
async function stopChild(child) {
    if (child.exitCode !== null || child.signalCode !== null) {
        return;
    }
    const exited = new Promise((resolve) => child.once("exit", resolve));
    child.send({ type: "shutdown" });
    let stopTimer;
    const graceful = await Promise.race([
        exited.then(() => {
            clearTimeout(stopTimer);
            return true;
        }),
        new Promise((resolve) => {
            stopTimer = setTimeout(() => resolve(false), CHILD_STOP_TIMEOUT_MS);
        }),
    ]);
    if (!graceful) {
        child.kill("SIGTERM");
        await exited;
    }
}

/** Launch and measure one transport in an isolated server process. */
async function measureTransport(transport, options, temporaryRoot) {
    const socketPath = path.join(temporaryRoot, `benchmark-${transport}.sock`);
    const child = fork(
        SCRIPT_PATH,
        [
            "--server-child",
            "--transport",
            transport,
            "--payload-bytes",
            String(options.payloadBytes),
            "--socket-path",
            socketPath,
        ],
        { stdio: ["ignore", "ignore", "inherit", "ipc"] },
    );
    try {
        const address = await waitForChildReady(child);
        const endpoint = requestEndpoint(transport, address);
        const latency = await measureLatency(
            endpoint,
            options.payloadBytes,
            options.warmupRequests,
            options.latencyRequests,
        );
        const throughput = await measureThroughput(
            endpoint,
            options.payloadBytes,
            options.warmupRequests,
            options.throughputRequests,
            options.concurrency,
        );
        return Object.freeze({ latency, throughput });
    } finally {
        await stopChild(child);
    }
}

/** Summarize trial values without hiding the observed range. */
function summarize(values) {
    return Object.freeze({
        median: rounded(percentile(values, 0.5)),
        min: rounded(Math.min(...values)),
        max: rounded(Math.max(...values)),
    });
}

/**
 * Run the full benchmark and return publication-ready evidence.
 * @param {typeof DEFAULT_BENCHMARK_OPTIONS} options - Validated options.
 * @returns {Promise<object>} Environment, method, raw trials, and summaries.
 */
export async function runBenchmark(options) {
    const startedAt = new Date();
    const temporaryRoot = await fs.mkdtemp(path.join(os.tmpdir(), "themadhatters-http-transport-"));
    const trials = [];
    try {
        for (let trialIndex = 0; trialIndex < options.trials; ++trialIndex) {
            const order = trialIndex % 2 === 0 ? TRANSPORTS : TRANSPORTS.toReversed();
            const measurements = {};
            for (const transport of order) {
                measurements[transport] = await measureTransport(transport, options, temporaryRoot);
            }
            trials.push(Object.freeze({ index: trialIndex + 1, order, ...measurements }));
        }
    } finally {
        await fs.rm(temporaryRoot, { recursive: true, force: true });
    }

    const summary = {};
    for (const transport of TRANSPORTS) {
        summary[transport] = Object.freeze({
            latencyMedianMicroseconds: summarize(
                trials.map(({ [transport]: measurement }) => measurement.latency.medianMicroseconds),
            ),
            latencyP95Microseconds: summarize(
                trials.map(({ [transport]: measurement }) => measurement.latency.p95Microseconds),
            ),
            throughputRequestsPerSecond: summarize(
                trials.map(({ [transport]: measurement }) => measurement.throughput.requestsPerSecond),
            ),
        });
    }
    const tcpLatency = summary.tcp.latencyMedianMicroseconds.median;
    const udsLatency = summary.uds.latencyMedianMicroseconds.median;
    const tcpThroughput = summary.tcp.throughputRequestsPerSecond.median;
    const udsThroughput = summary.uds.throughputRequestsPerSecond.median;
    const cpu = os.cpus();

    return Object.freeze({
        schemaVersion: 1,
        startedAt: startedAt.toISOString(),
        finishedAt: new Date().toISOString(),
        environment: Object.freeze({
            node: process.version,
            platform: os.platform(),
            release: os.release(),
            architecture: os.arch(),
            cpuModel: cpu[0]?.model ?? "unknown",
            logicalCpuCount: cpu.length,
            totalMemoryBytes: os.totalmem(),
            processPriority: os.getPriority(),
        }),
        method: Object.freeze({
            protocol: "HTTP/1.1",
            tcpEndpoint: "127.0.0.1 with an ephemeral port",
            udsEndpoint: "filesystem socket in an isolated temporary directory",
            controllerAndServerProcesses: 2,
            persistentConnections: true,
            order: "alternated by trial",
            ...options,
            output: undefined,
        }),
        summary,
        comparison: Object.freeze({
            udsMedianLatencyReductionPercent: rounded((1 - udsLatency / tcpLatency) * 100, 1),
            udsMedianThroughputDifferencePercent: rounded((udsThroughput / tcpThroughput - 1) * 100, 1),
        }),
        trials,
    });
}

/** Run the fixed-response child process used by the controller. */
async function runServerChild(args) {
    const { values } = parseArgs({
        args,
        allowPositionals: false,
        strict: true,
        options: {
            "server-child": { type: "boolean" },
            transport: { type: "string" },
            "payload-bytes": { type: "string" },
            "socket-path": { type: "string" },
        },
    });
    const transport = values.transport;
    if (!TRANSPORTS.includes(transport)) {
        throw new TypeError("child transport must be tcp or uds");
    }
    const payloadBytes = boundedInteger("payloadBytes", values["payload-bytes"], 0);
    const socketPath = values["socket-path"];
    if (typeof socketPath !== "string" || !path.isAbsolute(socketPath)) {
        throw new TypeError("child socket path must be absolute");
    }
    const payload = Buffer.alloc(payloadBytes, "x");
    const server = http.createServer((request, response) => {
        if (request.method !== "GET" || request.url !== "/") {
            response.writeHead(404, { "content-length": 0 });
            response.end();
            return;
        }
        response.writeHead(200, {
            "content-length": payload.length,
            "content-type": RESPONSE_CONTENT_TYPE,
        });
        response.end(payload);
    });
    server.requestTimeout = REQUEST_TIMEOUT_MS;
    server.headersTimeout = REQUEST_TIMEOUT_MS;
    await new Promise((resolve, reject) => {
        const onError = (error) => {
            server.off("listening", onListening);
            reject(error);
        };
        const onListening = () => {
            server.off("error", onError);
            resolve();
        };
        server.once("error", onError);
        server.once("listening", onListening);
        server.listen(transport === "tcp" ? { host: LOOPBACK_HOST, port: 0 } : socketPath);
    });
    const address = server.address();
    process.send?.({
        type: "ready",
        address: transport === "tcp" ? { port: address.port } : { socketPath },
    });
    await new Promise((resolve, reject) => {
        let closing = false;
        const close = () => {
            if (closing) {
                return;
            }
            closing = true;
            server.close((error) => (error ? reject(error) : resolve()));
        };
        process.once("message", (message) => {
            if (message?.type === "shutdown") {
                close();
            }
        });
        process.once("SIGTERM", close);
    });
}

/** Execute the command-line entry point and emit one complete JSON document. */
async function main() {
    const args = process.argv.slice(2);
    if (args.includes("--server-child")) {
        await runServerChild(args);
        return;
    }
    const options = parseBenchmarkOptions(args);
    const result = await runBenchmark(options);
    const json = `${JSON.stringify(result, null, 4)}\n`;
    if (options.output !== null) {
        await fs.mkdir(path.dirname(options.output), { recursive: true });
        await fs.writeFile(options.output, json, { encoding: "utf8", mode: 0o600 });
    }
    process.stdout.write(json);
}

if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
    try {
        await main();
    } catch (error) {
        process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
        process.exitCode = 1;
    }
}
