/**
 * Plugin Wrapper Generator
 *
 * Generates the code that wraps a plugin to run in a Worker Loader isolate.
 * The wrapper:
 * - Imports plugin hooks and routes from a separate module ("sandbox-plugin.js")
 * - Creates plugin context that proxies to BRIDGE service binding
 * - Exposes hooks and routes via RPC through WorkerEntrypoint
 *
 * Plugin code runs in its own module scope, isolated from the wrapper template.
 *
 */

import { normalizeCapabilities, type PluginManifest } from "emdash";

const TRAILING_SLASH_RE = /\/$/;
const NEWLINE_RE = /[\n\r]/g;
const COMMENT_CLOSE_RE = /\*\//g;

/**
 * Options for wrapper generation
 *
 * **Known limitation:** `site` info is baked into the generated wrapper code
 * at load time. If site settings change (e.g., admin updates site name/URL),
 * sandboxed plugins will see stale values until the worker restarts.
 * Trusted-mode plugins always read fresh values from the database.
 */
export interface WrapperOptions {
	/** Site info to inject into the context (no RPC needed) */
	site?: { name: string; url: string; locale: string };
}

export function generatePluginWrapper(manifest: PluginManifest, options?: WrapperOptions): string {
	const storageCollections = Object.keys(manifest.storage || {});
	const site = options?.site ?? { name: "", url: "", locale: "en" };
	// Normalize so manifests that still declare legacy names (`read:users`)
	// expose the same APIs as canonical names (`users:read`).
	const capabilities = normalizeCapabilities(manifest.capabilities ?? []);
	const hasReadUsers = capabilities.includes("users:read");
	const hasEmailSend = capabilities.includes("email:send");

	return `
// =============================================================================
// Sandboxed Plugin Wrapper
// Generated by @emdash-cms/cloudflare
// Plugin: ${sanitizeComment(manifest.id)}@${sanitizeComment(manifest.version)}
// =============================================================================

import { WorkerEntrypoint } from "cloudflare:workers";

// Plugin code lives in a separate module for scope isolation
import pluginModule from "sandbox-plugin.js";

// Extract hooks and routes from the plugin module
const hooks = pluginModule?.hooks || pluginModule?.default?.hooks || {};
const routes = pluginModule?.routes || pluginModule?.default?.routes || {};

// -----------------------------------------------------------------------------
// Context Factory - creates ctx that proxies to BRIDGE
// -----------------------------------------------------------------------------

function createContext(env) {
	const bridge = env.BRIDGE;
	const storageCollections = ${JSON.stringify(storageCollections)};
	
	// KV - proxies to bridge.kvGet/Set/Delete/List
	const kv = {
		get: (key) => bridge.kvGet(key),
		set: (key, value) => bridge.kvSet(key, value),
		delete: (key) => bridge.kvDelete(key),
		list: (prefix) => bridge.kvList(prefix)
	};
	
	// Storage collection factory
	function createStorageCollection(collectionName) {
		return {
			get: (id) => bridge.storageGet(collectionName, id),
			put: (id, data) => bridge.storagePut(collectionName, id, data),
			delete: (id) => bridge.storageDelete(collectionName, id),
			exists: async (id) => (await bridge.storageGet(collectionName, id)) !== null,
			query: (opts) => bridge.storageQuery(collectionName, opts),
			count: (where) => bridge.storageCount(collectionName, where),
			getMany: (ids) => bridge.storageGetMany(collectionName, ids),
			putMany: (items) => bridge.storagePutMany(collectionName, items),
			deleteMany: (ids) => bridge.storageDeleteMany(collectionName, ids)
		};
	}
	
	// Storage proxy that creates collections on access
	const storage = new Proxy({}, {
		get(_, collectionName) {
			if (typeof collectionName !== "string") return undefined;
			return createStorageCollection(collectionName);
		}
	});
	
	// Content access - proxies to bridge (capability enforced by bridge)
	const content = {
		get: (collection, id) => bridge.contentGet(collection, id),
		list: (collection, opts) => bridge.contentList(collection, opts),
		create: (collection, data) => bridge.contentCreate(collection, data),
		update: (collection, id, data) => bridge.contentUpdate(collection, id, data),
		delete: (collection, id) => bridge.contentDelete(collection, id)
	};
	
	// Media access - proxies to bridge (capability enforced by bridge)
	const media = {
		get: (id) => bridge.mediaGet(id),
		list: (opts) => bridge.mediaList(opts),
		upload: (filename, contentType, bytes) => bridge.mediaUpload(filename, contentType, bytes),
		getUploadUrl: () => { throw new Error("getUploadUrl is not available in sandbox mode. Use media.upload(filename, contentType, bytes) instead."); },
		delete: (id) => bridge.mediaDelete(id)
	};
	
	// HTTP access - proxies to bridge (capability + host enforced by bridge)
	const http = {
		fetch: async (url, init) => {
			const result = await bridge.httpFetch(url, init);
			// Bridge returns serialized response, reconstruct Response-like object
			return {
				status: result.status,
				ok: result.status >= 200 && result.status < 300,
				headers: new Headers(result.headers),
				text: async () => result.text,
				json: async () => JSON.parse(result.text)
			};
		}
	};
	
	// Logger - proxies to bridge
	const log = {
		debug: (msg, data) => bridge.log("debug", msg, data),
		info: (msg, data) => bridge.log("info", msg, data),
		warn: (msg, data) => bridge.log("warn", msg, data),
		error: (msg, data) => bridge.log("error", msg, data)
	};
	
	// Site info - injected at wrapper generation time, no RPC needed
	const site = ${JSON.stringify(site)};
	
	// URL helper - generates absolute URLs from paths
	const siteBaseUrl = ${JSON.stringify(site.url.replace(TRAILING_SLASH_RE, ""))};
	function url(path) {
		if (!path.startsWith("/")) {
			throw new Error('URL path must start with "/", got: "' + path + '"');
		}
		if (path.startsWith("//")) {
			throw new Error('URL path must not be protocol-relative, got: "' + path + '"');
		}
		return siteBaseUrl + path;
	}
	
	// User access - proxies to bridge (capability enforced by bridge)
	const users = ${hasReadUsers} ? {
		get: (id) => bridge.userGet(id),
		getByEmail: (email) => bridge.userGetByEmail(email),
		list: (opts) => bridge.userList(opts)
	} : undefined;
	
	// Email access - proxies to bridge (capability enforced by bridge)
	const email = ${hasEmailSend} ? {
		send: (message) => bridge.emailSend(message)
	} : undefined;
	
	return {
		plugin: {
			id: env.PLUGIN_ID,
			version: env.PLUGIN_VERSION
		},
		storage,
		kv,
		content,
		media,
		http,
		log,
		site,
		url,
		users,
		email
	};
}

// -----------------------------------------------------------------------------
// Worker Entrypoint (RPC interface)
// -----------------------------------------------------------------------------

export default class PluginEntrypoint extends WorkerEntrypoint {
	async invokeHook(hookName, event) {
		const ctx = createContext(this.env);
		
		// Find the hook handler
		const hookDef = hooks[hookName];
		
		if (!hookDef) {
			// No handler for this hook - that's ok, return undefined
			return undefined;
		}
		
		// Get the handler (might be wrapped in config object)
		const handler = typeof hookDef === "function" ? hookDef : hookDef.handler;
		
		if (typeof handler !== "function") {
			throw new Error(\`Hook \${hookName} handler is not a function\`);
		}
		
		// Execute the hook
		return handler(event, ctx);
	}
	
	async invokeRoute(routeName, input, serializedRequest) {
		const ctx = createContext(this.env);
		
		// Find the route handler
		const route = routes[routeName];
		
		if (!route) {
			throw new Error(\`Route not found: \${routeName}\`);
		}
		
		// Get handler (might be direct function or object with handler)
		const handler = typeof route === "function" ? route : route.handler;
		
		if (typeof handler !== "function") {
			throw new Error(\`Route \${routeName} handler is not a function\`);
		}
		
		// Execute the route handler with input, request metadata, and context
		return handler({ input, request: serializedRequest, requestMeta: serializedRequest.meta }, ctx);
	}
}
`;
}

/**
 * Sanitize a string for inclusion in a JavaScript comment.
 * Prevents comment injection via manifest.id or manifest.version containing
 * newlines or comment-closing sequences.
 */
function sanitizeComment(s: string): string {
	return s.replace(NEWLINE_RE, " ").replace(COMMENT_CLOSE_RE, "* /");
}
