"use strict"

var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k
    var desc = Object.getOwnPropertyDescriptor(m, k)
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k] } }
    }
    Object.defineProperty(o, k2, desc)
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k
    o[k2] = m[k]
}))

var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v })
}) : function(o, v) {
    o["default"] = v
})

var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = []
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k
            return ar
        }
        return ownKeys(o)
    }
    return function (mod) {
        if (mod && mod.__esModule) return mod
        var result = {}
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i])
        __setModuleDefault(result, mod)
        return result
    }
})()

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

const libsignal = __importStar(require("@itsukichan/libsignal-node"))
const Utils_1 = require("../Utils")
const WABinary_1 = require("../WABinary")
const sender_key_name_1 = require("./Group/sender-key-name")
const sender_key_record_1 = require("./Group/sender-key-record")
const Group_1 = require("./Group")
const LIDMappingStore_1 = require("./lid-mapping") 

function makeLibSignalRepository(auth, onWhatsAppFunc, logger) {
	const lidMapping = new LIDMappingStore_1.LIDMappingStore(auth.keys, onWhatsAppFunc, logger)
    const storage = signalStorage(auth, lidMapping)
    const parsedKeys = auth.keys
    
    function isLikelySyncMessage(addr) {
        const key = addr.toString()
        // Only bypass for WhatsApp system addresses, not regular user contacts
        // Be very specific about sync service patterns
        return (key.includes('@lid.whatsapp.net') || // WhatsApp system messages
            key.includes('@broadcast') || // Broadcast messages
            key.includes('@newsletter'))
    }
    
    const repository = {
        decryptGroupMessage({ group, authorJid, msg }) {
            const senderName = jidToSignalSenderKeyName(group, authorJid)
            const cipher = new Group_1.GroupCipher(storage, senderName)
            // Use transaction to ensure atomicity
            return parsedKeys.transaction(async () => {
                return cipher.decrypt(msg)
            }, group)
        },
        async processSenderKeyDistributionMessage({ item, authorJid }) {
            const builder = new Group_1.GroupSessionBuilder(storage)
            if (!item.groupId) {
                throw new Error('Group ID is required for sender key distribution message')
            }
            const senderName = jidToSignalSenderKeyName(item.groupId, authorJid)
            const senderMsg = new Group_1.SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage)
            const senderNameStr = senderName.toString()
            const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr])
            if (!senderKey) {
                await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord())
            }
            return parsedKeys.transaction(async () => {
                const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr])
                if (!senderKey) {
                    await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord())
                }
                await builder.process(senderName, senderMsg)
            }, item.groupId)
        },
        async decryptMessage({ jid, type, ciphertext }) {
            const addr = jidToSignalProtocolAddress(jid)
            const session = new libsignal.SessionCipher(storage, addr)
            async function doDecrypt() {
                let result
                switch (type) {
                    case 'pkmsg':
                        result = await session.decryptPreKeyWhisperMessage(ciphertext)
                        break
                    case 'msg':
                        result = await session.decryptWhisperMessage(ciphertext)
                        break
                }
                return result
            }
            if (isLikelySyncMessage(addr)) {
                // If it's a sync message, we can skip the transaction
                // as it is likely to be a system message that doesn't require strict atomicity
                return await doDecrypt()
            }
            // If it's not a sync message, we need to ensure atomicity
            // For regular messages, we use a transaction to ensure atomicity
            return parsedKeys.transaction(async () => {
                return await doDecrypt()
            }, jid)
        },
        async encryptMessage({ jid, data }) {
            // LID SINGLE SOURCE OF TRUTH: Always prefer LID when available
            let encryptionJid = jid
            // Check for LID mapping and use it if session exists
            if (jid.includes('@s.whatsapp.net')) {
                const lidForPN = await lidMapping.getLIDForPN(jid)
                if (lidForPN?.includes('@lid')) {
                    const lidAddr = jidToSignalProtocolAddress(lidForPN)
                    const { [lidAddr.toString()]: lidSession } = await auth.keys.get('session', [lidAddr.toString()])
                    if (lidSession) {
                        // LID session exists, use it
                        encryptionJid = lidForPN
                    }
                    else {
                        // Try to migrate if PN session exists
                        const pnAddr = jidToSignalProtocolAddress(jid)
                        const { [pnAddr.toString()]: pnSession } = await auth.keys.get('session', [pnAddr.toString()])
                        if (pnSession) {
                            // Migrate PN to LID
                            await repository.migrateSession([jid], lidForPN)
                            encryptionJid = lidForPN
                        }
                    }
                }
            }
            const addr = jidToSignalProtocolAddress(encryptionJid)
            const cipher = new libsignal.SessionCipher(storage, addr)
            // Use transaction to ensure atomicity
            return parsedKeys.transaction(async () => {
                const { type: sigType, body } = await cipher.encrypt(data)
                const type = sigType === 3 ? 'pkmsg' : 'msg'
                return { type, ciphertext: Buffer.from(body, 'binary') }
            }, jid)
        },
        async encryptGroupMessage({ group, meId, data }) {
            const senderName = jidToSignalSenderKeyName(group, meId)
            const builder = new Group_1.GroupSessionBuilder(storage)
            const senderNameStr = senderName.toString()
            return parsedKeys.transaction(async () => {
                const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr])
                if (!senderKey) {
                    await storage.storeSenderKey(senderName, new sender_key_record_1.SenderKeyRecord())
                }
                const senderKeyDistributionMessage = await builder.create(senderName)
                const session = new Group_1.GroupCipher(storage, senderName)
                const ciphertext = await session.encrypt(data)
                return {
                    ciphertext,
                    senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
                }
            }, group)
        }, 
        async injectE2ESession({ jid, session }) {
            const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid))
            return parsedKeys.transaction(async () => {
                await cipher.initOutgoing(session)
            }, jid)
        },
        jidToSignalProtocolAddress(jid) {
            return jidToSignalProtocolAddress(jid).toString()
        },
        // Optimized direct access to LID mapping store
        lidMapping,
        async validateSession(jid) {
            try {
                const addr = jidToSignalProtocolAddress(jid)
                const session = await storage.loadSession(addr.toString())
                if (!session) {
                    return { exists: false, reason: 'no session' }
                }
                if (!session.haveOpenSession()) {
                    return { exists: false, reason: 'no open session' }
                }
                return { exists: true }
            }
            catch (error) {
                return { exists: false, reason: 'validation error' }
            }
        },
        async deleteSession(jids) {
            if (!jids.length) return
            
            // Convert JIDs to signal addresses and prepare for bulk deletion
            const sessionUpdates = {}
            
            jids.forEach(jid => {
                const addr = jidToSignalProtocolAddress(jid)
                sessionUpdates[addr.toString()] = null
            })
            
            // Single transaction for all deletions
            return parsedKeys.transaction(async () => {
                await auth.keys.set({ session: sessionUpdates })
            }, `delete-${jids.length}-sessions`)
        },
        async migrateSession(fromJids, toJid) {
            if (!fromJids.length || !toJid.includes('@lid'))
                return { migrated: 0, skipped: 0, total: 0 }
                
            // Filter valid PN JIDs
            const validJids = fromJids.filter(jid => jid.includes('@s.whatsapp.net'))
            
            if (!validJids.length)
                return { migrated: 0, skipped: 0, total: fromJids.length }
                
            // Single optimized transaction for all migrations
            return parsedKeys.transaction(async () => {
                // 1. Batch store all LID mappings
                const mappings = validJids.map(jid => ({
                    lid: WABinary_1.transferDevice(jid, toJid),
                    pn: jid
                }))
                
                await lidMapping.storeLIDPNMappings(mappings)
                
                // 2. Prepare migration operations
                const migrationOps = validJids.map(jid => {
                    const lidWithDevice = WABinary_1.transferDevice(jid, toJid)
                    const fromDecoded = WABinary_1.jidDecode(jid)
                    const toDecoded = WABinary_1.jidDecode(lidWithDevice)
                    
                    return {
                        fromJid: jid,
                        toJid: lidWithDevice,
                        pnUser: fromDecoded.user,
                        lidUser: toDecoded.user,
                        deviceId: fromDecoded.device || 0,
                        fromAddr: jidToSignalProtocolAddress(jid),
                        toAddr: jidToSignalProtocolAddress(lidWithDevice)
                    }
                })
                
                // 3. Batch check which LID sessions already exist
                const lidAddrs = migrationOps.map(op => op.toAddr.toString())
                const existingSessions = await auth.keys.get('session', lidAddrs)
                
                // 4. Filter out sessions that already have LID sessions
                const opsToMigrate = migrationOps.filter(op => !existingSessions[op.toAddr.toString()])
                const skippedCount = migrationOps.length - opsToMigrate.length
                
                if (!opsToMigrate.length) {
                    return { migrated: 0, skipped: skippedCount, total: validJids.length };
                }
                
                // 5. Execute all migrations in parallel
                await Promise.all(opsToMigrate.map(async (op) => {
                    const fromSession = await storage.loadSession(op.fromAddr.toString())
                    
                    if (fromSession?.haveOpenSession()) {
                        // Copy session to LID address
                        const sessionBytes = fromSession.serialize()
                        const copiedSession = libsignal.SessionRecord.deserialize(sessionBytes)
                        await storage.storeSession(op.toAddr.toString(), copiedSession)
                        
                        // Delete PN session
                        await auth.keys.set({ session: { [op.fromAddr.toString()]: null } })
                    }
                }))
                
                return { migrated: opsToMigrate.length, skipped: skippedCount, total: validJids.length }
            }, `migrate-${validJids.length}-sessions-${WABinary_1.jidDecode(toJid)?.user}`)
        },
        async encryptMessageWithWire({ encryptionJid, wireJid, data }) {
            const result = await repository.encryptMessage({ jid: encryptionJid, data })
            return { ...result, wireJid }
        }
    }
    return repository
}

