import {
	type RPCSchema,
	type RPCTransport,
	type SparkBunRPCSchema,
	type SparkBunRPCConfig,
	type RPCWithTransport,
	type AnyRPC,
	createRPC,
	defineSparkBunRPC,
} from "../shared/rpc.js";
import {
	type WebviewTagElement,
	type WebviewEventTypes,
} from "./webviewtag";
import { type WgpuTagElement, type WgpuEventTypes } from "./wgputag";
import "./global.d.ts";

const WEBVIEW_ID = window.__sparkbunWebviewId;
const HOST_SOCKET_PORT =
	window.__sparkbunHostSocketPort ?? window.__sparkbunRpcSocketPort;

class Electroview<T extends RPCWithTransport = AnyRPC> {
	hostSocket?: WebSocket;
	hostSocketCanSend = false;
	// user's custom rpc browser <-> bun — always provided via the constructor
	rpc: T;
	rpcHandler?: (msg: unknown) => void;
	carrots = {
		invoke: <R = unknown>(
			carrotId: string,
			method: string,
			params?: unknown,
			options?: { windowId?: string },
		) => this.invokeCarrot<R>(carrotId, method, params, options),
	};

	constructor(config: { rpc: T }) {
		this.rpc = config.rpc;
		this.init();
	}

	init() {
		this.initSocketToHost();

		// Set up handler for user RPC messages from the host runtime.
		const hostMessageHandler = this.receiveMessageFromHost.bind(this);
		window.__sparkbun!.receiveMessageFromHost = hostMessageHandler;
		window.__sparkbun!.receiveMessageFromBun = hostMessageHandler;

		if (this.rpc) {
			this.rpc.setTransport(this.createTransport());
		}

		const pendingMessages = window.__sparkbunPendingHostMessages;
		if (pendingMessages?.length) {
			window.__sparkbunPendingHostMessages = [];
			for (const message of pendingMessages) {
				hostMessageHandler(message);
			}
		}
	}

	initSocketToHost() {
		// Skip native socket when running in a remote browser (no port/webview ID)
		if (!HOST_SOCKET_PORT || !WEBVIEW_ID) {
			return;
		}

		// Note: Using ws:// for localhost is intentional - all RPC messages are
		// encrypted with per-webview AES-GCM keys, making TLS redundant
		const socket = new WebSocket(
			`ws://localhost:${HOST_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
		);

		this.hostSocket = socket;

		socket.addEventListener("open", () => {
			// this.hostSocket?.send("Hello from webview " + WEBVIEW_ID);
		});

		socket.addEventListener("message", async (event) => {
			const message = event.data;
			if (typeof message === "string") {
				try {
					const encryptedPacket = JSON.parse(message);

					const decrypted = await window.__sparkbun_decrypt(
						encryptedPacket.encryptedData,
						encryptedPacket.iv,
						encryptedPacket.tag,
					);

					this.hostSocketCanSend = true;
					this.rpcHandler?.(JSON.parse(decrypted));
				} catch (err) {
					console.error("Error parsing bun message:", err);
				}
			} else if (message instanceof Blob) {
				// Handle binary data (e.g., convert Blob to ArrayBuffer if needed)
			} else {
				console.error("UNKNOWN DATA TYPE RECEIVED:", event.data);
			}
		});

		socket.addEventListener("error", (event) => {
			this.hostSocketCanSend = false;
			console.error("Socket error:", event);
		});

		socket.addEventListener("close", (_event) => {
			this.hostSocketCanSend = false;
			// console.log("Socket closed:", event);
		});
	}

	createTransport(): RPCTransport {
		const that = this;
		return {
			send(message: unknown) {
				try {
					const messageString = JSON.stringify(message);
					that.sendMessageToHost(messageString);
				} catch (error) {
					console.error("host: failed to serialize message to webview", error);
				}
			},
			registerHandler(handler: (msg: unknown) => void) {
				that.rpcHandler = handler;
			},
		};
	}

	async sendMessageToHost(msg: string) {
		if (
			this.hostSocketCanSend &&
			this.hostSocket?.readyState === WebSocket.OPEN
		) {
			try {
				const { encryptedData, iv, tag } =
					await window.__sparkbun_encrypt(msg);

				const encryptedPacket = {
					encryptedData: encryptedData,
					iv: iv,
					tag: tag,
				};
				const encryptedPacketString = JSON.stringify(encryptedPacket);
				this.hostSocket.send(encryptedPacketString);
				return;
			} catch (error) {
				console.error("Error sending message to host via socket:", error);
			}
		}

		// if socket's are unavailable, fallback to postMessage
		window.__sparkbunHostBridge?.postMessage(msg);
	}

	receiveMessageFromHost(msg: unknown) {
		// NOTE: in the webview messages are passed by executing window.__sparkbun.receiveMessageFromHost(object)
		// so they're already parsed into an object here
		if (this.rpcHandler) {
			this.rpcHandler(msg);
		}
	}

	async invokeCarrot<R = unknown>(
		carrotId: string,
		method: string,
		params?: unknown,
		options?: { windowId?: string },
	): Promise<R> {
		const requestProxy = (this.rpc as any)?.request;
		if (!requestProxy || typeof requestProxy.invokeCarrot !== "function") {
			throw new Error("Renderer carrot invocation is not available in this SparkBun host.");
		}
		return requestProxy.invokeCarrot({
			carrotId,
			method,
			params,
			windowId: options?.windowId,
		}) as Promise<R>;
	}
	static defineRPC<Schema extends SparkBunRPCSchema>(
		config: SparkBunRPCConfig<Schema, "webview">,
	) {
		return defineSparkBunRPC("webview", {
			...config,
			extraRequestHandlers: {
				evaluateJavascriptWithResponse: ({ script }: { script: string }) => {
					return new Promise((resolve) => {
						try {
							const resultFunction = new Function(script);
							const result = resultFunction();

							if (result instanceof Promise) {
								result
									.then((resolvedResult) => {
										resolve(resolvedResult);
									})
									.catch((error) => {
										console.error("bun: async script execution failed", error);
										resolve(String(error));
									});
							} else {
								resolve(result);
							}
						} catch (error) {
							console.error("bun: failed to eval script", error);
							resolve(String(error));
						}
					});
				},
			},
		});
	}
}

export {
	type RPCSchema,
	type SparkBunRPCSchema,
	type SparkBunRPCConfig,
	createRPC,
	Electroview,
	type WebviewTagElement,
	type WebviewEventTypes,
	type WgpuTagElement,
	type WgpuEventTypes,
};

const SparkBun = {
	Electroview,
};

export default SparkBun;
