import { WebRTCTransport } from '../webrtc-transport/WebRTCTransport';
import { getRandomUUID, getTimeBasedAddress, getRandomSet } from '../crypto';
import { MessageInterface, MessageListener } from '../communication-interface/MessageInterface';
import { EcdheAesGcmMessage } from '../crypto/ecdhe-aes-gcm-message';
import { LongAsyncRemoteFunction } from '../objectTransport/LongAsyncRemoteFunction';
import { TransportInterface } from '../communication-interface/TransportInterface';
import { WebRtcDht } from './DhtTransport';
import { PersistentTransport } from '../communication-interface/PersistentTransport';
import { ContactManager } from '../contact-management/ContactManager';
import { RsaOaepAesMessage } from '../crypto/rsa-oaep-aes-message';
import { DBFactory } from '../db/DBFactory';
import { BaseDto, ObjectStore } from '../db/DBAccess';

interface ExistingTransportParameterMap {
    'handshake': { originalFrom?: string, fromPK: string, pkSignature: string, webRTCAddressBundle: string, bootstrapAddress?: string },
    'handshakeResponse': { originalFrom?: string, fromPK: string, pkSignature: string, webRTCAddressBundle: string },

}

interface WebRtcTransportParameterMap {
    'connectionEstablished': { fromPK: string, pkSignature: string },
}

export class EcdheAesGcmKeyDto implements BaseDto {
    id: string;
    encryptionKey: string;
    signingKey: string;
    rsaPrivateKey: string;
    rsaPublicKey: string;
    timestamp: number;

    static get dbName() {
        return 'PrivDBEcdheAesGcmKey';
    }

    constructor(encryptionKey: string, signingKey: string, rsaPrivateKey: string, rsaPublicKey: string) {
        this.id = 'own';
        this.encryptionKey = encryptionKey;
        this.signingKey = signingKey;
        this.rsaPrivateKey = rsaPrivateKey;
        this.rsaPublicKey = rsaPublicKey;
        this.timestamp = Date.now();
    }
}

export class DhtTransportManager implements TransportInterface {
    private _initPromise: Promise<void>;
    private contactManager: ContactManager;
    private existingTransport: PersistentTransport;
    private existingTransportMessage?: EcdheAesGcmMessage;
    private webRTCTransport?: WebRTCTransport;
    private ecdheMessageWebRTCTransport?: EcdheAesGcmMessage;
    private rsaOaepAesMessage?: RsaOaepAesMessage;
    private existingMessageAsyncRF?: LongAsyncRemoteFunction<ExistingTransportParameterMap>;
    private webRTCAsyncRF?: LongAsyncRemoteFunction<WebRtcTransportParameterMap>;

    private dhtTransport?: WebRtcDht;
    private dhtTransportResolver?: (value: WebRtcDht) => void;
    private dhtTransportPromise = new Promise<WebRtcDht>(resolve => {
        this.dhtTransportResolver = resolve;
    });


    constructor(_existingTransport: PersistentTransport, _contactManager: ContactManager, private _db: DBFactory) {
        this.existingTransport = _existingTransport;
        this.contactManager = _contactManager;
        this._initPromise = this.init();
    }

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

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

    requiresBootstrap(): boolean {
        return true;
    }

