import SwiftUI

#if !TESTING
import CheckoutComponentsSDK
#endif

@objc(CheckoutManager)
public final class CheckoutManager: NSObject {
  
  private static let applePayCleanupDelay: TimeInterval = 2
  internal typealias TranslationKeysDict = [CheckoutComponentsSDK.CheckoutComponents.TranslationKey : String]
  
  @objc(sharedInstance)
  public static let shared = CheckoutManager()
  
  // MARK: - Properties
  
  internal var checkoutComponents: CheckoutComponentsSDK.CheckoutComponents? = nil
  
  private var componentsByViewController: [UIViewController: CheckoutComponentsSDK.CheckoutComponents.Actionable] = [:]
  
  internal var paymentSession: PaymentSession?
  internal var publicKey: String?
  internal var environment: CheckoutComponents.Environment?
  internal var appearance: CheckoutComponents.DesignTokens?
  internal var locale: String? = nil
  internal var translations: [String : TranslationKeysDict]?
  internal var merchantIdentifier: String?
  
  private let appearanceManager = Appearance()
  private let translationsManager = Translations()
  
  // MARK: - Pending callback storage
  
  private var pendingTokenizedContinuations: [
    String: CheckedContinuation<CheckoutComponents.CallbackResult, Never>
  ] = [:]
  private var pendingSubmitContinuations: [
    String: CheckedContinuation<CheckoutComponents.APICallResult, Never>
  ] = [:]
  
  private override init() {}
  
  // MARK: - Builders
  
  public func setPaymentSession(paymentSessionId: String, paymentSessionSecret: String) {
    if !paymentSessionId.isEmpty && !paymentSessionSecret.isEmpty {
      self.paymentSession = PaymentSession(
        id: paymentSessionId,
        paymentSessionSecret: paymentSessionSecret
      )
    } else {
      self.paymentSession = nil
    }
  }
  
  public func setPublicKey(value: String) {
    if !value.isEmpty {
      self.publicKey = value
    }
  }
  
  public func setEnvironment(value: String) {
    if value.lowercased() == "sandbox" {
      self.environment = .sandbox
    } else {
      self.environment = .production
    }
  }
  
  public func setAppearance(style: [String: Any]) {
    self.appearance = appearanceManager.processStyleConfiguration(from: style)
  }
  
  public func setLocale(locale: String) {
    if !locale.isEmpty {
      self.locale = locale
    }
  }
  
  public func setTranslations(translations: [String: Any]) {
    self.translations = translationsManager.extractTranslations(from: translations)
  }
  
  public func setMerchantIdentifier(value: String) {
    if !value.isEmpty {
      self.merchantIdentifier = value
    }
  }
  
  // MARK: - Module Functions
  
  public func initializeSDK(callbacks: [Event], completion: @escaping (Error?) -> Void) {
    guard let publicKey = self.publicKey else {
      completion(CheckoutError.missingPublicKey.nsError)
      return
    }
    
    guard let environment = self.environment else {
      completion(CheckoutError.missingEnvironment.nsError)
      return
    }
    
    Task {
      do {
        let configuration = try await CheckoutComponents.Configuration(
          paymentSession: paymentSession,
          publicKey: publicKey,
          environment: environment,
          appearance: appearance ?? CheckoutComponents.DesignTokens(),
          locale: self.locale,
          translations: self.translations ?? [:],
          callbacks: createCallbacks(registeredCallbacks: callbacks)
        )
        
        checkoutComponents = CheckoutComponentsSDK.CheckoutComponents(configuration: configuration)
        completion(nil)
      } catch {
        completion(error)
      }
    }
  }
  
  public func clear() {
    paymentSession = nil
    publicKey = nil
    environment = nil
    appearance = nil
    locale = nil
    translations = nil
    merchantIdentifier = nil
    pendingTokenizedContinuations.removeAll()
    pendingSubmitContinuations.removeAll()
  }
  
  @objc public func submitComponent(viewController: UIViewController) {
    if let component = componentsByViewController[viewController] {
      DispatchQueue.main.async {
        component.submit()
      }
    }
  }
  
  @objc public func tokenizeComponent(viewController: UIViewController) {
    if let component = componentsByViewController[viewController] {
      DispatchQueue.main.async {
        component.tokenize()
      }
    }
  }
  
  @objc public func cleanupComponent(viewController: UIViewController) {
    
    if let component = componentsByViewController[viewController] {
      let canShowApplePay = component.name == "Apple Pay" || component.name == "Flow"
      
      if canShowApplePay {
        DispatchQueue.main.asyncAfter(deadline: .now() + Self.applePayCleanupDelay) { [weak self] in
          self?.clearComponent()
        }
      } else {
        clearComponent()
      }
    } else {
      clearComponent()
    }
  }
  
