"use strict"

var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod }
}

Object.defineProperty(exports, "__esModule", { value: true })

const crypto_1 = require("crypto")
const node_cache_1 = __importDefault(require("@cacheable/node-cache"))
const Defaults_1 = require("../Defaults")
const LRUCache_1 = require("lru-cache") 
const crypto_2 = require("./crypto")
const generics_1 = require("./generics")
const mutex_1 = require("async-mutex") 

/**
 * Adds caching capability to a SignalKeyStore
 * @param store the store to add caching to
 * @param logger to log trace events
 * @param _cache cache store to use
 */
function makeCacheableSignalKeyStore(store, logger, _cache) {
    const cache = _cache || new node_cache_1.default({
        stdTTL: Defaults_1.DEFAULT_CACHE_TTLS.SIGNAL_STORE,
        useClones: false,
        deleteOnExpire: true,
    })
    
    // Mutex for protecting cache operations
    const cacheMutex = new mutex_1.Mutex()
    
    function getUniqueId(type, id) {
        return `${type}.${id}`
    }
    return {
        async get(type, ids) {
            return cacheMutex.runExclusive(async () => {
                const data = {}
                const idsToFetch = []
                for (const id of ids) {
                    const item = cache.get(getUniqueId(type, id))
                    if (typeof item !== 'undefined') {
                        data[id] = item
                    }
                    else {
                        idsToFetch.push(id)
                    }
                }
                if (idsToFetch.length) {
                    logger?.trace({ items: idsToFetch.length }, 'loading from store')
                    const fetched = await store.get(type, idsToFetch)
                    for (const id of idsToFetch) {
                        const item = fetched[id]
                        if (item) {
                            data[id] = item
                            cache.set(getUniqueId(type, id), item)
                        }
                    }
                }
                return data
            })
        },
        async set(data) {
            return cacheMutex.runExclusive(async () => {
                let keys = 0
                for (const type in data) {
                    for (const id in data[type]) {
                        await cache.set(getUniqueId(type, id), data[type][id])
                        keys += 1
                    }
                }
                logger?.trace({ keys }, 'updated cache')
                await store.set(data)
            })
        },
        async clear() {
            await cache.flushAll()
            await store.clear?.call(store)
        }
    }
}

// Module-level specialized mutexes for pre-key operations
const preKeyMutex = new mutex_1.Mutex()
const signedPreKeyMutex = new mutex_1.Mutex()

/**
 * Get the appropriate mutex for the key type
 */
const getPreKeyMutex = (keyType) => {
    return keyType === 'signed-pre-key' ? signedPreKeyMutex : preKeyMutex
}

/**
 * Handles pre-key operations with mutex protection
 */
