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

export class EcdheAesGcmContactDto implements BaseDto {
    id: string;
    keyInformation: string;

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

    get userId(): string {
        return this.id;
    }

    constructor(userId: string, keyInformation: string) {
        this.id = userId;
        this.keyInformation = keyInformation;
    }
}

export class EcdheAesGcmMessage implements MessageInterface {
    private _initPromise: Promise<void>;
    private encryptionKey?: CryptoKeyPair;
    private signingKey?: CryptoKeyPair;
    private factory?: DBFactory;
    private contactDao?: ObjectStore<EcdheAesGcmContactDto>;

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

    async init(encryptionKey?: string, signingKey?: string): Promise<void> {
        if ((encryptionKey && 1 || 0) ^ (signingKey && 1 || 0)) {
            throw new Error('Either both encryptionKey and signingKey must be provided or none of them');
        }
        if (encryptionKey && signingKey) {
            const { privateKeyParams: encPrivateKeyParams, publicKeyParams: encPublicKeyParams } = JSON.parse(encryptionKey);
            const encPrivateKey = await crypto.subtle.importKey('jwk', encPrivateKeyParams, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
            const encPublicKey = await crypto.subtle.importKey('jwk', encPublicKeyParams, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
            this.encryptionKey = { privateKey: encPrivateKey, publicKey: encPublicKey };

            const { privateKeyParams: signPrivateKeyParams, publicKeyParams: signPublicKeyParams } = JSON.parse(signingKey);
            const signPrivateKey = await crypto.subtle.importKey('jwk', signPrivateKeyParams, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
            const signPublicKey = await crypto.subtle.importKey('jwk', signPublicKeyParams, { name: 'ECDSA', namedCurve: 'P-256' }, true, []);
            this.signingKey = { privateKey: signPrivateKey, publicKey: signPublicKey };

        } else {
            this.encryptionKey = await crypto.subtle.generateKey({
                name: 'ECDH',
                namedCurve: 'P-256'
            },
                true,
                ['deriveKey']
            );

            this.signingKey = await crypto.subtle.generateKey({
                name: 'ECDSA',
                namedCurve: 'P-256'
            },
                true,
                ['sign']
            );
        }
        if (this.factory) {
            this.contactDao = await this.factory.getDao(EcdheAesGcmContactDto);
        }
    }

    /**
     * CONTAINS PRIVATE KEY! DO NOT SEND ANYWHERE! ONLY USE FOR LOCAL STORAGE!
     * @returns jwk encoded **private** and public key
     */
    async getEdchKey(): Promise<string> {
        await this._initPromise;
        const encryptionPrivateKeyParams = await crypto.subtle.exportKey('jwk', this.encryptionKey!.privateKey);
        const encryptionPublicKeyParams = await crypto.subtle.exportKey('jwk', this.encryptionKey!.publicKey);
        const encryptionKey = JSON.stringify({ privateKeyParams: encryptionPrivateKeyParams, publicKeyParams: encryptionPublicKeyParams });

        const signingPrivateKeyParams = await crypto.subtle.exportKey('jwk', this.signingKey!.privateKey);
        const signingPublicKeyParams = await crypto.subtle.exportKey('jwk', this.signingKey!.publicKey);
        const signingKey = JSON.stringify({ privateKeyParams: signingPrivateKeyParams, publicKeyParams: signingPublicKeyParams });
        return JSON.stringify({ encryptionKey, signingKey });
    }

    async send(userId: string, payload: string): Promise<void> {
        await this._initPromise;
        let contactKey = this.contacts.get(userId);
        if (!contactKey && this.contactDao) {
            const contact = await this.contactDao.get(userId);
            if (contact) {
                await this.addContact(contact.id, contact.keyInformation);
                contactKey = this.contacts.get(userId);
            }
        }
        if (!contactKey) {
            throw new Error('Contact not found');
        }
        const sharedKey = await crypto.subtle.deriveKey({
            name: 'ECDH',
            public: contactKey.encryptionKey
        },
            this.encryptionKey!.privateKey,
            {
                name: 'AES-GCM',
                length: 256
            },
            true,
            ['encrypt', 'decrypt']);

        const ivBuffer = crypto.getRandomValues(new Uint8Array(12));
        const iv = Base64.encode(ivBuffer);

        const encryptedBuffer = await crypto.subtle.encrypt({
            name: 'AES-GCM',
            iv: ivBuffer
        },
            sharedKey,
            new TextEncoder().encode(payload));

        //sign message
        const signatureBuffer = await crypto.subtle.sign({
            name: 'ECDSA',
            hash: 'SHA-256'
        },
            this.signingKey!.privateKey,
            encryptedBuffer);

        const encrypted = Base64.encode(encryptedBuffer);
        const signature = Base64.encode(signatureBuffer);
        await this.transport.send(userId, JSON.stringify({ encrypted, signature, iv, from: this.userId, fromPK: await this.getKeyInformation() }));
    }

    async messageReceived(message: { encrypted: string; signature: string; iv: string; from: string; fromPK: string; }): Promise<void> {
        await this._initPromise;
        if (typeof message === 'string') {
            message = JSON.parse(message);
        }
        const { encrypted, signature, iv, from, fromPK } = message;
        if (!encrypted || !signature || !iv || !from || !fromPK) {
            console.warn(`got invalid message from ${from}`);
            return;
        }
        if (!this.contacts.has(from)) {
            await this.addContact(from, fromPK);
        }
        const signatureBuffer = Base64.decode(signature);

        const verified = await crypto.subtle.verify({
            name: 'ECDSA',
            hash: 'SHA-256'
        },
            this.contacts.get(from)!.signingKey,
            signatureBuffer,
            Base64.decode(encrypted));

        if (verified === true) {
            const sharedKey = await crypto.subtle.deriveKey({
                name: 'ECDH',
                public: this.contacts.get(from)!.encryptionKey
            },
                this.encryptionKey!.privateKey,
                {
                    name: 'AES-GCM',
                    length: 256
                },
                true,
                ['encrypt', 'decrypt']);

            try {
                const decryptedBuffer = await crypto.subtle.decrypt({
                    name: 'AES-GCM',
                    iv: Base64.decode(iv)
                },
                    sharedKey,
                    Base64.decode(encrypted));

                const decrypted = new TextDecoder().decode(decryptedBuffer);
                this.messagelisteners.forEach(listener => listener(from, decrypted));
            } catch (err: any) {
                if (err?.message === '') {
                    throw new Error('failed to decrypt message');
                } else {
                    throw err;
                }
            }
        } else {
            throw new Error('failed to verify message');
        }

    }

    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, { encryptionKey: CryptoKey, signingKey: CryptoKey }> = new Map();

    async addContact(userId: string, keyInformation: any, noNotify = false, replaceExisting = false): Promise<string | void> {
        if (typeof keyInformation !== 'string') {
            throw new Error('keyInformation must be a string');
        }
        const { encryptionKey, signingKey } = JSON.parse(keyInformation);
        if (!encryptionKey || !signingKey) {
            throw new Error('keyInformation is invalid');
        }

        if (this.contactDao) {
            const existingContact = await this.contactDao.get(userId);
            if (existingContact) {
                if (replaceExisting) {
                    await this.contactDao.delete(existingContact.id);
                    await this.contactDao.add(new EcdheAesGcmContactDto(userId, keyInformation));
                }
            } else {
                await this.contactDao.add(new EcdheAesGcmContactDto(userId, keyInformation));
            }
        }

        const decodedEncryptionKey = Base64.decode(encryptionKey);
        const importedEncryptionKey = await crypto.subtle.importKey('spki', decodedEncryptionKey, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
        const decodedSigningKey = Base64.decode(signingKey);
        const importedSigningKey = await crypto.subtle.importKey('spki', decodedSigningKey, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
        if (this.contacts.has(userId) && !replaceExisting) {
            console.warn(`contact ${userId} already exists`);
            return;
        }
        this.addContactAtomic(userId, importedEncryptionKey, importedSigningKey, replaceExisting);
        if (!noNotify) {
            this.addContactListeners.forEach(listener => listener(userId, keyInformation));
        }
    }

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

    async getKeyInformation(): Promise<string> {
        await this._initPromise;
        const encryptionKeyBuffer = await crypto.subtle.exportKey('spki', this.encryptionKey!.publicKey);
        const signingKeyBuffer = await crypto.subtle.exportKey('spki', this.signingKey!.publicKey);
        return JSON.stringify({ encryptionKey: Base64.encode(encryptionKeyBuffer), signingKey: Base64.encode(signingKeyBuffer) });
    }

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

    async getContactPk(userId: string): Promise<string> {
        if (!this.contacts.has(userId)) {
            throw new Error('Contact not found');
        }
        return JSON.stringify({ encryptionKey: Base64.encode(await crypto.subtle.exportKey('spki', this.contacts.get(userId)!.encryptionKey)), signingKey: Base64.encode(await crypto.subtle.exportKey('spki', this.contacts.get(userId)!.signingKey)) });
    }

}