import { BaseDto } from '../lib/db/DBAccess';
import { createFactory, getFactory } from '../lib/db/DBFactory';
import { EcdheAesGcmMessage } from '../lib/crypto/ecdhe-aes-gcm-message';
import { RTCPushMessage } from '../lib/message-manager/RTCPushMessage';
import { LongAsyncRemoteFunction } from '../lib/objectTransport/LongAsyncRemoteFunction';
import { DhtTransportManager } from '../lib/dht-network/DhtTransportManager';
import { PushTransport } from '../lib/push-transport/PushTransport';
import { ContactManager } from '../lib/contact-management/ContactManager';

declare global {
    interface Window { tests: any; }
}

class PersonDto implements BaseDto {
    id?: string;

    name: string;
    constructor(_name: string) {
        this.name = _name;
    }

    static get dbName(): string {
        return 'Persons';
    }
}

class UniversityDto implements BaseDto {
    id?: string;

    name: string;
    constructor(_name: string) {
        this.name = _name;
    }

    static get dbName(): string {
        return 'Universities';
    }
}

createFactory([PersonDto, UniversityDto]);

window.tests = {};

function setBodyText(text: string) {
    document.body.textContent = text;
}

window.tests.insertPerson = async (name: string) => {
    const factory = getFactory();
    const personDao = await factory.getDao(PersonDto);
    setBodyText(`inserting ${name}`);
    const person = new PersonDto(name);
    return await personDao.add(person);
}

window.tests.updatePerson = async (person: PersonDto) => {
    const factory = getFactory();
    const personDao = await factory.getDao(PersonDto);
    setBodyText(`updating ${person.name}`);
    return await personDao.update(person);
}

window.tests.deletePerson = async (person: PersonDto) => {
    const factory = getFactory();
    const personDao = await factory.getDao(PersonDto);
    setBodyText(`deleting ${person.name}`);
    return await personDao.delete(person);
}

window.tests.getAllPersons = async () => {
    const factory = getFactory();
    const personDao = await factory.getDao(PersonDto);
    setBodyText('getting all persons');
    return await personDao.getAll();
}

window.tests.listenForPersonChange = async () => {
    const factory = getFactory();
    const personDao = await factory.getDao(PersonDto);
    const universityDao = await factory.getDao(UniversityDto);
    setBodyText('adding person change listener');
    const changePromise = new Promise(resolve => {
        void factory.transactionManager.addDataChangeListener((changedPerson: BaseDto) => {
            resolve(changedPerson);
        }, PersonDto);
    });
    setBodyText('person change listener called');
    await universityDao.add(new UniversityDto('mit'));
    await personDao.add(new PersonDto('marge'));
    return changePromise;
}

const rtcPM = new RTCPushMessage(getFactory());

window.tests.getOwnAddress = async () => {
    setBodyText('getting own address');
    return await rtcPM.getOwnAddress();
}

window.tests.addContact = async (otherAddress: string) => {
    setBodyText('adding contact');
    await rtcPM.addContact('', otherAddress);
}

window.tests.sendMessage = async (address: string, message: string) => {
    setBodyText('sending message');
    await rtcPM.send(address, message);
}

window.tests.waitForMessage = (numMessages?: number) => {
    return new Promise(resolve => {
        if (numMessages) {
            const values: object[] = [];
            void rtcPM.addMessageListener((from, msg) => {
                setBodyText(`got message from ${from}`);
                values.push({ from, msg });
                if (values.length === numMessages) {
                    resolve(values);
                }
            })
        } else {
            void rtcPM.addMessageListener((from, msg) => {
                setBodyText(`got message from ${from}`);
                resolve({ from, msg });
            });
        }
    });
}

type TestParam = {
    name?: string;
    age?: number;
};

type TestReturn = {
    ssn?: string;
}

interface RemoteFunctionParameterMap {
    'test': TestParam;
    'testreturn': TestReturn;
}
export class RemoteCallable {
    larf: LongAsyncRemoteFunction<RemoteFunctionParameterMap>;
    private _returnCallback?: (from: string) => any;
    constructor() {
        this.larf = new LongAsyncRemoteFunction<RemoteFunctionParameterMap>(rtcPM);
        this.larf.register('test', this.test.bind(this));
        this.larf.register('testreturn', this.testReturn.bind(this));
    }

    private async test(from: string, param: TestParam): Promise<void> {
        setBodyText(`got test from ${from}`);
        console.log(`age: ${param.age}, name: ${param.name}`);
        await this.call('testreturn', from, { ssn: '12345' });
    }

    setReturnCallback(cb: (from: string) => any) {
        this._returnCallback = cb;
    }

    private async testReturn(from: string, param: TestReturn) {
        setBodyText(`got testreturn from ${from}`);
        this._returnCallback!(from);
        param = param as TestReturn;
        console.log(`ssn: ${param.ssn}`);
    }

    public async call<K extends keyof RemoteFunctionParameterMap>(name: K, targetUserId: string, param: RemoteFunctionParameterMap[K]): Promise<void> {
        return await this.larf.call(name, targetUserId, param);
    }
}

const rc = new RemoteCallable();

window.tests.sendTest = (targetUserId: string, name: string, age: number) => {
    setBodyText('calling test remote function');
    void rc.call('test', targetUserId, { name, age });
}

window.tests.waitForReturnCallback = () => {
    setBodyText('waitForReturnCallback');
    return new Promise(resolve => {
        rc.setReturnCallback((from: string) => {
            setBodyText('returnCallback Called');
            resolve(from);
        });
    });
}

