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

import Foundation
import LibSignalClient

public indirect enum GroupUpdateSource {
    /// No source found.
    case unknown

    /// Source known to be the local user. The original source
    /// cannot itself be localUser or unknown.
    case localUser(originalSource: GroupUpdateSource)

    /// Legacy update (pre-GV2) with only an e164.
    case legacyE164(E164)

    /// Standard case. Most updates come from an ACI.
    case aci(Aci)

    /// A user who was invited by PNI rejected the invite.
    ///
    /// This case will refer to the ``GroupsProtoGroupChangeActionsDeletePendingMemberAction``,
    /// when the pending member in question is identified by a PNI.
    ///
    /// This is, at the time of writing, the only case in which the best
    /// identifier we have for the group update source is a PNI.
    case rejectedInviteToPni(Pni)

    /// If future updates introduce additional cases in which a PNI is the
    /// best/only identifier for the group update source, add them here. These
    /// cases are rare exceptions to the rule, so we prefer to enumerate them so
    /// as to make it easier for callers to understand exactly when they might
    /// be dealing with a PNI.
}

extension GroupUpdateSource {

    public func serviceIdUnsafeForLocalUserComparison() -> ServiceId? {
        switch self {
        case .unknown:
            return nil
        case .legacyE164:
            return nil
        case .aci(let aci):
            return aci
        case .rejectedInviteToPni(let pni):
            return pni
        case .localUser(let originalSource):
            return originalSource.serviceIdUnsafeForLocalUserComparison()
        }
    }
}

extension GroupUpdateSource: Equatable {}

extension TSInfoMessage {

    // MARK: - Serialization

    internal static func legacyGroupUpdateSource(
        infoMessageUserInfoDict: [InfoMessageUserInfoKey: Any]?
    ) -> GroupUpdateSource {
        guard let infoMessageUserInfoDict else {
            return .unknown
        }

        // Legacy cases stored if they were known local users.
        let isKnownLocalUser: () -> Bool = {
            if let storedValue = infoMessageUserInfoDict[.legacyUpdaterKnownToBeLocalUser] as? Bool {
                return storedValue
            }

            // Check for legacy persisted enum state.
            if
                let legacyPrecomputed = infoMessageUserInfoDict[.legacyGroupUpdateItems]
                    as? LegacyPersistableGroupUpdateItemsWrapper,
                case let .inviteRemoved(_, wasLocalUser) = legacyPrecomputed.updateItems.first
            {
                return wasLocalUser
            }
            return false
        }

        guard let address = infoMessageUserInfoDict[.groupUpdateSourceLegacyAddress] as? SignalServiceAddress else {
            return .unknown
        }
        if let aci = address.serviceId as? Aci {
            if isKnownLocalUser() {
                return .localUser(originalSource: .aci(aci))
            }
            return .aci(aci)
        } else if let pni = address.serviceId as? Pni {
            // When GroupUpdateSource was introduced, the _only_ way to have
            // a Pni (and not an aci) be the source address was when the update
            // came from someone invited by Pni rejecting that invitation.
            // Maybe other cases got added in the future, but if they did they'd
            // not use the legacy address storage, so if we find a legacy address
            // with a Pni, it _must_ be from the pni invite rejection case.
            if isKnownLocalUser() {
                return .localUser(originalSource: .rejectedInviteToPni(pni))
            } else {
                return .rejectedInviteToPni(pni)
            }
        } else if let e164 = address.e164 {
            if isKnownLocalUser() {
                return .localUser(originalSource: .legacyE164(e164))
            } else {
                return .legacyE164(e164)
            }
        } else {
            return .unknown
        }
    }
}
