import React
import Foundation

public enum BlazeAsyncBridgeTimeout {
    case defaultTimeout
    case noTimeout
    case seconds(_ seconds: Double)
    
    fileprivate var timeoutInSeconds: Double {
        switch self {
        case .defaultTimeout:
            return 2 // Default timeout
        case .noTimeout:
            return Double.infinity // Use Double.infinity to indicate no timeout
        case .seconds(seconds: let seconds):
            return seconds
        }
    }
}

/// Public protocol for the Blaze Async Bridge
/// Provides async communication between native code and JavaScript
public protocol BlazeAsyncBridge {
    
}

/// Private empty parameters struct for methods with no parameters
private struct EmptyCodable: Codable {}

/// Public constants and convenience methods for the Blaze Async Bridge
public extension BlazeAsyncBridge {
    
    /// Generic method that accepts Encodable parameters and returns the specific Decodable type
    /// This method automatically serializes the params to JSON and infers the return type
    func callJSMethod<T: Decodable, P: Encodable>(_ name: String,
                                                  params: P,
                                                  timeout: BlazeAsyncBridgeTimeout = .defaultTimeout) async throws -> T {
        guard let self = self as? BlazeAsyncBridgeModule else {
            // Should never reach here.
            throw BlazeAsyncBridgeModule.Errors.typeError(errorMessage: "BlazeAsyncBridge must be implemented by BlazeAsyncBridgeModule")
        }
        
        let jsonString = try await self.callJSMethodInternal(name,
                                                             params: params,
                                                             timeout: timeout) ?? "null"
        
        do {
            
            // Handle the case where jsonString is nil (empty response)
            guard let jsonData = jsonString.data(using: .utf8) else {
                throw BlazeAsyncBridgeModule.Errors.typeError(errorMessage: "Failed to convert JSON string to data")
            }
            
            let decoder = JSONDecoder()
            let result = try decoder.decode(T.self, from: jsonData)
            return result
            
        } catch {
            throw BlazeAsyncBridgeModule.Errors.typeError(errorMessage: "JS method '\(name)' parsing failed: \(error.localizedDescription)")
        }
    }
    
    func callJSMethod<T: Decodable>(_ name: String,
                                    timeout: BlazeAsyncBridgeTimeout = .defaultTimeout) async throws -> T {
        return try await callJSMethod(name,
                                      params: EmptyCodable(),
                                      timeout: timeout)
    }
    
    func callJSMethod(_ name: String,
                      timeout: BlazeAsyncBridgeTimeout = .defaultTimeout) async throws {
        let _: EmptyCodable? = try await callJSMethod(name,
                                                      params: EmptyCodable(),
                                                      timeout: timeout)
    }
    
    func callJSMethod<P: Encodable>(_ name: String,
                                    params: P,
                                    timeout: BlazeAsyncBridgeTimeout = .defaultTimeout) async throws {
        let _: EmptyCodable? = try await callJSMethod(name,
                                                      params: params,
                                                      timeout: timeout)
    }
    
}

@objc(RTNBlazeAsyncBridge)
public class BlazeAsyncBridgeModule: RCTEventEmitter, BlazeAsyncBridge {
    
    internal enum Constants {
        static let moduleName = "RTNBlazeAsyncBridge"
        static let bridgeAsyncFunctionName = "BlazeAsyncBridge.jsRequest"
    }
    
    enum Errors: Error {
        case timeoutError(errorMessage: String)
        case jsError(errorMessage: String)
        case typeError(errorMessage: String)
    }
    
    struct CollbackModel {
        let callbackAction: (Any?, Error?) -> Void
        let continuation: CheckedContinuation<String?, Error>
    }
    
    private var callbackMap = [String: CollbackModel]()
    
    override init() {
        super.init()
    }
    
    @objc public override static func moduleName() -> String {
        return Constants.moduleName
    }
    
    @objc public override static func requiresMainQueueSetup() -> Bool {
        return false
    }
    
    public override func supportedEvents() -> [String] {
        return [
            Constants.bridgeAsyncFunctionName
        ]
    }
    
