// Copyright © 2022 Olo Inc. All rights reserved.
// This software is made available under the Olo Pay SDK License (See LICENSE.md file)
//
//  NSDictionaryExtensions.swift
//  OlopaysdkReactNative
//
//  Created by Justin Anderson on 1/19/23.
//

import Foundation

extension NSDictionary {
    func getString(_ key: String) -> String? {
        guard let value = value(forKey: key) as? String else {
            return nil
        }

        return value
    }

    func getString(_ key: String, defaultValue: String) -> String {
        return getString(key) ?? defaultValue
    }

    func getBool(_ key: String) -> Bool? {
        guard let value = value(forKey: key) as? Bool else {
            return nil
        }

        return value
    }
    
    func getBool(_ key: String, defaultValue: Bool) -> Bool {
        return getBool(key) ?? defaultValue
    }
    

    func getDouble(_ key: String) -> Double? {
        guard let value = value(forKey: key) as? Double else {
            return nil
        }

        return value
    }
    
    func getDictionary(_ key: String) -> NSDictionary? {
        guard let value = value(forKey: key) as? NSDictionary else {
            return nil
        }
        
        return value
    }
    
    func getDictionaryArray(_ key: String) -> [NSDictionary]? {
        guard let value = value(forKey: key) as? [NSDictionary] else {
            return nil
        }
        
        return value
    }
    
    func getOrThrow<T>(_ key: String, defaultValue: T) throws -> T {
        do {
            return try getOrThrow(key)
        } catch OloError.MissingKeyError {
            return defaultValue
        } catch OloError.NullValueError {
            return defaultValue
        }
    }

    func getOrThrow<T>(_ key: String) throws -> T {
        if (!keyExists(key)) {
            throw OloError.MissingKeyError
        }
        
        let valueIsNil = (self[key]) is NSNull
        if valueIsNil {
            throw OloError.NullValueError
        }
        
        if let value: T = get(key) {
            return value
        }
        
        throw OloError.UnexpectedTypeError
    }
    
    func get<T>(_ key: String) -> T? {
        guard let value = self[key] as? T else {
            return nil
        }
        
        return value
    }
    
    func keyExists(_ key: String) -> Bool {
        return self[key] != nil
    }
    
    func getOrReject<T>(
         for key: String,
         withDefault defaultValue: T,
         baseError: String,
         reject: @escaping RCTPromiseRejectBlock
     ) throws -> T {
         do {
             return try self.getOrThrow(key, defaultValue: defaultValue)
         } catch let error {
             reject(
               ErrorCodes.InvalidParameter,
               "\(baseError): Value for '\(key)' is not of type \(String(describing: T.self))",
               nil
             )
                         
             throw error
         }
     }
     
     func getOrReject<T>(
         for key: String,
         baseError: String,
         reject: @escaping RCTPromiseRejectBlock
     ) throws -> T {
         var errorMessage: String = ""
         var errorCode: String = ""
         var oloError: OloError? = nil
         
         do {
             return try self.getOrThrow(key)
         } catch let error as OloError where error == .MissingKeyError {
             errorMessage = "\(baseError): Missing parameter '\(key)'"
             errorCode = ErrorCodes.MissingParameter
             oloError = error
         } catch let error as OloError where error == .NullValueError {
             errorMessage = "\(baseError): Missing parameter '\(key)'"
             errorCode = ErrorCodes.MissingParameter
             oloError = error
         } catch { // OloError.UnexpectedTypeError
             errorMessage = "\(baseError): Value for '\(key)' is not of type \(String(describing: T.self))"
             errorCode = ErrorCodes.InvalidParameter
             oloError = OloError.UnexpectedTypeError
         }
         
         reject(
             errorCode,
             errorMessage,
             nil
         )
         
         throw oloError!
     }
     
     func getStringOrReject(
         for key: String,
         withDefault defaultValue: String,
         baseError: String,
         acceptEmptyValue: Bool,
         reject: @escaping RCTPromiseRejectBlock
     ) throws -> String {
         do {
             var value: String = try self.getOrThrow(key, defaultValue: defaultValue)
             value = value.trim()
             
             if !acceptEmptyValue && value.isEmpty {
                 value = defaultValue
             }
             
             return value
         } catch let error {
             reject(
                 ErrorCodes.InvalidParameter,
                 "\(baseError): Value for '\(key)' is not of type String",
                 nil
             )
             
             throw error
         }
     }
     
     func getStringOrReject(
         for key: String,
         baseError: String,
         acceptEmptyValue: Bool,
         reject: @escaping RCTPromiseRejectBlock
     ) throws -> String {
         var value: String = try self.getOrReject(
             for: key,
             baseError: baseError,
             reject: reject
         )
         
         value = value.trim()
         
         if !acceptEmptyValue && value.isEmpty {
             reject(
                 ErrorCodes.InvalidParameter,
                 "\(baseError): Value for '\(key)' cannot be empty",
                 nil
             )
             throw OloError.EmptyValueError
         }
         
         return value
     }
    
    func toSwiftDictionary() -> [String: Any] {
        var swiftDict: [String: Any] = [:]
        
        for (key, value) in self {
            guard let stringKey = key as? String else { continue }
            
            switch value {
            case let nestedDict as NSDictionary:
                // Convert nested NSDictionary and store directly (no nil possible)
                swiftDict[stringKey] = nestedDict.toSwiftDictionary()
                
            case let array as NSArray:
                // Convert NSArray, skipping nil elements
                let convertedArray = array.compactMap { (element: Any) -> Any? in
                    switch element {
                    case let dict as NSDictionary:
                        return dict.toSwiftDictionary()
                    case let arr as NSArray:
                        // Convert nested array, skipping nil elements
                        return arr.compactMap { (subElement: Any) -> Any? in
                            if let dict = subElement as? NSDictionary {
                                return dict.toSwiftDictionary()
                            }
                            return convertValue(subElement)
                        }
                    default:
                        return convertValue(element)
                    }
                }
                swiftDict[stringKey] = convertedArray
                
            default:
                // Convert other types, skipping nil results
                if let converted = convertValue(value) {
                    swiftDict[stringKey] = converted
                }
            }
        }
        
        return swiftDict
    }

    private func convertValue(_ value: Any) -> Any? {
        switch value {
        case let nsString as NSString:
            return String(nsString)
        case let nsNumber as NSNumber:
             /*
             The nsNumber.objCType property returns a pointer to a C string (UnsafePointer<Int8>) that
             encodes the Objective-C type of the NSNumber's value. Accessing .pointee dereferences this
             pointer to retrieve the first character (Int8) of the string, indicating the stored type.
             In this case (the case of a boolean), the .pointee value is `99`. We use CChar("c") as a
             matcher because it returns the ascii value of "c", which is `99`.
             */
            if nsNumber.objCType.pointee == CChar("c") {
                return nsNumber.boolValue
            } else {
                return nsNumber
            }
        case is NSNull:
            return nil // Skip NSNull
        default:
            return value
        }
    }
 }
