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

import Foundation
import MobileCoin
import LibMobileCoin
import SignalServiceKit

extension MobileCoinAPI {

    // MARK: - Environment

    public enum Environment: CustomStringConvertible {
        case mobileCoinAlphaNet
        case mobileCoinMobileDev
        case signalTestNet
        case mobileCoinTestNet
        case signalMainNet
        case mobileCoinMainNet

        public static var current: Environment {
            if TSConstants.isUsingProductionService {
                return .signalMainNet
            } else {
                return .signalTestNet
            }
        }

        public var description: String {
            switch self {
            case .mobileCoinAlphaNet:
                return ".mobileCoinAlphaNet"
            case .mobileCoinMobileDev:
                return ".mobileCoinMobileDev"
            case .signalTestNet:
                return ".signalTestNet"
            case .mobileCoinTestNet:
                return ".mobileCoinTestNet"
            case .signalMainNet:
                return ".signalMainNet"
            case .mobileCoinMainNet:
                return ".mobileCoinMainNet"
            }
        }
    }

    // MARK: - MobileCoinNetworkConfig

    struct MobileCoinNetworkConfig {
        let consensusUrls: [String]
        let fogUrl: String
        let fogReportUrl: String

        static var signalMainNet: MobileCoinNetworkConfig {
            let consensusUrls = [
                "mc://node1.consensus.mob.production.namda.net",
                "mc://node2.consensus.mob.production.namda.net"
            ]
            let fogUrl = "fog://fog.prod.mobilecoinww.com"
            let fogReportUrl = fogUrl
            return MobileCoinNetworkConfig(consensusUrls: consensusUrls, fogUrl: fogUrl, fogReportUrl: fogReportUrl)
        }

        static var mobileCoinMainNet: MobileCoinNetworkConfig {
            let consensusUrls = ["mc://node1.prod.mobilecoinww.com"]
            let fogUrl = "fog://fog.prod.mobilecoinww.com"
            let fogReportUrl = fogUrl
            return MobileCoinNetworkConfig(consensusUrls: consensusUrls, fogUrl: fogUrl, fogReportUrl: fogReportUrl)
        }

        static var signalTestNet: MobileCoinNetworkConfig {
            let consensusUrls = [
                "mc://node1.consensus.mob.staging.namda.net",
                "mc://node2.consensus.mob.staging.namda.net"
            ]
            let fogUrl = "fog://fog.test.mobilecoin.com"
            let fogReportUrl = fogUrl
            return MobileCoinNetworkConfig(consensusUrls: consensusUrls, fogUrl: fogUrl, fogReportUrl: fogReportUrl)
        }

        static var mobileCoinTestNet: MobileCoinNetworkConfig {
            let consensusUrls = ["mc://node1.test.mobilecoin.com"]
            let fogUrl = "fog://fog.test.mobilecoin.com"
            let fogReportUrl = fogUrl
            return MobileCoinNetworkConfig(consensusUrls: consensusUrls, fogUrl: fogUrl, fogReportUrl: fogReportUrl)
        }

        static var mobileCoinAlphaNet: MobileCoinNetworkConfig {
            let consensusUrls = ["mc://node1.alpha.development.mobilecoin.com"]
            let fogUrl = "fog://fog.alpha.development.mobilecoin.com"
            let fogReportUrl = fogUrl
            return MobileCoinNetworkConfig(consensusUrls: consensusUrls, fogUrl: fogUrl, fogReportUrl: fogReportUrl)
        }

        static var mobileCoinMobileDev: MobileCoinNetworkConfig {
            let consensusUrls = ["mc://consensus.mobiledev.mobilecoin.com"]
            let fogUrl = "fog://fog.mobiledev.mobilecoin.com"
            let fogReportUrl = fogUrl
            return MobileCoinNetworkConfig(consensusUrls: consensusUrls, fogUrl: fogUrl, fogReportUrl: fogReportUrl)
        }

        static func networkConfig(environment: Environment) -> MobileCoinNetworkConfig {
            switch environment {
            case .mobileCoinAlphaNet:
                return MobileCoinNetworkConfig.mobileCoinAlphaNet
            case .mobileCoinMobileDev:
                return MobileCoinNetworkConfig.mobileCoinMobileDev
            case .mobileCoinTestNet:
                return MobileCoinNetworkConfig.mobileCoinTestNet
            case .signalTestNet:
                return MobileCoinNetworkConfig.signalTestNet
            case .mobileCoinMainNet:
                return MobileCoinNetworkConfig.mobileCoinMainNet
            case .signalMainNet:
                return MobileCoinNetworkConfig.signalMainNet
            }
        }
    }