async function handlePreKeyOperations(data, keyType, transactionCache, mutations, logger, isInTransaction, state) {
    const mutex = getPreKeyMutex(keyType)
    await mutex.runExclusive(async () => {
        const keyData = data[keyType]
        if (!keyData)
            return
            
        // Ensure structures exist
        transactionCache[keyType] = transactionCache[keyType] || {}
        mutations[keyType] = mutations[keyType] || {}
        
        // Separate deletions from updates for batch processing
        const deletionKeys = []
        const updateKeys = []
        
        for (const keyId in keyData) {
            if (keyData[keyId] === null) {
                deletionKeys.push(keyId)
            }
            else {
                updateKeys.push(keyId)
            }
        }
        
        // Process updates first (no validation needed)
        for (const keyId of updateKeys) {
            if (transactionCache[keyType]) {
                transactionCache[keyType][keyId] = keyData[keyId]
            }
            if (mutations[keyType]) {
                mutations[keyType][keyId] = keyData[keyId]
            }
        }
        
        // Process deletions with validation
        if (deletionKeys.length === 0)
            return
            
        if (isInTransaction) {
            // In transaction, only allow deletion if key exists in cache
            for (const keyId of deletionKeys) {
                if (transactionCache[keyType]) {
                    transactionCache[keyType][keyId] = null
                    if (mutations[keyType]) {
                        // Mark for deletion in mutations
                        mutations[keyType][keyId] = null
                    }
                }
                else {
                    logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`)
                }
            }
            return
        }
        
        // Outside transaction, batch validate all deletions
        if (!state)
            return
            
        const existingKeys = await state.get(keyType, deletionKeys)
        for (const keyId of deletionKeys) {
            if (existingKeys[keyId]) {
                if (transactionCache[keyType])
                    transactionCache[keyType][keyId] = null
                if (mutations[keyType])
                    mutations[keyType][keyId] = null
            }
            else {
                logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
            }
        }
    })
}

/**
 * Handles normal key operations for transactions
 */
function handleNormalKeyOperations(data, key, transactionCache, mutations) {
    Object.assign(transactionCache[key], data[key])
    mutations[key] = mutations[key] || {}
    Object.assign(mutations[key], data[key])
}

/**
 * Process pre-key deletions with validation
 */
async function processPreKeyDeletions(data, keyType, state, logger) {
    const mutex = getPreKeyMutex(keyType)
    await mutex.runExclusive(async () => {
        const keyData = data[keyType]
        if (!keyData)
            return
            
        // Validate deletions
        for (const keyId in keyData) {
            if (keyData[keyId] === null) {
                const existingKeys = await state.get(keyType, [keyId])
                if (!existingKeys[keyId]) {
                    logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
                    if (data[keyType])
                        delete data[keyType][keyId]
                }
            }
        }
    })
}

/**
 * Executes a function with mutexes acquired for given key types
 * Uses async-mutex's runExclusive with efficient batching
 */
async function withMutexes(keyTypes, getKeyTypeMutex, fn) {
    if (keyTypes.length === 0) {
        return fn()
    }
    
    if (keyTypes.length === 1) {
        return getKeyTypeMutex(keyTypes[0]).runExclusive(fn)
    }
    
    // For multiple mutexes, sort by key type to prevent deadlocks
    // Then acquire all mutexes in order using Promise.all for better efficiency
    const sortedKeyTypes = [...keyTypes].sort()
    const mutexes = sortedKeyTypes.map(getKeyTypeMutex)
    
    // Acquire all mutexes in order to prevent deadlocks
    const releases = []
    try {
        for (const mutex of mutexes) {
            releases.push(await mutex.acquire())
        }
        return await fn()
    }
    finally {
        // Release in reverse order
        while (releases.length > 0) {
            const release = releases.pop()
            if (release)
                release()
        }
    }
}

/**
 * Adds DB like transaction capability (https://en.wikipedia.org/wiki/Database_transaction) to the SignalKeyStore,
 * this allows batch read & write operations & improves the performance of the lib
 * @param state the key store to apply this capability to
 * @param logger logger to log events
 * @returns SignalKeyStore with transaction capability
 */
const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetweenTriesMs }) => {
    // number of queries made to the DB during the transaction
    // only there for logging purposes
    let dbQueriesInTransaction = 0
    let transactionCache = {}
    let mutations = {}
    
    // LRU Cache to hold mutexes for different key types
    const mutexCache = new LRUCache_1.LRUCache({
        ttl: 60 * 60 * 1000, // 1 hour
        ttlAutopurge: true,
        updateAgeOnGet: true
    })
    
    let transactionsInProgress = 0
    
    function getKeyTypeMutex(type) {
        return getMutex(`keytype:${type}`)
    }
    
    function getSenderKeyMutex(senderKeyName) {
        return getMutex(`senderkey:${senderKeyName}`)
    }
    
    function getTransactionMutex(key) {
        return getMutex(`transaction:${key}`)
    }
    
    // Get or create a mutex for a specific key name
    function getMutex(key) {
        let mutex = mutexCache.get(key)
        if (!mutex) {
            mutex = new mutex_1.Mutex()
            mutexCache.set(key, mutex)
            logger.info({ key }, 'created new mutex')
        }
        return mutex
    }
    
    // Sender key operations with proper mutex sequencing
    function queueSenderKeyOperation(senderKeyName, operation) {
        return getSenderKeyMutex(senderKeyName).runExclusive(operation)
    }
    
    // Check if we are currently in a transaction
    function isInTransaction() {
        return transactionsInProgress > 0
    }
    
    // Helper function to handle transaction commit with retries
    async function commitTransaction() {
        if (!Object.keys(mutations).length) {
            logger.trace('no mutations in transaction')
            return
        }
        logger.trace('committing transaction');
        let tries = maxCommitRetries
        while (tries > 0) {
            tries -= 1;
            try {
                await state.set(mutations)
                logger.trace({ dbQueriesInTransaction }, 'committed transaction')
                return
            }
            catch (error) {
                logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`)
                if (tries > 0) {
                    await generics_1.delay(delayBetweenTriesMs)
                }
            }
        }
    }
    
    // Helper function to clean up transaction state
    function cleanupTransactionState() {
        transactionsInProgress -= 1
        if (transactionsInProgress === 0) {
            transactionCache = {}
            mutations = {}
            dbQueriesInTransaction = 0
        }
    }
    
    // Helper function to execute work within transaction
    async function executeTransactionWork(work) {
        const result = await work()
        // commit if this is the outermost transaction
        if (transactionsInProgress === 1) {
            await commitTransaction()
        }
        return result
    }
    
    return {
        get: async (type, ids) => {
            if (isInTransaction()) {
                const dict = transactionCache[type]
                const idsRequiringFetch = dict ? ids.filter(item => typeof dict[item] === 'undefined') : ids
                
                // only fetch if there are any items to fetch
                if (idsRequiringFetch.length) {
                    dbQueriesInTransaction += 1
                    
                    // Use per-sender-key queue for sender-key operations when possible
                    if (type === 'sender-key') {
                        logger.info({ idsRequiringFetch }, 'processing sender keys in transaction')
                        // For sender keys, process each one with queued operations to maintain serialization
                        for (const senderKeyName of idsRequiringFetch) {
                            await queueSenderKeyOperation(senderKeyName, async () => {
                                logger.info({ senderKeyName }, 'fetching sender key in transaction')
                                const result = await state.get(type, [senderKeyName])
                                // Update transaction cache
                                transactionCache[type] || (transactionCache[type] = {})
                                Object.assign(transactionCache[type], result)
                                logger.info({ senderKeyName, hasResult: !!result[senderKeyName] }, 'sender key fetch complete')
                            })
                        }
                    }
                    else {
                        // Use runExclusive for cleaner mutex handling
                        await getKeyTypeMutex(type).runExclusive(async () => {
                            const result = await state.get(type, idsRequiringFetch)
                            // Update transaction cache
                            transactionCache[type] || (transactionCache[type] = {})
                            Object.assign(transactionCache[type], result)
                        })
                    }
                }
                return ids.reduce((dict, id) => {
                    const value = transactionCache[type]?.[id]
                    if (value) {
                        dict[id] = value
                    }
                    return dict
                }, {})
            }
            else {
                // Not in transaction, fetch directly with queue protection
                if (type === 'sender-key') {
                    // For sender keys, use individual queues to maintain per-key serialization
                    const results = {}
                    for (const senderKeyName of ids) {
                        const result = await queueSenderKeyOperation(senderKeyName, async () => await state.get(type, [senderKeyName]))
                        Object.assign(results, result)
                    }
                    return results
                }
                else {
                    return await getKeyTypeMutex(type).runExclusive(() => state.get(type, ids))
                }
            }
        },
        set: async (data) => {
            if (isInTransaction()) {
                logger.trace({ types: Object.keys(data) }, 'caching in transaction')
                for (const key_ in data) {
                    const key = key_
                    transactionCache[key] = transactionCache[key] || {}
                    // Special handling for pre-keys and signed-pre-keys
                    if (key === 'pre-key') {
                        await handlePreKeyOperations(data, key, transactionCache, mutations, logger, true)
                    }
                    else {
                        // Normal handling for other key types
                        handleNormalKeyOperations(data, key, transactionCache, mutations)
                    }
                }
            }
            else {
                // Not in transaction, apply directly with mutex protection
                const hasSenderKeys = 'sender-key' in data
                const senderKeyNames = hasSenderKeys ? Object.keys(data['sender-key'] || {}) : []
                if (hasSenderKeys) {
                    logger.info({ senderKeyNames }, 'processing sender key set operations')
                    // Handle sender key operations with per-key queues
                    for (const senderKeyName of senderKeyNames) {
                        await queueSenderKeyOperation(senderKeyName, async () => {
                            // Create data subset for this specific sender key
                            const senderKeyData = {
                                'sender-key': {
                                    [senderKeyName]: data['sender-key'][senderKeyName]
                                }
                            };
                            logger.trace({ senderKeyName }, 'storing sender key')
                            // Apply changes to the store
                            await state.set(senderKeyData)
                            logger.trace({ senderKeyName }, 'sender key stored')
                        })
                    }
                    // Handle any non-sender-key data with regular mutexes
                    const nonSenderKeyData = { ...data }
                    delete nonSenderKeyData['sender-key']
                    if (Object.keys(nonSenderKeyData).length > 0) {
                        await withMutexes(Object.keys(nonSenderKeyData), getKeyTypeMutex, async () => {
                            // Process pre-keys and signed-pre-keys separately with specialized mutexes
                            for (const key_ in nonSenderKeyData) {
                                const keyType = key_
                                if (keyType === 'pre-key') {
                                    await processPreKeyDeletions(nonSenderKeyData, keyType, state, logger)
                                }
                            }
                            // Apply changes to the store
                            await state.set(nonSenderKeyData)
                        })
                    }
                }
                else {
                    // No sender keys - use original logic
                    await withMutexes(Object.keys(data), getKeyTypeMutex, async () => {
                        // Process pre-keys and signed-pre-keys separately with specialized mutexes
                        for (const key_ in data) {
                            const keyType = key_
                            if (keyType === 'pre-key') {
                                await processPreKeyDeletions(data, keyType, state, logger)
                            }
                        }
                        // Apply changes to the store
                        await state.set(data)
                    })
                }
            }
        },
        isInTransaction,
        async transaction(work, key) {
            const releaseTxMutex = await getTransactionMutex(key).acquire()
            try {
                transactionsInProgress += 1;
                if (transactionsInProgress === 1) {
                    logger.trace('entering transaction')
                }
                // Release the transaction mutex now that we've updated the counter
                // This allows other transactions to start preparing
                releaseTxMutex()
                try {
                    return await executeTransactionWork(work)
                }
                finally {
                    cleanupTransactionState();
                }
            }
            catch (error) {
                releaseTxMutex()
                throw error
            }
        }
    }
}

const initAuthCreds = () => {
    const identityKey = crypto_2.Curve.generateKeyPair()
    return {
        noiseKey: crypto_2.Curve.generateKeyPair(),
        pairingEphemeralKeyPair: crypto_2.Curve.generateKeyPair(),
        signedIdentityKey: identityKey,
        signedPreKey: crypto_2.signedKeyPair(identityKey, 1),
        registrationId: generics_1.generateRegistrationId(),
        advSecretKey: crypto_1.randomBytes(32).toString('base64'),
        processedHistoryMessages: [],
        nextPreKeyId: 1,
        firstUnuploadedPreKeyId: 1,
        accountSyncCounter: 0,
        accountSettings: {
            unarchiveChats: false
        },
        registered: false,
        pairingCode: undefined,
        lastPropHash: undefined,
        routingInfo: undefined
    }
}

module.exports = {
  makeCacheableSignalKeyStore, 
  addTransactionCapability, 
  initAuthCreds
}