//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

public struct TSGroupModelBuilder: Dependencies {

    public var groupId: Data?
    public var name: String?
    public var descriptionText: String?
    public var avatarData: Data?
    public var groupMembership = GroupMembership()
    public var groupAccess: GroupAccess?
    public var groupsVersion: GroupsVersion?
    public var groupV2Revision: UInt32 = 0
    public var groupSecretParamsData: Data?
    public var newGroupSeed: NewGroupSeed?
    public var avatarUrlPath: String?
    public var inviteLinkPassword: Data?
    public var isAnnouncementsOnly: Bool = false

    public var isJoinRequestPlaceholder: Bool = false
    public var addedByAddress: SignalServiceAddress?
    public var wasJustMigrated: Bool = false
    public var didJustAddSelfViaGroupLink: Bool = false
    public var droppedMembers = [SignalServiceAddress]()

    public init() {}

    // Convert a group state proto received from the service
    // into a group model.
    private init(groupV2Snapshot: GroupV2Snapshot) throws {
        self.groupId = try groupsV2.groupId(forGroupSecretParamsData: groupV2Snapshot.groupSecretParamsData)
        self.name = groupV2Snapshot.title
        self.descriptionText = groupV2Snapshot.descriptionText
        self.avatarData = groupV2Snapshot.avatarData
        self.groupMembership = groupV2Snapshot.groupMembership
        self.groupAccess = groupV2Snapshot.groupAccess
        self.groupsVersion = GroupsVersion.V2
        self.groupV2Revision = groupV2Snapshot.revision
        self.groupSecretParamsData = groupV2Snapshot.groupSecretParamsData
        self.avatarUrlPath = groupV2Snapshot.avatarUrlPath
        self.inviteLinkPassword = groupV2Snapshot.inviteLinkPassword
        self.isAnnouncementsOnly = groupV2Snapshot.isAnnouncementsOnly
        self.isJoinRequestPlaceholder = false
        self.wasJustMigrated = false
        self.didJustAddSelfViaGroupLink = false
    }

    public
    static func builderForSnapshot(groupV2Snapshot: GroupV2Snapshot,
                                   transaction: SDSAnyWriteTransaction) throws -> TSGroupModelBuilder {

        var builder = try TSGroupModelBuilder(groupV2Snapshot: groupV2Snapshot)

        guard let groupId = builder.groupId else {
            owsFailDebug("Missing groupId.")
            return builder
        }
        guard let oldGroupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else {
            // Group not yet in db.
            return builder
        }
        let oldGroupModel = oldGroupThread.groupModel
        builder.droppedMembers = oldGroupModel.asBuilder.droppedMembers
        return builder
    }

    public mutating func apply(options: TSGroupModelOptions) {
        if options.contains(.didJustAddSelfViaGroupLink) {
            didJustAddSelfViaGroupLink = true
        }
    }

    private func checkUsers() throws {
        let allUsers = groupMembership.allMembersOfAnyKind
        for recipientAddress in allUsers {
            guard recipientAddress.isValid else {
                throw OWSAssertionError("Invalid address.")
            }
        }
    }

    public func buildForMinorChanges() throws -> TSGroupModel {

        try checkUsers()

        guard let groupsVersion = self.groupsVersion else {
            throw OWSAssertionError("Missing groupsVersion.")
        }
        guard let groupId = self.groupId else {
            throw OWSAssertionError("Missing groupId.")
        }

        var groupSecretParamsData: Data?
        if groupsVersion == .V2 {
            guard let secretParamsData = self.groupSecretParamsData else {
                throw OWSAssertionError("Missing groupSecretParamsData.")
            }
            groupSecretParamsData = secretParamsData
        }

        return try build(groupsVersion: groupsVersion,
                         groupId: groupId,
                         groupSecretParamsData: groupSecretParamsData)
    }

    public func build() throws -> TSGroupModel {

        try checkUsers()

        let groupsVersion = self.groupsVersion ?? .V2

        let newGroupSeed = self.newGroupSeed ?? NewGroupSeed()

        let groupId = try buildGroupId(groupsVersion: groupsVersion,
                                       newGroupSeed: newGroupSeed)

        var groupSecretParamsData: Data?
        if groupsVersion == .V2 {
            groupSecretParamsData = try buildGroupSecretParamsData(newGroupSeed: newGroupSeed)
        }

        return try build(groupsVersion: groupsVersion,
                         groupId: groupId,
                         groupSecretParamsData: groupSecretParamsData)
    }

