import SessionUtilitiesKit

@objc(SNConfigurationMessage)
public final class ConfigurationMessage : ControlMessage {
    public var closedGroups: Set<ClosedGroup> = []
    public var openGroups: Set<String> = []
    public var displayName: String?
    public var profilePictureURL: String?
    public var profileKey: Data?
    public var contacts: Set<Contact> = []

    public override var isSelfSendValid: Bool { true }
    
    // MARK: Initialization
    public override init() { super.init() }

    public init(displayName: String?, profilePictureURL: String?, profileKey: Data?, closedGroups: Set<ClosedGroup>, openGroups: Set<String>, contacts: Set<Contact>) {
        super.init()
        self.displayName = displayName
        self.profilePictureURL = profilePictureURL
        self.profileKey = profileKey
        self.closedGroups = closedGroups
        self.openGroups = openGroups
        self.contacts = contacts
    }

    // MARK: Coding
    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        if let closedGroups = coder.decodeObject(forKey: "closedGroups") as! Set<ClosedGroup>? { self.closedGroups = closedGroups }
        if let openGroups = coder.decodeObject(forKey: "openGroups") as! Set<String>? { self.openGroups = openGroups }
        if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
        if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
        if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
        if let contacts = coder.decodeObject(forKey: "contacts") as! Set<Contact>? { self.contacts = contacts }
    }

    public override func encode(with coder: NSCoder) {
        super.encode(with: coder)
        coder.encode(closedGroups, forKey: "closedGroups")
        coder.encode(openGroups, forKey: "openGroups")
        coder.encode(displayName, forKey: "displayName")
        coder.encode(profilePictureURL, forKey: "profilePictureURL")
        coder.encode(profileKey, forKey: "profileKey")
        coder.encode(contacts, forKey: "contacts")
    }

    // MARK: Proto Conversion
    public override class func fromProto(_ proto: SNProtoContent) -> ConfigurationMessage? {
        guard let configurationProto = proto.configurationMessage else { return nil }
        let displayName = configurationProto.displayName
        let profilePictureURL = configurationProto.profilePicture
        let profileKey = configurationProto.profileKey
        let closedGroups = Set(configurationProto.closedGroups.compactMap { ClosedGroup.fromProto($0) })
        let openGroups = Set(configurationProto.openGroups)
        let contacts = Set(configurationProto.contacts.compactMap { Contact.fromProto($0) })
        return ConfigurationMessage(displayName: displayName, profilePictureURL: profilePictureURL, profileKey: profileKey,
            closedGroups: closedGroups, openGroups: openGroups, contacts: contacts)
    }

    public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? {
        let configurationProto = SNProtoConfigurationMessage.builder()
        if let displayName = displayName { configurationProto.setDisplayName(displayName) }
        if let profilePictureURL = profilePictureURL { configurationProto.setProfilePicture(profilePictureURL) }
        if let profileKey = profileKey { configurationProto.setProfileKey(profileKey) }
        configurationProto.setClosedGroups(closedGroups.compactMap { $0.toProto() })
        configurationProto.setOpenGroups([String](openGroups))
        configurationProto.setContacts(contacts.compactMap { $0.toProto() })
        let contentProto = SNProtoContent.builder()
        do {
            contentProto.setConfigurationMessage(try configurationProto.build())
            return try contentProto.build()
        } catch {
            SNLog("Couldn't construct configuration proto from: \(self).")
            return nil
        }
    }

    // MARK: Description
    public override var description: String {
        """
        ConfigurationMessage(
            closedGroups: \([ClosedGroup](closedGroups).prettifiedDescription),
            openGroups: \([String](openGroups).prettifiedDescription),
            displayName: \(displayName ?? "null"),
            profilePictureURL: \(profilePictureURL ?? "null"),
            profileKey: \(profileKey?.toHexString() ?? "null"),
            contacts: \([Contact](contacts).prettifiedDescription)
        )
        """
    }
}

// MARK: Closed Group
extension ConfigurationMessage {

