#!/usr/bin/env bun
import { join, relative, dirname } from 'node:path';
import { readdirSync, statSync, rmSync, cpSync } from 'node:fs';
import { Spinner, GREEN, DIM, BOLD, RED, CYAN, RESET } from './lib/spinner.ts';

const verbose = process.argv.includes('--verbose');

const ROOT = dirname(import.meta.dir);
const API_DIR = join(ROOT, 'packages/api');
const WEB_SDK_DIR = join(ROOT, 'packages/web-sdk');
const WEB_DIR = join(ROOT, 'apps/web');
const CLI_DIR = join(ROOT, 'apps/cli');
const WEB_DIST = join(WEB_DIR, 'dist');
const CLI_WEB_DIST = join(CLI_DIR, 'src/web-dist');

const startTime = performance.now();
const spinner = new Spinner();

const bunExe = process.execPath;

async function run(label: string, cmd: string[], cwd: string) {
	spinner.begin(label);

	if (verbose) {
		spinner.succeed();
		const result = Bun.spawnSync(cmd, {
			cwd,
			stdout: 'inherit',
			stderr: 'inherit',
		});
		if (!result.success) {
			console.error(`\n${RED}Build failed at: ${label}${RESET}`);
			process.exit(1);
		}
		return;
	}

	const proc = Bun.spawn(cmd, { cwd, stdout: 'pipe', stderr: 'pipe' });
	const exitCode = await proc.exited;

	if (exitCode !== 0) {
		spinner.fail();
		const stderr = await new Response(proc.stderr).text();
		const stdout = await new Response(proc.stdout).text();
		console.error(`\n${RED}Build failed at: ${label}${RESET}`);
		if (stderr.trim()) console.error(stderr.trim());
		if (stdout.trim()) console.error(stdout.trim());
		process.exit(1);
	}

	spinner.succeed();
}

console.log(`\n${BOLD}${CYAN}otto${RESET} ${DIM}build${RESET}\n`);

await run('Building @ottocode/api', [bunExe, 'run', 'build'], API_DIR);
await run('Building @ottocode/web-sdk', [bunExe, 'run', 'build'], WEB_SDK_DIR);
await run('Building web UI', [bunExe, 'run', 'build'], WEB_DIR);

spinner.begin('Copying web assets to CLI');
rmSync(CLI_WEB_DIST, { recursive: true, force: true });
cpSync(WEB_DIST, CLI_WEB_DIST, { recursive: true });
spinner.succeed();

spinner.begin('Generating asset manifest');

interface Manifest {
	html: string;
	assets: {
		js: string[];
		css: string[];
		other: string[];
	};
}

function scanDirectory(dir: string, baseDir: string = dir): string[] {
	const files: string[] = [];
	const entries = readdirSync(dir);

	for (const entry of entries) {
		const fullPath = join(dir, entry);
		const stat = statSync(fullPath);

		if (stat.isDirectory()) {
			files.push(...scanDirectory(fullPath, baseDir));
		} else {
			const relativePath = relative(baseDir, fullPath);
			files.push(relativePath);
		}
	}

	return files;
}

const allFiles = scanDirectory(CLI_WEB_DIST);

const assetFiles: {
	urlPath: string;
	relativePath: string;
	importName: string;
}[] = [];

const manifest: Manifest = {
	html: '/index.html',
	assets: {
		js: [],
		css: [],
		other: [],
	},
};

for (const file of allFiles) {
	const urlPath = `/${file}`;
	assetFiles.push({
		urlPath,
		relativePath: file,
		importName: `asset${assetFiles.length}`,
	});

	if (file.endsWith('.html')) {
	} else if (file.endsWith('.js')) {
		manifest.assets.js.push(urlPath);
	} else if (file.endsWith('.css')) {
		manifest.assets.css.push(urlPath);
	} else if (!file.endsWith('.map')) {
		manifest.assets.other.push(urlPath);
	}
}

const manifestPath = join(CLI_WEB_DIST, 'manifest.json');
await Bun.write(manifestPath, JSON.stringify(manifest, null, 2));
spinner.succeed(
	`${manifest.assets.js.length} js, ${manifest.assets.css.length} css, ${manifest.assets.other.length} other`,
);

spinner.begin('Generating web-assets.ts');

function normalizePath(path: string): string {
	return path.replace(/^\//, '');
}

const assetFileByUrl = new Map(
	assetFiles.map((asset) => [asset.urlPath, asset.importName]),
);

const assetImports = assetFiles
	.map(
		(asset) =>
			`import ${asset.importName} from './web-dist/${asset.relativePath}' with { type: 'file' };`,
	)
	.join('\n');

const embeddedAssetsEntries = assetFiles
	.map((asset) => `\t['${asset.urlPath}', ${asset.importName}],`)
	.join('\n');

function formatAssetArray(files: string[]): string {
	if (files.length === 0) {
		return '[]';
	}

	const items = files
		.map(
			(file) => `\t\t${assetFileByUrl.get(file) ?? `'${normalizePath(file)}'`}`,
		)
		.join(',\n');

	return `[
${items}
\t]`;
}

const importsFile = `/**
 * Auto-generated file - DO NOT EDIT
 * Generated by scripts/build-web.ts
 * 
 * This file contains paths to all web UI assets.
 * The actual files are embedded using Bun's file loader.
 */

${assetImports}

// Export asset paths (filesystem paths for dev + resolveAsset helper)
export const webAssetPaths = {
\thtml: ${assetFileByUrl.get('/index.html') ?? "'index.html'"},
\tjs: ${formatAssetArray(manifest.assets.js)},
\tcss: ${formatAssetArray(manifest.assets.css)},
\tother: ${formatAssetArray(manifest.assets.other)},
};

// Export URL paths (for mapping requests to embedded files)
export const assetPaths = ${JSON.stringify(manifest, null, 2)};

const embeddedAssetFiles = new Map<string, string>([
${embeddedAssetsEntries}
]);

const embeddedAssetCache = new Map<string, Uint8Array>();

export async function getEmbeddedAsset(path: string): Promise<Uint8Array | undefined> {
\tconst cached = embeddedAssetCache.get(path);
\tif (cached) {
\t\treturn cached;
\t}

\tconst filePath = embeddedAssetFiles.get(path);
\tif (!filePath) {
\t\treturn undefined;
\t}

\tconst buffer = await Bun.file(filePath).bytes();
\tembeddedAssetCache.set(path, buffer);
\treturn buffer;
}
`;

const importsFilePath = join(CLI_DIR, 'src/web-assets.ts');
await Bun.write(importsFilePath, importsFile);
spinner.succeed();

const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
console.log(
	`\n${GREEN}${BOLD}  ✓${RESET} Web build complete ${DIM}in ${elapsed}s${RESET}\n`,
);
