import Base64 from '../base64';
import { getTimeBasedAddress, getRandomUUID } from '../crypto';
import { ObjectStore } from '../db/DBAccess';
import { DBFactory } from '../db/DBFactory';
import { ContactAddressDto } from '../libDto/ContactAddressDto';

const EXPONENT = new Uint8Array([1, 0, 1]);
export class ContactManager {
    private _contactAddressDao?: ObjectStore<ContactAddressDto>;
    private _initPromise: Promise<void>;
    private _ownUserName: string | null;
    private autoConfirm = true;

    constructor(private dbFactory: DBFactory, private existingOwnAddress?: string, autoConfirm = true) {
        this.dbFactory = dbFactory;
        this.existingOwnAddress = existingOwnAddress;
        this._ownUserName = localStorage.getItem('ownUserName');
        this.autoConfirm = autoConfirm;
        this._initPromise = this._init();
    }
    private async _init() {
        this._contactAddressDao = await this.dbFactory.getDao(ContactAddressDto);
        await this._computeRainbowTable();
    }

    set ownUserName(userName: string | null) {
        this._ownUserName = userName;
        if (userName) {
            localStorage.setItem('ownUserName', userName);
        } else {
            localStorage.removeItem('ownUserName');
        }
    }

    get ownUserName() {
        return this._ownUserName || '';
    }

    private timeBasedAddresses: Map<string, string> = new Map();
    private ownAddress?: string;
    private ownTimeBasedAddress?: string;
    private ownPrivateKey?: CryptoKey;
    private ownPublicKey?: CryptoKey;

    private async _computeRainbowTable() {
        console.info('compute rainbow table');
        for (const contact of await this._contactAddressDao!.getAll()) {
            const timeBasedAddresses = await getTimeBasedAddress(contact.address);
            if (contact.id === 'own') {
                this.ownAddress = contact.address;
                this.ownTimeBasedAddress = timeBasedAddresses;
                this.ownPrivateKey = await crypto.subtle.importKey('pkcs8', Base64.decode(contact.privateKey!), {
                    name: 'RSA-PSS',
                    hash: 'SHA-256',
                }, true, ['sign']);
                this.ownPublicKey = await crypto.subtle.importKey('spki', Base64.decode(contact.publicKey!), {
                    name: 'RSA-PSS',
                    hash: 'SHA-256',
                }, true, ['verify']);
            }
            else {
                this.timeBasedAddresses.set(timeBasedAddresses, contact.id);
            }
        }

        console.info('rainbow table computed');

        if (!this.ownAddress) {
            console.info('generate new own address');
            //generate new own address
            await this.dbFactory.transactionManager.runTransaction(async dbTransaction => {
                const ownAddress = this.existingOwnAddress || await getRandomUUID();

                const keyPairSign = await crypto.subtle.generateKey({
                    name: 'RSA-PSS',
                    modulusLength: 4096,
                    publicExponent: EXPONENT,
                    hash: 'SHA-256',
                }, true, ['sign', 'verify']);

                const signPublicKey = await crypto.subtle.exportKey('spki', keyPairSign.publicKey);
                const signPrivateKey = await crypto.subtle.exportKey('pkcs8', keyPairSign.privateKey);
                const ownAddressDto = new ContactAddressDto('own', ownAddress, Base64.encode(signPublicKey), Base64.encode(signPrivateKey));
                void this._contactAddressDao!.add(ownAddressDto, dbTransaction);
                this.ownAddress = ownAddress;
                this.ownTimeBasedAddress = await getTimeBasedAddress(ownAddress);
                this.ownPrivateKey = keyPairSign.privateKey;
                this.ownPublicKey = keyPairSign.publicKey;
            });
            console.info('new own address generated');
        }
    }

    public async getPublicKeyFromTimeBasedAddress(timeBasedAddress: string): Promise<string | undefined> {
        const id = this.timeBasedAddresses.get(timeBasedAddress);
        if (id) {
            await this._initPromise;
            if (id.startsWith('own')) return undefined;
            if (id.startsWith('ownPrivate')) return undefined;
            return (await this._contactAddressDao!.get(id))?.publicKey;
        }
    }