    // MARK: - AttestationInfo

    struct AttestationRawInfo {
        let measurement: Data
        let hardeningAdvisories: [String]

        static func of(_ measurement: Data, _ hardeningAdvisories: [String] = []) -> Self {
            return AttestationRawInfo(measurement: measurement, hardeningAdvisories: hardeningAdvisories)
        }
    }

    private struct AttestationInfo {
        let productId: UInt16
        let minimumSecurityVersion: UInt16
        let allowedConfigAdvisories: [String]
        let allowedHardeningAdvisories: [String]
        let measurement: Measurement

        enum Measurement {
            case enclave(data: Data)
            case signer(data: Data)
        }

        static let CONSENSUS_PRODUCT_ID: UInt16 = 1
        static let CONSENSUS_SECURITY_VERSION: UInt16 = 1
        static let FOG_VIEW_PRODUCT_ID: UInt16 = 3
        static let FOG_VIEW_SECURITY_VERSION: UInt16 = 1
        static let FOG_LEDGER_PRODUCT_ID: UInt16 = 2
        static let FOG_LEDGER_SECURITY_VERSION: UInt16 = 1
        static let FOG_REPORT_PRODUCT_ID: UInt16 = 4
        static let FOG_REPORT_SECURITY_VERSION: UInt16 = 1

        static var allAllowedHardeningAdvisories: [String] { ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"] }

        init(
            measurement: Measurement,
            productId: UInt16,
            minimumSecurityVersion: UInt16,
            allowedConfigAdvisories: [String] = [],
            allowedHardeningAdvisories: [String] = []
        ) {

            self.measurement = measurement
            self.productId = productId
            self.minimumSecurityVersion = minimumSecurityVersion
            self.allowedConfigAdvisories = allowedConfigAdvisories
            self.allowedHardeningAdvisories = allowedHardeningAdvisories
        }

        static func consensus(
            measurement: Measurement,
            allowedConfigAdvisories: [String] = [],
            allowedHardeningAdvisories: [String] = []
        ) -> AttestationInfo {
            .init(
                measurement: measurement,
                productId: CONSENSUS_PRODUCT_ID,
                minimumSecurityVersion: CONSENSUS_SECURITY_VERSION,
                allowedConfigAdvisories: allowedConfigAdvisories,
                allowedHardeningAdvisories: allowedHardeningAdvisories)
        }

        static func fogView(
            measurement: Measurement,
            allowedConfigAdvisories: [String] = [],
            allowedHardeningAdvisories: [String] = []
        ) -> AttestationInfo {
            .init(
                measurement: measurement,
                productId: FOG_VIEW_PRODUCT_ID,
                minimumSecurityVersion: FOG_VIEW_SECURITY_VERSION,
                allowedConfigAdvisories: allowedConfigAdvisories,
                allowedHardeningAdvisories: allowedHardeningAdvisories)
        }

        static func fogKeyImage(
            measurement: Measurement,
            allowedConfigAdvisories: [String] = [],
            allowedHardeningAdvisories: [String] = []
        ) -> AttestationInfo {
            .init(
                measurement: measurement,
                productId: FOG_LEDGER_PRODUCT_ID,
                minimumSecurityVersion: FOG_LEDGER_SECURITY_VERSION,
                allowedConfigAdvisories: allowedConfigAdvisories,
                allowedHardeningAdvisories: allowedHardeningAdvisories)
        }

        static func fogMerkleProof(
            measurement: Measurement,
            allowedConfigAdvisories: [String] = [],
            allowedHardeningAdvisories: [String] = []
        ) -> AttestationInfo {
            .init(
                measurement: measurement,
                productId: FOG_LEDGER_PRODUCT_ID,
                minimumSecurityVersion: FOG_LEDGER_SECURITY_VERSION,
                allowedConfigAdvisories: allowedConfigAdvisories,
                allowedHardeningAdvisories: allowedHardeningAdvisories)
        }

        static func fogReport(
            measurement: Measurement,
            allowedConfigAdvisories: [String] = [],
            allowedHardeningAdvisories: [String] = []
        ) -> AttestationInfo {
            .init(
                measurement: measurement,
                productId: FOG_REPORT_PRODUCT_ID,
                minimumSecurityVersion: FOG_REPORT_SECURITY_VERSION,
                allowedConfigAdvisories: allowedConfigAdvisories,
                allowedHardeningAdvisories: allowedHardeningAdvisories)
        }
    }