  @objc public func updateComponent(viewController: UIViewController, amount: Int, currency: String?) {
    if componentsByViewController[viewController] != nil {
      DispatchQueue.main.async {
        do {
          var updateDetails = CheckoutComponents.UpdateDetails(amount: amount)
          updateDetails.currency = currency
          try self.checkoutComponents?.update(with: updateDetails)
        } catch {
          print("Update amount error: \(error.localizedDescription).\nCheck if your input is correct.")
        }
      }
    }
  }
  
  // MARK: - JS callback tokenized
  
  // update SDK to use async callback
  @objc public func resolveTokenizedCallback(
    callbackId: String,
    success: Bool,
    error: String?,
    completion: @escaping (Error?) -> Void
  ) {
    guard let continuation = pendingTokenizedContinuations.removeValue(forKey: callbackId) else {
      let errorMessage = "No pending tokenized continuation found for callbackId: \(callbackId). It may have already timed out or been resolved."
      let userInfo: [String: Any] = [
        NSLocalizedDescriptionKey: errorMessage,
        "callbackId": callbackId,
        "callbackType": "onTokenized",
        "pendingContinuationsCount": pendingTokenizedContinuations.count,
        "availableCallbackIds": Array(pendingTokenizedContinuations.keys)
      ]
      completion(NSError(domain: "CheckoutManager", code: -1, userInfo: userInfo))
      return
    }
    
    if(success) {
      continuation.resume(returning: .accepted)
    } else {
      continuation.resume(returning: .rejected(message: error))
    }
    completion(nil)
  }
  
  // MARK: - JS callback resolve handle submit
  @objc public func resolveSubmitCallback(
    callbackId: String,
    args: [String: Any],
    completion: @escaping (Error?) -> Void
  ) {
    guard let continuation = pendingSubmitContinuations.removeValue(forKey: callbackId) else {
      let errorMessage = "No pending submit continuation found for callbackId: \(callbackId). It may have already timed out or been resolved."
      let userInfo: [String: Any] = [
        NSLocalizedDescriptionKey: errorMessage,
        "callbackId": callbackId,
        "callbackType": "handleSubmit",
        "pendingContinuationsCount": pendingSubmitContinuations.count,
        "availableCallbackIds": Array(pendingSubmitContinuations.keys)
      ]
      completion(NSError(domain: "CheckoutManager", code: -1, userInfo: userInfo))
      return
    }

    let success = args["success"] as? Bool ?? false
    var result: CheckoutComponents.APICallResult = .failure

    if success,
      let psrDict = args["paymentSessionSubmissionResult"] as? [String: Any] {
      let id = psrDict["id"] as? String ?? ""
      let type = psrDict["type"] as? String ?? ""
      let status = psrDict["status"] as? String ?? ""

      var action: CheckoutComponents.Action? = nil
      
      if let actionDict = psrDict["action"] as? [String: Any] {
        let actionType = actionDict["type"] as? String ?? ""
        let url = actionDict["url"] as? String ?? ""
        action = CheckoutComponents.Action(type: actionType, url: url)
      }

      let declineReasonRaw = psrDict["declineReason"] as? String
      let declineReason = declineReasonRaw.flatMap(DeclineReason.init(rawValue:))

      let submissionResult = CheckoutComponents.PaymentSessionSubmissionResult(
        id: id,
        status: status,
        declineReason: declineReason,
        type: type,
        action: action
      )
      result = .success(submissionResult)
    }

    continuation.resume(returning: result)
    completion(nil)
  }
  
  private func clearComponent() {
    componentsByViewController = [:]
    checkoutComponents = nil
  }
  
  // MARK: - Components
  
  private func extractCardConfig(from config: [String: Any]) -> (
    showPayButton: Bool,
    paymentButtonAction: CheckoutComponentsSDK.CheckoutComponents.PaymentButtonAction
  ) {
    let showPayButton = config[FlowConfigKeys.showPayButton] as? Bool ?? true
    let paymentButtonActionString = config[FlowConfigKeys.paymentButtonAction] as? String
    let paymentButtonAction: CheckoutComponentsSDK.CheckoutComponents.PaymentButtonAction =
    paymentButtonActionString == PaymentButtonAction.tokenize ? .tokenization : .payment
    return (showPayButton, paymentButtonAction)
  }
  
  @objc public func createComponent(
    type: CheckoutComponentType,
    config: [String: Any],
    onDimensionsChanged: @escaping (@convention(block) (CGSize) -> Void),
    completion: @escaping (UIViewController?, Error?) -> Void
  ) {
    switch type {
    case .card:
      createCard(config: config, onDimensionsChanged: onDimensionsChanged, completion: completion)
    case .flow:
      createFlow(config: config, onDimensionsChanged: onDimensionsChanged, completion: completion)
    case .applePay:
      createApplePay(config: config, onDimensionsChanged: onDimensionsChanged, completion: completion)
    }
  }
  