    public async getOwnIdentity(): Promise<string> {
        await this._initPromise;
        const address = this.ownAddress;
        const publicKey = await crypto.subtle.exportKey('spki', this.ownPublicKey!);
        return JSON.stringify({ address, publicKey: Base64.encode(publicKey), userName: this.ownUserName });
    }

    public async getOwnAddress(): Promise<string> {
        await this._initPromise;
        return this.ownAddress!;
    }

    public async getOwnTimeBasedAddress(): Promise<string> {
        await this._initPromise;
        return this.ownTimeBasedAddress!;
    }

    /**
     * 
     * @param address the address of the contact (contact primary key)
     * @param publicKey the public key of the address
     */
    public async addContactAddress(addressBundle: string) {
        await this._initPromise;
        const { address, publicKey, userName } = JSON.parse(addressBundle);
        const contact = await this._contactAddressDao?.get(address);
        if (contact) {
            if (contact.publicKey === publicKey) {
                console.info(`contact ${userName} (${address}) already exists`);
                return;
            }
            throw new Error('contact already exists');
        }
        if (this.autoConfirm || confirm(`Do you want to add ${userName} (${address}) as a contact? They will be able to see when you are online and send you messages.`)) {
            await this._contactAddressDao?.add(new ContactAddressDto(address, address, publicKey, undefined, userName));
            await this._computeRainbowTable();
        }
    }

    async isContact(userId: string) {
        const contact = (await this._contactAddressDao?.get(userId)) || (this.timeBasedAddresses.has(userId));
        return !!contact;
    }

    async getAllContacts() {
        const contacts = await this._contactAddressDao?.getAll();
        return contacts?.map(contact => ({ address: contact.address, userName: contact.userName }));
    }

    async getTimeBasedAddress(userId: string): Promise<string> {
        return getTimeBasedAddress(userId);
    }

    async getAddressFromTimeBasedAddress(from: string): Promise<string> {
        if (!this.timeBasedAddresses.has(from)) throw new Error('unknown time based address');
        return this.timeBasedAddresses.get(from)!;
    }

    public async sign(message: string) {
        await this._initPromise;
        const key = this.ownPrivateKey;
        if (!key) throw new Error('own key not found');
        const signature = await crypto.subtle.sign({ name: 'RSA-PSS', hash: 'SHA-256', saltLength: 32 }, key, new TextEncoder().encode(message));
        return Base64.encode(signature);
    }

    /**
     * Verifies message signatures against long term public keys of contacts.
     * If no contact is found, false is returned.
     * @param from the from address (time based address)
     * @param message the signed message (usually the dht entry)
     * @param signature the signature
     * @returns true if the signature is valid, false if the contact is unknown, throws an error if the signature is invalid
     */
    public async verify(from: string, message: string, signature: string): Promise<boolean> {
        await this._initPromise;
        const origAddress = this.timeBasedAddresses.get(from) || from;
        if (!origAddress) return false;
        const contactAddress = origAddress === this.ownAddress ? (await this._contactAddressDao?.get('own')) : (await this._contactAddressDao?.get(origAddress));
        if (!contactAddress) return false;
        const publicKey = contactAddress.publicKey;
        const key = await crypto.subtle.importKey('spki', Base64.decode(publicKey), {
            name: 'RSA-PSS',
            hash: 'SHA-256',
        }, true, ['verify']);
        if ((await crypto.subtle.verify({ name: 'RSA-PSS', hash: 'SHA-256', saltLength: 32 }, key, Base64.decode(signature), new TextEncoder().encode(message))) === true) {
            return true;
        }
        throw new Error('trustviolation: signature validation failed');
    }

    public async getUserName(address: string): Promise<string | undefined> {
        await this._initPromise;
        const contact = await this._contactAddressDao?.get(address);
        return contact?.userName;
    }
}