import * as ws from 'ws';
import getLogger from '../../logger.js';
import {socketKey} from '../../utils.js';
import {HttpServerRequest} from '../../server/exchange.js';
import {IOGateway} from '@interopio/gateway';
import GatewayEncoders = IOGateway.Encoding;

const log = getLogger('ws');
const codec = GatewayEncoders.json<IOGateway.Message>();

function initClient(this: IOGateway.Gateway, key: string, socket: ws.WebSocket, host?: string): IOGateway.GatewayClient<string> | undefined {
    const opts = {
        key,
        host,
        codec,
        onPing: () => {
            socket.ping((err?: Error) => {
                if (err) {
                    log.warn(`failed to ping ${key}`, err);
                }
                else {
                    log.info(`ping sent to ${key}`);
                }
            });
        },
        onDisconnect: (reason: 'inactive' | 'shutdown') => {
            switch (reason) {
                case 'inactive': {
                    log.warn(`no heartbeat (ping) received from ${key}, closing socket`);
                    socket.close(4001, 'ping expected');
                    break;
                }
                case 'shutdown': {
                    socket.close(1001, 'shutdown');
                    break;
                }
            }
        }
    };
    try {
        return this.client((data) => socket.send(data), opts);
    } catch (err) {
        log.warn(`${key} failed to create client`, err);
    }
}

async function create(this: IOGateway.Gateway, server: {endpoint: string, wss: ws.WebSocketServer}): Promise<{ close: () => Promise<void> }> {
    log.info('start gateway');
    await this.start({endpoint: server.endpoint});

    server.wss
        .on('error', (err: Error) => {
            log.error('error starting the gateway websocket server', err);
        })
        .on('connection', (socket, req) => {
            const request = new HttpServerRequest(req);
            const key = socketKey(request.socket);
            const host = request.host;
            log.info(`${key} connected on gw from ${host}`);
            const client = initClient.call(this, key, socket);
            if (!client) {
                log.error(`${key} gw client init failed`);
                socket.terminate();
                return;
            }
            socket.on('error', (err: Error) => {
                log.error(`${key} websocket error: ${err}`, err);
            });
            socket.on('message', (data, _isBinary) => {
                if (Array.isArray(data)) {
                    data = Buffer.concat(data);
                }
                // JSON.parse will invoke abstract operation ToString (coerce it) prior parsing (https://262.ecma-international.org/5.1/#sec-15.12.2)
                client.send(data as unknown as string);

            });
            socket.on('close', (code) => {
                log.info(`${key} disconnected from gw. code: ${code}`);
                client.close();
            });
        });
    return {
        close: async () => {
            server.wss.close();
            await this.stop();
        }
    }
}

export default create;
