// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.

import Foundation
import Sodium
import SessionUtilitiesKit

extension SnodeAPI {
    public class ONSResolveResponse: SnodeResponse {
        private struct Result: Codable {
            enum CodingKeys: String, CodingKey {
                case nonce
                case encryptedValue = "encrypted_value"
            }
            
            fileprivate let nonce: String?
            fileprivate let encryptedValue: String
        }
        
        enum CodingKeys: String, CodingKey {
            case result
        }
        
        private let result: Result
        
        // MARK: - Initialization
        
        required init(from decoder: Decoder) throws {
            let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
            
            result = try container.decode(Result.self, forKey: .result)
            
            try super.init(from: decoder)
        }
        
        // MARK: - Convenience
        
        func sessionId(sodium: Sodium, nameBytes: [UInt8], nameHashBytes: [UInt8]) throws -> String {
            let ciphertext: [UInt8] = Data(hex: result.encryptedValue).bytes
            
            // Handle old Argon2-based encryption used before HF16
            guard let hexEncodedNonce: String = result.nonce else {
                let salt: [UInt8] = Data(repeating: 0, count: sodium.pwHash.SaltBytes).bytes
                
                guard
                    let key: [UInt8] = sodium.pwHash.hash(
                        outputLength: sodium.secretBox.KeyBytes,
                        passwd: nameBytes,
                        salt: salt,
                        opsLimit: sodium.pwHash.OpsLimitModerate,
                        memLimit: sodium.pwHash.MemLimitModerate,
                        alg: .Argon2ID13
                    )
                else { throw SnodeAPIError.hashingFailed }
                
                let nonce: [UInt8] = Data(repeating: 0, count: sodium.secretBox.NonceBytes).bytes
                
                guard let sessionIdAsData: [UInt8] = sodium.secretBox.open(authenticatedCipherText: ciphertext, secretKey: key, nonce: nonce) else {
                    throw SnodeAPIError.decryptionFailed
                }

                return sessionIdAsData.toHexString()
            }
            
            let nonceBytes: [UInt8] = Data(hex: hexEncodedNonce).bytes

            // xchacha-based encryption
            // key = H(name, key=H(name))
            guard let key: [UInt8] = sodium.genericHash.hash(message: nameBytes, key: nameHashBytes) else {
                throw SnodeAPIError.hashingFailed
            }
            guard
                // Should always be equal in practice
                ciphertext.count >= (SessionId.byteCount + sodium.aead.xchacha20poly1305ietf.ABytes),
                let sessionIdAsData = sodium.aead.xchacha20poly1305ietf.decrypt(
                    authenticatedCipherText: ciphertext,
                    secretKey: key,
                    nonce: nonceBytes
                )
            else { throw SnodeAPIError.decryptionFailed }

            return sessionIdAsData.toHexString()
        }
    }
}
