import Base64 from '../base64';
import { MessageInterface, MessageListener } from '../communication-interface/MessageInterface';
import { TransportInterface } from '../communication-interface/TransportInterface';
import { getRandomUUID } from '../crypto';
import { ObjectStore } from '../db/DBAccess';
import { DBFactory } from '../db/DBFactory';
import { NonceDto } from '../libDto/NonceDto';

/**
 * This class implements a message encryption scheme using RSA-OAEP and AES-CTR.
 * This class does not implement the messageInterface for semantic reasons as it does not guarantee integrity because messages are not signed.
 */
export class RsaOaepAesMessage {
    private _initPromise: Promise<void>;
    private rsaEncryptionKey?: CryptoKeyPair;
    private _nonceDao?: ObjectStore<NonceDto>;

    constructor(private dbFactory: DBFactory, private transport: TransportInterface, private userId: string, rsaEncryptionKey?: string) {
        this._initPromise = this.init(rsaEncryptionKey);
        this.transport = transport;
        void this.transport.addMessageListener(this.messageReceived.bind(this));
        this.userId = userId;
    }

    async init(rsaEncryptionKey?: string): Promise<void> {
        if (rsaEncryptionKey) {
            const { privateKeyParams, publicKeyParams } = JSON.parse(rsaEncryptionKey);
            const privateKey = await crypto.subtle.importKey('jwk', privateKeyParams, { name: 'RSA-OAEP', hash: { name: 'SHA-256' } }, true, ['decrypt']);
            const publicKey = await crypto.subtle.importKey('jwk', publicKeyParams, { name: 'RSA-OAEP', hash: { name: 'SHA-256' } }, true, ['encrypt']);
            this.rsaEncryptionKey = { privateKey, publicKey };
        } else {
            this.rsaEncryptionKey = await crypto.subtle.generateKey({
                name: 'RSA-OAEP',
                modulusLength: 4096,
                publicExponent: new Uint8Array([1, 0, 1]),
                hash: { name: 'SHA-256' }
            },
                true,
                ['encrypt', 'decrypt']
            );
        }
        this._nonceDao = await this.dbFactory.getDao(NonceDto);
    }

    async getRSAKey(): Promise<string> {
        await this._initPromise;
        const privateKeyParams = await crypto.subtle.exportKey('jwk', this.rsaEncryptionKey!.privateKey);
        const publicKeyParams = await crypto.subtle.exportKey('jwk', this.rsaEncryptionKey!.publicKey);
        return JSON.stringify({ privateKeyParams, publicKeyParams });
    }

    async encryptTo(userId: string, payload: string): Promise<string> {
        await this._initPromise;
        const contactKey = this.contacts.get(userId);
        if (!contactKey) {
            throw new Error('Contact not found');
        }

        const nonce = await getRandomUUID();
        payload = JSON.stringify({ payload, from: this.userId, fromPK: await this.getKeyInformation(), nonce });

        //create shared aes key
        const sharedKey = crypto.getRandomValues(new Uint8Array(16));
        const sharedKeyEncoded = await crypto.subtle.importKey('raw', sharedKey.buffer, 'AES-CTR', true, ['encrypt', 'decrypt']);

        //encrypt payload with shared key
        const iv = crypto.getRandomValues(new Uint8Array(16));
        const encryptedBuffer = await crypto.subtle.encrypt({
            name: 'AES-CTR',
            counter: iv,
            length: 128
        },
            sharedKeyEncoded,
            new TextEncoder().encode(payload));

        //encrypt shared key with rsa
        const encryptedSharedKey = await crypto.subtle.encrypt({
            name: 'RSA-OAEP'
        },
            contactKey,
            sharedKey);


        const encrypteIv = await crypto.subtle.encrypt({
            name: 'RSA-OAEP'
        },
            contactKey,
            iv);

        const encrypted = Base64.encode(encryptedBuffer);
        const encryptedSharedKeyEncoded = Base64.encode(encryptedSharedKey);
        const encryptedIvEncoded = Base64.encode(encrypteIv);
        return JSON.stringify({ payload: encrypted, sk: encryptedSharedKeyEncoded, iv: encryptedIvEncoded });
    }

