import { openDB, deleteDB, wrap, unwrap, IDBPDatabase } from 'idb';
import { getRandomUUID, getObjectId } from '../crypto';
import { Transaction, TransactionManager } from './Transaction';


export interface BaseDto {
    id?: string;
}

export interface DtoClass {
    new(...args: any[]): BaseDto;
    get dbName(): string;
}

export class ObjectStore<T extends BaseDto> {
    storeName: string;
    private db: IDBPDatabase;
    private transactionManager: TransactionManager;

    constructor(objectStoreName: string, transactionManager: TransactionManager) {
        this.storeName = objectStoreName;
        this.transactionManager = transactionManager;
        if (!transactionManager.db) {
            throw new Error('transactionManager does not have a db');
        }
        this.db = transactionManager.db;
    }

    private async _addDb(object: T): Promise<T> {
        if (!object.id) {
            object.id = `${this.storeName}:${await getRandomUUID()}`;
        }
        await this.db.add(this.storeName, object);
        return object;
    }

    /**
     * Adds an object to the database and overwrites the id property with a new one.
     * @param object the object to be inserted
     * @returns the update with the updated id
     */
    async add(object: T, transaction?: Transaction): Promise<T> {
        if (transaction) {
            transaction.addEntity(this.storeName);
            const promise: Promise<T> = new Promise(resolve => {
                let objectId: string;
                transaction.addEntry({
                    onExecute: () => this._addDb(object).then(retObj => {
                        if (!retObj.id) {
                            throw new Error('added object does not have an id!');
                        }
                        objectId = retObj.id;
                        resolve(retObj);
                        return retObj;
                    }),
                    onGetPostTransactionState: () => {
                        if (!objectId) {
                            throw new Error('added object does not have an id!');
                        }
                        return this.get(objectId);
                    }
                });
            });
            return promise;
        }

        //if this function is called without a transaction, we create one
        await this.transactionManager.runTransaction(async tx => {
            void this.add(object, tx);
        });
        return object;
    }

    private async _updateDb(object: T): Promise<T> {
        await this.db.put(this.storeName, object);
        return object;
    }

    async update(object: T, transaction?: Transaction): Promise<T> {
        if (!object.id) {
            throw new Error('Cannot update object without id');
        }
        if (transaction) {
            transaction.addEntity(this.storeName);
            const promise: Promise<T> = new Promise(resolve => {
                transaction.addEntry({
                    onExecute: () => this._updateDb(object).then(result => {
                        resolve(result);
                        return result;
                    }),
                    onGetPostTransactionState: () => {
                        if (!object.id) {
                            throw new Error('Cannot update object without id');
                        }
                        return this.get(object.id);
                    }
                });
            });
            return promise;
        }
        await this.transactionManager.runTransaction(async tx => {
            void this.update(object, tx);
        });
        return object;
    }

    private async _deleteDb(object: T): Promise<T> {
        if (!object.id) {
            throw new Error('Cannot delete object without id');
        }
        await this.db.delete(this.storeName, object.id);
        return object;
    }

    async delete(object: T | string, transaction?: Transaction): Promise<T> {
        if (typeof object === 'string') {
            const obj = await this.get(object);
            if (!obj) {
                throw new Error('ObjectId not found');
            }
            object = obj;
        }
        const dbObject: T = object;
        if (!dbObject.id) {
            throw new Error('Cannot delete object without id');
        }
        if (transaction) {
            transaction.addEntity(this.storeName);
            const promise: Promise<T> = new Promise(resolve => {
                transaction.addEntry({
                    onExecute: () => this._deleteDb(dbObject).then(result => {
                        resolve(result);
                        return result;
                    }),
                    onGetPostTransactionState: async () => {
                        if (!dbObject.id) {
                            throw new Error('Cannot delete object without id');
                        }
                        return dbObject;
                    }
                });
            });
            return promise;
        }
        await this.transactionManager.runTransaction(async tx => {
            void this.delete(object, tx);
        });
        return object;
    }

    async get(id: string, transaction?: Transaction,): Promise<T | undefined> {
        if (transaction) {
            transaction.addEntity(this.storeName);
            const promise: Promise<T | undefined> = new Promise(resolve => {
                transaction.addEntry({
                    onExecute: () => this.get(id).then(result => {
                        resolve(result);
                        return result;
                    })
                });
            });
            return promise;
        }
        const element = await this.db.get(this.storeName, id);
        if (!element) {
            return undefined;
        }
        return element;
    }

    async getAll(query?: IDBKeyRange | IDBValidKey, transaction?: Transaction): Promise<T[]> {
        const result: T[] = [];
        if (!transaction) {
            const values = await this.db.getAllFromIndex(this.storeName, 'id', query);
            return values;
        } else {
            transaction.addEntity(this.storeName);
            const promise: Promise<T[]> = new Promise(resolve => {
                transaction?.addEntry({
                    onExecute: () => this.getAll(query).then(result => {
                        resolve(result);
                        return result;
                    }),
                });
            });
            return promise;
        }
    }

}