    // MARK: - OWSAttestationConfig

    private struct OWSAttestationConfig {
        let consensus: Attestation
        let fogView: Attestation
        let fogKeyImage: Attestation
        let fogMerkleProof: Attestation
        let fogReport: Attestation

        private static func buildAttestation(attestationInfo: [AttestationInfo]) throws -> MobileCoin.Attestation {
            do {
                let mrEnclaves = try attestationInfo.compactMap { attestationInfo -> MobileCoin.Attestation.MrEnclave? in
                    guard case let .enclave(measurement) = attestationInfo.measurement else {
                        return nil
                    }
                    return try MobileCoin.Attestation.MrEnclave.make(
                        mrEnclave: measurement,
                        allowedConfigAdvisories: attestationInfo.allowedConfigAdvisories,
                        allowedHardeningAdvisories: attestationInfo.allowedHardeningAdvisories
                    ).get()
                }

                let mrSigners = try attestationInfo.compactMap { attestationInfo -> MobileCoin.Attestation.MrSigner? in
                    guard case let .signer(measurement) = attestationInfo.measurement else {
                        return nil
                    }
                    return try MobileCoin.Attestation.MrSigner.make(
                        mrSigner: measurement,
                        productId: attestationInfo.productId,
                        minimumSecurityVersion: attestationInfo.minimumSecurityVersion,
                        allowedConfigAdvisories: attestationInfo.allowedConfigAdvisories,
                        allowedHardeningAdvisories: attestationInfo.allowedHardeningAdvisories
                    ).get()
                }

                return MobileCoin.Attestation(mrEnclaves: mrEnclaves, mrSigners: mrSigners)
            } catch {
                owsFailDebug("Error: \(error)")
                throw error
            }
        }

        private static func buildAttestationConfig(
            consensus: [AttestationRawInfo],
            fogView: [AttestationRawInfo],
            fogLedger: [AttestationRawInfo],
            fogReport: [AttestationRawInfo]
        ) -> OWSAttestationConfig {
            let consensusAttestations = consensus
                .map { info -> AttestationInfo in
                    .consensus(
                        measurement: .enclave(data: info.measurement),
                        allowedHardeningAdvisories: info.hardeningAdvisories)
                }
            let fogViewAttestations = fogView
                .map { info -> AttestationInfo in
                    .fogView(
                        measurement: .enclave(data: info.measurement),
                        allowedHardeningAdvisories: info.hardeningAdvisories)
                }
            let fogKeyImageAttestations = fogLedger
                .map { info -> AttestationInfo in
                    .fogKeyImage(
                        measurement: .enclave(data: info.measurement),
                        allowedHardeningAdvisories: info.hardeningAdvisories)
                }
            let fogMerkleProofAttestations = fogLedger
                .map { info -> AttestationInfo in
                    .fogMerkleProof(
                        measurement: .enclave(data: info.measurement),
                        allowedHardeningAdvisories: info.hardeningAdvisories)
                }
            let fogReportAttestations = fogReport
                .map { info -> AttestationInfo in
                    .fogReport(
                        measurement: .enclave(data: info.measurement),
                        allowedHardeningAdvisories: info.hardeningAdvisories)
                }

            return buildAttestationConfig(
                consensus: consensusAttestations,
                fogView: fogViewAttestations,
                fogKeyImage: fogKeyImageAttestations,
                fogMerkleProof: fogMerkleProofAttestations,
                fogReport: fogReportAttestations)
        }

        private static func buildAttestationConfig(
            mrSigner mrSignerData: Data,
            allowedHardeningAdvisories: [String] = AttestationInfo.allAllowedHardeningAdvisories
        ) -> OWSAttestationConfig {
            let consensus = AttestationInfo.consensus(
                measurement: .signer(data: mrSignerData),
                allowedHardeningAdvisories: allowedHardeningAdvisories)
            let fogView = AttestationInfo.fogView(
                measurement: .signer(data: mrSignerData),
                allowedHardeningAdvisories: allowedHardeningAdvisories)
            let fogReport = AttestationInfo.fogReport(
                measurement: .signer(data: mrSignerData),
                allowedHardeningAdvisories: allowedHardeningAdvisories)
            let fogMerkleProof = AttestationInfo.fogMerkleProof(
                measurement: .signer(data: mrSignerData),
                allowedHardeningAdvisories: allowedHardeningAdvisories)
            let fogKeyImage = AttestationInfo.fogKeyImage(
                measurement: .signer(data: mrSignerData),
                allowedHardeningAdvisories: allowedHardeningAdvisories)

            return buildAttestationConfig(
                consensus: [consensus],
                fogView: [fogView],
                fogKeyImage: [fogKeyImage],
                fogMerkleProof: [fogMerkleProof],
                fogReport: [fogReport])
        }

