import { MessageInterface, MessageListener } from '../communication-interface/MessageInterface';
import { getRandomElements, getRandomSet } from '../crypto';
import { DBFactory } from '../db/DBFactory';
import { RTCPushMessage } from './RTCPushMessage';

enum MessageType {
    PAYLOAD = 'p',
    LOOKUP = 'l',
    LOOKUP_RESULT = 'lr',
    GET_ADDRESS = 'ga',
    GET_ADDRESS_RESULT = 'gar',
    GET_PREKEY = 'gp',
    GET_PREKEY_RESULT = 'gpr',
    PROXY_MESSAGE = 'pm',
}

type PayloadMessage = {
    type: MessageType.PAYLOAD,
    payload: any
}

type LookupMessage = {
    type: MessageType.LOOKUP,
    userId: string,
    path: string[]
}

type LookupMessageResult = {
    type: MessageType.LOOKUP_RESULT,
    userId: string,
    path: string[]
}

type ProtocolMessage = PayloadMessage | LookupMessage | LookupMessageResult;

export class RTCRoutingMessage implements MessageInterface {
    private rtcPushMessage: RTCPushMessage;
    private _messageCallbacks: MessageListener[] = [];


    constructor(private dbFactory: DBFactory) {
        this.dbFactory = dbFactory;
        this.rtcPushMessage = new RTCPushMessage(this.dbFactory);
        this.rtcPushMessage.addMessageListener(this._messageRecieved.bind(this));
    }

    private _messageRecieved(fromId: string, payload: string): void {
        const parsedMessage = JSON.parse(payload) as LookupMessage | LookupMessageResult | PayloadMessage;

        if (parsedMessage.type === MessageType.PAYLOAD) {
            this._messageCallbacks.forEach(cb => {
                cb(fromId, parsedMessage.payload);
            });
        }
        if (parsedMessage.type === MessageType.LOOKUP) {
            this.lookup(fromId, parsedMessage);
        }
        if (parsedMessage.type === MessageType.LOOKUP_RESULT) {
            this.extendPeerChain(fromId, parsedMessage);
        }
    }

    async lookup(fromId: string, message: LookupMessage): Promise<void> {
        const connectedUsers = this.rtcPushMessage.getRTCConnectedUsers();
        message.path.push(await this.rtcPushMessage.getOwnUserId());
        if (connectedUsers.includes(message.userId)) {
            await this.send(fromId, JSON.stringify({
                type: MessageType.LOOKUP_RESULT,
                userId: message.userId,
                path: message.path
            }));
        } else if (connectedUsers.length > 0) {
            await this.send(fromId, JSON.stringify({
                type: MessageType.LOOKUP_RESULT,
                userId: getRandomElements(connectedUsers, 1)[0],
                path: message.path
            }));
        }
    }

    private connectionTargets: Map<string, Promise<void>> = new Map();
    private connectionTargetsResolvers: Map<string, () => void> = new Map();

    async establishConnectionChain(userId: string, proxyUser?: string): Promise<void> {
        this.connectionTargets.set(userId, new Promise(resolve => this.connectionTargetsResolvers.set(userId, resolve)));
        const connectedUsers = this.rtcPushMessage.getRTCConnectedUsers();
        if (!proxyUser) {
            proxyUser = getRandomElements(connectedUsers, 1)[0];
        }
        if (!proxyUser) {
            throw new Error('No connected users to establish a chain with!');
        }
        await this.rtcPushMessage.send(proxyUser, JSON.stringify({
            type: MessageType.LOOKUP,
            userId,
            path: []
        }));
    }

    private peerChains: Map<string, string[]> = new Map();

    async extendPeerChain(from: string, payload: LookupMessageResult): Promise<void> {
        const { path, userId } = payload;
        if (path[path.length - 1] !== from) {
            throw new Error('Invalid path');
        }
        this.peerChains.set(userId, path);
        if (!this.connectionTargets.has(userId)) {
            this.establishConnectionChain(getRandomElements(Array.from(this.connectionTargets.keys()), 1)[0], userId);
        }
    }


    async send(userId: string, payload: ProtocolMessage | string): Promise<void> {
        if (typeof payload === 'string') {
            payload = JSON.stringify({ type: MessageType.PAYLOAD, payload });
        }
        const connectedUsers = this.rtcPushMessage.getRTCConnectedUsers();

        if (connectedUsers.length == 0) {
            return await this.rtcPushMessage.send(userId, JSON.stringify({ type: MessageType.PAYLOAD, payload }));
        } else if (!connectedUsers.includes(userId)) {
            await this.establishConnectionChain(userId);
        }
    }

    addMessageListener(listener: MessageListener): void | Promise<void> {
        this._messageCallbacks.push(listener);
    }

    addContact(userId: string, keyInformation: any): Promise<string | void> {
        return this.rtcPushMessage.addContact(userId, keyInformation);
    }

    getKeyInformation(): Promise<any> {
        return this.rtcPushMessage.getKeyInformation();
    }

    getOwnUserId(): Promise<string> {
        return this.rtcPushMessage.getOwnUserId();
    }

}