import { createServer } from "node:net";
import { createDefaultPiboPluginRegistry } from "../plugins/builtin.js";
import { PiboSessionRouter } from "../core/session-router.js";
import { DEFAULT_GATEWAY_HOST, DEFAULT_GATEWAY_PORT, encodeFrame, errorResponse, isGatewayRequestFrame, } from "./protocol.js";
function parseJsonLine(line) {
    try {
        return JSON.parse(line);
    }
    catch {
        throw new Error("Invalid JSON frame");
    }
}
function createConnection(socket) {
    return {
        socket,
        send(frame) {
            if (socket.destroyed)
                return;
            socket.write(encodeFrame(frame));
        },
    };
}
async function createGatewaySessionStore(options) {
    const { createDefaultPiboSessionStore, SqlitePiboSessionStore } = await import("../sessions/sqlite-store.js");
    return options.sessionDbPath
        ? new SqlitePiboSessionStore(options.sessionDbPath)
        : createDefaultPiboSessionStore();
}
export class PiboGatewayServer {
    options;
    pluginRegistry;
    sessionStore;
    ownsSessionStore = false;
    router;
    startedChannels = [];
    connections = new Set();
    server;
    unsubscribe;
    constructor(options = {}) {
        this.options = options;
        this.pluginRegistry = options.pluginRegistry ?? createDefaultPiboPluginRegistry();
    }
    async start() {
        if (this.server)
            return;
        this.validateChannels();
        this.sessionStore = this.options.sessionStore ?? (await createGatewaySessionStore(this.options));
        this.ownsSessionStore = !this.options.sessionStore;
        this.router = new PiboSessionRouter({
            persistSession: this.options.persistSession,
            pluginRegistry: this.pluginRegistry,
            sessionStore: this.sessionStore,
        });
        this.unsubscribe = this.router.subscribe((event) => this.broadcastRouterEvent(event));
        this.server = createServer((socket) => this.handleSocket(socket));
        await this.pluginRegistry.getAuthService()?.start?.();
        await new Promise((resolve, reject) => {
            this.server.once("error", reject);
            this.server.listen(this.options.port ?? DEFAULT_GATEWAY_PORT, this.options.host ?? DEFAULT_GATEWAY_HOST, () => {
                this.server.off("error", reject);
                resolve();
            });
        });
        if (this.options.startChannels !== false) {
            await this.startChannels();
        }
    }
    async stop() {
        await this.stopChannels();
        await this.pluginRegistry.getAuthService()?.stop?.();
        this.unsubscribe?.();
        this.unsubscribe = undefined;
        for (const connection of this.connections) {
            connection.socket.destroy();
        }
        this.connections.clear();
        if (this.server) {
            await new Promise((resolve, reject) => {
                this.server.close((error) => (error ? reject(error) : resolve()));
            });
            this.server = undefined;
        }
        await this.router?.disposeAll();
        this.router = undefined;
        if (this.ownsSessionStore) {
            this.sessionStore?.close?.();
        }
        this.sessionStore = undefined;
        this.ownsSessionStore = false;
    }
    handleSocket(socket) {
        const connection = createConnection(socket);
        this.connections.add(connection);
        let buffer = "";
        socket.setEncoding("utf-8");
        socket.on("data", (chunk) => {
            buffer += chunk;
            let newlineIndex = buffer.indexOf("\n");
            while (newlineIndex !== -1) {
                const line = buffer.slice(0, newlineIndex).trim();
                buffer = buffer.slice(newlineIndex + 1);
                if (line) {
                    void this.handleLine(connection, line);
                }
                newlineIndex = buffer.indexOf("\n");
            }
        });
        socket.once("close", () => {
            this.connections.delete(connection);
        });
        socket.once("error", () => {
            this.connections.delete(connection);
        });
    }
    async handleLine(connection, line) {
        let frame;
        try {
            frame = parseJsonLine(line);
            if (!isGatewayRequestFrame(frame)) {
                throw new Error("Invalid request frame");
            }
        }
        catch (error) {
            connection.send(errorResponse("invalid", error));
            return;
        }
        try {
            const output = await this.requireRouter().emit(frame.event);
            const response = {
                type: "res",
                id: frame.id,
                ok: true,
                payload: output,
            };
            connection.send(response);
        }
        catch (error) {
            connection.send(errorResponse(frame.id, error));
        }
    }
    broadcastRouterEvent(event) {
        for (const connection of this.connections) {
            connection.send({ type: "event", event: "router", payload: event });
        }
    }
    async startChannels() {
        const context = this.createChannelContext();
        for (const channel of this.pluginRegistry.getChannels()) {
            if (channel.auth.mode === "none") {
                console.error(`Warning: channel "${channel.name}" starts without auth`);
            }
            await channel.start(context);
            this.startedChannels.push(channel);
        }
    }
    validateChannels() {
        for (const channel of this.pluginRegistry.getChannels()) {
            if (channel.auth.mode === "required" && !this.pluginRegistry.getAuthService()) {
                throw new Error(`Channel "${channel.name}" requires auth, but no auth service is registered`);
            }
        }
    }
    async stopChannels() {
        while (this.startedChannels.length > 0) {
            const channel = this.startedChannels.pop();
            await channel.stop?.();
        }
    }
    createChannelContext() {
        return {
            emit: (event) => this.requireRouter().emit(event),
            subscribe: (listener) => this.requireRouter().subscribe(listener),
            getSession: (id) => this.requireSessionStore().get(id),
            createSession: (input) => {
                const profile = this.pluginRegistry.resolveProfileName(input.profile);
                return this.requireSessionStore().create({ ...input, profile });
            },
            updateSession: (id, input) => this.requireSessionStore().update(id, input),
            deleteSession: (id) => this.requireSessionStore().delete?.(id) ?? false,
            findSessions: (input) => this.requireSessionStore().find(input),
            listSessions: () => this.requireSessionStore().list?.() ?? [],
            getGatewayActions: () => this.pluginRegistry.getGatewayActionInfos(),
            getProfiles: () => this.pluginRegistry.getProfileInfos(),
            getCapabilityCatalog: () => this.pluginRegistry.getCapabilityCatalog(),
            upsertProfile: (profile) => this.pluginRegistry.upsertProfile(profile),
            removeProfile: (name) => this.pluginRegistry.removeProfile(name),
            upsertContextFile: (contextFile) => this.pluginRegistry.upsertContextFile(contextFile),
            removeContextFile: (key) => this.pluginRegistry.removeContextFile(key),
            emitProductEvent: (event) => this.pluginRegistry.emitProductEvent(event),
            subscribeProductEvents: (listener) => this.pluginRegistry.onProductEvent(listener),
            auth: this.pluginRegistry.getAuthService(),
            getWebApps: () => this.pluginRegistry.getWebApps(),
        };
    }
    requireRouter() {
        if (!this.router)
            throw new Error("Gateway router is not started");
        return this.router;
    }
    requireSessionStore() {
        if (!this.sessionStore)
            throw new Error("Gateway session store is not started");
        return this.sessionStore;
    }
}
export async function runGatewayServer(options = {}) {
    const server = new PiboGatewayServer(options);
    await server.start();
    const host = options.host ?? DEFAULT_GATEWAY_HOST;
    const port = options.port ?? DEFAULT_GATEWAY_PORT;
    console.error(`pibo gateway listening on ${host}:${port}`);
    const stop = async () => {
        await server.stop();
    };
    process.once("SIGINT", () => {
        void stop().finally(() => process.exit(0));
    });
    process.once("SIGTERM", () => {
        void stop().finally(() => process.exit(0));
    });
}
