/**
 * SparkBun Installer Wrapper Template
 *
 * This file is the entrypoint for installers built with `bun sparkbun installer`.
 * It gets compiled into a single executable that contains:
 *   - A small runtime bundle (DLLs, views, app code — ~1MB compressed)
 *   - The installer's payload archives (tar + zstd/gzip)
 *
 * The binary uses a two-phase execution model with a single Bun runtime:
 *
 *   Phase 1 (first launch — user double-clicks the exe):
 *     No native DLLs next to the exe, so we can't load SparkBunCore yet.
 *     Extract the runtime bundle to a temp directory, copy ourselves there
 *     (so the same binary now has DLLs next to it), and re-launch the copy
 *     with admin elevation via ShellExecuteW("runas"). The UAC prompt shows
 *     the exe's PE metadata (app name, publisher) since Windows reads it
 *     from the binary being elevated.
 *
 *   Phase 2 (second launch — from temp, elevated):
 *     DLLs are next to us. Load SparkBunCore via FFI, start the app code
 *     as a Worker thread, and run the native event loop. The app code opens
 *     a webview window (the installer wizard UI) and can access the embedded
 *     payload archives via globalThis.__installerArchives when the user
 *     triggers installation.
 *
 * The payload archives stay embedded in the binary at all times — they are
 * never extracted to temp. Only when the user clicks "Install" does the app
 * code read them (via Bun.embeddedFiles, guided by the Resources/archives.json
 * manifest) and stream them to disk.
 */

import runtimeArchive from "./runtime-bundle.tar.gz" with { type: "file" };
import metadataJson from "./metadata.json" with { type: "file" };
import { embeddedArchives } from "./embedded-archives";
import { join, dirname, basename } from "path";
import { existsSync, mkdirSync, readdirSync, copyFileSync } from "fs";
import { dlopen, suffix, ptr } from "bun:ffi";

const metadata = await Bun.file(metadataJson).json();
const binDir = dirname(process.execPath);
const coreLibName = process.platform === "win32"
	? "SparkBunCore.dll"
	: `libSparkBunCore.${suffix}`;
const hasRuntime = existsSync(join(binDir, coreLibName));

if (hasRuntime) {
	// ── Phase 2: DLLs are next to us — run the SparkBun app ──

	process.chdir(binDir);

	// Expose embedded archives to the app code via globalThis.
	// The app's install logic reads these paths to extract payloads.
	(globalThis as any).__installerArchives = embeddedArchives;

	const coreLibPath = join(binDir, coreLibName);
	const lib = dlopen(coreLibPath, {
		sparkbun_core_run_main_thread: {
			args: ["cstring", "cstring", "cstring", "i32"],
			returns: "i32",
		},
	});

	const resourcesDir = join(binDir, "..", "Resources");
	const appEntrypointPath = join(resourcesDir, "app", "bun", "index.js");

	// Read app identity from build.json for SparkBunCore initialization
	let identifier = metadata.name || "";
	let name = metadata.name || "";
	let channel = "";
	try {
		const buildJsonPath = join(resourcesDir, "app", "build.json");
		if (existsSync(buildJsonPath)) {
			const buildConfig = JSON.parse(require("fs").readFileSync(buildJsonPath, "utf-8"));
			identifier = buildConfig.identifier || identifier;
			name = buildConfig.name || name;
			channel = buildConfig.channel || "";
		}
	} catch {}

	// Suppress default signal handling so the native event loop controls shutdown
	process.on("SIGINT", () => {});
	process.on("SIGTERM", () => {});

	// Start the app code in a Worker thread (same pattern as SparkBun's launcher).
	// The Worker runs the developer's Bun entrypoint which creates BrowserWindows,
	// sets up RPC handlers, etc.
	new Worker(appEntrypointPath);

	// Run the native event loop on the main thread (blocks until app exits)
	lib.symbols.sparkbun_core_run_main_thread(
		ptr(new Uint8Array(Buffer.from(identifier + "\0", "utf8"))),
		ptr(new Uint8Array(Buffer.from(name + "\0", "utf8"))),
		ptr(new Uint8Array(Buffer.from(channel + "\0", "utf8"))),
		0,
	);
} else {
	// ── Phase 1: No DLLs — extract runtime, copy self, launch elevated ──

	const tempDir = join(
		process.env.TEMP || process.env.LOCALAPPDATA || ".",
		`.sparkbun-installer-${metadata.name.replace(/[^a-zA-Z0-9]/g, "_")}`,
	);

	mkdirSync(tempDir, { recursive: true });
	const archiveBytes = await Bun.file(runtimeArchive).bytes();
	const archive = new Bun.Archive(archiveBytes);
	await archive.extract(tempDir);

	// The runtime bundle extracts as AppName/bin/ with DLLs inside
	let extractedBinDir = tempDir;
	for (const entry of readdirSync(tempDir)) {
		const entryPath = join(tempDir, entry);
		if (existsSync(join(entryPath, "bin"))) {
			extractedBinDir = join(entryPath, "bin");
			break;
		}
	}

	// Copy ourselves next to the DLLs so the second launch detects hasRuntime
	const destExe = join(extractedBinDir, basename(process.execPath));
	copyFileSync(process.execPath, destExe);

	// Launch the copy with elevation using the same pattern as installer-template.ts.
	// ShellExecuteW("runas") triggers the UAC prompt showing the exe's PE metadata.
	if (process.platform === "win32" && metadata.requireAdmin) {
		const shell32 = dlopen("shell32.dll", {
			ShellExecuteW: {
				args: ["ptr", "ptr", "ptr", "ptr", "ptr", "i32"],
				returns: "ptr",
			},
		});
		const encode = (s: string) => ptr(new Uint8Array(Buffer.from(s + "\0", "utf-16le")));
		shell32.symbols.ShellExecuteW(
			null,
			encode("runas"),
			encode(destExe),
			null,
			encode(extractedBinDir),
			1,
		);
		shell32.close();
	} else {
		Bun.spawn([destExe], { cwd: extractedBinDir });
	}
	await Bun.sleep(1000);
}