    private async init() {
        const tba = await this.contactManager.getOwnTimeBasedAddress();
        const ecdheDao = await this._db.getDao<ObjectStore<EcdheAesGcmKeyDto>>(EcdheAesGcmKeyDto);
        let existingEcdhKey: EcdheAesGcmKeyDto | undefined = await ecdheDao.get('own');
        const todayMidnightUTC = new Date();
        todayMidnightUTC.setUTCHours(0, 0, 0, 0);
        const todayMidnightUTCTimestamp = todayMidnightUTC.getTime();
        if (existingEcdhKey && existingEcdhKey.timestamp < todayMidnightUTCTimestamp) {
            console.warn('existing ecdh key is stale, deleting');
            await ecdheDao.delete('own');
            existingEcdhKey = undefined;
        }
        this.existingTransportMessage = new EcdheAesGcmMessage(this.existingTransport, tba, existingEcdhKey?.encryptionKey, existingEcdhKey?.signingKey);
        const rsaKey = existingEcdhKey ? JSON.stringify({ privateKeyParams: existingEcdhKey?.rsaPrivateKey, publicKeyParams: existingEcdhKey?.rsaPublicKey }) : undefined;
        this.rsaOaepAesMessage = new RsaOaepAesMessage(this._db, new WebRTCTransport('/dev/null'), tba, rsaKey);
        if (!existingEcdhKey) {
            const { encryptionKey, signingKey } = JSON.parse(await this.existingTransportMessage.getEdchKey());
            const { privateKeyParams, publicKeyParams } = JSON.parse(await this.rsaOaepAesMessage.getRSAKey());
            await ecdheDao.add(new EcdheAesGcmKeyDto(encryptionKey, signingKey, privateKeyParams, publicKeyParams));
        }

        this.webRTCTransport = new WebRTCTransport(await getRandomUUID());
        const existingTransportPrivKey = JSON.parse(await this.existingTransportMessage.getEdchKey());
        this.ecdheMessageWebRTCTransport = new EcdheAesGcmMessage(this.webRTCTransport, tba, existingTransportPrivKey.encryptionKey, existingTransportPrivKey.signingKey);
        this.dhtTransport = new WebRtcDht(tba, this.webRTCTransport, this.ecdheMessageWebRTCTransport, this.rsaOaepAesMessage, this.contactManager);
        this.dhtTransport.addMessageListener(this.onMessage.bind(this));

        //we need to listen to unencrypted but authenticated requests for the handshake request
        void this.existingTransport.addMessageListener(this.onHandshakeRequest.bind(this));

        //when a keypair is added to the push transport, add it to the webrtc transport and the other way around
        this.existingTransportMessage.addAddContactListener((uid, keyInfo) => this.ecdheMessageWebRTCTransport!.addContact(uid, keyInfo, true, true));
        this.ecdheMessageWebRTCTransport.addAddContactListener((uid, keyInfo) => this.existingTransportMessage!.addContact(uid, keyInfo, true, true));

        this.existingMessageAsyncRF = new LongAsyncRemoteFunction<ExistingTransportParameterMap>(this.existingTransportMessage);
        this.existingMessageAsyncRF.register('handshake', this._r_handshake.bind(this));
        this.existingMessageAsyncRF.register('handshakeResponse', this._r_handshakeResponse.bind(this));

        this.webRTCAsyncRF = new LongAsyncRemoteFunction<WebRtcTransportParameterMap>(this.ecdheMessageWebRTCTransport, 'DhtTransportManager');
        this.webRTCAsyncRF.register('connectionEstablished', this._r_connectionEstablished.bind(this));

        await this.bootstrap();
        setInterval(async () => {
            if (((await this.getRTCConnectedUsers()).length || 0) < 1) {
                void this.bootstrap();
            }
        }, 1000 * 25 + Math.random() * 1000 * 10);
    }

    async bootstrap() {
        const contacts = await this.existingTransport.getAllContacts();
        if (contacts.length > 0) {
            const randomContacts = getRandomSet(contacts, 1);
            if (this._webRTCConnectionPromises.has(randomContacts[0])) {
                console.info('already in the process of connecting to this contact, skipping bootstrap');
                return;
            }
            for (const contact of randomContacts) {
                void (async () => {
                    try {
                        void this.sendHandshakeRequest(contact);
                    } catch (err) {
                        console.error(err);
                    }
                })();
            }
        }
    }

    private async initDhtTransport(userId: string, fromPK: string, signature: string) {
        await this.dhtTransport?.addRTCConnection(userId, fromPK, signature);
        this.dhtTransportResolver?.(this.dhtTransport!);
    }

    //existing transport details
    async getOwnAddress(): Promise<string> {
        await this._initPromise;
        return JSON.stringify({
            // keyInfo: await this.existingTransportMessage!.getKeyInformation(),
            userId: await this.existingTransportMessage!.getOwnUserId(),//time based address
            transportInfo: await this.existingTransport!.getOwnAddress(),
            contactInfo: await this.contactManager.getOwnIdentity()
        });
    }

    async addContact(_: string, address: any): Promise<string | void> {
        await this._initPromise;
        if (typeof address === 'string') {
            address = JSON.parse(address);
        }
        const { keyInfo, userId, transportInfo, contactInfo } = address;
        await this.existingTransport!.addContact(userId, transportInfo);
        // await this.existingTransportMessage!.addContact(userId, keyInfo);
        await this.contactManager.addContactAddress(contactInfo);
        await this.sendHandshakeRequest(userId);
    }