    @objc(SNClosedGroup)
    public final class ClosedGroup : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
        public let publicKey: String
        public let name: String
        public let encryptionKeyPair: ECKeyPair
        public let members: Set<String>
        public let admins: Set<String>
        public let expirationTimer: UInt32

        public var isValid: Bool { !members.isEmpty && !admins.isEmpty }

        public init(publicKey: String, name: String, encryptionKeyPair: ECKeyPair, members: Set<String>, admins: Set<String>, expirationTimer: UInt32) {
            self.publicKey = publicKey
            self.name = name
            self.encryptionKeyPair = encryptionKeyPair
            self.members = members
            self.admins = admins
            self.expirationTimer = expirationTimer
        }

        public required init?(coder: NSCoder) {
            guard let publicKey = coder.decodeObject(forKey: "publicKey") as! String?,
                let name = coder.decodeObject(forKey: "name") as! String?,
                let encryptionKeyPair = coder.decodeObject(forKey: "encryptionKeyPair") as! ECKeyPair?,
                let members = coder.decodeObject(forKey: "members") as! Set<String>?,
                let admins = coder.decodeObject(forKey: "admins") as! Set<String>? else { return nil }
                let expirationTimer = coder.decodeObject(forKey: "expirationTimer") as? UInt32 ?? 0
            self.publicKey = publicKey
            self.name = name
            self.encryptionKeyPair = encryptionKeyPair
            self.members = members
            self.admins = admins
            self.expirationTimer = expirationTimer
        }

        public func encode(with coder: NSCoder) {
            coder.encode(publicKey, forKey: "publicKey")
            coder.encode(name, forKey: "name")
            coder.encode(encryptionKeyPair, forKey: "encryptionKeyPair")
            coder.encode(members, forKey: "members")
            coder.encode(admins, forKey: "admins")
            coder.encode(expirationTimer, forKey: "expirationTimer")
        }

        public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> ClosedGroup? {
            guard let publicKey = proto.publicKey?.toHexString(),
                let name = proto.name,
                let encryptionKeyPairAsProto = proto.encryptionKeyPair else { return nil }
            let encryptionKeyPair: ECKeyPair
            do {
                encryptionKeyPair = try ECKeyPair(publicKeyData: encryptionKeyPairAsProto.publicKey, privateKeyData: encryptionKeyPairAsProto.privateKey)
            } catch {
                SNLog("Couldn't construct closed group from proto: \(self).")
                return nil
            }
            let members = Set(proto.members.map { $0.toHexString() })
            let admins = Set(proto.admins.map { $0.toHexString() })
            let expirationTimer = proto.expirationTimer
            let result = ClosedGroup(publicKey: publicKey, name: name, encryptionKeyPair: encryptionKeyPair, members: members, admins: admins, expirationTimer: expirationTimer)
            guard result.isValid else { return nil }
            return result
        }

        public func toProto() -> SNProtoConfigurationMessageClosedGroup? {
            guard isValid else { return nil }
            let result = SNProtoConfigurationMessageClosedGroup.builder()
            result.setPublicKey(Data(hex: publicKey))
            result.setName(name)
            do {
                let encryptionKeyPairAsProto = try SNProtoKeyPair.builder(publicKey: encryptionKeyPair.publicKey, privateKey: encryptionKeyPair.privateKey).build()
                result.setEncryptionKeyPair(encryptionKeyPairAsProto)
            } catch {
                SNLog("Couldn't construct closed group proto from: \(self).")
                return nil
            }
            result.setMembers(members.map { Data(hex: $0) })
            result.setAdmins(admins.map { Data(hex: $0) })
            result.setExpirationTimer(expirationTimer)
            do {
                return try result.build()
            } catch {
                SNLog("Couldn't construct closed group proto from: \(self).")
                return nil
            }
        }

        public override var description: String { name }
    }
}

// MARK: Contact
extension ConfigurationMessage {

    @objc(SNConfigurationMessageContact)
    public final class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
        public var publicKey: String?
        public var displayName: String?
        public var profilePictureURL: String?
        public var profileKey: Data?
        
