import { createConnection, type Socket } from "node:net";
import { DEFAULT_MAX_FRAME_LENGTH } from "@earendil-works/pi-protocol";
import type { ByteTransport, ByteTransportFactory, ByteTransportHandlers } from "./transport.ts";

const MAX_UNIX_SOCKET_PATH_BYTES = process.platform === "linux" ? 107 : 103;

export interface UnixTransportOptions {
	path: string;
	maxPendingBytes?: number;
}

/** Creates fresh Unix-domain socket transports for PiClient connection attempts in Node-compatible runtimes. */
export function createUnixTransportFactory(options: UnixTransportOptions): ByteTransportFactory {
	if (options.path.length === 0) throw new TypeError("Unix transport path must not be empty");
	if (Buffer.byteLength(options.path) > MAX_UNIX_SOCKET_PATH_BYTES) {
		throw new TypeError(`Unix transport path is too long; maximum is ${MAX_UNIX_SOCKET_PATH_BYTES} UTF-8 bytes`);
	}
	const maxPendingBytes = options.maxPendingBytes ?? DEFAULT_MAX_FRAME_LENGTH * 4;
	if (!Number.isSafeInteger(maxPendingBytes) || maxPendingBytes <= 0) {
		throw new TypeError("Unix transport maxPendingBytes must be a positive safe integer");
	}
	if (process.platform === "win32") throw new Error("Unix transport is not supported on Windows");
	return (handlers) => connectUnixSocket(options.path, maxPendingBytes, handlers);
}

function connectUnixSocket(
	path: string,
	maxPendingBytes: number,
	handlers: ByteTransportHandlers,
): Promise<ByteTransport> {
	return new Promise<ByteTransport>((resolve, reject) => {
		const socket = createConnection(path);
		let connected = false;
		let terminal = false;

		const close = (): void => {
			if (terminal) return;
			terminal = true;
			socket.destroy();
			if (connected) handlers.onClose();
			else reject(new Error("Unix transport closed before connecting"));
		};

		socket.once("connect", () => {
			if (terminal) return;
			connected = true;
			resolve(
				new UnixByteTransport(socket, maxPendingBytes, () => {
					terminal = true;
				}),
			);
		});
		socket.on("data", (chunk) => {
			if (!terminal) handlers.onData(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
		});
		socket.once("end", close);
		socket.once("close", close);
		socket.once("error", (error) => {
			if (terminal) return;
			terminal = true;
			socket.destroy();
			if (connected) handlers.onError(error);
			else reject(error);
		});
	});
}

class UnixByteTransport implements ByteTransport {
	readonly #socket: Socket;
	readonly #maxPendingBytes: number;
	readonly #markLocalClose: () => void;
	#closed = false;
	#pendingBytes = 0;
	#writeTail: Promise<void> = Promise.resolve();

	constructor(socket: Socket, maxPendingBytes: number, markLocalClose: () => void) {
		this.#socket = socket;
		this.#maxPendingBytes = maxPendingBytes;
		this.#markLocalClose = markLocalClose;
	}

	send(chunk: Uint8Array): Promise<void> {
		if (!(chunk instanceof Uint8Array)) {
			return Promise.reject(new TypeError("Unix transport chunks must be Uint8Array"));
		}
		if (this.#closed) return Promise.reject(new Error("Unix transport is closed"));
		if (this.#pendingBytes + chunk.byteLength > this.#maxPendingBytes) {
			return Promise.reject(new Error("Unix transport exceeded its pending byte limit"));
		}
		this.#pendingBytes += chunk.byteLength;
		const bytes = chunk.slice();
		const write = this.#writeTail.then(() => this.#write(bytes));
		const tracked = write.finally(() => {
			this.#pendingBytes -= bytes.byteLength;
		});
		this.#writeTail = tracked.catch(() => {});
		return tracked;
	}

	close(): void {
		if (this.#closed) return;
		this.#closed = true;
		this.#markLocalClose();
		this.#socket.destroy();
	}

	#write(chunk: Uint8Array): Promise<void> {
		if (this.#closed || !this.#socket.writable) return Promise.reject(new Error("Unix transport is closed"));
		return new Promise<void>((resolve, reject) => {
			let callbackComplete = false;
			let drainComplete = false;
			let requiresDrain: boolean | undefined;
			let settled = false;

			const onDrain = (): void => {
				drainComplete = true;
				finish();
			};
			const cleanup = (): void => {
				this.#socket.off("drain", onDrain);
				this.#socket.off("close", onClose);
			};
			const fail = (error: Error): void => {
				if (settled) return;
				settled = true;
				cleanup();
				reject(error);
			};
			const finish = (): void => {
				if (settled || !callbackComplete || requiresDrain === undefined) return;
				if (requiresDrain && !drainComplete) return;
				settled = true;
				cleanup();
				resolve();
			};
			const onClose = (): void => fail(new Error("Unix transport closed during write"));

			try {
				this.#socket.once("close", onClose);
				const accepted = this.#socket.write(chunk, (error) => {
					if (error) {
						fail(error);
						return;
					}
					callbackComplete = true;
					finish();
				});
				requiresDrain = !accepted;
				if (requiresDrain) this.#socket.once("drain", onDrain);
				finish();
			} catch (error) {
				fail(error instanceof Error ? error : new Error(String(error)));
			}
		});
	}
}