    private func build(groupsVersion: GroupsVersion,
                       groupId: Data,
                       groupSecretParamsData: Data?) throws -> TSGroupModel {

        let allUsers = groupMembership.allMembersOfAnyKind
        for recipientAddress in allUsers {
            guard recipientAddress.isValid else {
                throw OWSAssertionError("Invalid address.")
            }
        }

        var name: String?
        if let strippedName = self.name?.stripped.nilIfEmpty {
            name = strippedName
        }

        guard GroupManager.isValidGroupId(groupId, groupsVersion: groupsVersion) else {
            throw OWSAssertionError("Invalid groupId.")
        }

        switch groupsVersion {
        case .V1:
            if !groupMembership.invitedMembers.isEmpty {
                owsFailDebug("v1 group has pending profile key members.")
            }
            if !groupMembership.requestingMembers.isEmpty {
                owsFailDebug("v1 group has pending request members.")
            }
            owsAssertDebug(!isJoinRequestPlaceholder)
            return TSGroupModel(groupId: groupId,
                                name: name,
                                avatarData: avatarData,
                                members: Array(groupMembership.fullMembers),
                                addedBy: addedByAddress)
        case .V2:
            owsAssertDebug(addedByAddress == nil)

            var descriptionText: String?
            if let strippedDescriptionText = self.descriptionText?.stripped.nilIfEmpty {
                descriptionText = strippedDescriptionText
            }

            let groupAccess = buildGroupAccess(groupsVersion: groupsVersion)
            guard let groupSecretParamsData = groupSecretParamsData else {
                throw OWSAssertionError("Missing groupSecretParamsData.")
            }
            // Don't set avatarUrlPath unless we have avatarData.
            let avatarUrlPath = avatarData != nil ? self.avatarUrlPath : nil

            // Update droppedMembers, removing any current members.
            let droppedMembers = Array(Set(self.droppedMembers).subtracting(groupMembership.allMembersOfAnyKind))
            return TSGroupModelV2(groupId: groupId,
                                  name: name,
                                  descriptionText: descriptionText,
                                  avatarData: avatarData,
                                  groupMembership: groupMembership,
                                  groupAccess: groupAccess,
                                  revision: groupV2Revision,
                                  secretParamsData: groupSecretParamsData,
                                  avatarUrlPath: avatarUrlPath,
                                  inviteLinkPassword: inviteLinkPassword,
                                  isAnnouncementsOnly: isAnnouncementsOnly,
                                  isJoinRequestPlaceholder: isJoinRequestPlaceholder,
                                  wasJustMigrated: wasJustMigrated,
                                  didJustAddSelfViaGroupLink: didJustAddSelfViaGroupLink,
                                  addedByAddress: addedByAddress,
                                  droppedMembers: droppedMembers)
        }
    }

    public func buildAsV2() throws -> TSGroupModelV2 {
        guard let model = try build() as? TSGroupModelV2 else {
            throw OWSAssertionError("[GV1] Should be impossible to create a V1 group!")
        }
        return model
    }

    private func buildGroupId(groupsVersion: GroupsVersion,
                              newGroupSeed: NewGroupSeed) throws -> Data {
        if let value = groupId {
            return value
        }

        switch groupsVersion {
        case .V1:
            return newGroupSeed.groupIdV1
        case .V2:
            guard let groupIdV2 = newGroupSeed.groupIdV2 else {
                throw OWSAssertionError("Missing groupIdV2.")
            }
            return groupIdV2
        }
    }

    private func buildGroupSecretParamsData(newGroupSeed: NewGroupSeed) throws -> Data {
        if let value = groupSecretParamsData {
            return value
        }

        guard let value = newGroupSeed.groupSecretParamsData else {
            throw OWSAssertionError("Missing groupSecretParamsData.")
        }
        return value
    }

    private func buildGroupAccess(groupsVersion: GroupsVersion) -> GroupAccess {
        if let value = groupAccess {
            return value
        }

        switch groupsVersion {
        case .V1:
            return GroupAccess.defaultForV1
        case .V2:
            return GroupAccess.defaultForV2
        }
    }
}

// MARK: -

public extension TSGroupModel {
    var asBuilder: TSGroupModelBuilder {
        var builder = TSGroupModelBuilder()
        builder.groupId = self.groupId
        builder.name = self.groupName
        builder.avatarData = self.avatarData
        builder.groupMembership = self.groupMembership
        builder.groupsVersion = self.groupsVersion
        builder.addedByAddress = self.addedByAddress

        if let v2 = self as? TSGroupModelV2 {
            builder.groupAccess = v2.access
            builder.groupV2Revision = v2.revision
            builder.groupSecretParamsData = v2.secretParamsData
            builder.avatarUrlPath = v2.avatarUrlPath
            builder.inviteLinkPassword = v2.inviteLinkPassword
            builder.isAnnouncementsOnly = v2.isAnnouncementsOnly
            builder.droppedMembers = v2.droppedMembers
            builder.descriptionText = v2.descriptionText

            // Do not copy transient properties:
            //
            // * isJoinRequestPlaceholder
            // * wasJustMigrated
            // * didJustAddSelfViaGroupLink
            //
            // We want to discard these values when updating group models.
        }

        return builder
    }
}

// MARK: -

public struct TSGroupModelOptions: OptionSet {
    public let rawValue: Int
    public static let didJustAddSelfViaGroupLink  = TSGroupModelOptions(rawValue: 1 << 0)

    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}