        private static func buildAttestationConfig(
            consensus: [AttestationInfo],
            fogView: [AttestationInfo],
            fogKeyImage: [AttestationInfo],
            fogMerkleProof: [AttestationInfo],
            fogReport: [AttestationInfo]
        ) -> OWSAttestationConfig {
            do {
                return OWSAttestationConfig(
                    consensus: try buildAttestation(attestationInfo: consensus),
                    fogView: try buildAttestation(attestationInfo: fogView),
                    fogKeyImage: try buildAttestation(attestationInfo: fogKeyImage),
                    fogMerkleProof: try buildAttestation(attestationInfo: fogMerkleProof),
                    fogReport: try buildAttestation(attestationInfo: fogReport)
                )
            } catch {
                owsFail("Invalid attestationConfig: \(error)")
            }
        }

        static var mobileCoinMainNet: OWSAttestationConfig {
            // These networks currently share the same attestation config.
            signalMainNet
        }

        static var signalMainNet: OWSAttestationConfig {
            // We need the old and new enclave values here.
            let mrEnclaveConsensus: [AttestationRawInfo] = [
                // ~May 30th, 2023
                .of(Data.data(fromHex: "cd86d300c78f74ec23558cdaf734f90dd3e1bcdf8ae43fc827c6b4734ccb8862")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6th, 2024
                .of(Data.data(fromHex: "82c14d06951a2168763c8ddb9c34174f7d2059564146650661da26ab62224b8a")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]

            let mrEnclaveFogView: [AttestationRawInfo] = [
                // ~May 30th, 2023
                .of(Data.data(fromHex: "e94f6e6557b3fb85b27d804e2d005ee14a564cc50fc477797f2e5f9984b0bd79")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6th, 2024
                .of(Data.data(fromHex: "2f542dcd8f682b72e8921d87e06637c16f4aa4da27dce55b561335326731fa73")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]

            // Report aka Ingest.
            let mrEnclaveFogReport: [AttestationRawInfo] = [
                // ~May 30th, 2023
                .of(Data.data(fromHex: "7d10f5e72cacc87a6027b2be42ed4a74a6370a03c3476be754933eb18c404b0b")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6th, 2024
                .of(Data.data(fromHex: "34881106254a626842fa8557e27d07cdf863083e9e6f888d5a492a456720916f")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]

            let mrEnclaveFogLedger: [AttestationRawInfo] = [
                // ~May 30th, 2023
                .of(Data.data(fromHex: "1dee8e2e98b7dc684506991d62856b2e572a0c23f5a7d698086e62f08fb997cc")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6th, 2024
                .of(Data.data(fromHex: "2494f1542f30a6962707d0bf2aa6c8c08d7bed35668c9db1e5c61d863a0176d1")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]

            return buildAttestationConfig(consensus: mrEnclaveConsensus,
                                          fogView: mrEnclaveFogView,
                                          fogLedger: mrEnclaveFogLedger,
                                          fogReport: mrEnclaveFogReport)
        }

        static var mobileCoinTestNet: OWSAttestationConfig {
            // These networks currently share the same attestation config.
            signalTestNet
        }

        static var signalTestNet: OWSAttestationConfig {
            // We need the old and new enclave values here.
            let mrEnclaveConsensus: [AttestationRawInfo] = [
                // ~May 30, 2023
                .of(Data.data(fromHex: "5341c6702a3312243c0f049f87259352ff32aa80f0f6426351c3dd063d817d7a")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6, 2024
                .of(Data.data(fromHex: "ae7930646f37e026806087d2a3725d3f6d75a8e989fb320e6ecb258eb829057a")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]
            let mrEnclaveFogView: [AttestationRawInfo] = [
                // ~May 30, 2023
                .of(Data.data(fromHex: "ac292a1ad27c0338a5159d5fab2bed3917ea144536cb13b5c1226d09a2fbc648")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6, 2024
                .of(Data.data(fromHex: "44de03c2ba34c303e6417480644f9796161eacbe5af4f2092e413b4ebf5ccf6a")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]
            // Report aka Ingest.
            let mrEnclaveFogReport: [AttestationRawInfo]  = [
                // ~May 30, 2023
                .of(Data.data(fromHex: "248356aa0d3431abc45da1773cfd6191a4f2989a4a99da31f450bd7c461e312b")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6, 2024
                .of(Data.data(fromHex: "4a5daa23db5efa4b18071291cfa24a808f58fb0cedce7da5de804b011e87cfde")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]
            let mrEnclaveFogLedger: [AttestationRawInfo]  = [
                // ~May 30, 2023
                .of(Data.data(fromHex: "b61188a6c946557f32e612eff5615908abd1b72ec11d8b7070595a92d4abbbf1")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"]),
                // ~May 6, 2024
                .of(Data.data(fromHex: "065b1e17e95f2c356d4d071d434cea7eb6b95bc797f94954146736efd47057a7")!, ["INTEL-SA-00334", "INTEL-SA-00615", "INTEL-SA-00657"])
            ]

            return buildAttestationConfig(consensus: mrEnclaveConsensus,
                                          fogView: mrEnclaveFogView,
                                          fogLedger: mrEnclaveFogLedger,
                                          fogReport: mrEnclaveFogReport)
        }

        static var mobileCoinAlphaNet: OWSAttestationConfig {
            let mrSigner = Data([
                126, 229, 226, 157, 116, 98, 63, 219, 198, 251, 241, 69, 75, 230, 243, 187, 11, 134, 193,
                35, 102, 183, 180, 120, 173, 19, 53, 62, 68, 222, 132, 17
            ])
            return buildAttestationConfig(mrSigner: mrSigner)
        }

        static var mobileCoinMobileDev: OWSAttestationConfig {
            let mrSigner = Data([
                191, 127, 169, 87, 166, 169, 74, 203, 88, 136, 81, 188, 135, 103, 224, 202, 87, 112, 108,
                121, 244, 252, 42, 166, 188, 185, 147, 1, 44, 60, 56, 108
            ])
            return buildAttestationConfig(mrSigner: mrSigner)
        }

        static func attestationConfig(environment: Environment) -> OWSAttestationConfig {
            switch environment {
            case .mobileCoinAlphaNet:
                return mobileCoinAlphaNet
            case .mobileCoinMobileDev:
                return mobileCoinMobileDev
            case .mobileCoinTestNet:
                return mobileCoinTestNet
            case .signalTestNet:
                return signalTestNet
            case .mobileCoinMainNet:
                return mobileCoinMainNet
            case .signalMainNet:
                return signalMainNet
            }
        }
    }

    // MARK: - OWSAuthorization

    struct OWSAuthorization {
        let username: String
        let password: String

        private static let testAuthUsername = "user20220713"
        private static let testAuthPassword = "user20220713:1657845591:298d68fd6b1438082b15"

        static var mobileCoinAlpha: OWSAuthorization {
            OWSAuthorization(username: testAuthUsername,
                             password: testAuthPassword)
        }

        static var mobileCoinMobileDev: OWSAuthorization {
            OWSAuthorization(username: testAuthUsername,
                             password: testAuthPassword)
        }

        static var mobileCoinTestNet: OWSAuthorization {
            owsFail("TODO: Set this value.")
        }

        static var mobileCoinMainNet: OWSAuthorization {
            owsFail("TODO: Set this value.")
        }
    }

    // MARK: - Certificates

    private class Certificates: NSObject {

        enum CertificateBundle {
            case mainApp
            case ssk
        }

        static func certificateData(forService certFilename: String,
                                    type: String,
                                    certificateBundle: CertificateBundle,
                                    verifyDer: Bool = false) -> Data {
            let bundle: Bundle = {
                switch certificateBundle {
                case .mainApp:
                    return Bundle(for: self)
                case .ssk:
                    return Bundle(for: OWSHTTPSecurityPolicy.self)
                }
            }()
            guard let filepath = bundle.path(forResource: certFilename, ofType: type) else {
                owsFail("Missing cert: \(certFilename)")
            }
            guard OWSFileSystem.fileOrFolderExists(atPath: filepath) else {
                owsFail("Missing cert: \(certFilename)")
            }
            let data = try! Data(contentsOf: URL(fileURLWithPath: filepath))
            guard !data.isEmpty else {
                owsFail("Invalid cert: \(certFilename)")
            }
            if verifyDer {
                guard let certificate = SecCertificateCreateWithData(nil, data as CFData) else {
                    owsFail("Invalid cert: \(certFilename)")
                }
                let derData = SecCertificateCopyData(certificate) as Data
                return derData
            } else {
                return data
            }
        }
    }

    // MARK: - TrustRootCerts

    private class TrustRootCerts: NSObject {

        static func anchorCertificates_mobileCoin() -> [Data] {
            [
                Certificates.certificateData(forService: "isrgrootx1", type: "der", certificateBundle: .ssk, verifyDer: true)
            ]
        }

        static func pinPolicy(environment: Environment) -> Result<OWSHTTPSecurityPolicy, Error> {
            let trustRootCertDatas: [Data] = anchorCertificates_mobileCoin()
            guard !trustRootCertDatas.isEmpty else {
                return .failure(OWSAssertionError("No certificate data"))
            }

            let securityPolicy =  OWSHTTPSecurityPolicy(pinnedCertificates: Set(trustRootCertDatas))
            return .success(securityPolicy)
        }
    }

    // MARK: - MobileCoinAccount

    struct MobileCoinAccount {
        let environment: Environment
        let accountKey: MobileCoin.AccountKey
        var publicAddress: MobileCoin.PublicAddress {
            accountKey.publicAddress
        }

        fileprivate func authorization(signalAuthorization: OWSAuthorization) -> OWSAuthorization {
            switch environment {
            case .signalTestNet, .signalMainNet:
                return signalAuthorization
            case .mobileCoinAlphaNet:
                return OWSAuthorization.mobileCoinAlpha
            case .mobileCoinMobileDev:
                return OWSAuthorization.mobileCoinMobileDev
            case .mobileCoinTestNet, .mobileCoinMainNet:
                return signalAuthorization
            }
        }

        func buildClient(signalAuthorization: OWSAuthorization) throws -> MobileCoinClient {
            Logger.info("Environment: \(environment)")
            let networkConfig = MobileCoinNetworkConfig.networkConfig(environment: environment)
            let authorization = self.authorization(signalAuthorization: signalAuthorization)
            let attestationConfig = OWSAttestationConfig.attestationConfig(environment: environment)
            let configResult = MobileCoinClient.Config.make(consensusUrls: networkConfig.consensusUrls,
                                                            consensusAttestation: attestationConfig.consensus,
                                                            fogUrls: [networkConfig.fogUrl],
                                                            fogViewAttestation: attestationConfig.fogView,
                                                            fogKeyImageAttestation: attestationConfig.fogKeyImage,
                                                            fogMerkleProofAttestation: attestationConfig.fogMerkleProof,
                                                            fogReportAttestation: attestationConfig.fogReport,
                                                            transportProtocol: .http)

            let securityPolicy: OWSHTTPSecurityPolicy
            do {
                securityPolicy = try TrustRootCerts.pinPolicy(environment: environment).get()
            } catch {
                owsFailDebug("Error: \(error)")
                throw error
            }

            switch configResult {
            case .success(var config):
                config.httpRequester = MobileCoinHttpRequester(securityPolicy: securityPolicy)
                let clientResult = MobileCoinClient.make(accountKey: accountKey, config: config)
                switch clientResult {
                case .success(let client):
                    // There are separate FOG and consensus auth credentials which correspond to
                    // the consensus URL and fog URLs.
                    //
                    // We currently use a MobileCoin Consensus node and Signal Fog; Signal Fog
                    // requires an auth token but MobileCoin Consensus doesn't.
                    //
                    // TODO: We'll need to setConsensusBasicAuthorization() if/when we
                    // switch to Signal consensus.
                    client.setFogBasicAuthorization(username: authorization.username,
                                                    password: authorization.password)
                    return client
                case .failure(let error):
                    owsFailDebug("Error: \(error)")
                    throw error
                }
            case .failure(let error):
                owsFailDebug("Error: \(error)")
                throw error
            }
        }
    }

    // MARK: - Fog Authority

    private static func fogAuthoritySpki(environment: Environment) -> Data {
        switch environment {
        case .mobileCoinAlphaNet,
             .mobileCoinMobileDev:
            return Data(base64Encoded: "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyFOockvCEc9TcO1NvsiUfFVzvtDsR64UIRRUl3tBM2Bh8KBA932/Up86RtgJVnbslxuUCrTJZCV4dgd5hAo/mzuJOy9lAGxUTpwWWG0zZJdpt8HJRVLX76CBpWrWEt7JMoEmduvsCR8q7WkSNgT0iIoSXgT/hfWnJ8KGZkN4WBzzTH7hPrAcxPrzMI7TwHqUFfmOX7/gc+bDV5ZyRORrpuu+OR2BVObkocgFJLGmcz7KRuN7/dYtdYFpiKearGvbYqBrEjeo/15chI0Bu/9oQkjPBtkvMBYjyJPrD7oPP67i0ZfqV6xCj4nWwAD3bVjVqsw9cCBHgaykW8ArFFa0VCMdLy7UymYU5SQsfXrw/mHpr27Pp2Z0/7wpuFgJHL+0ARU48OiUzkXSHX+sBLov9X6f9tsh4q/ZRorXhcJi7FnUoagBxewvlfwQfcnLX3hp1wqoRFC4w1DC+ki93vIHUqHkNnayRsf1n48fSu5DwaFfNvejap7HCDIOpCCJmRVR8mVuxi6jgjOUa4Vhb/GCzxfNIn5ZYym1RuoE0TsFO+TPMzjed3tQvG7KemGFz3pQIryb43SbG7Q+EOzIigxYDytzcxOO5Jx7r9i+amQEiIcjBICwyFoEUlVJTgSpqBZGNpznoQ4I2m+uJzM+wMFsinTZN3mp4FU5UHjQsHKG+ZMCAwEAAQ==")!
        case .signalTestNet, .mobileCoinTestNet:
            return Data(base64Encoded:
                """
                MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvnB9wTbTOT5uoizRYaYbw7XIEkInl8E7MGOA\
                Qj+xnC+F1rIXiCnc/t1+5IIWjbRGhWzo7RAwI5sRajn2sT4rRn9NXbOzZMvIqE4hmhmEzy1YQNDnfALA\
                WNQ+WBbYGW+Vqm3IlQvAFFjVN1YYIdYhbLjAPdkgeVsWfcLDforHn6rR3QBZYZIlSBQSKRMY/tywTxeT\
                CvK2zWcS0kbbFPtBcVth7VFFVPAZXhPi9yy1AvnldO6n7KLiupVmojlEMtv4FQkk604nal+j/dOplTAT\
                V8a9AJBbPRBZ/yQg57EG2Y2MRiHOQifJx0S5VbNyMm9bkS8TD7Goi59aCW6OT1gyeotWwLg60JRZTfyJ\
                7lYWBSOzh0OnaCytRpSWtNZ6barPUeOnftbnJtE8rFhF7M4F66et0LI/cuvXYecwVwykovEVBKRF4HOK\
                9GgSm17mQMtzrD7c558TbaucOWabYR04uhdAc3s10MkuONWG0wIQhgIChYVAGnFLvSpp2/aQEq3xrRSE\
                TxsixUIjsZyWWROkuA0IFnc8d7AmcnUBvRW7FT/5thWyk5agdYUGZ+7C1o69ihR1YxmoGh69fLMPIEOh\
                Yh572+3ckgl2SaV4uo9Gvkz8MMGRBcMIMlRirSwhCfozV2RyT5Wn1NgPpyc8zJL7QdOhL7Qxb+5WjnCV\
                rQYHI2cCAwEAAQ==
                """
            )!
        case .mobileCoinMainNet, .signalMainNet:
            let mainNetFogAuthoritySpkiB64Encoded = """
                MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyr/99fvxi104MLgDgvWPVt01TuTJ+rN4qcNBUbF5i3EMM5z\
                DZlugFHKPYPv7flCh5yDDYyLQHfWkxPQqCBAqlhSrCakvQH3HqDSpbM5FJg7pt0k5w+UQGWvP079iSEO5fMRhjE/lOR\
                kvk3/UKr2yIXjZ19iEgP8hlhk9xkI42DSg0iIhk59k3wEYPMGSkVarqlPoKBzx2+11CieXnbCkRvoNwLvdzLceY8QNo\
                Lc6h2/nht4bcjDCdB0MKNSKFLVp6XNHkVF66jC7QWTZRA/d4pgI5xa+GmkQ90zDZC2sBc+xfquVIVtk0nEvqSkUDZjv\
                7AcJaq/VdPu4uj773ojrZz094PI4Q6sdbg7mfWrcq3ZQG8t9RDXD+6cgugCTFx2Cq/vJhDAPbQHmCEaMoXv2sRSfOhR\
                jtMP1KmKUw5zXmAZa7s88+e7UXRQC+SS77V8s3hinE/I5Gqa/lzl73smhXx8l4CwGnXzlQ5h1lgEHnYLRFnIenNw/md\
                MGKlWH5HwHLX3hIujERCPAnGLDt+4MjcUiU0spDH3hC9mjPVA3ltaA3+Mk2lDw0kLrZ4Gv3/Ik9WPlYetOuWteMkR1f\
                z6VOc13+WoTJPz0dVrJsK2bUz+YvdBsoHQBbUpCkmnQ5Ok+yiuWa5vYikEJ24SEr8wUiZ4Oe12KVEcjyDIxp6QoE8kC\
                AwEAAQ==
                """
            return Data(base64Encoded: mainNetFogAuthoritySpkiB64Encoded)!
        }
    }

    class func buildAccount(forPaymentsEntropy paymentsEntropy: Data) throws -> MobileCoinAccount {
        let environment = Environment.current
        let networkConfig = MobileCoinNetworkConfig.networkConfig(environment: environment)
        let accountKey = try buildAccountKey(forPaymentsEntropy: paymentsEntropy,
                                             networkConfig: networkConfig)
        return MobileCoinAccount(environment: environment,
                                 accountKey: accountKey)
    }

    class func buildAccountKey(forPaymentsEntropy paymentsEntropy: Data,
                               networkConfig: MobileCoinNetworkConfig) throws -> MobileCoin.AccountKey {
        let passphrase = try Self.passphrase(forPaymentsEntropy: paymentsEntropy)
        let mnemonic = passphrase.asPassphrase
        let fogAuthoritySpki = Self.fogAuthoritySpki(environment: .current)
        let fogReportId = ""
        let accountIndex: UInt32 = 0
        let result = MobileCoin.AccountKey.make(mnemonic: mnemonic,
                                                fogReportUrl: networkConfig.fogReportUrl,
                                                fogReportId: fogReportId,
                                                fogAuthoritySpki: fogAuthoritySpki,
                                                accountIndex: accountIndex)

        switch result {
        case .success(let accountKey):
            return accountKey
        case .failure(let error):
            owsFailDebug("Error: \(error)")
            throw error
        }
    }
}

final class MobileCoinHttpRequester: NSObject, HttpRequester {
    static let defaultConfiguration: URLSessionConfiguration = {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 30
        return config
    }()

    private let securityPolicy: OWSHTTPSecurityPolicy

    init(securityPolicy: OWSHTTPSecurityPolicy) {
        self.securityPolicy = securityPolicy
    }

    func request(
        url: URL,
        method: LibMobileCoin.HTTPMethod,
        headers: [String: String]?,
        body: Data?,
        completion: @escaping (Result<LibMobileCoin.HTTPResponse, Error>) -> Void
    ) {
        var request = URLRequest(url: url.absoluteURL)
        request.httpMethod = method.rawValue
        request.httpBody = body
        request.allHTTPHeaderFields = headers

        let owsUrlSession = OWSURLSession(securityPolicy: securityPolicy, configuration: Self.defaultConfiguration)

        let promise = firstly(on: DispatchQueue.sharedUtility) {
            owsUrlSession.dataTaskPromise(url.absoluteString, method: method.sskHTTPMethod, headers: headers, body: body)
        }
        promise.done { response in
            let headerFields = response.responseHeaders
            let statusCode = response.responseStatusCode
            let responseData = response.responseBodyData
            let url = response.requestUrl
            let httpResponse = LibMobileCoin.HTTPResponse(statusCode: statusCode, url: url, allHeaderFields: headerFields, responseData: responseData)
            completion(.success(httpResponse))
        }.catch { error in
            if let statusCode = error.httpStatusCode {
                completion(.success(LibMobileCoin.HTTPResponse(statusCode: statusCode, url: nil, allHeaderFields: [:], responseData: nil)))
            } else {
                Logger.warn("MobileCoin http request failed \(error)")
                completion(.failure(ConnectionError.invalidServerResponse("No Response")))
            }
        }
    }
}

extension LibMobileCoin.HTTPMethod {
    var sskHTTPMethod: SignalServiceKit.HTTPMethod {
        switch self {
        case .GET:
            return .get
        case .POST:
            return .post
        case .PUT:
            return .put
        case .HEAD:
            return .head
        case .PATCH:
            return .patch
        case .DELETE:
            return .delete
        }
    }
}
