import { connect as tlsConnect } from "node:tls";
import type { Duplex } from "node:stream";
import { createEvent } from "../hub/events";
import type { InMemoryEventBus } from "../hub/eventBus";

type AdapterLog = (message: string, fields?: Record<string, unknown>) => void;
type ConnectImpl = () => Duplex;

export type TwitchAdapterOptions = {
    eventBus: InMemoryEventBus;
    oauthToken: string;
    botNick: string;
    channel: string;
    host: string;
    port: number;
    reconnectMs: number;
    log: AdapterLog;
    connectImpl?: ConnectImpl;
};

type ParsedPrivmsg = {
    messageId: string | null;
    author: string;
    message: string;
};

const DEFAULT_RECONNECT_MS = 3000;

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

function parseTags(raw: string): Record<string, string> {
    const tags: Record<string, string> = {};
    for (const pair of raw.split(";")) {
        const idx = pair.indexOf("=");
        if (idx === -1) {
            tags[pair] = "";
            continue;
        }
        tags[pair.slice(0, idx)] = pair.slice(idx + 1);
    }
    return tags;
}

function parsePrivmsgLine(line: string): ParsedPrivmsg | null {
    let cursor = line.trim();
    if (!cursor) {
        return null;
    }

    let tags: Record<string, string> = {};
    if (cursor.startsWith("@")) {
        const endTags = cursor.indexOf(" ");
        if (endTags === -1) {
            return null;
        }
        tags = parseTags(cursor.slice(1, endTags));
        cursor = cursor.slice(endTags + 1).trim();
    }

    let prefix = "";
    if (cursor.startsWith(":")) {
        const endPrefix = cursor.indexOf(" ");
        if (endPrefix === -1) {
            return null;
        }
        prefix = cursor.slice(1, endPrefix);
        cursor = cursor.slice(endPrefix + 1).trim();
    }

    const trailingIdx = cursor.indexOf(" :");
    if (trailingIdx === -1) {
        return null;
    }

    const commandPart = cursor.slice(0, trailingIdx).trim();
    const message = cursor.slice(trailingIdx + 2);
    const parts = commandPart.split(/\s+/);
    if (parts.length < 2 || parts[0] !== "PRIVMSG") {
        return null;
    }

    const fallbackAuthor = prefix.includes("!") ? prefix.split("!")[0] : "twitch-user";
    const author = tags["display-name"] || fallbackAuthor;
    if (!message.trim()) {
        return null;
    }

    return {
        messageId: tags.id ?? null,
        author,
        message
    };
}

function normalizeOauthToken(value: string): string {
    const trimmed = value.trim();
    if (trimmed.startsWith("oauth:")) {
        return trimmed;
    }
    return `oauth:${trimmed}`;
}

function normalizeChannel(value: string): string {
    return value.trim().replace(/^#/, "").toLowerCase();
}

function normalizeNick(value: string): string {
    return value.trim().toLowerCase();
}

export function startTwitchAdapter(options: TwitchAdapterOptions): () => void {
    const connectImpl =
        options.connectImpl ??
        (() =>
            tlsConnect({
                host: options.host,
                port: options.port,
                servername: options.host
            }));

    const oauthToken = normalizeOauthToken(options.oauthToken);
    const channel = normalizeChannel(options.channel);
    const botNick = normalizeNick(options.botNick);
    const reconnectMs = options.reconnectMs || DEFAULT_RECONNECT_MS;
    const seenMessageIds = new Set<string>();
    let stopped = false;
    let activeSocket: Duplex | null = null;

    const emitStatus = (
        state: "connecting" | "connected" | "reconnecting" | "disconnected" | "error",
        message: string,
        details: Record<string, unknown> = {}
    ): void => {
        void options.eventBus.emit(
            createEvent("platform_status", {
                platform: "twitch",
                state,
                message,
                details: {
                    channel,
                    ...details
                }
            })
        );
    };

    const run = async (): Promise<void> => {
        while (!stopped) {
            emitStatus("connecting", "Connecting to Twitch IRC.", {
                host: options.host,
                port: options.port
            });
            await new Promise<void>((resolveConnection) => {
                const socket = connectImpl();
                activeSocket = socket;
                let settled = false;
                let buffer = "";

                const settle = (): void => {
                    if (settled) {
                        return;
                    }
                    settled = true;
                    activeSocket = null;
                    resolveConnection();
                };

                const onData = (chunk: Buffer | string): void => {
                    buffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
                    while (true) {
                        const newLineIdx = buffer.indexOf("\n");
                        if (newLineIdx === -1) {
                            break;
                        }
                        const rawLine = buffer.slice(0, newLineIdx).replace(/\r$/, "");
                        buffer = buffer.slice(newLineIdx + 1);
                        if (!rawLine) {
                            continue;
                        }

                        if (rawLine.startsWith("PING ")) {
                            const payload = rawLine.slice("PING ".length);
                            socket.write(`PONG ${payload}\r\n`);
                            continue;
                        }

                        const parsed = parsePrivmsgLine(rawLine);
                        if (!parsed) {
                            continue;
                        }
                        if (parsed.messageId) {
                            if (seenMessageIds.has(parsed.messageId)) {
                                continue;
                            }
                            seenMessageIds.add(parsed.messageId);
                            if (seenMessageIds.size > 5000) {
                                const oldest = seenMessageIds.values().next();
                                if (!oldest.done) {
                                    seenMessageIds.delete(oldest.value);
                                }
                            }
                        }

                        void options.eventBus.emit(
                            createEvent("chat_ingest", {
                                platform: "twitch",
                                author: parsed.author,
                                message: parsed.message
                            })
                        );
                    }
                };

                const onSecureConnect = (): void => {
                    options.log("twitch_irc_connected", {
                        host: options.host,
                        port: options.port,
                        channel
                    });
                    emitStatus("connected", "Connected to Twitch IRC.", {
                        host: options.host,
                        port: options.port
                    });
                    socket.write("CAP REQ :twitch.tv/tags twitch.tv/commands\r\n");
                    socket.write(`PASS ${oauthToken}\r\n`);
                    socket.write(`NICK ${botNick}\r\n`);
                    socket.write(`JOIN #${channel}\r\n`);
                };

                const onError = (error: Error): void => {
                    options.log("twitch_irc_error", {
                        error: error.message
                    });
                    emitStatus("error", "Twitch IRC connection error.", {
                        error: error.message
                    });
                    settle();
                };

                const onClose = (): void => {
                    options.log("twitch_irc_closed", {
                        channel
                    });
                    emitStatus(stopped ? "disconnected" : "reconnecting", stopped ? "Twitch IRC stopped." : "Twitch IRC disconnected.", {
                        reconnectMs
                    });
                    settle();
                };

                socket.on("secureConnect", onSecureConnect);
                socket.on("data", onData);
                socket.on("error", onError);
                socket.on("close", onClose);
            });

            if (!stopped) {
                emitStatus("reconnecting", "Waiting before Twitch IRC reconnect.", {
                    reconnectMs
                });
                await waitMs(reconnectMs);
            }
        }
    };

    void run();

    return () => {
        const hadActiveSocket = Boolean(activeSocket);
        stopped = true;
        activeSocket?.destroy();
        activeSocket = null;
        if (!hadActiveSocket) {
            emitStatus("disconnected", "Twitch IRC stopped.");
        }
    };
}
