import { LibsignalMessage } from '../libsignal-communication/LibsignalMessage';
import { PushTransport } from '../push-transport/PushTransport';
import { WebRTCTransport } from '../webrtc-transport/WebRTCTransport';
import { getRandomUUID } from '../crypto';
import { MessageInterface, MessageListener } from '../communication-interface/MessageInterface';
import { DBFactory } from '../db/DBFactory';

enum MessageType {
    PAYLOAD = 'p',
    WEBRTC_OFFER = 'rtc_o',
    WEBRTC_ANSWER = 'rtc_a',
    WEBRTC_ESTABLISH = 'rtc_e',
}



export class RTCPushMessage implements MessageInterface {
    private _initPromise: Promise<void>;
    private pushTransport?: PushTransport;
    private signalMessagePushTransport?: LibsignalMessage;
    private webRTCTransport?: WebRTCTransport;
    private signalMessageWebRTCTransport?: LibsignalMessage;


    constructor(private dbFactory: DBFactory) {
        this.dbFactory = dbFactory;
        this._initPromise = this.init();
    }

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

    async getOwnUserId(): Promise<string> {
        await this._initPromise;
        return await this.signalMessagePushTransport!.getOwnUserId();
    }

    requiresBootstrap(): boolean {
        return true;
    }

    private async init() {
        this.pushTransport = PushTransport.getInstance(this.dbFactory);

        this.signalMessagePushTransport = new LibsignalMessage(this.pushTransport, this.dbFactory);
        void this.signalMessagePushTransport.addMessageListener(this._messageRecieved.bind(this));

        this.webRTCTransport = new WebRTCTransport(await getRandomUUID());
        this.signalMessageWebRTCTransport = new LibsignalMessage(this.webRTCTransport, this.dbFactory);
        void this.signalMessageWebRTCTransport.addMessageListener(this._messageRecieved.bind(this));
    }

    async getOwnAddress(): Promise<string> {
        await this._initPromise;
        return JSON.stringify({
            keyInfo: await this.signalMessagePushTransport!.getKeyInformation(),
            userId: await this.signalMessagePushTransport!.getOwnUserId(),
            transportInfo: await this.pushTransport!.getOwnAddress()
        });
    }

    async addContact(_userId: string, address: any): Promise<string | void> {
        await this._initPromise;
        const { keyInfo, userId, transportInfo } = JSON.parse(address);
        await this.signalMessagePushTransport!.addContact(userId, keyInfo);
        await this.pushTransport!.addContact(userId, transportInfo);
    }

    async send(userIdTo: string, message: string): Promise<void> {
        await this._initPromise;
        try {
            return await this.signalMessageWebRTCTransport!.send(userIdTo, JSON.stringify({ type: MessageType.PAYLOAD, payload: message }));
        } catch (err: any) {
            if (err.message && err.message === 'Connection to user not established') {
                await this.connectWebRTC(userIdTo);
                await this.send(userIdTo, message);
            } else {
                throw err;
            }
        }
    }

    private _webRTCConnectionPromiseResolvers = new Map<string, (value?: any) => any>();
    private _webRTCConnectionPromises = new Map<string, Promise<void>>();

    async connectWebRTC(userId: string, messageHandler?: (message: string) => Promise<void>): Promise<void> {
        await this._initPromise;
        const webRTCAddress = await this.webRTCTransport!.getOwnAddress(userId);
        if (!webRTCAddress) {
            throw new Error('WebRTC address not found');
        }
        if (this._webRTCConnectionPromises.has(userId)) {
            return this._webRTCConnectionPromises.get(userId);
        }
        const connectionPromise: Promise<void> = new Promise(resolve => {
            this._webRTCConnectionPromiseResolvers.set(userId, resolve);
        });
        this._webRTCConnectionPromises.set(userId, connectionPromise);
        const payload = JSON.stringify({ webRTCAddress, type: MessageType.WEBRTC_OFFER });
        if (messageHandler) {
            await messageHandler(payload);
        } else {
            await this.signalMessagePushTransport?.send(userId, payload);
        }
        return await connectionPromise;
    }

    /**
     * Processes a webRTC connection "offer" 
     * @param fromId the userId of the party aiming to connect
     * @param otherAddress the webRTC address of the party aiming to connect
     * @returns a message to return to the party aiming to connect
     */
    async webRTCOffer(fromId: string, otherAddress: string): Promise<string> {
        await this._initPromise;
        const webRTCAddress = await this.webRTCTransport!.addContact(fromId, otherAddress);
        this._webRTCConnectionPromises.set(fromId, new Promise(resolve => {
            this._webRTCConnectionPromiseResolvers.set(fromId, resolve);
        }));
        return JSON.stringify({ webRTCAddress, type: MessageType.WEBRTC_ANSWER });
    }

    private _messageCallbacks: MessageListener[] = [];

    private async _messageRecieved(fromId: string, payload: any) {
        console.log(payload);
        await this._initPromise;
        if (typeof payload === 'string') {
            try {
                payload = JSON.parse(payload);
            } catch (err) {
                throw new Error('Invalid payload');
            }
        }
        if (payload.type === MessageType.PAYLOAD) {
            this._messageCallbacks.forEach(cb => cb(fromId, payload.payload));
        } else if (payload.type === MessageType.WEBRTC_OFFER) {
            try {
                const response = await this.webRTCOffer(fromId, payload.webRTCAddress);
                await this.signalMessagePushTransport!.send(fromId, response);
            } catch (err: any) {
                if (err.message && typeof err.message === 'string' && err.message.startsWith('invalid connection state')) {
                    return;
                }
                console.error(err);
            }


        } else if (payload.type === MessageType.WEBRTC_ANSWER) {
            await this.webRTCTransport!.addContact(fromId, payload.webRTCAddress);
            await this.signalMessageWebRTCTransport!.send(fromId, JSON.stringify({ type: MessageType.WEBRTC_ESTABLISH }));
            console.log('webrtc established!');
            this._webRTCConnectionPromiseResolvers.get(fromId)?.();
            this._webRTCConnectionPromiseResolvers.delete(fromId);
            this._webRTCConnectionPromises.delete(fromId);
        } else if (payload.type === MessageType.WEBRTC_ESTABLISH) {
            console.log('webrtc established!')
            this._webRTCConnectionPromiseResolvers.get(fromId)?.();
            this._webRTCConnectionPromiseResolvers.delete(fromId);
            this._webRTCConnectionPromises.delete(fromId);
        } else {
            console.error('Unknown message type');
            console.error(payload);
        }

    }

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

    /**
     * get a list of clients directly connected via webRTC
     * @returns the user ids that currently have an established webRTC connection
     */
    getRTCConnectedUsers(): string[] {
        return this.webRTCTransport?.getAllContacts() || [];
    }
}

