#!/usr/bin/env node
/**
 * Shared metrics sender for CI scripts.
 * See .github/CI-TELEMETRY.md for payload shape and BigQuery schema.
 *
 * Usage (fire-and-forget — best-effort, never blocks CI):
 *   import { sendMetrics, metric } from './send-metrics.mjs';
 *   sendMetrics([metric('build-duration', 45.2, 's', { package: '@n8n/cli' })])
 *     .catch((err) => console.warn(`[metrics] send failed: ${err.message}`));
 *
 * Env: QA_METRICS_WEBHOOK_URL, QA_METRICS_WEBHOOK_USER, QA_METRICS_WEBHOOK_PASSWORD
 */

import * as os from 'node:os';

/** Build a single metric object. */
export function metric(name, value, unit, dimensions = {}) {
	return { metric_name: name, value, unit, dimensions };
}

/** Build git/ci/runner context from environment variables. */
export function buildContext(benchmarkName = null) {
	// Use `||` (not `??`) when falling back across GitHub env vars: on `push` and
	// `merge_group` events GITHUB_HEAD_REF is set to '' (empty string, not undefined),
	// so `??` would never fall through to GITHUB_REF_NAME and every non-PR row would
	// land in BigQuery with git_branch = ''. The same applies to any other GITHUB_*
	// var whose intended fallback is null — preserve null, not ''.
	const ref = process.env.GITHUB_REF || '';
	const prMatch = ref.match(/refs\/pull\/(\d+)/);
	const runId = process.env.GITHUB_RUN_ID || null;

	return {
		timestamp: new Date().toISOString(),
		benchmark_name: benchmarkName,
		git: {
			sha: process.env.GITHUB_SHA?.slice(0, 8) || null,
			branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || null,
			pr: prMatch ? parseInt(prMatch[1], 10) : null,
		},
		ci: {
			runId,
			runUrl:
				runId && process.env.GITHUB_REPOSITORY
					? `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${runId}`
					: null,
			job: process.env.GITHUB_JOB || null,
			workflow: process.env.GITHUB_WORKFLOW || null,
			attempt: process.env.GITHUB_RUN_ATTEMPT
				? parseInt(process.env.GITHUB_RUN_ATTEMPT, 10)
				: null,
		},
		runner: {
			provider: !process.env.CI
				? 'local'
				: process.env.RUNNER_ENVIRONMENT === 'github-hosted'
					? 'github'
					: 'blacksmith',
			cpuCores: os.cpus().length,
			memoryGb: Math.round((os.totalmem() / (1024 * 1024 * 1024)) * 10) / 10,
		},
	};
}

export async function sendMetrics(metrics, benchmarkName = null) {
	const webhookUrl = process.env.QA_METRICS_WEBHOOK_URL;
	const webhookUser = process.env.QA_METRICS_WEBHOOK_USER;
	const webhookPassword = process.env.QA_METRICS_WEBHOOK_PASSWORD;

	if (!webhookUrl) {
		console.log('QA_METRICS_WEBHOOK_URL not set, skipping.');
		return;
	}

	if (!webhookUser || !webhookPassword) {
		console.log('QA_METRICS_WEBHOOK_USER/PASSWORD not set, skipping.');
		return;
	}

	const payload = { ...buildContext(benchmarkName), metrics };
	const basicAuth = Buffer.from(`${webhookUser}:${webhookPassword}`).toString('base64');

	// Best-effort telemetry: never throw into the caller. Slow/down webhook
	// becomes a one-line warning instead of an uncaught rejection. 5s caps
	// wall-time in callers that do await this; fire-and-forget callers exit
	// before then anyway.
	try {
		const response = await fetch(webhookUrl, {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				Authorization: `Basic ${basicAuth}`,
			},
			body: JSON.stringify(payload),
			signal: AbortSignal.timeout(5_000),
		});

		if (!response.ok) {
			const body = await response.text().catch(() => '');
			console.warn(
				`[metrics] webhook ${response.status} ${response.statusText}${body ? `\n${body}` : ''}`,
			);
			return;
		}

		console.log(`Sent ${metrics.length} metric(s): ${response.status}`);
	} catch (err) {
		console.warn(`[metrics] send failed (non-fatal): ${err.message}`);
	}
}
