#!/usr/bin/env node
// Serve the checked-out tree as a preview instance, from inside a codespace.
// Idempotent: it stops any previous preview backend first, so `preview up` and
// `preview refresh` share this one entry point.
//
//   pnpm preview:serve
//
// dev:up detaches the backend, which leaves nothing for a refresh to stop. So
// this owns the lifecycle instead: dev:be runs in a named tmux session.
import { execFileSync, spawnSync } from 'node:child_process';
import { openSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { serveHealthPath, servePort, waitForHealth } from './serve-ready.mjs';

const SESSION = 'n8n-preview';
const BUILD_LOG = '/tmp/preview-build.log';
const BE_LOG = '/tmp/preview-be.log';
// The preview box has 2 cores and 8gb. Cap both, for the reason spelled out in
// scripts/agent-setup.mjs: unbounded turbo concurrency is what exhausts a small box.
const CONCURRENCY = process.env.PREVIEW_BUILD_CONCURRENCY ?? '2';
// A fresh instance opens on the owner-setup wall, which is friction for a reviewer
// who only wants to look at the UI. Running with no auth is not an option: the
// `skipInstanceOwnerSetup` setting was removed in migration 1681134145997. So seed a
// known owner instead. These are not secrets — the security boundary is the
// org-visible forwarded port, which already requires a GitHub sign-in and n8n org
// membership. Never point this at anything publicly reachable.
// The password must satisfy passwordSchema: 8-64 chars, >=1 digit, >=1 uppercase.
const OWNER_EMAIL = process.env.PREVIEW_OWNER_EMAIL ?? 'preview@n8n.io';
const OWNER_PASSWORD = process.env.PREVIEW_OWNER_PASSWORD ?? 'PreviewInstance1';
const HEAP_MB = process.env.PREVIEW_BUILD_HEAP_MB ?? '6144';
// Resolve from this file, not cwd, so the tmux command lands in the right tree.
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const port = servePort();
const healthPath = serveHealthPath();

const tmux = (...args) => spawnSync('tmux', args, { stdio: 'ignore' });

// Stop the old backend before rebuilding: it frees the port, and it stops the
// previous build being served alongside newly written assets.
tmux('kill-session', '-t', SESSION);

console.log(`Building (turbo cache — fast when warm; log: ${BUILD_LOG})…`);
const buildFd = openSync(BUILD_LOG, 'w');
try {
	execFileSync('pnpm', ['exec', 'turbo', 'run', 'build', `--concurrency=${CONCURRENCY}`], {
		cwd: repoRoot,
		stdio: ['ignore', buildFd, buildFd],
		// Appended last so it wins over an inherited value: node takes the final flag.
		env: {
			...process.env,
			NODE_OPTIONS: [process.env.NODE_OPTIONS, `--max-old-space-size=${HEAP_MB}`]
				.filter(Boolean)
				.join(' '),
		},
	});
} catch {
	console.error(`Build failed — see ${BUILD_LOG}:`);
	execFileSync('tail', ['-n', '30', BUILD_LOG], { stdio: 'inherit' });
	process.exit(1);
}

// One-click sign-in, so a reviewer lands in the editor rather than a login form.
// N8N_ADDITIONAL_NON_UI_ROUTES is required: `n8n.ready` runs after the SPA catch-all
// is registered, so without it historyApiHandler answers /preview-signin with
// index.html. dev:be runs turbo with --env-mode=loose, which passes these through.
//
// Set with tmux `-e` rather than interpolated into the command: tmux does not
// inherit the caller's environment for arbitrary variables (the same trap the
// codespaces README documents), and this keeps the values out of a shell string.
const SIGNIN_ROUTE = 'preview-signin';
const signinEnv = [
	`EXTERNAL_HOOK_FILES=${repoRoot}/.devcontainer/preview/preview-signin-hook.cjs`,
	`N8N_ADDITIONAL_NON_UI_ROUTES=${SIGNIN_ROUTE}`,
	'N8N_PREVIEW_SIGNIN=1',
	`PREVIEW_OWNER_EMAIL=${OWNER_EMAIL}`,
	`PREVIEW_OWNER_PASSWORD=${OWNER_PASSWORD}`,
].flatMap((pair) => ['-e', pair]);

console.log(`Starting backend in tmux session '${SESSION}' (log: ${BE_LOG})…`);
const started = tmux(
	'new',
	'-d',
	'-s',
	SESSION,
	...signinEnv,
	`cd ${repoRoot} && exec pnpm dev:be > ${BE_LOG} 2>&1`,
);
if (started.status !== 0) {
	console.error(`Could not start tmux session '${SESSION}'.`);
	process.exit(1);
}

if (!(await waitForHealth(port, healthPath))) {
	console.error(`\nBackend did not answer ${healthPath} within 2 min — check ${BE_LOG}:`);
	execFileSync('tail', ['-n', '30', BE_LOG], { stdio: 'inherit' });
	process.exit(1);
}

// Promote the shell user into the owner so the instance opens on a login page with
// known credentials, not the setup wizard. Idempotent: on a refresh the sqlite file
// survives, the owner already exists, and the endpoint rejects the second attempt.
async function seedOwner() {
	let res;
	try {
		res = await fetch(`http://127.0.0.1:${port}/rest/owner/setup`, {
			method: 'POST',
			headers: { 'content-type': 'application/json' },
			body: JSON.stringify({
				email: OWNER_EMAIL,
				firstName: 'Preview',
				lastName: 'User',
				password: OWNER_PASSWORD,
			}),
			signal: AbortSignal.timeout(15_000),
		});
	} catch (error) {
		console.error(`Could not reach the owner setup endpoint: ${error.message}`);
		return;
	}

	if (res.ok) {
		console.log(`Seeded the instance owner: ${OWNER_EMAIL} / ${OWNER_PASSWORD}`);
		return;
	}
	// Already set up is the normal refresh case, not a failure.
	console.log(
		`Owner already set up (HTTP ${res.status}) — sign in as ${OWNER_EMAIL} / ${OWNER_PASSWORD}`,
	);
}

await seedOwner();
console.log(`One-click sign-in path: /${SIGNIN_ROUTE}`);

// The host shares the port and prints the URL: its gh holds the codespace scope.
console.log(`Ready: the backend answers ${healthPath} on port ${port}.`);