    async send(userIdTo: string, message: string): Promise<void> {
        await this._initPromise;
        const dht = await this.dhtTransportPromise;
        const tbUserId = await getTimeBasedAddress(userIdTo);
        return await dht.send(tbUserId, message);
    }

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

    async sendHandshakeRequest(userIdTo: string): Promise<void> {
        //TODO store future keypairs and send handshake directly instead of handshakeRequest; also include this in addContact address to make first time handshake secure
        const fromPK = JSON.stringify({
            userId: await this.existingTransportMessage?.getOwnUserId(), // time based address (=message.from)
            keyInfo: await this.existingTransportMessage?.getKeyInformation(),
        });
        const message = {
            from: await this.contactManager.getOwnTimeBasedAddress(),
            fromPK,
            pkSignature: await this.contactManager.sign(fromPK),
            transportAddress: this.existingTransport.requiresBootstrap() ? undefined : await this.existingTransport.getOwnAddress(),
            hr: true
        }
        await this.existingTransport.send(userIdTo, JSON.stringify(message));
    }

    async onHandshakeRequest(message: { from?: string, fromPK?: string, pkSignature?: string, transportAddress?: string, hr?: boolean }) {
        if (typeof message === 'string') {
            message = JSON.parse(message);
        }
        const { from, fromPK, pkSignature, transportAddress, hr } = message;
        console.info(`received handshake request from ${from}`);
        await this._initPromise;
        if (hr !== true || !from || !fromPK || !pkSignature) {
            return;
        }
        await this.contactManager.verify(from, fromPK, pkSignature);
        if (transportAddress) {
            await this.existingTransport.addContact(from, transportAddress);
        }
        const { userId, keyInfo } = JSON.parse(fromPK);
        await this.existingTransportMessage!.addContact(userId, keyInfo, false, true);
        await this.connectWebRTC(from);
    }

    async connectWebRTC(userId: string): Promise<void> {
        console.info(`attempting to connect to ${userId}`);
        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);
        setTimeout(() => {
            if (this._webRTCConnectionPromises.has(userId)) {
                console.warn(`connection attempt to ${userId} timed out`);
                this._webRTCConnectionPromises.delete(userId);
            }
        }, 10 * 1000 * (Math.random() + 0.5));
        let bootstrapAddress: string | undefined;
        if (!this.existingTransport.requiresBootstrap()) {
            bootstrapAddress = await this.existingTransport.getOwnAddress();
        }
        const fromPK = JSON.stringify({ ecdhKeyInfo: await this.ecdheMessageWebRTCTransport!.getKeyInformation(), rsaKeyInfo: await this.rsaOaepAesMessage!.getKeyInformation() });
        const pkSignature = await this.contactManager.sign(fromPK);
        const originalFrom = (await this.contactManager.isContact(userId)) ? await this.contactManager.getOwnIdentity() : undefined;
        if (originalFrom) {
            console.log('connecting to known contact, sending own identity');
        }
        await this.existingMessageAsyncRF!.call('handshake', userId, { originalFrom, fromPK, pkSignature, webRTCAddressBundle: webRTCAddress, bootstrapAddress }, { functionName: 'handshakeResponse', timeout: 30000 * (Math.random() + 0.5) });
        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 _r_handshake(fromId: string, { originalFrom, webRTCAddressBundle, bootstrapAddress, fromPK, pkSignature }: ExistingTransportParameterMap['handshake']): Promise<void> {
        await this._initPromise;

        if (originalFrom) {
            console.log('received handshake with identity');
            await this.contactManager.addContactAddress(originalFrom);
        }

        if (bootstrapAddress) {
            await this.existingTransport.addContact(fromId, bootstrapAddress);
        }

        const verified = await this.contactManager.verify(fromId, fromPK, pkSignature);
        await this.existingTransportMessage!.addContact(fromId, JSON.parse(fromPK).ecdhKeyInfo, false, verified);
        await this.rsaOaepAesMessage!.addContact(fromId, JSON.parse(fromPK).rsaKeyInfo, verified);
        const responseWebRTCAddressBundle = await this.webRTCTransport!.addContact(fromId, webRTCAddressBundle);
        if (!responseWebRTCAddressBundle) {
            throw new Error('could not generate webRTC address bundle');
        }

        this._webRTCConnectionPromises.set(fromId, new Promise(resolve => {
            this._webRTCConnectionPromiseResolvers.set(fromId, resolve);
        }));
        const ownPK = JSON.stringify({ ecdhKeyInfo: await this.ecdheMessageWebRTCTransport!.getKeyInformation(), rsaKeyInfo: await this.rsaOaepAesMessage!.getKeyInformation() });
        const myPkSignature = await this.contactManager.sign(ownPK);
        const myOriginalFrom = (await this.contactManager.isContact(fromId)) ? await this.contactManager.getOwnIdentity() : undefined;
        if (myOriginalFrom) {
            console.log('connecting to known contact, sending own identity');
        }
        void this.existingMessageAsyncRF!.call('handshakeResponse', fromId, { originalFrom: myOriginalFrom, fromPK: ownPK, pkSignature: myPkSignature, webRTCAddressBundle: responseWebRTCAddressBundle });
        await this.webRTCAsyncRF!.expectCallInvocation({ functionName: 'connectionEstablished', timeout: 30000 * (Math.random() + 0.5) }, fromId);
    }