const jidToSignalProtocolAddress = (jid) => {
    const decoded = WABinary_1.jidDecode(jid)
    const { user, device, server } = decoded
    
    if (!user) {
        throw new Error(`JID decoded but user is empty: "${jid}" -> user: "${user}", server: "${server}", device: ${device}`)
    }
    
    // LID addresses get _1 suffix for Signal protocol
    const signalUser = server === 'lid' ? `${user}_1` : user
    const finalDevice = device || 0
    
    return new libsignal.ProtocolAddress(signalUser, finalDevice)
}

const jidToSignalSenderKeyName = (group, user) => {
    return new sender_key_name_1.SenderKeyName(group, jidToSignalProtocolAddress(user))
}

function signalStorage({ creds, keys }, lidMapping) {
    return {
        loadSession: async (id) => {
            try {
                // LID SINGLE SOURCE OF TRUTH: Auto-redirect PN to LID if mapping exists
                let actualId = id
                if (id.includes('.') && !id.includes('_1')) {
                    // This is a PN signal address format (e.g., "1234567890.0")
                    // Convert back to JID to check for LID mapping
                    const parts = id.split('.')
                    const device = parts[1] || '0'
                    const pnJid = device === '0' ? `${parts[0]}@s.whatsapp.net` : `${parts[0]}:${device}@s.whatsapp.net`
                    const lidForPN = await lidMapping.getLIDForPN(pnJid)
                    if (lidForPN?.includes('@lid')) {
                        const lidAddr = jidToSignalProtocolAddress(lidForPN)
                        const lidId = lidAddr.toString()
                        // Check if LID session exists
                        const { [lidId]: lidSession } = await keys.get('session', [lidId])
                        if (lidSession) {
                            actualId = lidId
                        }
                    }
                }
                const { [actualId]: sess } = await keys.get('session', [actualId])
                if (sess) {
                    return libsignal.SessionRecord.deserialize(sess)
                }
            }
            catch (e) {
                return null
            }
            return null
        },
        // TODO: Replace with libsignal.SessionRecord when type exports are added to libsignal
        storeSession: async (id, session) => {
            await keys.set({ session: { [id]: session.serialize() } })
        },
        isTrustedIdentity: () => {
            return true
        },
        loadPreKey: async (id) => {
            const keyId = id.toString()
            const { [keyId]: key } = await keys.get('pre-key', [keyId])
            if (key) {
                return {
                    privKey: Buffer.from(key.private),
                    pubKey: Buffer.from(key.public)
                }
            }
        },
        removePreKey: (id) => keys.set({ 'pre-key': { [id]: null } }),
        loadSignedPreKey: () => {
            const key = creds.signedPreKey
            return {
                privKey: Buffer.from(key.keyPair.private),
                pubKey: Buffer.from(key.keyPair.public)
            }
        },
        loadSenderKey: async (senderKeyName) => {
            const keyId = senderKeyName.toString()
            const { [keyId]: key } = await keys.get('sender-key', [keyId])
            if (key) {
                return sender_key_record_1.SenderKeyRecord.deserialize(key)
            }
            return new sender_key_record_1.SenderKeyRecord()
        },
        storeSenderKey: async (senderKeyName, key) => {
            const keyId = senderKeyName.toString()
            const serialized = JSON.stringify(key.serialize())
            await keys.set({ 'sender-key': { [keyId]: Buffer.from(serialized, 'utf-8') } })
        },
        getOurRegistrationId: () => creds.registrationId,
        getOurIdentity: () => {
            const { signedIdentityKey } = creds
            return {
                privKey: Buffer.from(signedIdentityKey.private),
                pubKey: Buffer.from(Utils_1.generateSignalPubKey(signedIdentityKey.public)) 
            }
        }
    }
}

module.exports = {
  makeLibSignalRepository
}