import { MethodLogger, ManageStatusUpdates, ConvertStringToRequestID } from '../internal.js';
import { Errors } from '../common.js';
import { asleep } from '../../utils/asleep.js';
export default class KeetaAnchorQueueStorageDriverFirestore {
    logger;
    firestoreInternal = null;
    name = 'KeetaAnchorQueueStorageDriverFirestore';
    id;
    path = [];
    pathStr;
    namespace;
    toctouDelay = undefined;
    constructor(options) {
        this.id = options?.id ?? crypto.randomUUID();
        this.logger = options?.logger;
        this.firestoreInternal = options.firestore;
        this.path = options.path ?? [];
        this.pathStr = ['root', ...this.path].join('.');
        this.namespace = options.namespace;
        Object.freeze(this.path);
        this.methodLogger('new')?.debug('Initialized Firestore queue storage driver');
    }
    methodLogger(method) {
        return (MethodLogger(this.logger, {
            class: 'KeetaAnchorQueueStorageDriverFirestore',
            file: 'src/lib/queue/drivers/queue_firestore.ts',
            method: method,
            instanceID: this.id
        }));
    }
    async getFirestore() {
        if (this.firestoreInternal === null) {
            throw (new Error('Firestore connection is not available'));
        }
        return (await this.firestoreInternal());
    }
    async getCollection() {
        const firestore = await this.getFirestore();
        return (firestore.collection(`queue_entries_${this.namespace}_${this.pathStr}`));
    }
    async getIdempotentCollection() {
        const firestore = await this.getFirestore();
        return (firestore.collection(`queue_idempotent_keys_${this.namespace}_${this.pathStr}`));
    }
    async add(request, info) {
        const firestore = await this.getFirestore();
        const collection = await this.getCollection();
        const idempotentCollection = await this.getIdempotentCollection();
        const logger = this.methodLogger('add');
        let entryID = ConvertStringToRequestID(info?.id);
        entryID ??= ConvertStringToRequestID(crypto.randomUUID());
        logger?.debug(`Enqueuing request with id ${String(entryID)}`);
        const currentTime = Date.now();
        const requestJSON = JSON.stringify(request);
        /**
         * The status to use for the new entry
         */
        const status = info?.status ?? 'pending';
        const entryData = {
            id: String(entryID),
            request: requestJSON,
            output: null,
            lastError: null,
            status: status,
            created: currentTime,
            updated: currentTime,
            worker: null,
            failures: 0
        };
        const idempotentIDs = info?.idempotentKeys;
        if (idempotentIDs && idempotentIDs.size > 0) {
            entryData.idempotentKeys = Array.from(idempotentIDs).map(String);
        }
        await this.toctouDelay?.();
        // Use Firestore transaction for atomicity
        const result = await firestore.runTransaction(async (transaction) => {
            const docRef = collection.doc(String(entryID));
            const docSnapshot = await transaction.get(docRef);
            if (docSnapshot.exists) {
                logger?.debug(`Request with id ${String(entryID)} already exists, ignoring`);
                return (entryID);
            }
            // Check idempotent keys
            if (idempotentIDs && idempotentIDs.size > 0) {
                const matchingIdempotentEntries = new Set();
                for (const idempotentID of idempotentIDs) {
                    const idempotentDocRef = idempotentCollection.doc(String(idempotentID));
                    const idempotentSnapshot = await transaction.get(idempotentDocRef);
                    if (idempotentSnapshot.exists) {
                        matchingIdempotentEntries.add(idempotentID);
                    }
                }
                if (matchingIdempotentEntries.size !== 0) {
                    throw (new Errors.IdempotentExistsError('One or more idempotent entries already exist in the queue', matchingIdempotentEntries));
                }
            }
            // Create entry
            transaction.set(docRef, entryData);
            // Create idempotent keys
            if (idempotentIDs && idempotentIDs.size > 0) {
                for (const idempotentID of idempotentIDs) {
                    const idempotentDocRef = idempotentCollection.doc(String(idempotentID));
                    transaction.set(idempotentDocRef, {
                        entryId: String(entryID),
                        idempotentId: String(idempotentID)
                    });
                }
            }
            return (entryID);
        });
        return (result);
    }
    async setStatus(id, status, ancillary) {
        const firestore = await this.getFirestore();
        const collection = await this.getCollection();
        const logger = this.methodLogger('setStatus');
        const { oldStatus } = ancillary ?? {};
        await firestore.runTransaction(async (transaction) => {
            const docRef = collection.doc(String(id));
            const docSnapshot = await transaction.get(docRef);
            if (!docSnapshot.exists) {
                throw (new Error(`Request with ID ${String(id)} not found`));
            }
            await this.toctouDelay?.();
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            const currentEntry = docSnapshot.data();
            if (!currentEntry) {
                throw (new Error(`Request with ID ${String(id)} not found`));
            }
            const currentEntryForUpdate = {
                status: currentEntry.status,
                failures: currentEntry.failures,
                last_error: currentEntry.lastError,
                output: currentEntry.output
            };
            const newEntry = ManageStatusUpdates(id, currentEntryForUpdate, status, ancillary, logger);
            const currentTime = newEntry.updated.getTime();
            const workerValue = newEntry.worker;
            const newFailures = newEntry.failures ?? currentEntry.failures;
            const newLastError = newEntry.lastError !== undefined ? newEntry.lastError : currentEntry.lastError;
            const newOutput = newEntry.output !== undefined ? JSON.stringify(newEntry.output) : currentEntry.output;
            if (oldStatus && currentEntry.status !== oldStatus) {
                throw (new Errors.IncorrectStateAssertedError(id, oldStatus, currentEntry.status));
            }
            transaction.update(docRef, {
                status: status,
                updated: currentTime,
                worker: workerValue,
                failures: newFailures,
                lastError: newLastError,
                output: newOutput
            });
        });
    }
    async get(id) {
        const collection = await this.getCollection();
        const idempotentCollection = await this.getIdempotentCollection();
        const docRef = collection.doc(String(id));
        const docSnapshot = await docRef.get();
        if (!docSnapshot.exists) {
            return (null);
        }
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        const entry = docSnapshot.data();
        if (!entry) {
            return (null);
        }
        // Get idempotent keys
        const idempotentQuery = await idempotentCollection.where('entryId', '==', String(id)).get();
        const idempotentKeys = idempotentQuery.empty
            ? undefined
            : new Set(idempotentQuery.docs.map((doc) => {
                const data = doc.data();
                // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                return (ConvertStringToRequestID(data['idempotentId']));
            }));
        return ({
            id: ConvertStringToRequestID(entry.id),
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            request: JSON.parse(entry.request),
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            output: entry.output ? JSON.parse(entry.output) : null,
            lastError: entry.lastError,
            status: entry.status,
            created: new Date(entry.created),
            updated: new Date(entry.updated),
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            worker: entry.worker,
            failures: entry.failures,
            idempotentKeys: idempotentKeys
        });
    }
    async query(filter) {
        const collection = await this.getCollection();
        const idempotentCollection = await this.getIdempotentCollection();
        const logger = this.methodLogger('query');
        logger?.debug(`Querying queue with id ${this.id} with filter:`, filter);
        let query = collection.orderBy('updated');
        if (filter?.status) {
            query = query.where('status', '==', filter.status);
        }
        if (filter?.updatedBefore) {
            query = query.where('updated', '<', filter.updatedBefore.getTime());
        }
        if (filter?.limit !== undefined) {
            query = query.limit(filter.limit);
        }
        const querySnapshot = await query.get();
        const entries = [];
        for (const doc of querySnapshot.docs) {
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            const row = doc.data();
            // Get idempotent keys for this entry
            const idempotentQuery = await idempotentCollection.where('entryId', '==', row.id).get();
            const idempotentKeys = idempotentQuery.empty
                ? undefined
                : new Set(idempotentQuery.docs.map((idempotentDoc) => {
                    const data = idempotentDoc.data();
                    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                    return (ConvertStringToRequestID(data['idempotentId']));
                }));
            entries.push({
                id: ConvertStringToRequestID(row.id),
                // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                request: JSON.parse(row.request),
                // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                output: row.output ? JSON.parse(row.output) : null,
                lastError: row.lastError,
                status: row.status,
                created: new Date(row.created),
                updated: new Date(row.updated),
                // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
                worker: row.worker,
                failures: row.failures,
                idempotentKeys: idempotentKeys
            });
        }
        logger?.debug(`Queried queue with id ${this.id} with filter:`, filter, '-- found', entries.length, 'entries');
        return (entries);
    }
    async partition(path) {
        this.methodLogger('partition')?.debug(`Creating partitioned queue storage driver for path: ${path}`);
        if (this.firestoreInternal === null) {
            throw (new Error('Asked to partition but the instance has been destroyed'));
        }
        const retval = new KeetaAnchorQueueStorageDriverFirestore({
            id: `${this.id}::${path}`,
            logger: this.logger,
            firestore: this.firestoreInternal,
            namespace: this.namespace,
            path: [...this.path, path]
        });
        return (retval);
    }
    async destroy() {
        this.methodLogger('destroy')?.debug('Destroying instance');
        this.firestoreInternal = null;
    }
    async [Symbol.asyncDispose]() {
        return (await this.destroy());
    }
    /** @internal */
    _Testing(key) {
        if (key !== 'bc81abf8-e43b-490b-b486-744fb49a5082') {
            throw (new Error('This is a testing only method'));
        }
        return ({
            setToctouDelay: (delay) => {
                this.toctouDelay = async () => {
                    return (await asleep(delay));
                };
            },
            unsetToctouDelay: () => {
                this.toctouDelay = undefined;
            }
        });
    }
}
//# sourceMappingURL=queue_firestore.js.map