import Foundation
import React
import Sahha

@objc(SahhaReactNative)
class SahhaReactNative: NSObject {

  var storedValue: Int = 0
  override init() {
    super.init()
  }

  @objc static func requiresMainQueueSetup() -> Bool {
    return false
  }

  @objc func constantsToExport() -> [AnyHashable: Any]! {
    return ["message": "Sahha Demo"]
  }

  enum SahhaSettingsIdentifier: String {
    case environment
    case sensors
    case enableMotionTrigger
  }

  @objc(configure:callback:)
  func configure(
    _ settings: NSDictionary, callback: @escaping RCTResponseSenderBlock
  ) {
    if let configSettings = settings as? [String: Any],
      let environment = configSettings[SahhaSettingsIdentifier.environment.rawValue] as? String,
      let configEnvironment: SahhaEnvironment = SahhaEnvironment(rawValue: environment)
    {

      // ✅ NEW: Optional enableMotionTrigger (default false), supports Bool or NSNumber
      let enableMotionTrigger: Bool = {
        if let boolVal = configSettings[SahhaSettingsIdentifier.enableMotionTrigger.rawValue]
          as? Bool
        {
          return boolVal
        }
        if let numVal = configSettings[SahhaSettingsIdentifier.enableMotionTrigger.rawValue]
          as? NSNumber
        {
          return numVal.boolValue
        }
        return false
      }()

      // ✅ UPDATED: Pass enableMotionTrigger into settings
      var sahhaSettings = SahhaSettings(
        environment: configEnvironment,
        enableMotionTrigger: enableMotionTrigger
      )
      sahhaSettings.framework = .react_native

      Sahha.configure(sahhaSettings) {
        print("Sahha | ReactNative configure success")
        callback([NSNull(), true])
      }
    } else {
      let message = "Sahha configure settings invalid"
      var body = "INVALID"
      if let jsonData = try? JSONSerialization.data(
        withJSONObject: settings, options: [.prettyPrinted]),
        let jsonString = String(data: jsonData, encoding: .utf8)
      {
        body = jsonString
      }
      print("Sahha | ReactNative configure error")
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "configure", body: body)
      callback([message, false])
    }
  }

  @objc(openAppSettings)
  func openAppSettings() {
    Sahha.openAppSettings()
  }

  @objc(postSensorData)
  func postSensorData() {
    Sahha.postSensorData()
  }

  @objc(isAuthenticated:)
  func isAuthenticated(_ callback: @escaping RCTResponseSenderBlock) {
    callback([NSNull(), Sahha.isAuthenticated])
  }

  @objc(authenticate:appSecret:externalId:callback:)
  func authenticate(
    _ appId: String, appSecret: String, externalId: String,
    callback: @escaping RCTResponseSenderBlock
  ) {
    Sahha.authenticate(
      appId: appId, appSecret: appSecret, externalId: externalId
    ) { error, success in
      callback([error ?? NSNull(), success])
    }
  }

  @objc(authenticateToken:refreshToken:callback:)
  func authenticateToken(
    _ profileToken: String, refreshToken: String,
    callback: @escaping RCTResponseSenderBlock
  ) {
    Sahha.authenticate(profileToken: profileToken, refreshToken: refreshToken) {
      error, success in
      callback([error ?? NSNull(), success])
    }
  }

  @objc(deauthenticate:)
  func deauthenticate(_ callback: @escaping RCTResponseSenderBlock) {
    Sahha.deauthenticate { error, success in
      callback([error ?? NSNull(), success])
    }
  }

  @objc(getProfileToken:)
  func getProfileToken(_ callback: @escaping RCTResponseSenderBlock) {
    callback([NSNull(), Sahha.profileToken ?? NSNull()])
  }

  @objc(getDemographic:)
  func getDemographic(_ callback: @escaping RCTResponseSenderBlock) {
    Sahha.getDemographic { error, value in
      var string: String?
      if let value = value {
        do {
          let jsonEncoder = JSONEncoder()
          jsonEncoder.outputFormatting = .prettyPrinted
          let jsonData = try jsonEncoder.encode(value)
          string = String(data: jsonData, encoding: .utf8)
        } catch let encodingError {
          print(encodingError)
          Sahha.postError(
            framework: .react_native,
            message: encodingError.localizedDescription,
            path: "SahhaReactNative", method: "getDemographic",
            body: "if let value = value")
          callback([encodingError.localizedDescription, NSNull()])
          return
        }
      }
      callback([error ?? NSNull(), string ?? NSNull()])
    }
  }

  @objc(postDemographic:callback:)
  func postDemographic(
    _ demographic: NSDictionary, callback: @escaping RCTResponseSenderBlock
  ) {
    if let configDemographic = demographic as? [String: Any] {

      var requestDemographic = SahhaDemographic()

      if let gender = configDemographic["gender"] as? String {
        requestDemographic.gender = gender
      }

      if let birthDate = configDemographic["birthDate"] as? String {
        requestDemographic.birthDate = birthDate
      }

      Sahha.postDemographic(requestDemographic) { error, success in
        callback([error ?? NSNull(), success])
      }

    } else {
      let message = "Sahha demographic invalid"
      var body = "INVALID"
      if let jsonData = try? JSONSerialization.data(
        withJSONObject: demographic, options: [.prettyPrinted]),
        let jsonString = String(data: jsonData, encoding: .utf8)
      {
        body = jsonString
      }
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "postDemographic", body: body)
      callback([message, false])
    }
  }

  /// Watchdog for enableSensors: the JS callback must always settle, even if the
  /// native SDK callback never fires (e.g. a HealthKit authorization request that
  /// never resolves on older SDK versions). Longer than the SDK's own 120 second
  /// authorization timeout so the SDK's more specific error wins when both apply.
  private static let enableSensorsWatchdogSeconds: TimeInterval = 150

  /// Maps sensor names from JS to native sensors, logging any names this SDK
  /// version doesn't recognize instead of silently dropping them.
  private func mapSensors(_ sensors: [String], method: String) -> Set<SahhaSensor> {
    var configSensors: Set<SahhaSensor> = []
    var unrecognizedSensors: [String] = []
    for sensor in sensors {
      if let configSensor = SahhaSensor(rawValue: sensor) {
        configSensors.insert(configSensor)
      } else {
        unrecognizedSensors.append(sensor)
      }
    }
    if !unrecognizedSensors.isEmpty {
      let message =
        "Sahha | \(method) ignoring unrecognized sensors: \(unrecognizedSensors.joined(separator: ", "))"
      print(message)
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: method, body: "\(unrecognizedSensors.count) of \(sensors.count) sensors unrecognized")
    }
    return configSensors
  }

  /// Maps a sensor status to the TS SahhaSensorStatus enum ordinal: 0..3
  private static func statusOrdinal(_ sensorStatus: SahhaSensorStatus) -> Int {
    switch sensorStatus {
    case .pending: return 0
    case .unavailable: return 1
    case .disabled: return 2
    case .enabled: return 3
    // The SDK reports .indeterminate externally as enabled (see
    // HealthKitManager.getSensorStatus); mirror that if it ever surfaces here.
    case .indeterminate: return 3
    @unknown default: return 0
    }
  }

  @objc(getSensorStatus:callback:)
  func getSensorStatus(
    _ sensors: [String],
    callback: @escaping RCTResponseSenderBlock
  ) {
    let configSensors = mapSensors(sensors, method: "getSensorStatus")

    Sahha.getSensorStatus(configSensors) { error, sensorStatus in
      callback([error ?? NSNull(), Self.statusOrdinal(sensorStatus)])
    }
  }

  @objc(enableSensors:callback:)
  func enableSensors(
    _ sensors: [String],
    callback: @escaping RCTResponseSenderBlock
  ) {
    let configSensors = mapSensors(sensors, method: "enableSensors")

    // React Native response blocks are single-use (a second invocation is fatal),
    // so every path below settles through a fire-once guard.
    let settle = SettleOnce(callback)

    DispatchQueue.global().asyncAfter(deadline: .now() + Self.enableSensorsWatchdogSeconds) {
      let message =
        "Sahha | enableSensors did not complete within \(Int(Self.enableSensorsWatchdogSeconds)) seconds"
      guard settle.settle([message, NSNull()]) else { return }
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "enableSensors", body: "sensorCount: \(configSensors.count)")
    }

    // The SDK's enableSensors callback already reports the post-enable sensor
    // status — no second getSensorStatus round trip is needed.
    Sahha.enableSensors(configSensors) { error, sensorStatus in
      if let error = error {
        settle.settle([error, NSNull()])
        return
      }
      settle.settle([NSNull(), Self.statusOrdinal(sensorStatus)])
    }
  }

  @objc(getScores:startDateTime:endDateTime:callback:)
  func getScores(
    _ types: [String], startDateTime: NSNumber, endDateTime: NSNumber,
    callback: @escaping RCTResponseSenderBlock
  ) {
    let startDateTimeInterval = TimeInterval(startDateTime.doubleValue / 1000)
    let endDateTimeInterval = TimeInterval(endDateTime.doubleValue / 1000)
    guard startDateTimeInterval > 0, endDateTimeInterval > 0 else {
      let message = "Sahha analyze date range invalid"
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "getScores",
        body: "\(startDateTime.stringValue) | \(endDateTime.stringValue)")
      callback([message, NSNull()])
      return
    }
    let startDate = Date(timeIntervalSince1970: startDateTimeInterval)
    let endDate = Date(timeIntervalSince1970: endDateTimeInterval)

    var scoreTypes: Set<SahhaScoreType> = []
    for type in types {
      if let scoreType = SahhaScoreType(rawValue: type) {
        scoreTypes.insert(scoreType)
      }
    }

    Sahha.getScores(types: scoreTypes, startDateTime: startDate, endDateTime: endDate) {
      error, value in
      callback([error ?? NSNull(), value ?? NSNull()])
    }
  }

  @objc(getBiomarkers:types:startDateTime:endDateTime:callback:)
  func getBiomarkers(
    categories: [String], types: [String], startDateTime: NSNumber, endDateTime: NSNumber,
    callback: @escaping RCTResponseSenderBlock
  ) {
    let startDateTimeInterval = TimeInterval(startDateTime.doubleValue / 1000)
    let endDateTimeInterval = TimeInterval(endDateTime.doubleValue / 1000)
    guard startDateTimeInterval > 0, endDateTimeInterval > 0 else {
      let message = "Sahha getBiomarkers date range invalid"
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "getBiomarkers",
        body: "\(startDateTime.stringValue) | \(endDateTime.stringValue)")
      callback([message, NSNull()])
      return
    }
    let startDate = Date(timeIntervalSince1970: startDateTimeInterval)
    let endDate = Date(timeIntervalSince1970: endDateTimeInterval)

    var biomarkerCategories: Set<SahhaBiomarkerCategory> = []
    var biomarkerTypes: Set<SahhaBiomarkerType> = []
    for category in categories {
      if let biomarkerCategory = SahhaBiomarkerCategory(rawValue: category) {
        biomarkerCategories.insert(biomarkerCategory)
      }
    }
    for type in types {
      if let biomarkerType = SahhaBiomarkerType(rawValue: type) {
        biomarkerTypes.insert(biomarkerType)
      }
    }

    Sahha.getBiomarkers(
      categories: biomarkerCategories, types: biomarkerTypes, startDateTime: startDate,
      endDateTime: endDate
    ) { error, value in
      callback([error ?? NSNull(), value ?? NSNull()])
    }
  }

  @objc(getStats:startDateTime:endDateTime:callback:)
  func getStats(
    _ sensor: String, startDateTime: NSNumber, endDateTime: NSNumber,
    callback: @escaping RCTResponseSenderBlock
  ) {
    let startDateTimeInterval = TimeInterval(startDateTime.doubleValue / 1000)
    let endDateTimeInterval = TimeInterval(endDateTime.doubleValue / 1000)
    guard startDateTimeInterval > 0, endDateTimeInterval > 0 else {
      let message = "Sahha getStats date range invalid"
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "getStats",
        body: "\(startDateTime.stringValue) | \(endDateTime.stringValue)")
      callback([message, NSNull()])
      return
    }
    let start = Date(timeIntervalSince1970: startDateTimeInterval)
    let end = Date(timeIntervalSince1970: endDateTimeInterval)

    if let sahhaSensor = SahhaSensor(rawValue: sensor) {
      Sahha.getStats(sensor: sahhaSensor, startDateTime: start, endDateTime: end) { error, value in
        var string: String?
        do {
          let jsonEncoder = JSONEncoder()
          jsonEncoder.outputFormatting = .prettyPrinted
          let jsonData = try jsonEncoder.encode(value)
          string = String(data: jsonData, encoding: .utf8)
        } catch let encodingError {
          print(encodingError)
          Sahha.postError(
            framework: .react_native,
            message: encodingError.localizedDescription,
            path: "SahhaReactNative", method: "getStats",
            body: "jsonEncoder")
          callback([encodingError.localizedDescription, NSNull()])
          return
        }
        callback([error ?? NSNull(), string ?? NSNull()])
      }
    } else {
      callback(["Sahha | Invalid \(sensor) sensor for getStats", NSNull()])
    }
  }

  @objc(getSamples:startDateTime:endDateTime:callback:)
  func getSamples(
    _ sensor: String, startDateTime: NSNumber, endDateTime: NSNumber,
    callback: @escaping RCTResponseSenderBlock
  ) {
    let startDateTimeInterval = TimeInterval(startDateTime.doubleValue / 1000)
    let endDateTimeInterval = TimeInterval(endDateTime.doubleValue / 1000)
    guard startDateTimeInterval > 0, endDateTimeInterval > 0 else {
      let message = "Sahha getSamples date range invalid"
      Sahha.postError(
        framework: .react_native, message: message, path: "SahhaReactNative",
        method: "getSamples",
        body: "\(startDateTime.stringValue) | \(endDateTime.stringValue)")
      callback([message, NSNull()])
      return
    }
    let start = Date(timeIntervalSince1970: startDateTimeInterval)
    let end = Date(timeIntervalSince1970: endDateTimeInterval)

    if let sahhaSensor = SahhaSensor(rawValue: sensor) {
      Sahha.getSamples(sensor: sahhaSensor, startDateTime: start, endDateTime: end) {
        error, value in
        var string: String?
        do {
          let jsonEncoder = JSONEncoder()
          jsonEncoder.outputFormatting = .prettyPrinted
          let jsonData = try jsonEncoder.encode(value)
          string = String(data: jsonData, encoding: .utf8)
        } catch let encodingError {
          print(encodingError)
          Sahha.postError(
            framework: .react_native,
            message: encodingError.localizedDescription,
            path: "SahhaReactNative", method: "getSamples",
            body: "jsonEncoder")
          callback([encodingError.localizedDescription, NSNull()])
          return
        }
        callback([error ?? NSNull(), string ?? NSNull()])
      }
    } else {
      callback(["Sahha | Invalid \(sensor) sensor for getSamples", NSNull()])
    }
  }
}

/// Wraps a single-use React Native response block so it fires exactly once.
/// A second invocation of an RCTResponseSenderBlock is fatal in React Native,
/// so racing paths (SDK callback vs watchdog timeout) must both settle through
/// this guard: whichever loses the race becomes a no-op.
private final class SettleOnce {
  private let lock = NSLock()
  private var callback: RCTResponseSenderBlock?

  init(_ callback: @escaping RCTResponseSenderBlock) {
    self.callback = callback
  }

  /// Fires the wrapped callback the first time only. Returns true if this call
  /// fired it, false if it had already been settled.
  @discardableResult
  func settle(_ args: [Any]) -> Bool {
    lock.lock()
    let callbackToFire = callback
    callback = nil
    lock.unlock()
    guard let callbackToFire else { return false }
    callbackToFire(args)
    return true
  }
}
