import {WebSocketServer} from 'ws';
import http from 'node:http';
import https from 'node:https';
import {SecureContextOptions} from 'node:tls';
import {AddressInfo} from 'node:net';
import {readFileSync} from 'node:fs';
import {AsyncLocalStorage} from 'node:async_hooks';
import {IOGateway} from '@interopio/gateway';
import wsGateway from './gateway/ws/core.js';
import NodeConnections from './mesh/connections.js';
import restDirectory from './mesh/rest-directory/routes.js';
import meshBroker from './mesh/ws/broker/core.js';
import meshRelays from './mesh/ws/relays/core.js';
import meshCluster from './mesh/ws/cluster/core.js';
import metrics from './metrics/routes.js';
import {compose} from './common/compose.js';
import {WebExchange, Middleware} from './server/types.js';
import {DefaultWebExchange, HttpServerRequest, HttpServerResponse} from './server/exchange.js';
import {socketKey} from './utils.js';
import getLogger from './logger.js';
import {localIp, portRange} from './server/address.js';
import * as monitoring from './server/monitoring.js';
import {acceptsOrigin, ProcessedOriginFilters, regexifyOriginFilters} from './server/ws-client-verify.js';
import {GatewayServer} from '../gateway-server';
import cors from './server/cors.ts';

const logger = getLogger('app');

function secureContextOptions(ssl: GatewayServer.SslConfig): SecureContextOptions {
    const options: SecureContextOptions = {};
    if (ssl.key) options.key = readFileSync(ssl.key);
    if (ssl.cert) options.cert = readFileSync(ssl.cert);
    if (ssl.ca) options.ca = readFileSync(ssl.ca);
    return options;
}

type RequestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => void;

function createListener(middleware: Middleware<HttpServerRequest, HttpServerResponse>,
                        routes: Map<string, RouteInfo>) {
    const storage = new AsyncLocalStorage();

    const listener = compose<WebExchange<HttpServerRequest, HttpServerResponse>>(
        async ({response}, next) => {
            response.headers.set('server', 'gateway-server');
            await next();
        },
        ...cors({
            origins: {allow: [/http:\/\/localhost(:\d+)?/]},
            methods: {allow: ['GET', 'HEAD', 'POST', 'DELETE']},
            headers: {allow: '*'},
            credentials: {allow: true}
        }),
        ...middleware,
        async ({request, response}, next) => {
            if (request.method === 'GET' && request.path === '/health') {
                response.statusCode = 200;
                response._res.end(http.STATUS_CODES[200]);
            } else {
                await next();
            }
        },
        async ({request, response}, next) => {
            if (request.method === 'GET' && request.path === '/') {
                response._res.end(`io.Gateway Server`);
            } else {
                await next();
            }
        },
        async ({request, response}, _next) => {
            const route = routes.get(request.path);
            if (route) {
                response.statusCode = 426;
                response._res
                    .appendHeader('Upgrade', 'websocket')
                    .appendHeader('Connection', 'Upgrade')
                    .appendHeader('Content-Type', 'text/plain');
                response._res.end(`This service [${request.path}] requires use of the websocket protocol.`);
            } else {
                response.statusCode = 404;
                response._res.end(http.STATUS_CODES[404]);
            }
        }
    );

    return (request: http.IncomingMessage, response: http.ServerResponse) => {
        const exchange = new DefaultWebExchange(new HttpServerRequest(request), new HttpServerResponse(response));
        return storage.run(exchange, async () => {
            if (logger.enabledFor('debug')) {
                const socket = exchange.request._req.socket;
                if (logger.enabledFor('debug')) {
                    logger.debug(`received ${exchange.method} request for ${exchange.path} from ${socket.remoteAddress}:${socket.remotePort}`);
                }
            }
            return await listener(exchange);
        });
    };
}

function promisify<T>(fn: (callback?: (err?: Error) => void) => T): Promise<T> {
    return new Promise<T>((resolve, reject) => {
        const r = fn((err?: Error) => {
            if (err) {
                reject(err);
            } else {
                resolve(r);
            }
        });
    });
}

type RouteInfo = {
    readonly default?: boolean,
    readonly ping?: number,
    readonly maxConnections?: number
    readonly originFilters?: ProcessedOriginFilters
    readonly factory: (server: { endpoint: string, wss: WebSocketServer }) => Promise<{
        info?: string,
        close?: () => Promise<void>
    }>,
    // set later in listening
    wss?: WebSocketServer,
    close?: () => Promise<void>
}

function memoryMonitor(config?: GatewayServer.ServerConfig['memory']) {
    if (config) {
        return monitoring.start({
            memoryLimit: config.memory_limit,
            dumpLocation: config.dump_location,
            dumpPrefix: config.dump_prefix,
            reportInterval: config.report_interval,
            maxBackups: config.max_backups
        });
    }
}

function regexAwareReplacer<T>(_key: string, value: T): string | T {
    return value instanceof RegExp ? value.toString() : value;
}