  @objc public func createCard(
    config: [String: Any],
    onDimensionsChanged: @escaping (@convention(block) (CGSize) -> Void),
    completion: @escaping (UIViewController?, Error?) -> Void
  ) {
    let (showPayButton, paymentButtonAction) = extractCardConfig(from: config)
    
    createAndRenderComponent(
      componentType: "Card",
      componentBuilder: { [weak self] in
        guard let self = self, let checkoutComponents = self.checkoutComponents else {
          throw CheckoutError.createFailed
        }
        return try await checkoutComponents.create(
          .card(
            showPayButton: showPayButton,
            paymentButtonAction: paymentButtonAction
          )
        )
      },
      onDimensionsChanged: onDimensionsChanged,
      completion: completion
    )
  }
  
  @objc public func createFlow(
    config: [String: Any],
    onDimensionsChanged: @escaping (@convention(block) (CGSize) -> Void),
    completion: @escaping (UIViewController?, Error?) -> Void
  ) {
    let (showPayButton, paymentButtonAction) = extractCardConfig(from: config)
    
    createAndRenderComponent(
      componentType: "Flow",
      componentBuilder: { [weak self] in
        guard let self = self, let checkoutComponents = self.checkoutComponents else {
          throw CheckoutError.createFailed
        }
        return try await checkoutComponents.create(
          .flow(options: [
            .card(
              showPayButton: showPayButton,
              paymentButtonAction: paymentButtonAction
            ),
            .applePay(merchantIdentifier: self.merchantIdentifier ?? "")
          ])
        )
      },
      onDimensionsChanged: onDimensionsChanged,
      completion: completion
    )
  }
  
  @objc public func createApplePay(
    config: [String: Any],
    onDimensionsChanged: @escaping (@convention(block) (CGSize) -> Void),
    completion: @escaping (UIViewController?, Error?) -> Void
  ) {
    createAndRenderComponent(
      componentType: "Apple Pay",
      componentBuilder: { [weak self] in
        guard let self = self, let checkoutComponents = self.checkoutComponents else {
          throw CheckoutError.createFailed
        }
        return try await checkoutComponents.create(
          .applePay(merchantIdentifier: self.merchantIdentifier ?? "")
        )
      },
      onDimensionsChanged: onDimensionsChanged,
      completion: completion
    )
  }
  
  private func createAndRenderComponent(
    componentType: String,
    componentBuilder: @escaping () async throws -> CheckoutComponents.Actionable,
    onDimensionsChanged: @escaping (@convention(block) (CGSize) -> Void),
    completion: @escaping (UIViewController?, Error?) -> Void
  ) {
    guard checkoutComponents != nil else {
      completion(nil, CheckoutError.createFailed)
      return
    }
    
    Task {
      do {
        let component = try await componentBuilder()
        
        await MainActor.run {
          if component.isAvailable {
            
            let view = MeasuringView {
              component.render()
            } onChange: { newSize in
              onDimensionsChanged(newSize)
            }
            let controller = UIHostingController(rootView: view)
            
            self.componentsByViewController[controller] = component
            
            completion(controller, nil)
          } else {
            completion(nil, nil)
            let message = "\(componentType) component unavailable"
            let errorData = ["message": message]
            EventEmitter.shared.sendEvent(type: .onError, data: errorData)
            logError(name: "Render Error", message: message, stack:  nil, errorType: "integration_error")
          }
        }
      } catch {
        // TODO "Implement complete error set"
        let errorCode = "Render error"
        let errorMessage = "\(componentType) component create error"
        EventEmitter.shared.sendEvent(type: .onError, data: [
          "errorCode": errorCode,
          "message": errorMessage
        ])
        logError(
          name: errorMessage,
          message: error.localizedDescription,
          stack: nil,
          errorType: "integration_error"
        )
        completion(nil, error)
      }
    }
  }
  
  // MARK: - Helper Methods
  
  @objc public func logError(name: String, message: String, stack: String?, errorType: String) {
    if let stack = stack, !stack.isEmpty {
      print("Log error: \(name), \(message), \(errorType), stack: \(stack)")
    } else {
      print("Log error: \(name), \(message), \(errorType)")
    }
  }
  
  // MARK: - Error Mapping Helpers
  
  private func mapErrorTypeToString(_ errorType: CheckoutComponentsSDK.CheckoutComponents.ErrorType) -> String {
    switch errorType {
    case .integration:
      return "integration_error"
    case .request:
      return "request_error"
    case .paymentMethod:
      return "payment_method_error"
    case .submit:
      return "submit_error"
    case .validation:
      return "validation_error"
    @unknown default:
      return "unknown_error"
    }
  }
  