    // Replace setupTimeout with performAsyncTaskWithTimeout
    fileprivate func callJSMethodInternal<P: Encodable>(_ name: String,
                                                        params: P,
                                                        timeout: BlazeAsyncBridgeTimeout) async throws -> String? {
        let callbackId = UUID().uuidString
        
        // Determine the effective timeout
        let effectiveTimeout = timeout.timeoutInSeconds
        
        do {
            return try await withTimeout(effectiveTimeout) {
                try await withTaskCancellationHandler(operation: {
                    try await withCheckedThrowingContinuation { continuation in
                        Task { [weak self] in
                            guard let self = self else { return }
                            
                            // Store callback handling
                            await self.storeCallback(callbackId: callbackId,
                                                     continuation: continuation)
                            
                            // Send request to JS
                            try self.sendJSRequest(name: name,
                                                   params: params,
                                                   callbackId: callbackId)
                        }
                    }
                }, onCancel: { [weak self] in
                    Task { [weak self] in
                        guard let self = self else { return }
                        
                        // Handle all cancellation (timeout + external) - resume continuation and cleanup
                        await self.resumeWithCancellation(callbackId: callbackId)
                        blazeLogDebug("Operation cancelled for JS method call: \(name)")
                    }
                })
            }
        } catch WithTimeoutError.timeoutError {
            let timeoutError = Errors.timeoutError(errorMessage: "JS method call timed out: \(name)")
            await removeCallback(callbackId: callbackId)
            
            throw timeoutError
        } catch {
            // Handle other errors (including `CancellationError`).
            await removeCallback(callbackId: callbackId)
            throw error
        }
    }
    
    // MARK: - Private Helper Methods
    
    @MainActor
    private func resumeWithCancellation(callbackId: String) {
        guard let callback = callbackMap[callbackId] else {
            return
        }
        
        // Resume continuation with cancellation error if it exists
        callback.continuation.resume(throwing: CancellationError())
        
        // Remove callback from both maps
        removeCallback(callbackId: callbackId)
    }
    
    @MainActor
    private func removeCallback(callbackId: String) {
        callbackMap.removeValue(forKey: callbackId)
    }
    
    @MainActor
    private func storeCallback(callbackId: String,
                               continuation: CheckedContinuation<String?, Error>) {
        callbackMap[callbackId] = .init(
            callbackAction: { [weak self] (result, error) in
                guard let self = self else { return }
                
                self.handleCallbackResponse(callbackId: callbackId,
                                            result: result,
                                            error: error,
                                            continuation: continuation)
            },
            continuation: continuation)
    }
    
    @MainActor
    private func handleCallbackResponse(callbackId: String,
                                        result: Any?,
                                        error: Error?,
                                        continuation: CheckedContinuation<String?, Error>) {
        // Resume continuation
        if let error = error {
            continuation.resume(throwing: error)
        } else {
            // Return JSON string directly - result should already be a JSON string from JS
            let jsonString = result as? String
            continuation.resume(returning: jsonString)
        }
        
        // Remove callback from map
        removeCallback(callbackId: callbackId)
    }
    
    private func sendJSRequest<P: Encodable>(name: String,
                                             params: P,
                                             callbackId: String) throws {
        // Serialize params to JSON string using the existing helper
        let paramsJson: String
        if params is EmptyCodable {
            paramsJson = "null"
        } else if let jsonString = params.asJsonString {
            paramsJson = jsonString
        } else {
            throw Errors.typeError(errorMessage: "Failed to serialize params to JSON string")
        }
        
        let event: [String: Any] = [
            "methodName": name,
            "params": paramsJson, // Send as JSON string instead of dictionary
            "callbackId": callbackId
        ]
        sendEvent(withName: Constants.bridgeAsyncFunctionName, body: event)
    }
    
    // Method called by JS to resolve/reject a native call
    @objc func resolveJSResponse(_ response: [String: Any],
                                 resolver: @escaping RCTPromiseResolveBlock,
                                 rejecter: @escaping RCTPromiseRejectBlock) {
        Task { [weak self] in
            await self?.handleJSResponse(response: response, resolver: resolver, rejecter: rejecter)
        }
    }
    
    @MainActor
    private func handleJSResponse(response: [String: Any],
                                  resolver: @escaping RCTPromiseResolveBlock,
                                  rejecter: @escaping RCTPromiseRejectBlock) {
        guard let callbackId = response["callbackId"] as? String else {
            handleError(rejecter, errMessage: "Invalid response format: missing callbackId")
            return
        }
        
        guard let callback = callbackMap[callbackId] else {
            // Callback might have already been handled (e.g., due to timeout or cancellation)
            resolver(nil)
            return
        }
        
        let success = response["success"] as? Bool ?? false
        
        if success {
            // Extract JSON string from response - JS should send data as JSON string
            let jsonString = response["data"] as? String
            callback.callbackAction(jsonString, nil)
        } else {
            let errorMessage = response["errorMessage"] as? String ?? "Unknown error"
            let error = Errors.jsError(errorMessage: errorMessage)
            callback.callbackAction(nil, error)
        }
        
        resolver(nil)
    }
}

extension RCTEventEmitter {
    
    public var blazeAsyncBridge: BlazeAsyncBridge? {
        return bridge?.blazeAsyncBridge
    }
    
}

extension RCTBridge {
    
    public var blazeAsyncBridge: BlazeAsyncBridge? {
        guard let asyncBridge = module(forName: BlazeAsyncBridgeModule.Constants.moduleName) as? BlazeAsyncBridgeModule else {
            return nil
        }
        
        return asyncBridge
    }
    
}
