import getLogger from '../logger.js';

const logger = getLogger('mesh.connections');

export type Node = { node: string, endpoint: string, users?: string[] };
export type NodeConnection = { node: string, connect: Node[] }

type NodeValue = Omit<Node, 'users'> & {
    users: Set<string>, memberId: number, lastAccess: number
}

export interface NodeConnections {
    announce(nodes: Node[]): NodeConnection[];
    remove(node: string): boolean;
}

export default class InMemoryNodeConnections implements NodeConnections {
    private readonly nodes = new Map<string, NodeValue>();
    private readonly nodesByEndpoint = new Map<string, string>();
    private memberIds: number = 0;

    constructor(private readonly timeout: number = 60000) {
    }

    announce(nodes: Node[]): NodeConnection[] {
        for (const node of nodes) {
            const {node: nodeId, users, endpoint} = node;
            const foundId = this.nodesByEndpoint.get(endpoint);
            if (foundId) {
                if (foundId !== nodeId) {
                    logger.warn(`endpoint ${endpoint} clash. replacing node ${foundId} with ${nodeId}`);
                    this.nodesByEndpoint.set(endpoint, nodeId);
                    this.nodes.delete(foundId);
                }
            } else {
                logger.info(`endpoint ${endpoint} announced for ${nodeId}`);
                this.nodesByEndpoint.set(endpoint, nodeId);
            }
            this.nodes.set(nodeId, this.updateNode(node, new Set<string>(users ?? []), nodeId, this.nodes.get(nodeId)));
        }
        this.cleanupOldNodes();
        const sortedNodes = Array.from(this.nodes.values()).sort((a, b) => a.memberId - b.memberId);
        return nodes.map((e) => {
            const node = e.node;
            const connect = this.findConnections(sortedNodes, this.nodes.get(node));
            return {node, connect};
        });
    }

    remove(nodeId: string) {
        const removed = this.nodes.get(nodeId);
        if (removed) {
            this.nodes.delete(nodeId);
            const endpoint = removed.endpoint;
            this.nodesByEndpoint.delete(endpoint);
            logger.info(`endpoint ${endpoint} removed for ${nodeId}`);
            return true;
        }
        return false;
    }

    private updateNode(newNode: Node, users: Set<string>, _key: string, oldNode?: NodeValue): NodeValue {
        const node: Omit<NodeValue, 'lastAccess' | 'users'> = !oldNode ? {...newNode, memberId: this.memberIds++} : oldNode;
        return {...node, users, lastAccess: Date.now()};
    }

    private cleanupOldNodes() {
        const threshold = Date.now() - this.timeout;
        for (const [nodeId,v] of this.nodes) {
            if (v.lastAccess < threshold) {
                if (logger.enabledFor('debug')) {
                    logger.debug(`${nodeId} expired - no announcement since ${new Date(v.lastAccess).toISOString()}, timeout is ${this.timeout} ms.`);
                }
                this.nodes.delete(nodeId);
            }
        }
    }

    private findConnections(sortedNodes: NodeValue[], node?: NodeValue): Node[] {
        return sortedNodes.reduce((l, c) => {
            if (node !== undefined && c.memberId < node.memberId) {
                const intersection = new Set(c.users);
                node.users.forEach(user => {
                    if (!c.users.has(user)) {
                        intersection.delete(user);
                    }
                });
                c.users.forEach(user => {
                    if (!node.users.has(user)) {
                        intersection.delete(user);
                    }
                });
                if (intersection.size > 0) {
                    const e: Node = {node: c.node, endpoint: c.endpoint};
                    return l.concat(e);
                }
            }
            return l;
        }, new Array<Node>());
    }
}