    async send(userId: string, payload: string): Promise<void> {
        await this._initPromise;

        const transportAddress = this.transport.requiresBootstrap() ? undefined : await this.transport.getOwnAddress();
        payload = JSON.stringify({ payload, transportAddress });
        await this.transport.send(userId, await this.encryptTo(userId, payload));
    }

    async decryptMessage(message: string): Promise<{ from: string, payload: string }> {
        await this._initPromise;
        const { payload, iv, sk } = JSON.parse(message);

        try {
            const decryptedSharedKey = await crypto.subtle.decrypt({
                name: 'RSA-OAEP'
            },
                this.rsaEncryptionKey!.privateKey,
                Base64.decode(sk));
            const decryptedSharedKeyEncoded = await crypto.subtle.importKey('raw', decryptedSharedKey, 'AES-CTR', true, ['encrypt', 'decrypt']);
            const decryptedIv = await crypto.subtle.decrypt({
                name: 'RSA-OAEP'
            },
                this.rsaEncryptionKey!.privateKey,
                Base64.decode(iv));
            const decryptedBuffer = await crypto.subtle.decrypt({
                name: 'AES-CTR',
                counter: decryptedIv,
                length: 128
            },
                decryptedSharedKeyEncoded,
                Base64.decode(payload));

            const decryptedBundle = new TextDecoder().decode(decryptedBuffer);
            const { payload: decryptedPayload, from, fromPK, nonce } = JSON.parse(decryptedBundle);
            const knownNonce: NonceDto | undefined = await this._nonceDao!.get(nonce);
            if (knownNonce) {
                throw new Error('Nonce already known');
            }
            await this._nonceDao!.add(new NonceDto(nonce));


            if (!this.contacts.has(from)) {
                await this.addContact(from, fromPK);
            }

            return { from, payload: decryptedPayload };
        } catch (err: any) {
            if (err?.message === '') {
                throw new Error('failed to decrypt message');
            } else {
                throw err;
            }
        }
    }

    async messageReceived(message: string): Promise<void> {
        await this._initPromise;
        const { from, payload: decryptedPayload } = await this.decryptMessage(message);
        const { payload, transportAddress } = JSON.parse(decryptedPayload);
        if (transportAddress) {
            await this.transport.addContact(from, transportAddress);
        }
        this.messagelisteners.forEach(listener => listener(from, payload));
    }

    private messagelisteners: MessageListener[] = [];

    addMessageListener(listener: MessageListener): void {
        this.messagelisteners.push(listener);
    }

    private addContactListeners: ((userId: string, keyInformation: string) => any)[] = [];

    addAddContactListener(listener: (userId: string, keyInformation: string) => void): void {
        this.addContactListeners.push(listener);
    }

    private contacts: Map<string, CryptoKey> = new Map();

    async addContact(userId: string, keyInformation: any, replaceExisting = false): Promise<string | void> {
        if (typeof keyInformation !== 'string') {
            throw new Error('keyInformation must be a string');
        }
        const decoded = Base64.decode(keyInformation);
        const key = await crypto.subtle.importKey('spki', decoded, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt']);
        if (this.contacts.has(userId) && !replaceExisting) {
            console.warn(`contact ${userId} already exists`);
            return;
        }
        this.addContactAtomic(userId, key, replaceExisting);
        for (const listener of this.addContactListeners) {
            listener(userId, keyInformation);
        }
    }

    private addContactAtomic(userId: string, key: CryptoKey, replaceExisting = false): void {
        if (this.contacts.has(userId) && !replaceExisting) {
            throw new Error('Contact already exists');
        }
        this.contacts.set(userId, key);
    }

    async getKeyInformation(): Promise<string> {
        await this._initPromise;
        const buffer = await crypto.subtle.exportKey('spki', this.rsaEncryptionKey!.publicKey);
        return Base64.encode(buffer);
    }

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


}