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

import SignalCoreKit

/// Represents an error encountered while making a receipt credential request.
///
/// These are persisted in the ``SubscriptionReceiptCredentialRequestResultStore``
/// by the ``ReceiptCredentialRedemptionJobQueue``'s
/// ``SubscriptionReceiptCredentailRedemptionOperation``.
public struct ReceiptCredentialRequestError: Codable, Equatable {
    public enum ErrorCode: Int {
        case paymentStillProcessing = 204
        case paymentFailed = 402

        case localValidationFailed = 1
        case serverValidationFailed = 400
        case paymentNotFound = 404
        case paymentIntentRedeemed = 409
    }

    public let errorCode: ErrorCode

    /// If our error code is `.paymentFailed`, this holds the associated charge
    /// failure.
    ///
    /// We may have legacy or exceptional `.paymentFailed` errors persisted with
    /// this `nil`.
    public let chargeFailureCodeIfPaymentFailed: String?

    /// We may have legacy errors persisted with this `nil`.
    public let badge: ProfileBadge?

    /// We may have legacy errors persisted with this `nil`.
    public let amount: FiatMoney?

    /// We may have legacy errors persisted with this `nil`.
    public let paymentMethod: DonationPaymentMethod?

    /// The epoch timestamp at which this error was created.
    public let timestampMs: UInt64

    public init(
        errorCode: ErrorCode,
        chargeFailureCodeIfPaymentFailed: String?,
        badge: ProfileBadge,
        amount: FiatMoney,
        paymentMethod: DonationPaymentMethod
    ) {
        owsAssert(
            chargeFailureCodeIfPaymentFailed == nil || errorCode == .paymentFailed,
            "Charge failure must only be populated if the error code is payment failed!"
        )

        self.errorCode = errorCode
        self.chargeFailureCodeIfPaymentFailed = chargeFailureCodeIfPaymentFailed
        self.badge = badge
        self.amount = amount
        self.paymentMethod = paymentMethod
        self.timestampMs = Date().ows_millisecondsSince1970
    }

    /// When dealing with legacy persisted data, we may only have the raw error
    /// code available. This should only be used if a full error cannot be
    /// constructed!
    public init(legacyErrorCode: ErrorCode) {
        self.errorCode = legacyErrorCode
        self.chargeFailureCodeIfPaymentFailed = nil
        self.badge = nil
        self.amount = nil
        self.paymentMethod = nil
        self.timestampMs = Date().ows_millisecondsSince1970
    }

    // MARK: - Codable

    private enum CodingKeys: String, CodingKey {
        case errorCode
        case chargeFailureCodeIfPaymentFailed
        case badge
        case amount
        case paymentMethod
        case timestampMs
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        chargeFailureCodeIfPaymentFailed = try container.decodeIfPresent(String.self, forKey: .chargeFailureCodeIfPaymentFailed)
        badge = try container.decodeIfPresent(ProfileBadge.self, forKey: .badge)
        amount = try container.decodeIfPresent(FiatMoney.self, forKey: .amount)
        timestampMs = try container.decode(UInt64.self, forKey: .timestampMs)

        let errorCodeInt = try container.decode(Int.self, forKey: .errorCode)
        let paymentMethodString = try container.decodeIfPresent(String.self, forKey: .paymentMethod)

        guard let errorCode = ErrorCode(rawValue: errorCodeInt) else {
            throw DecodingError.dataCorrupted(DecodingError.Context(
                codingPath: [CodingKeys.errorCode],
                debugDescription: "Unexpected error code value: \(errorCodeInt)"
            ))
        }
        self.errorCode = errorCode

        guard let paymentMethodString else {
            paymentMethod = nil
            return
        }

        guard let paymentMethod = DonationPaymentMethod(rawValue: paymentMethodString) else {
            throw DecodingError.dataCorrupted(DecodingError.Context(
                codingPath: [CodingKeys.paymentMethod],
                debugDescription: "Unexpected payment method value: \(paymentMethodString)"
            ))
        }
        self.paymentMethod = paymentMethod
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encodeIfPresent(chargeFailureCodeIfPaymentFailed, forKey: .chargeFailureCodeIfPaymentFailed)
        try container.encodeIfPresent(badge, forKey: .badge)
        try container.encodeIfPresent(amount, forKey: .amount)
        try container.encode(timestampMs, forKey: .timestampMs)

        try container.encode(errorCode.rawValue, forKey: .errorCode)
        try container.encodeIfPresent(paymentMethod?.rawValue, forKey: .paymentMethod)
    }
}
