import * as ws from 'ws';
import {socketKey} from '../../../utils.js';
import getLogger from '../../../logger.js';

import {IOGateway} from '@interopio/gateway';
import GatewayEncoders = IOGateway.Encoding;

const logger = getLogger('mesh.ws.relay');

type Command
    = { type: 'hello', 'from': string, to: string }
    | { type: 'bye', 'from': string, to: 'all' | string }
    | { type: 'data', from: string, to: 'all' | string, data: {body: {type: string}}}

    ;

const codec = GatewayEncoders.transit<Command>({
    keywordize: new Map<string, GatewayEncoders.KeywordizeCommand>([
        ['/type', '*'],
        ['/message/body/type', '*'],
        ['/message/origin', '*'],
        ['/message/receiver/type', '*'],
        ['/message/source/type', '*'],
        ['/message/body/type', '*'],
    ])
});

export interface Relays {
    onMsg?: ((key: string, node: string, msg: string) => void);
    onErr?: ((key: string, err: Error) => void)
    send(key: string, node: string | undefined, msg: string,  cb: (key: string, err?: Error) => void): void;
}

class InternalRelays implements Relays {
    // key -> socket
    private readonly clients = new Map<string, ws.WebSocket>();
    // node -> key
    private readonly links = new Map<string, string>();
    public onMsg?: (key: string, node: string, msg: string) => void
    public onErr?: (key: string, err: Error) => void

    add(key: string, soc: ws.WebSocket) {
        this.clients.set(key, soc);
    }

    remove(key: string) {
        this.clients.delete(key);
        for (const [node, k] of this.links) {
            if (k === key) {
                this.links.delete(node);
            }
        }
    }

    receive(key: string, msg: string) {
        const node = this.link(key, msg);

        if (node && this.onMsg) {
            this.onMsg(key, node, msg);
        }
    }

    private link(key: string, msg: string): string | undefined {
        try {
            const decoded = codec.decode(msg);
            const {type, from, to} = decoded;
            if (to === 'all') {
                switch (type) {
                    case 'hello': {
                        if (logger.enabledFor('debug')) {
                            logger.debug(`${key} registers node ${from}`);
                        }
                        this.links.set(from, key);
                        break;
                    }
                    case 'bye': {
                        if (logger.enabledFor('debug')) {
                            logger.debug(`${key} unregisters node ${from}`);
                        }
                        this.links.delete(from);
                        break;
                    }
                }
                return;
            }
            return from;
        } catch (e) {
            if (this.onErr) {
                // probably a decode error or a bug
                this.onErr(key, e instanceof Error ? e : new Error(`link failed :${e}`));
            }
            else {
                logger.warn(`${key} unable to process ${msg}`, e);
            }
        }
    }

    send(key: string, node: string, msg: string, cb: (k: string, err?: Error) => void): void {
        const decoded = codec.decode(msg);
        if (logger.enabledFor('debug')) {
            logger.debug(`${key} sending msg to ${node} ${JSON.stringify(decoded)}`);
        }

        const clientKey = this.links.get(node);
        if (clientKey) {
            const client = this.clients.get(clientKey);
            if (client) {
                client.send(msg, {binary: false}, (err?: Error) => {
                    cb(clientKey, err)
                });
                return;
            }
        }
        throw new Error(`${key} no active link for ${decoded.to}`);
    }
}
const internal = new InternalRelays();
export const relays: Relays = internal;


async function create(server: {wss:ws.WebSocketServer}): Promise<{ close?: () => Promise<void> }> {

    logger.info(`relays server is listening`);
    server.wss
        .on('error', () => {
            logger.error(`error starting relays server`);
        })
        .on('connection', (socket, request) => {
            const key = socketKey(request.socket);
            logger.info(`${key} connected on relays`);
            internal.add(key, socket);
            //onOpen(connectedNodes, key);
            socket.on('error', (err: Error) => {
                logger.error(`[${key}] websocket error: ${err}`, err);
            });
            socket.on('message', (data, _isBinary) => {
                if (Array.isArray(data)) {
                    data = Buffer.concat(data);
                }
                try {
                    internal.receive(key, data as unknown as string);
                } catch (e) {
                    logger.warn(`[${key}] error processing received data '${data}'`, e);
                }
            });
            socket.on('close', (code, reason) => {
                internal.remove(key);

                logger.info(`${key} disconnected from relays`);
            });
        });
    return {
        close: async () => {
            server.wss.close();
        }
    };
}

export default create;
