// Copyright © 2022 Olo Inc. All rights reserved.
// This software is made available under the Olo Pay SDK License (See LICENSE.md file)
//
//  CAPPluginCallExtensions.swift
//  Plugin
//
//  Created by Justin Anderson on 5/16/25.
//

import Capacitor

extension CAPPluginCall {
    func getOrReject<T>(
        for key: String,
        baseError: String,
        withDefault defaultValue: T? = nil,
        in data: [String: Any]? = nil
    ) throws -> T {
        let errorMessage: String
        let errorCode: String
        let oloError: OloError
        
        do {
            return try getOrThrow(
                for: key,
                withDefault: defaultValue,
                in: data
            )
        } 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 value for parameter '\(key)'"
            errorCode = ErrorCodes.MissingParameter
            oloError = error
        } catch {
            errorMessage = "\(baseError): Value for parameter '\(key)' is not of type \(String(describing: T.self))"
            errorCode = ErrorCodes.InvalidParameter
            oloError = OloError.UnexpectedTypeError
        }
        
        reject(errorMessage, errorCode, nil)
        throw oloError
    }
    
    func getStringOrReject(
        for key: String,
        baseError: String,
        allowEmptyValue: Bool,
        withDefault defaultValue: String? = nil,
        in data: [String: Any]? = nil
    ) throws -> String {
        let value: String = try getOrReject(
            for: key,
            baseError: baseError,
            withDefault: defaultValue,
            in: data
        ).trim()
        
        if !allowEmptyValue && value.isEmpty {
            guard let defaultValue = defaultValue else {
                let errorMessage = "\(baseError): Value for '\(key)' cannot be empty"
                reject(errorMessage, ErrorCodes.InvalidParameter, nil)
                throw OloError.EmptyValueError
            }
            
            return defaultValue
        }
        
        return value
    }
    
    func getOrThrow<T>(
        for key: String,
        withDefault defaultValue: T? = nil,
        in data: [String: Any]? = nil
    ) throws -> T {
        guard let source = data ?? getOptions() else {
            guard let defaultValue = defaultValue else {
                throw OloError.NullValueError
            }
            
            return defaultValue
        }
        
        guard keyExists(key, in: source) else {
            guard let defaultValue = defaultValue else {
                throw OloError.MissingKeyError
            }
            
            return defaultValue
        }
        
        guard !valueIsNil(for: key, in: source) else {
            guard let defaultValue = defaultValue else {
                throw OloError.NullValueError
            }
            
            return defaultValue
        }
        
        guard let value: T = get(for: key, in: source) else {
            throw OloError.UnexpectedTypeError
        }
        
        return value
    }
    
    private func get<T>(for key: String, in source: [String: Any]) -> T? {
        return get(for: key, with: T.self, in: source)
    }

    private func get<T>(for key: String, with type: T.Type, in source: [String: Any]) -> T? {
        guard let value = source[key] else {
            return nil
        }
        
        // Handle Bool specifically
        if type == Bool.self {
            return convertToBool(value) as? T
        }
        
        // Handle numeric types (Int, Double, Float)
        if type == Int.self || type == Double.self || type == Float.self {
            return convertToNumber(value, type: type)
        }
        
        // Fallback to default casting for other types
        return value as? T
    }

    private func convertToBool(_ value: Any?) -> Bool? {
        guard let nsNumber = value as? NSNumber else {
            return nil
        }
        
        // Ensure the NSNumber is a boolean (objCType == "c")
        guard nsNumber.objCType.pointee == CChar("c") else {
            return nil
        }
        
        return nsNumber.boolValue
    }

    private func convertToNumber<T>(_ value: Any?, type: T.Type) -> T? {
        guard let nsNumber = value as? NSNumber else {
            return nil
        }
        
        let objCType = nsNumber.objCType.pointee
        
        // Reject if the NSNumber is a boolean (objCType == "c")
        guard objCType != CChar("c") else {
            return nil
        }
        
        // Define valid numeric objCType values
        let numericTypes: Set<CChar?> = [
            CChar("i"), // Int
            CChar("s"), // Short
            CChar("l"), // Long
            CChar("q"), // Long long
            CChar("f"), // Float
            CChar("d")  // Double
        ]
        
        // Ensure the value is a numeric type
        guard numericTypes.contains(objCType) else {
            return nil
        }
        
        // Convert to the requested numeric type
        if type == Int.self {
            // Only allow integer types for Int
            let integerTypes: Set<CChar?> = [CChar("i"), CChar("s"), CChar("l"), CChar("q")]
            if !integerTypes.contains(objCType) {
                return nil
            }
            
            // Check for overflow by comparing to Int.max and Int.min
            let int64Value = nsNumber.int64Value
            if int64Value > Int64(Int.max) || int64Value < Int64(Int.min) {
                return nil // Overflow detected
            }
            return Int(int64Value) as? T
        } else if type == Double.self {
            // Allow integer or floating-point types for Double
            return nsNumber.doubleValue as? T
        } else if type == Float.self {
            // Allow integer or floating-point types for Float
            return nsNumber.floatValue as? T
        }
        
        return nil
    }
    
    func keyExists(_ key: String, in source: [String: Any]) -> Bool {
        return source[key] != nil
    }
    
    private func valueIsNil(for key: String, in source: [String: Any]) -> Bool {
        if let value = source[key] {
            return value is NSNull
        }
        
        return false
    }
    
    func getOptions() -> [String: Any]? {
        return options?.compactMapKeys { key in
            key as? String // Convert AnyHashable keys to String
        }
    }
}
