/*
 * Copyright (c) 2022.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import {Context} from "@nuxt/types";
import {
    createPublicEncryptionKeyPair, createSymmetricEncryptionKey,
    decryptMessageSymmetric,
    decryptPublicEncryptionMessage,
    encryptMessageSymmetric,
    encryptPublicEncryptionMessage
} from "~/modules/security/encryption/utils";
import * as localforage from "localforage";
import * as yup from "yup";

import {mutations, getters} from './store';

export type SecurityKeyPair = {
    publicKey: string,
    privateKey: string
}

type ContactKeysSchema = {
    selfKeyPairs: {
        id: string,
        keyPair: SecurityKeyPair
    }[],
    relatedKeys: {
        id: string,
        keyPair: SecurityKeyPair
    }[]
}

export class SecurityModule {
    protected ctx: Context;

    private contactSelfKeys : LocalForage;
    private contactRelatedKeys : LocalForage;
    private roomKeys: LocalForage;

    constructor(ctx: Context) {
        this.ctx = ctx;

        this.contactSelfKeys = localforage.createInstance({driver: localforage.INDEXEDDB, name: 'chatApp', storeName: 'contactSefKeys'});
        this.contactRelatedKeys = localforage.createInstance({driver: localforage.INDEXEDDB, name: 'chatApp', storeName: 'contactRelatedKeys'});
        this.roomKeys = localforage.createInstance({driver: localforage.INDEXEDDB, name: 'chatApp', storeName: 'roomKeys'});

        if(!process.server) {
            this.init().then(r => r);
        }
    }

    async init() {
        await this.initContactSelfKeys();
        await this.intContactedRelatedKeys();
        await this.initRoomKeys();
    }

    // --------------------------------------------------------------------

    public async initContactSelfKeys() {
        const keys : string[] = await this.contactSelfKeys.keys();
        for(let i=0; i<keys.length; i++) {
            const key : string = keys[i];
            const keyPair = <SecurityKeyPair> await this.contactSelfKeys.getItem(key);

            mutations.setContactSelfKeyPair(key, keyPair);
        }
    }

    public async intContactedRelatedKeys() {
        const keys : string[] = await this.contactRelatedKeys.keys();
        for(let i=0; i<keys.length; i++) {
            const key : string = keys[i];
            const keyPair = <SecurityKeyPair> await this.contactRelatedKeys.getItem(key);

            mutations.setContactRelatedKey(key, keyPair.publicKey);
        }
    }

    public async initRoomKeys() {
        const keys : string[] = await this.roomKeys.keys();
        for(let i=0; i<keys.length; i++) {
            const key : string = keys[i];
            const keyPair = <SecurityKeyPair> await this.roomKeys.getItem(key);

            mutations.setChatRoomKey(key, keyPair.publicKey);
        }
    }

    // --------------------------------------------------------------------
    // Contact - Keys ( self + related )
    // --------------------------------------------------------------------

    /**
     * Export Keys for and of contacts.
     */
    public async exportContactKeys() : Promise<ContactKeysSchema> {
        let schema : ContactKeysSchema = {
            selfKeyPairs: [],
            relatedKeys: []
        }

        let keys : string[] = [];

        keys = await this.contactSelfKeys.keys();
        for(let i=0; i<keys.length; i++) {
            schema.selfKeyPairs.push({
                id: keys[i],
                keyPair: <SecurityKeyPair> await this.contactSelfKeys.getItem<SecurityKeyPair>(keys[i])
            })
        }

        keys = await this.contactRelatedKeys.keys();
        for(let i=0; i<keys.length; i++) {
            schema.relatedKeys.push({
                id: keys[i],
                keyPair: <SecurityKeyPair> await this.contactRelatedKeys.getItem<SecurityKeyPair>(keys[i])
            })
        }

        return schema;
    }

    /**
     * Import keys for and of contacts.
     *
     *
     * @param contactKeys
     */
    public async importContactKeys(contactKeys: ContactKeysSchema | string) {
        let data : ContactKeysSchema | undefined;

        if(typeof contactKeys === 'string') {
            try {
                data = JSON.parse(contactKeys);
            } catch (e) {
                throw new Error('Die Zeichenkette ist nicht gültig.');
            }
        } else {
            data = contactKeys;
        }

        let validationData = await useContactKeysImportValidator().validate(data);
        if(typeof validationData === 'undefined') {
            return;
        }

        let selfKeys = validationData.selfKeyPairs;
        if(typeof selfKeys !== 'undefined') {
            for (let i = 0; i < selfKeys.length; i++) {
                await this.contactSelfKeys.setItem<SecurityKeyPair>(selfKeys[i].id, <SecurityKeyPair>selfKeys[i].keyPair);
                mutations.setContactSelfKeyPair(selfKeys[i].id, <SecurityKeyPair> selfKeys[i].keyPair);
            }
        }

        let relatedKeys = validationData.relatedKeys;
        if(typeof relatedKeys !== 'undefined') {
            for (let i = 0; i < relatedKeys.length; i++) {
                await this.contactRelatedKeys.setItem<SecurityKeyPair>(relatedKeys[i].id, <SecurityKeyPair>relatedKeys[i].keyPair);

                mutations.setContactRelatedKey(relatedKeys[i].id, (<SecurityKeyPair>relatedKeys[i].keyPair).publicKey);
            }
        }
    }

    // --------------------------------------------------------------------

    public async saveContactSelfKeyPair(contactId: string, keys: SecurityKeyPair) : Promise<void> {
        await this.contactSelfKeys.setItem(contactId, keys);

        mutations.setContactSelfKeyPair(contactId, keys);
    }

    public async createAndSaveContactSelfKeyPair(contactId: string) {
        const keyPair = this.createKeyPair();

        await this.saveContactSelfKeyPair(contactId, keyPair);

        return keyPair;
    }

    // --------------------------------------------------------------------

    public async getContactRelatedKey(contactId: string) : Promise<string> {
        const keyPair = await this.contactRelatedKeys.getItem<SecurityKeyPair>(contactId);

        if(!keyPair) {
            throw new Error('Key not found');
        }

        return keyPair.publicKey;
    }

    public async dropContactRelatedKey(contactId: string) : Promise<void> {
        await this.contactRelatedKeys.removeItem(contactId)
    }

    public async saveContactRelatedKey(contactId: string, key: string) : Promise<void> {
        await this.contactRelatedKeys.setItem(contactId, {publicKey: key});

        mutations.setContactRelatedKey(contactId, key);
    }

    // --------------------------------------------------------------------

    public async getChatRoomKey(roomId: string) : Promise<string> {
        const keyPair = await this.roomKeys.getItem<string>(roomId);

        if(!keyPair) {
            throw new Error('Key not found');
        }

        return keyPair;
    }

    public async dropChatRoomKey(roomId: string) : Promise<void> {
        await this.roomKeys.removeItem(roomId)
    }

    public async saveChatRoomKey(roomId: string, key: string) : Promise<void> {
        await this.roomKeys.setItem(roomId, key);
    }

    // --------------------------------------------------------------------

    createKeyPair() : SecurityKeyPair {
        return createPublicEncryptionKeyPair();
    }

    createSymmetricKey() : string {
        return createSymmetricEncryptionKey();
    }

    // --------------------------------------------------------------------

    encodeMessageWithContact(message: string, contactId: string) {
        const selfKeyPair : SecurityKeyPair | undefined = getters.contactSelfKeyPair(contactId);
        const relatedKey = getters.contactRelatedKey(contactId);

        if(typeof selfKeyPair === 'undefined' || typeof relatedKey === 'undefined') {
            throw new Error('Die benötigten Schlüssel sind nicht verfügbar..');
        }

        return encryptPublicEncryptionMessage(message, relatedKey, selfKeyPair.privateKey);
    }

    decodeMessageWithContact(message: string, contactId: string) {
        const selfKeyPair : SecurityKeyPair | undefined = getters.contactSelfKeyPair(contactId);
        const relatedKey = getters.contactRelatedKey(contactId);

        if(typeof selfKeyPair === 'undefined' || typeof relatedKey === 'undefined') {
            throw new Error('Die benötigten Schlüssel sind nicht verfügbar..');
        }

        return decryptPublicEncryptionMessage(message, relatedKey, selfKeyPair.privateKey)
    }

    // --------------------------------------------------------------------

    encodeMessageWithChatRoom(message: string, roomId: string) {
        const symmetricKey : string | undefined = getters.roomKey(roomId);

        if(typeof symmetricKey === 'undefined') {
            throw new Error('Die benötigte symmetrische Schlüssel existiert nicht...');
        }

        return encryptMessageSymmetric(message, symmetricKey);
    }

    decodeMessageWithChatRoom(message: string, roomId: string) {
        const symmetricKey : string | undefined = getters.roomKey(roomId);

        if(typeof symmetricKey === 'undefined') {
            throw new Error('Die benötigte symmetrische Schlüssel existiert nicht...');
        }

        return decryptMessageSymmetric(message, symmetricKey);
    }
}

export function useContactKeysImportValidator() {
    return yup.object().shape({
        selfKeyPairs: yup.array().of(yup.object({
            id: yup.string().required(),
            keyPair: yup.object({
                privateKey: yup.string().required(),
                publicKey: yup.string().required()
            }).noUnknown(true)
        }).required()).default([]),
        relatedKeys: yup.array().of(yup.object({
            id: yup.string().required(),
            keyPair: yup.object({
                publicKey: yup.string().required()
            }).noUnknown(true)
        }).required()).default([])
    }).noUnknown().required();
}