  private func mapComponentTypeToString(_ componentType: CheckoutComponents.Error.ComponentType) -> String {
    switch componentType {
    case .applePay:
      return "applePay"
    case .card:
      return "card"
    case .flow:
      return "flow"
    case .rememberMe:
      return "rememberMe"
    @unknown default:
      return "unknown"
    }
  }
  
  internal func createCallbacks(registeredCallbacks: [Event]) -> CheckoutComponents.Callbacks {
    CheckoutComponents.Callbacks(
      onReady: { paymentMethod in
        EventEmitter.shared.sendEvent(type: .onReady, data: [
          "paymentMethod": paymentMethod.name
        ])
      },
      onChange: { paymentMethod in
        EventEmitter.shared.sendEvent(type: .onChange, data: [
          "paymentMethod": paymentMethod.name,
          "isValid": paymentMethod.isValid
        ])
      },
      onSubmit: { paymentMethod in
        EventEmitter.shared.sendEvent(type: .onSubmit, data: [
          "paymentMethod": paymentMethod.name,
        ])
      },
      
      onTokenized: registeredCallbacks.contains(.onTokenized) ? onTokenized() : nil,
      
      handleSubmit: registeredCallbacks.contains(.handleSubmit) ? handleSubmit() : nil,
      
      onSuccess: { paymentMethod, paymentID in
        EventEmitter.shared.sendEvent(type: .onSuccess, data: [
          "paymentMethod": paymentMethod.name,
          "paymentID": paymentID
        ])
      },
      
      onError: { [weak self] error in
        guard let self else { return }
#if TESTING
        // In testing environment, use mock error data
        let errorData = [
          "errorCode": "TEST_ERROR",
          "details": "Mock error details",
          "message": error.localizedDescription,
          "type": "TestError"
        ]
        EventEmitter.shared.sendEvent(type: .onError, data: errorData)
#else
        Task { @MainActor in
          
          // Build details dictionary
          var detailsDict: [String: Any] = [
            "mobileSessionID": error.details.mobileSessionID,
            "type": self.mapComponentTypeToString(error.details.type)
          ]
          if let paymentSessionID = error.details.paymentSessionID {
            detailsDict["paymentSessionID"] = paymentSessionID
          }
          
          let errorData: [String: Any] = [
            "message": error.localizedDescription,
            "errorCode": String(describing: error.errorCode),
            "type": self.mapErrorTypeToString(error.type),
            "details": detailsDict
          ]
          
          EventEmitter.shared.sendEvent(type: .onError, data: errorData)
        }
#endif
      }
    )
  }
  
  internal func onTokenized() -> (@Sendable (CheckoutComponents.TokenizationResult) async -> CheckoutComponents.CallbackResult)? {
    { [weak self] tokenizationResult async -> CheckoutComponents.CallbackResult in
      guard let self else { return .rejected(message: nil) }
      
      let callbackId = UUID().uuidString
      var tokenizationPayload: [String: Any] = [
        "type": tokenizationResult.type.rawValue
      ]
      
      if let dataDict = try? JSONEncoder().encode(tokenizationResult.data),
         let dataDictionary = try? JSONSerialization.jsonObject(with: dataDict) as? [String: Any] {
        tokenizationPayload["data"] = dataDictionary
      }
      
      if let cardMetadata = tokenizationResult.cardMetadata,
         let cardMetadataData = try? JSONEncoder().encode(cardMetadata),
         let cardMetadataDict = try? JSONSerialization.jsonObject(with: cardMetadataData) as? [String: Any] {
        tokenizationPayload["cardMetadata"] = cardMetadataDict
      }
      
      if let preferredScheme = tokenizationResult.preferredScheme {
        tokenizationPayload["preferredScheme"] = preferredScheme
      }

      return await withCheckedContinuation { (continuation: CheckedContinuation<CheckoutComponents.CallbackResult, Never>) in
        self.pendingTokenizedContinuations[callbackId] = continuation
        
        EventEmitter.shared.sendEvent(
          type: .onTokenized,
          data: [
            "callbackId": callbackId,
            "tokenizationResult": tokenizationPayload,
          ]
        )
      }
    }
  }
  
  internal func handleSubmit() -> (@Sendable (CheckoutComponents.SessionData) async -> CheckoutComponents.APICallResult)? {
    
    return { [weak self] submitData async -> CheckoutComponents.APICallResult in
      guard let self else { return .failure }
      
      let callbackId = UUID().uuidString
      
      return await withCheckedContinuation { (continuation: CheckedContinuation<CheckoutComponents.APICallResult, Never>) in
        self.pendingSubmitContinuations[callbackId] = continuation
        
        EventEmitter.shared.sendEvent(
          type: .handleSubmit,
          data: [
            "callbackId": callbackId,
            "sessionData": submitData,
          ]
        )
      }
    }
  }
}