    async _r_handshakeResponse(fromId: string, { originalFrom, fromPK, pkSignature, webRTCAddressBundle }: ExistingTransportParameterMap['handshakeResponse']): Promise<void> {
        await this._initPromise;
        console.info(`received handshake response from ${fromId}`);
        if (originalFrom) {
            console.log('received handshake with identity');
            await this.contactManager.addContactAddress(originalFrom);
        }
        const response = await this.webRTCTransport!.addContact(fromId, webRTCAddressBundle);
        if (response) {
            throw new Error('could not add contact to webRTC transport');
        }
        const verified = await this.contactManager.verify(fromId, fromPK, pkSignature);
        await this.ecdheMessageWebRTCTransport!.addContact(fromId, JSON.parse(fromPK).ecdhKeyInfo, verified);
        await this.rsaOaepAesMessage!.addContact(fromId, JSON.parse(fromPK).rsaKeyInfo, verified);
        await this.initDhtTransport(fromId, fromPK, pkSignature);
        const ownPK = JSON.stringify({ ecdhKeyInfo: await this.ecdheMessageWebRTCTransport!.getKeyInformation(), rsaKeyInfo: await this.rsaOaepAesMessage!.getKeyInformation() });
        const ownSignature = await this.contactManager.sign(ownPK);
        await this.webRTCAsyncRF!.call('connectionEstablished', fromId, { fromPK: ownPK, pkSignature: ownSignature });
        this._webRTCConnectionPromiseResolvers.get(fromId)?.();
        this._webRTCConnectionPromises.delete(fromId);
        console.log(`connection established after ${Math.round(performance.now())} ms`);
    }

    async _r_connectionEstablished(fromId: string, { fromPK, pkSignature }: WebRtcTransportParameterMap['connectionEstablished']): Promise<void> {
        console.info(`received connection established from ${fromId}`);
        await this.contactManager.verify(fromId, fromPK, pkSignature);
        await this.initDhtTransport(fromId, fromPK, pkSignature);
        this._webRTCConnectionPromiseResolvers.get(fromId)?.();
        this._webRTCConnectionPromises.delete(fromId);
        const actualFromAddress = await this.contactManager.getAddressFromTimeBasedAddress(fromId);
        this._connectionEstablishedCallbacks.forEach(callback => callback(actualFromAddress));
        console.log(`connection established after ${Math.round(performance.now())} ms`);
    }

    private _messageCallbacks: ((message: string) => void)[] = [];

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

    private _connectionEstablishedCallbacks: ((userId: string) => void)[] = [];

    addConnectionEstablishedListener(listener: ((userId: string) => void)): void | Promise<void> {
        this._connectionEstablishedCallbacks.push(listener);
    }

    private onMessage(fromId: string, message: string): void {
        this._messageCallbacks.forEach(callback => callback(message));
    }

    /**
     * get a list of clients directly connected via webRTC
     * @returns the user ids that currently have an established webRTC connection
     */
    async getRTCConnectedUsers(): Promise<string[]> {
        return (await Promise.all((this.webRTCTransport?.getAllContacts() || []).map(async uid => {
            try {
                return await this.contactManager.getAddressFromTimeBasedAddress(uid)
            } catch (err) {
                return null;
            }
        }))).filter(uid => uid !== null) as string[];
    }
}

