import { randomBytes } from "node:crypto";
import { rmSync } from "node:fs";
import { createServer } from "node:http";
import { Socket } from "node:net";
import { resolve } from "node:path";
import { FileDataStore } from "../db/fileStore";
import { loadEnv } from "../env";
import { issueToken, resolveToken } from "../hub/auth";
import { InMemoryEventBus } from "../hub/eventBus";
import { LIMITS } from "../hub/limits";
import { WsTransport } from "../web/wsTransport";

function delay(ms: number): Promise<void> {
    return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}

function encodeClientTextFrame(message: string): Buffer {
    const payload = Buffer.from(message, "utf8");
    const payloadLength = payload.length;
    const mask = randomBytes(4);
    let header: Buffer;

    if (payloadLength < 126) {
        header = Buffer.from([0x81, payloadLength | 0x80]);
    } else if (payloadLength <= 65535) {
        header = Buffer.alloc(4);
        header[0] = 0x81;
        header[1] = 126 | 0x80;
        header.writeUInt16BE(payloadLength, 2);
    } else {
        header = Buffer.alloc(10);
        header[0] = 0x81;
        header[1] = 127 | 0x80;
        header.writeUInt32BE(0, 2);
        header.writeUInt32BE(payloadLength, 6);
    }

    const maskedPayload = Buffer.alloc(payloadLength);
    for (let i = 0; i < payloadLength; i += 1) {
        maskedPayload[i] = payload[i] ^ mask[i % 4];
    }

    return Buffer.concat([header, mask, maskedPayload]);
}

async function openWebSocket(
    host: string,
    port: number,
    path: string
): Promise<{ statusCode: number; socket: Socket | null }> {
    const socket = new Socket();

    await new Promise<void>((resolveConnect, rejectConnect) => {
        socket.once("error", rejectConnect);
        socket.connect(port, host, () => {
            socket.off("error", rejectConnect);
            resolveConnect();
        });
    });

    const secKey = randomBytes(16).toString("base64");
    socket.write(
        `GET ${path} HTTP/1.1\r\n` +
            `Host: ${host}:${port}\r\n` +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            `Sec-WebSocket-Key: ${secKey}\r\n` +
            "Sec-WebSocket-Version: 13\r\n\r\n"
    );

    const rawHeader = await new Promise<string>((resolveHeader, rejectHeader) => {
        let headerBuffer = Buffer.alloc(0);
        const timeout = setTimeout(() => {
            cleanup();
            rejectHeader(new Error("Handshake timeout."));
        }, 2000);

        const onData = (chunk: Buffer): void => {
            headerBuffer = Buffer.concat([headerBuffer, Buffer.from(chunk)]);
            const marker = headerBuffer.indexOf("\r\n\r\n");
            if (marker === -1) {
                return;
            }
            const headerPart = headerBuffer.subarray(0, marker + 4).toString("utf8");
            cleanup();
            resolveHeader(headerPart);
        };

        const onError = (error: Error): void => {
            cleanup();
            rejectHeader(error);
        };

        const cleanup = (): void => {
            clearTimeout(timeout);
            socket.off("data", onData);
            socket.off("error", onError);
        };

        socket.on("data", onData);
        socket.on("error", onError);
    });

    const statusLine = rawHeader.split("\r\n")[0] ?? "";
    const statusCode = Number(statusLine.split(" ")[1] ?? "0");
    if (statusCode !== 101) {
        socket.destroy();
        return { statusCode, socket: null };
    }

    return { statusCode, socket };
}

function assert(condition: boolean, message: string): void {
    if (!condition) {
        throw new Error(message);
    }
}

async function main(): Promise<void> {
    const env = loadEnv();
    const smokeDataFile = resolve(
        process.cwd(),
        "data",
        `phase12-smoke-${process.pid}-${Date.now()}.json`
    );
    const store = new FileDataStore(smokeDataFile);
    const eventBus = new InMemoryEventBus();
    const server = createServer((_req, res) => {
        res.writeHead(404);
        res.end();
    });
    const wsTransport = new WsTransport(
        server,
        eventBus,
        (token) => resolveToken(store, token, env.TOKEN_PEPPER),
        () => {}
    );

    const adminCommands: string[] = [];
    let socket: Socket | null = null;

    try {
        await store.init();
        wsTransport.attach();
        const stream = await store.createStream({ name: "phase12-smoke" });
        const admin = await issueToken(store, stream.id, "admin", env.TOKEN_PEPPER);

        eventBus.on("admin_command", async (event) => {
            adminCommands.push(event.payload.command);
        });

        await new Promise<void>((resolveListen, rejectListen) => {
            server.once("error", rejectListen);
            server.listen(0, "127.0.0.1", () => {
                server.off("error", rejectListen);
                resolveListen();
            });
        });

        const address = server.address();
        if (!address || typeof address === "string") {
            throw new Error("Failed to resolve smoke server address.");
        }

        const conn = await openWebSocket(
            "127.0.0.1",
            address.port,
            `/ws?token=${encodeURIComponent(admin.plainToken)}`
        );
        assert(conn.statusCode === 101, "Expected admin websocket status 101.");
        if (!conn.socket) {
            throw new Error("Expected admin websocket socket.");
        }
        const adminSocket = conn.socket;
        socket = adminSocket;

        for (let i = 0; i < LIMITS.ADMIN_COMMAND_RATE_LIMIT_COUNT + 5; i += 1) {
            adminSocket.write(
                encodeClientTextFrame(
                    JSON.stringify({
                        command: "poll",
                        args: ["status", String(i)]
                    })
                )
            );
        }

        await delay(200);
        assert(
            adminCommands.length === LIMITS.ADMIN_COMMAND_RATE_LIMIT_COUNT,
            "Expected admin command count to be capped by rate limit."
        );

        console.log(
            JSON.stringify(
                {
                    ok: true,
                    accepted: adminCommands.length,
                    configuredLimit: LIMITS.ADMIN_COMMAND_RATE_LIMIT_COUNT
                },
                null,
                2
            )
        );
    } finally {
        socket?.destroy();
        wsTransport.shutdown();
        await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
        rmSync(smokeDataFile, { force: true });
    }
}

main().catch((error: unknown) => {
    const message = error instanceof Error ? error.message : String(error);
    console.error(message);
    process.exit(1);
});
