import { MessageInterface } from '../communication-interface/MessageInterface';
import { NullMessageInterface } from '../communication-interface/NullMessageInterface';
import { ContactManager } from '../contact-management/ContactManager';
import { getRandomUUID } from '../crypto';
import { BaseDataStore, BaseDataStoreClass } from '../db/DataStore';
import { BaseDto, ObjectStore } from '../db/DBAccess';
import { DBFactory } from '../db/DBFactory';
import { timeout } from '../helper';
import { LongAsyncRemoteFunction } from '../objectTransport/LongAsyncRemoteFunction';
import sem from 'semaphore';

interface TimestampedBaseDto extends BaseDto {
    timestamp: number;
}

class TransactionDto implements BaseDto {
    id?: string;
    userId: string;/* the address (not time based) of the user causing the transaction */
    larfCode: string;

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

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

export class TransactionChain {
    private _initPromise: Promise<void>;
    private _transactionDbFactory: DBFactory;
    private _transactionDao?: ObjectStore<TransactionDto>;


    constructor(private dbFactory: DBFactory, private contactManager: ContactManager, private dataStoreClasses: BaseDataStoreClass[], private messageInterface: MessageInterface) {
        this._transactionDbFactory = new DBFactory([TransactionDto], 'TransactionDB');
        this._initPromise = this.init();
    }

    private dbDataStores = new Map<BaseDataStoreClass, BaseDataStore>();
    private dbLarfs: LongAsyncRemoteFunction<any>[] = [];


    async init(): Promise<void> {
        this._transactionDao = await this._transactionDbFactory.getDao(TransactionDto);
        const larfListPromises = [];
        for (const dataStoreClass of this.dataStoreClasses) {
            const dataStoreInstance = new dataStoreClass(this.dbFactory, this.messageInterface, this.contactManager);
            this.dbDataStores.set(dataStoreClass, dataStoreInstance);
            larfListPromises.push((async () => this.dbLarfs.push(...await dataStoreInstance.getLarfList()))());
        }
        await Promise.all(larfListPromises);
    }

    private addTimestampToDto<T extends BaseDto>(t: T): T & TimestampedBaseDto {
        const tExtended = t as T & TimestampedBaseDto;
        const uid_arr = t.id?.split('-');
        if (!uid_arr) throw new Error('invalid transaction id');
        tExtended.timestamp = parseInt([uid_arr[2].substring(1), uid_arr[1], uid_arr[0]].join(''), 16);
        return tExtended;
    }

    async addTransaction(newTransaction: TransactionDto): Promise<void> {
        await this.dbFactory.transactionManager.lock();
        try {

            await this._initPromise;

            //TODO check that this doesn't break performance
            const timeStampedNewTransaction = this.addTimestampToDto(newTransaction);
            const transactions = await this._transactionDao?.getAll();
            if (!transactions) return;
            const timeStampedTransactions = transactions.map(this.addTimestampToDto).sort((a, b) => a.timestamp - b.timestamp);
            const transactionsBefore = timeStampedTransactions.filter(t => t.timestamp < timeStampedNewTransaction.timestamp);
            const transactionsAfter = timeStampedTransactions.filter(t => t.timestamp > timeStampedNewTransaction.timestamp);
            const result = await this.emulateTransactions([...transactionsBefore, timeStampedNewTransaction, ...transactionsAfter]);
            if (result) {
                if (result.id === newTransaction.id) {
                    throw new Error('invalid transaction');
                } else if (result.timestamp > timeStampedNewTransaction.timestamp) {
                    //we have a conflict!
                    //we need to find the conflicting transaction and remove it
                    const conflictingTransaction = timeStampedTransactions.find(t => t.id === result.id);
                    if (!conflictingTransaction) throw new Error('this should never happen!');
                    console.info(`found conflicting transaction: ${JSON.stringify(conflictingTransaction)}`);
                    await this._transactionDao!.delete(conflictingTransaction);
                    return await this.addTransaction(newTransaction);
                }
                else if (result.timestamp < timeStampedNewTransaction.timestamp) {
                    throw new Error('this should never happen!');
                }
            }
            await this._transactionDao!.add(newTransaction);
            await this.restoreDBFromTransactionlog();
        } finally {
            this.dbFactory.transactionManager.unlock();
        }
    }

    private async restoreDBFromTransactionlog() {
        const transactions = await this._transactionDao?.getAll();
        if (!transactions) throw new Error('this should never happen!');
        const timeStampedTransactions = transactions.map(this.addTimestampToDto).sort((a, b) => a.timestamp - b.timestamp);
        await this.emulateTransactions(timeStampedTransactions, async (tempDbFactory) => {
            for (const dtoClass of this.dbFactory.getDtoClasses()) {
                const tempDao = await tempDbFactory.getDao(dtoClass);
                const dao = await this.dbFactory.getDao(dtoClass);
                const tempObjects = await tempDao.getAll();
                const objects = await dao.getAll();
                for (const object of tempObjects) {
                    const oldObject = objects.find(o => o.id === object.id);
                    if (!oldObject) {
                        await dao.add(object);
                    }
                    else {
                        if (JSON.stringify(oldObject) !== JSON.stringify(object)) {
                            await dao.update(object);
                        }
                    }
                }
                for (const object of objects) {
                    if (!tempObjects.find(o => o.id === object.id)) {
                        await dao.delete(object);
                    }
                }
            }
        });
    }

    private emulationSemaphore = sem(1);
    private async emulateTransactions(transactions: (TransactionDto & TimestampedBaseDto)[], dbStateCallback?: (dbFactory: DBFactory) => Promise<void>): Promise<TransactionDto & TimestampedBaseDto | undefined> {
        await new Promise<void>(r => this.emulationSemaphore.take(() => { r() }));
        try {

            const tempDBFactory = new DBFactory(this.dbFactory.getDtoClasses(), 'tempDB');
            const tempDataStores: BaseDataStore[] = [];
            const tempLarfs: LongAsyncRemoteFunction<any>[] = [];
            for (const dataStoreClass of this.dataStoreClasses) {
                const tempInstance = new dataStoreClass(tempDBFactory, new NullMessageInterface(), this.contactManager);
                tempDataStores.push(tempInstance);
                tempLarfs.push(...await tempInstance.getLarfList());
            }

            for (const transaction of transactions) {
                try {
                    const larfResults = await Promise.all(tempLarfs.map(larf => larf.messageRecieved(transaction.userId, transaction.larfCode)));
                    const larfResult = larfResults.reduce((prev, curr) => prev || curr, false);
                    if (!larfResult) {
                        //TODO this could be a problem when deploy a new version of the code!
                        throw new Error('invalid transaction');
                    }
                } catch (err: unknown) {
                    if ((err as Error)?.message === 'invalid transaction') {
                        throw err;
                    }
                    return transaction;
                }
            }
            if (dbStateCallback) {
                await dbStateCallback(tempDBFactory);
            }
            await tempDBFactory.transactionManager.deleteDB('tempDB');
        } finally {
            this.emulationSemaphore.leave();
        }
    }

}