export const Factory = async (options: GatewayServer.ServerConfig): Promise<GatewayServer.Server> => {
    const ssl = options.ssl;
    const createServer = ssl ? (options: http.ServerOptions, handler: RequestHandler) => https.createServer({...options, ...secureContextOptions(ssl)}, handler) : (options: http.ServerOptions, handler: RequestHandler) => http.createServer(options, handler);
    const monitor = memoryMonitor(options.memory);
    const middleware: Middleware<HttpServerRequest, HttpServerResponse> = [];
    const routes: Map<string, RouteInfo> = new Map<string, RouteInfo>();
    const gw = IOGateway.Factory({...options.gateway});
    if (options.gateway) {
        const config = options.gateway;
        routes.set(config.route ?? '/', {
            default: config.route === undefined,
            ping: options.gateway.ping,
            factory: wsGateway.bind(gw),
            maxConnections: config.limits?.max_connections,
            originFilters: regexifyOriginFilters(config.origins)
        });
    }
    if (options.mesh) {
        const connections = new NodeConnections(options.mesh.timeout ?? 60000);
        middleware.push(...restDirectory(connections));
        const ping = options.mesh.ping ?? 30000;
        routes.set('/broker', {factory: meshBroker, ping: ping});
        routes.set('/cluster', {factory: meshCluster, ping: ping});
        routes.set('/relays', {factory: meshRelays, ping: ping});
    }
    if (options.metrics) {
        middleware.push(...(await metrics(options.metrics)));
    }

    const ports = portRange(options.port ?? 0);
    const host = options.host;
    const serverP: Promise<http.Server> = new Promise((resolve, reject) => {
        const onSocketError = (err: Error) => logger.error(`socket error: ${err}`, err);
        const server = createServer(
            {},
            createListener(middleware, routes));

        server.on('error', (e: Error) => {
            if (e['code'] === 'EADDRINUSE') {
                logger.debug(`port ${e['port']} already in use on address ${e['address']}`);
                const {value: port} = ports.next();
                if (port) {
                    logger.info(`retry starting server on port ${port} and host ${host ?? '<unspecified>'}`);
                    server.close();
                    server.listen(port, host);
                } else {
                    logger.warn(`all configured port(s) ${options.port} are in use. closing...`);
                    server.close();
                    reject(e);
                }
            } else {
                logger.error(`server error: ${e.message}`, e);
                reject(e);
            }
        });
        server
            .on('listening', async () => {
                const info = server.address() as AddressInfo;
                for (const [path, route] of routes) {
                    try {
                        logger.info(`creating ws server for [${path}]. max connections: ${route.maxConnections ?? '<unlimited>'}, origin filters: ${route.originFilters ? JSON.stringify(route.originFilters, regexAwareReplacer) : '<none>'}`);
                        const wss = new WebSocketServer({noServer: true});
                        const endpoint = `${ssl ? 'wss' : 'ws'}://${localIp}:${info.port}${path}`;
                        const handler = await route.factory({endpoint, wss});
                        const pingInterval = route.ping;
                        if (pingInterval) {
                            const pingIntervalId = setInterval(() => {
                                for (const client of wss.clients) {
                                    if (client['connected'] === false) {
                                        client.terminate();
                                    }
                                    client['connected'] = false;
                                    client.ping();
                                }
                            }, pingInterval);
                            wss.on('close', () => {
                                clearInterval(pingIntervalId);
                            });
                        }
                        route.wss = wss;
                        route.close = handler.close?.bind(handler);
                    } catch (e) {
                        logger.warn(`failed to init route ${path}`, e);
                    }
                }
                logger.info(`http server listening on ${info.address}:${info.port}`);
                resolve(server);
            });
        server
            .on('upgrade', (req, socket, head) => {
                socket.addListener('error', onSocketError);
                try {
                    const request = new HttpServerRequest(req);
                    const path = request.path ?? '/';
                    const route = (routes.get(path) ?? Array.from(routes.values()).find(route => {
                        if (path === '/' && route.default === true) {
                            return true;
                        }
                    }));
                    const host = request.host;
                    const info = socketKey(request.socket);
                    if (route?.wss) {
                        socket.removeListener('error', onSocketError);
                        const wss = route.wss;
                        if (route.maxConnections !== undefined && wss.clients?.size >= route.maxConnections) {
                            logger.warn(`${info} dropping ws connection request from ${host} on ${path}. max connections exceeded.`);
                            socket.destroy();
                            return;
                        }

                        const origin = request.headers['origin'];
                        if (!acceptsOrigin(origin, route.originFilters)) {
                            logger.info(`${info} dropping ws connection request from ${host} on ${path}. origin ${origin ?? '<missing>'}`);
                            socket.destroy();
                            return;
                        }
                        if (logger.enabledFor('debug')) {
                            logger.debug(`${info} accepted new ws connection request from ${host} on ${path}`);
                        }

                        wss.handleUpgrade(req, socket, head, (ws) => {
                            ws.on('pong', () => ws['connected'] = true);
                            ws.on('ping', () => {
                            });
                            wss.emit('connection', ws, req);
                        });
                    } else {
                        logger.warn(`${info} rejected upgrade request from ${host} on ${path}`);
                        socket.destroy();
                    }
                } catch (err) {
                    logger.error(`upgrade error: ${err}`, err);
                }
            })
            .on('close', async () => {
                logger.info(`http server closed.`);
            });
        try {
            const {value: port} = ports.next();
            server.listen(port, host);
        } catch (e) {
            logger.error(`error starting web socket server`, e);
            reject(e instanceof Error ? e : new Error(`listen failed: ${e}`));
        }
    });
    const server = await serverP;
    return new class implements GatewayServer.Server {
        readonly gateway = gw;

        async close(): Promise<void> {
            for (const [path, route] of routes) {
                try {
                    if (route.close) {
                        await route.close();
                    }
                    logger.info(`stopping ws server for [${path}]. clients: ${route.wss?.clients?.size ?? 0}`);
                    route.wss?.clients?.forEach(client => {
                        client.terminate();
                    });
                    route.wss?.close();
                } catch (e) {
                    logger.warn(`error closing route ${path}`, e);
                }
            }
            await promisify(cb => {
                server.closeAllConnections();
                server.close(cb);
            });
            if (monitor) {
                await monitoring.stop(monitor);
            }
        }
    }

}