window.tests.ecdhAesGcmMessageEncryptDecrypt = async (input: string) => {
    try {
        let decrypt: (input: string) => any;
        const transportMockDecrypt = {
            send: async (to: string, msg: string) => {
                throw new Error('not implemented');
            },
            getOwnAddress: async () => 'ownAddress',
            addMessageListener: (cb: (input: string) => any) => { decrypt = cb; },
            addContact: async () => { throw new Error('not implemented'); },
            requiresBootstrap: () => { return false; },
        }
        const ecdhAesGcmMessageDecrypt = new EcdheAesGcmMessage(transportMockDecrypt, 'test2');

        // eslint-disable-next-line no-async-promise-executor
        const encrypted: { to: string, msg: string } = await new Promise(async (resolve, reject) => {
            try {
                const transportMockEncrypt = {
                    send: async (to: string, msg: string) => {
                        resolve({ to, msg });
                    },
                    getOwnAddress: async () => 'ownAddress',
                    addMessageListener: () => {/**lol */ },
                    addContact: async () => { throw new Error('not implemented'); },
                    requiresBootstrap: () => { return false; },
                }
                const ecdhAesGcmMessage = new EcdheAesGcmMessage(transportMockEncrypt, 'test1');
                console.log('adding contacts');
                await ecdhAesGcmMessage.addContact('test2', await ecdhAesGcmMessageDecrypt.getKeyInformation());
                console.log('sending message');
                await ecdhAesGcmMessage.send('test2', input);
                console.log('message sent');

            } catch (err) {
                reject(err);
            }
        });

        const decrypted = await new Promise((resolve, reject) => {
            try {
                ecdhAesGcmMessageDecrypt.addMessageListener((from, msg) => resolve({ from, msg }));
                decrypt!(encrypted.msg);
            } catch (err) {
                reject(err);
            }
        });
        return decrypted;
    } catch (err: any) {
        return err?.message + err?.stack;

    }
}

window.tests.ecdhAesGcmMessageEncryptDecryptSpoofing = async (input: string) => {
    let decrypt: (input: string) => any;
    const transportMockDecrypt = {
        send: async (to: string, msg: string) => {
            throw new Error('not implemented');
        },
        getOwnAddress: async () => 'test2_physical',
        addMessageListener: (cb: (input: string) => any) => { decrypt = cb; },
        addContact: async () => { throw new Error('not implemented'); },
        requiresBootstrap: () => { return false; },
    }
    const ecdhAesGcmMessageDecrypt = new EcdheAesGcmMessage(transportMockDecrypt, 'test2');

    // eslint-disable-next-line no-async-promise-executor
    const encrypted: { to: string, msg: string } = await new Promise(async (resolve, reject) => {
        try {
            const transportMockEncrypt = {
                send: async (to: string, msg: string) => {
                    resolve({ to, msg });
                },
                getOwnAddress: async () => 'test1_physical',
                addMessageListener: () => {/**lol */ },
                addContact: async () => { throw new Error('not implemented'); },
                requiresBootstrap: () => { return false; },
            }
            const ecdhAesGcmMessage = new EcdheAesGcmMessage(transportMockEncrypt, 'test1');
            console.log('adding contacts');
            await ecdhAesGcmMessage.addContact('test2', await ecdhAesGcmMessageDecrypt.getKeyInformation());
            console.log('sending message');
            await ecdhAesGcmMessage.send('test2', input);
            console.log('message sent');

        } catch (err) {
            reject(err);
        }
    });

    // eslint-disable-next-line no-async-promise-executor
    const encrypted2: { to: string, msg: string } = await new Promise(async (resolve, reject) => {
        try {
            const transportMockEncrypt = {
                send: async (to: string, msg: string) => {
                    resolve({ to, msg });
                },
                getOwnAddress: async () => 'ownAddress',
                addMessageListener: () => {/**lol */ },
                addContact: async () => { throw new Error('not implemented'); },
                requiresBootstrap: () => { return false; },
            }
            const ecdhAesGcmMessage = new EcdheAesGcmMessage(transportMockEncrypt, 'test1');
            console.log('adding contacts');
            await ecdhAesGcmMessage.addContact('test2', await ecdhAesGcmMessageDecrypt.getKeyInformation());
            console.log('sending message');
            await ecdhAesGcmMessage.send('test2', input);
            console.log('message sent');

        } catch (err) {
            reject(err);
        }
    });

    try {
        // eslint-disable-next-line no-async-promise-executor
        const decrypted = await new Promise(async (resolve, reject) => {
            const messages = [];

            try {
                ecdhAesGcmMessageDecrypt.addMessageListener((from, msg) => {
                    try {
                        messages.push({ from, msg });
                        if (messages.length === 2) {
                            resolve({ from, msg });
                        }
                    } catch (err) {
                        reject(err);
                    }
                });
                await decrypt!(encrypted.msg);
                await decrypt!(encrypted2.msg);
            } catch (err) {
                reject(err);
            }
        });
        return { decrypted, encrypted, encrypted2 };
    } catch (err: any) {
        return err?.message;
    }
}

const pushTransport = PushTransport.getInstance(getFactory());
const contactManager = new ContactManager(getFactory());
const dhtTM = new DhtTransportManager(pushTransport, contactManager, getFactory());


window.tests.getDhtAddress = async () => {
    return await dhtTM.getOwnAddress();
}

window.tests.dhtAddContact = async (address: string) => {
    await dhtTM.addContact('', address);
}

window.tests.dhtSendMessage = async (address: string, message: string) => {
    await dhtTM.send(address, message);
}

window.tests.waitForDhtMessage = (numMessages?: number) => {
    return new Promise(resolve => {
        if (numMessages) {
            const values: object[] = [];
            void dhtTM.addMessageListener((msg) => {
                values.push({ msg });
                if (values.length === numMessages) {
                    resolve(values);
                }
            })
        } else {
            void dhtTM.addMessageListener((msg) => {
                resolve({ msg });
            });
        }
    });
}