        public var hasIsApproved: Bool
        public var isApproved: Bool
        public var hasIsBlocked: Bool
        public var isBlocked: Bool
        public var hasDidApproveMe: Bool
        public var didApproveMe: Bool

        public var isValid: Bool { publicKey != nil && displayName != nil }

        public init(
            publicKey: String,
            displayName: String,
            profilePictureURL: String?,
            profileKey: Data?,
            hasIsApproved: Bool,
            isApproved: Bool,
            hasIsBlocked: Bool,
            isBlocked: Bool,
            hasDidApproveMe: Bool,
            didApproveMe: Bool
        ) {
            self.publicKey = publicKey
            self.displayName = displayName
            self.profilePictureURL = profilePictureURL
            self.profileKey = profileKey
            self.hasIsApproved = hasIsApproved
            self.isApproved = isApproved
            self.hasIsBlocked = hasIsBlocked
            self.isBlocked = isBlocked
            self.hasDidApproveMe = hasDidApproveMe
            self.didApproveMe = didApproveMe
        }

        public required init?(coder: NSCoder) {
            guard let publicKey = coder.decodeObject(forKey: "publicKey") as! String?,
                let displayName = coder.decodeObject(forKey: "displayName") as! String? else { return nil }
            self.publicKey = publicKey
            self.displayName = displayName
            self.profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String?
            self.profileKey = coder.decodeObject(forKey: "profileKey") as! Data?
            self.hasIsApproved = (coder.decodeObject(forKey: "hasIsApproved") as? Bool ?? false)
            self.isApproved = (coder.decodeObject(forKey: "isApproved") as? Bool ?? false)
            self.hasIsBlocked = (coder.decodeObject(forKey: "hasIsBlocked") as? Bool ?? false)
            self.isBlocked = (coder.decodeObject(forKey: "isBlocked") as? Bool ?? false)
            self.hasDidApproveMe = (coder.decodeObject(forKey: "hasDidApproveMe") as? Bool ?? false)
            self.didApproveMe = (coder.decodeObject(forKey: "didApproveMe") as? Bool ?? false)
        }

        public func encode(with coder: NSCoder) {
            coder.encode(publicKey, forKey: "publicKey")
            coder.encode(displayName, forKey: "displayName")
            coder.encode(profilePictureURL, forKey: "profilePictureURL")
            coder.encode(profileKey, forKey: "profileKey")
            coder.encode(hasIsApproved, forKey: "hasIsApproved")
            coder.encode(isApproved, forKey: "isApproved")
            coder.encode(hasIsBlocked, forKey: "hasIsBlocked")
            coder.encode(isBlocked, forKey: "isBlocked")
            coder.encode(hasDidApproveMe, forKey: "hasDidApproveMe")
            coder.encode(didApproveMe, forKey: "didApproveMe")
        }

        public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> Contact? {
            let result: Contact = Contact(
                publicKey: proto.publicKey.toHexString(),
                displayName: proto.name,
                profilePictureURL: proto.profilePicture,
                profileKey: proto.profileKey,
                hasIsApproved: proto.hasIsApproved,
                isApproved: proto.isApproved,
                hasIsBlocked: proto.hasIsBlocked,
                isBlocked: proto.isBlocked,
                hasDidApproveMe: proto.hasDidApproveMe,
                didApproveMe: proto.didApproveMe
            )
            
            guard result.isValid else { return nil }
            return result
        }

        public func toProto() -> SNProtoConfigurationMessageContact? {
            guard isValid else { return nil }
            guard let publicKey = publicKey, let displayName = displayName else { return nil }
            let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
            if let profilePictureURL = profilePictureURL { result.setProfilePicture(profilePictureURL) }
            if let profileKey = profileKey { result.setProfileKey(profileKey) }
            
            if hasIsApproved { result.setIsApproved(isApproved) }
            if hasIsBlocked { result.setIsBlocked(isBlocked) }
            if hasDidApproveMe { result.setDidApproveMe(didApproveMe) }
            
            do {
                return try result.build()
            } catch {
                SNLog("Couldn't construct contact proto from: \(self).")
                return nil
            }
        }

        public override var description: String { displayName ?? "" }
    }
}