// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.

import Foundation
import GRDB
import Curve25519Kit
import SessionUtilitiesKit

enum _002_YDBToGRDBMigration: Migration {
    static let identifier: String = "YDBToGRDBMigration"
    
    // TODO: Autorelease pool???.
    static func migrate(_ db: Database) throws {
        // MARK: - Contacts & Threads
        
        var shouldFailMigration: Bool = false
        var contacts: Set<Legacy.Contact> = []
        var contactThreadIds: Set<String> = []
        var threads: Set<TSThread> = []
        var disappearingMessagesConfiguration: [String: Legacy.DisappearingMessagesConfiguration] = [:]
        var closedGroupKeys: [String: (timestamp: TimeInterval, keys: SessionUtilitiesKit.Legacy.KeyPair)] = [:]
        var closedGroupName: [String: String] = [:]
        var closedGroupFormation: [String: UInt64] = [:]
        var closedGroupModel: [String: TSGroupModel] = [:]
        var closedGroupZombieMemberIds: [String: Set<String>] = [:]
        var openGroupInfo: [String: OpenGroupV2] = [:]
        
        Storage.read { transaction in
            // Process the Contacts
            transaction.enumerateRows(inCollection: Legacy.contactCollection) { _, object, _, _ in
                guard let contact = object as? Legacy.Contact else { return }
                contacts.insert(contact)
            }
            
            let userClosedGroupPublicKeys: [String] = transaction.allKeys(inCollection: Legacy.closedGroupPublicKeyCollection)
            
            // Process the threads
            transaction.enumerateKeysAndObjects(inCollection: Legacy.threadCollection) { key, object, _ in
                guard let thread: TSThread = object as? TSThread else { return }
                guard let threadId: String = thread.uniqueId else { return }
                
                threads.insert(thread)
                
                // Want to exclude threads which aren't visible (ie. threads which we started
                // but the user never ended up sending a message)
                if key.starts(with: Legacy.contactThreadPrefix) && thread.shouldBeVisible {
                    contactThreadIds.insert(key)
                }
             
                // Get the disappearing messages config
                disappearingMessagesConfiguration[threadId] = transaction
                    .object(forKey: threadId, inCollection: Legacy.disappearingMessagesCollection)
                    .asType(Legacy.DisappearingMessagesConfiguration.self)
                    .defaulting(to: Legacy.DisappearingMessagesConfiguration.defaultWith(threadId))
                
                // Process group-specific info
                guard let groupThread: TSGroupThread = thread as? TSGroupThread else { return }
                
                if groupThread.isClosedGroup {
                    // The old threadId for closed groups was in the below format, we don't
                    // really need the unnecessary complexity so process the key and extract
                    // the publicKey from it
                    // `g{base64String(Data(__textsecure_group__!{publicKey}))}
                    let base64GroupId: String = String(threadId.suffix(from: threadId.index(after: threadId.startIndex)))
                    guard
                        let groupIdData: Data = Data(base64Encoded: base64GroupId),
                        let groupId: String = String(data: groupIdData, encoding: .utf8),
                        let publicKey: String = groupId.split(separator: "!").last.map({ String($0) }),
                        let formationTimestamp: UInt64 = transaction.object(forKey: publicKey, inCollection: Legacy.closedGroupFormationTimestampCollection) as? UInt64
                    else {
                        SNLog("Unable to decode Closed Group during migration")
                        shouldFailMigration = true
                        return
                    }
                    guard userClosedGroupPublicKeys.contains(publicKey) else {
                        SNLog("Found unexpected invalid closed group public key during migration")
                        shouldFailMigration = true
                        return
                    }
                    
                    let keyCollection: String = "\(Legacy.closedGroupKeyPairPrefix)\(publicKey)"
                    
                    closedGroupName[threadId] = groupThread.name(with: transaction)
                    closedGroupModel[threadId] = groupThread.groupModel
                    closedGroupFormation[threadId] = formationTimestamp
                    closedGroupZombieMemberIds[threadId] = transaction.object(
                        forKey: publicKey,
                        inCollection: Legacy.closedGroupZombieMembersCollection
                    ) as? Set<String>
                    
                    transaction.enumerateKeysAndObjects(inCollection: keyCollection) { key, object, _ in
                        guard let timestamp: TimeInterval = TimeInterval(key), let keyPair: SessionUtilitiesKit.Legacy.KeyPair = object as? SessionUtilitiesKit.Legacy.KeyPair else {
                            return
                        }
                        
                        closedGroupKeys[threadId] = (timestamp, keyPair)
                    }
                }
                else if groupThread.isOpenGroup {
                    
                }
                
                
                
            }
        }
        
        // We can't properly throw within the 'enumerateKeysAndObjects' block so have to throw here
        guard !shouldFailMigration else { throw GRDBStorageError.migrationFailed }
        
        // Insert the data into GRDB
        
        // MARK: - Insert Contacts
        
        let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
        
        try contacts.forEach { contact in
            let isCurrentUser: Bool = (contact.sessionID == currentUserPublicKey)
            let contactThreadId: String = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
            
            // Create the "Profile" for the legacy contact
            try Profile(
                id: contact.sessionID,
                name: (contact.name ?? contact.sessionID),
                nickname: contact.nickname,
                profilePictureUrl: contact.profilePictureURL,
                profilePictureFileName: contact.profilePictureFileName,
                profileEncryptionKey: contact.profileEncryptionKey
            ).insert(db)
            
            // Determine if this contact is a "real" contact (don't want to create contacts for
            // every user in the new structure but still want profiles for every user)
            if
                isCurrentUser ||
                contactThreadIds.contains(contactThreadId) ||
                contact.isApproved ||
                contact.didApproveMe ||
                contact.isBlocked ||
                contact.hasBeenBlocked {
                // Create the contact
                // TODO: Closed group admins???
                try Contact(
                    id: contact.sessionID,
                    isTrusted: (isCurrentUser || contact.isTrusted),
                    isApproved: (isCurrentUser || contact.isApproved),
                    isBlocked: (!isCurrentUser && contact.isBlocked),
                    didApproveMe: (isCurrentUser || contact.didApproveMe),
                    hasBeenBlocked: (!isCurrentUser && (contact.hasBeenBlocked || contact.isBlocked))
                ).insert(db)
            }
        }
        
        // MARK: - Insert Threads
        
        try threads.forEach { thread in
            guard let legacyThreadId: String = thread.uniqueId else { return }
            
            let id: String
            let variant: SessionThread.Variant
            let notificationMode: SessionThread.NotificationMode
            
            switch thread {
                case let groupThread as TSGroupThread:
                    if groupThread.isOpenGroup {
                        id = legacyThreadId//openGroup.id
                        variant = .openGroup
                    }
                    else {
                        guard let publicKey: Data = closedGroupKeys[legacyThreadId]?.keys.publicKey else {
                            throw GRDBStorageError.migrationFailed
                        }
                        
                        id = publicKey.toHexString()
                        variant = .closedGroup
                    }
                    
                    notificationMode = (thread.isMuted ? .none :
                        (groupThread.isOnlyNotifyingForMentions ?
                            .mentionsOnly :
                            .all
                        )
                    )
                    
                default:
                    id = legacyThreadId.substring(from: Legacy.contactThreadPrefix.count)
                    variant = .contact
                    notificationMode = (thread.isMuted ? .none : .all)
            }
            
            try SessionThread(
                id: id,
                variant: variant,
                creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
                shouldBeVisible: thread.shouldBeVisible,
                isPinned: thread.isPinned,
                messageDraft: thread.messageDraft,
                notificationMode: notificationMode,
                mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
            ).insert(db)
            
            // Disappearing Messages Configuration
            if let config: Legacy.DisappearingMessagesConfiguration = disappearingMessagesConfiguration[id] {
                try DisappearingMessagesConfiguration(
                    id: id,
                    isEnabled: config.isEnabled,
                    durationSeconds: TimeInterval(config.durationSeconds)
                ).insert(db)
            }
            
            // Closed Groups
            if (thread as? TSGroupThread)?.isClosedGroup == true {
                guard
                    let keyInfo = closedGroupKeys[legacyThreadId],
                    let name: String = closedGroupName[legacyThreadId],
                    let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
                    let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
                else { throw GRDBStorageError.migrationFailed }
                
                try ClosedGroup(
                    publicKey: keyInfo.keys.publicKey.toHexString(),
                    name: name,
                    formationTimestamp: TimeInterval(formationTimestamp)
                ).insert(db)
                
                try ClosedGroupKeyPair(
                    publicKey: keyInfo.keys.publicKey.toHexString(),
                    secretKey: keyInfo.keys.privateKey,
                    receivedTimestamp: keyInfo.timestamp
                ).insert(db)
                
                try groupModel.groupMemberIds.forEach { memberId in
                    try GroupMember(
                        groupId: id,
                        profileId: memberId,
                        role: .standard
                    ).insert(db)
                }
                
                try groupModel.groupAdminIds.forEach { adminId in
                    try GroupMember(
                        groupId: id,
                        profileId: adminId,
                        role: .admin
                    ).insert(db)
                }
                
                try (closedGroupZombieMemberIds[legacyThreadId] ?? []).forEach { zombieId in
                    try GroupMember(
                        groupId: id,
                        profileId: zombieId,
                        role: .zombie
                    ).insert(db)
                }
            }
        }
    }
}