import HealthKit
import NitroModules

func emptyStatisticsResponse(from: Date?, to: Date?) -> QueryStatisticsResponse {
  return QueryStatisticsResponse(
    averageQuantity: nil,
    maximumQuantity: nil,
    minimumQuantity: nil,
    sumQuantity: nil,
    mostRecentQuantity: nil,
    mostRecentQuantityDateInterval: nil,
    duration: nil,
    startDate: from,
    endDate: to,
    sources: []
  )
}

func queryStatisticsForQuantityInternal(
  quantityType: HKQuantityType,
  statistics: [StatisticsOptions],
  options: StatisticsQueryOptionsWithStringUnit?
) async throws -> HKStatistics? {
  let predicate = createPredicateForSamples(options?.filter)

  return try await withCheckedThrowingContinuation { continuation in
    let query = HKStatisticsQuery.init(
      quantityType: quantityType,
      quantitySamplePredicate: predicate,
      options: buildStatisticsOptions(statistics: statistics, quantityType: quantityType)
    ) { (_, stats: HKStatistics?, error: Error?) in
      DispatchQueue.main.async {
        if let error = error {
          return handleHKNoDataOrThrow(error: error, continuation: continuation, noDataFallback: {
            return nil
          })
        }

        if let stats = stats {
          return continuation.resume(returning: stats)
        } else {
          return continuation.resume(
            throwing: runtimeErrorWithPrefix(
              "queryStatisticsForQuantityInternal: unexpected empty response"))
        }

      }
    }

    store.execute(query)
  }
}

func serializeStatistics(gottenStats: HKStatistics, unit: HKUnit) -> QueryStatisticsResponse {
  let sources = gottenStats.sources?.map { source in
    return serializeSource(source)
  } ?? []

  var averageQuantity: Quantity?
  if let quantity = gottenStats.averageQuantity()?.doubleValue(for: unit) {
    averageQuantity = Quantity(
      unit: unit.unitString,
      quantity: quantity
    )
  }
  var maximumQuantity: Quantity?
  if let quantity = gottenStats.maximumQuantity()?.doubleValue(for: unit) {
    maximumQuantity = Quantity(
      unit: unit.unitString,
      quantity: quantity
    )
  }
  var minimumQuantity: Quantity?
  if let quantity = gottenStats.minimumQuantity()?.doubleValue(for: unit) {
    minimumQuantity = Quantity(
      unit: unit.unitString,
      quantity: quantity
    )
  }
  var sumQuantity: Quantity?
  if let quantity = gottenStats.sumQuantity()?.doubleValue(for: unit) {
    sumQuantity = Quantity(
      unit: unit.unitString,
      quantity: quantity
    )
  }

  var mostRecentQuantity: Quantity?
  if let quantity = gottenStats.mostRecentQuantity()?.doubleValue(for: unit) {
    mostRecentQuantity = Quantity(
      unit: unit.unitString,
      quantity: quantity
    )
  }
  var mostRecentQuantityDateInterval: QuantityDateInterval?
  if let mostRecentDateInterval = gottenStats.mostRecentQuantityDateInterval() {
    mostRecentQuantityDateInterval = QuantityDateInterval(
      from: mostRecentDateInterval.start,
      to: mostRecentDateInterval.end
    )
  }

  var duration: Quantity?
  let durationUnit = HKUnit.second()
  if let quantity = gottenStats.duration()?.doubleValue(for: durationUnit) {
    duration = Quantity(
      unit: durationUnit.unitString,
      quantity: quantity
    )
  }

  return QueryStatisticsResponse(
    averageQuantity: averageQuantity,
    maximumQuantity: maximumQuantity,
    minimumQuantity: minimumQuantity,
    sumQuantity: sumQuantity,
    mostRecentQuantity: mostRecentQuantity,
    mostRecentQuantityDateInterval: mostRecentQuantityDateInterval,
    duration: duration,
    startDate: gottenStats.startDate,
    endDate: gottenStats.endDate,
    sources: sources
  )
}

func queryStatisticsCollectionForQuantityInternal(
  quantityType: HKQuantityType,
  statistics: [StatisticsOptions],
  anchorDate: Date,
  intervalComponents: IntervalComponents,
  options: StatisticsQueryOptionsWithStringUnit?
) async throws -> HKStatisticsCollection? {
  let predicate = createPredicateForSamples(options?.filter)

  // Create date components from interval
  var dateComponents = DateComponents()
  if let minute = intervalComponents.minute {
    dateComponents.minute = Int(minute)
  }
  if let hour = intervalComponents.hour {
    dateComponents.hour = Int(hour)
  }
  if let day = intervalComponents.day {
    dateComponents.day = Int(day)
  }
  if let month = intervalComponents.month {
    dateComponents.month = Int(month)
  }
  if let year = intervalComponents.year {
    dateComponents.year = Int(year)
  }

  // Build statistics options
  let opts = buildStatisticsOptions(statistics: statistics, quantityType: quantityType)

  return try await withCheckedThrowingContinuation { continuation in
    let query = HKStatisticsCollectionQuery.init(
      quantityType: quantityType,
      quantitySamplePredicate: predicate,
      options: opts,
      anchorDate: anchorDate,
      intervalComponents: dateComponents
    )

    query.initialResultsHandler = { (_, results: HKStatisticsCollection?, error: Error?) in
      DispatchQueue.main.async {
        if let error = error {
          return handleHKNoDataOrThrow(error: error, continuation: continuation, noDataFallback: {
            return nil
          })
        }

        guard let statistics = results else {
          return continuation.resume(throwing: runtimeErrorWithPrefix("queryStatisticsCollectionForQuantityInternal: unexpected empty results"))
        }

        return continuation.resume(returning: statistics)
      }
    }

    store.execute(query)
  }
}

func serializeStatisticsPerSource(gottenStats: HKStatistics, unit: HKUnit)
  -> [QueryStatisticsResponseFromSingleSource] {
  if let sources = gottenStats.sources {
    return sources.map { source in
      var averageQuantity: Quantity?
      if let quantity = gottenStats.averageQuantity(for: source)?.doubleValue(for: unit) {
        averageQuantity = Quantity(
          unit: unit.unitString,
          quantity: quantity
        )
      }

      var maximumQuantity: Quantity?
      if let quantity = gottenStats.maximumQuantity(for: source)?.doubleValue(for: unit) {
        maximumQuantity = Quantity(
          unit: unit.unitString,
          quantity: quantity
        )
      }

      var minimumQuantity: Quantity?
      if let quantity = gottenStats.minimumQuantity(for: source)?.doubleValue(for: unit) {
        minimumQuantity = Quantity(
          unit: unit.unitString,
          quantity: quantity
        )
      }

      var sumQuantity: Quantity?
      if let quantity = gottenStats.sumQuantity(for: source)?.doubleValue(for: unit) {
        sumQuantity = Quantity(
          unit: unit.unitString,
          quantity: quantity
        )
      }

      var mostRecentQuantity: Quantity?
      if let quantity = gottenStats.mostRecentQuantity(for: source)?.doubleValue(for: unit) {
        mostRecentQuantity = Quantity(
          unit: unit.unitString,
          quantity: quantity
        )
      }

      var mostRecentQuantityDateInterval: QuantityDateInterval?
      if let mostRecentDateInterval = gottenStats.mostRecentQuantityDateInterval(for: source) {
        mostRecentQuantityDateInterval = QuantityDateInterval(
          from: mostRecentDateInterval.start,
          to: mostRecentDateInterval.end
        )
      }

      var duration: Quantity?
      let durationUnit = HKUnit.second()
      if let quantity = gottenStats.duration(for: source)?.doubleValue(for: durationUnit) {
        duration = Quantity(
          unit: durationUnit.unitString,
          quantity: quantity
        )
      }

      return QueryStatisticsResponseFromSingleSource(
        source: serializeSource(source),
        startDate: gottenStats.startDate,
        endDate: gottenStats.endDate,
        duration: duration,
        averageQuantity: averageQuantity,
        maximumQuantity: maximumQuantity,
        minimumQuantity: minimumQuantity,
        sumQuantity: sumQuantity,
        mostRecentQuantity: mostRecentQuantity,
        mostRecentQuantityDateInterval: mostRecentQuantityDateInterval
      )
    }
  }
  return []
}

func getAnyMapValue(_ anyMap: AnyMap, key: String) -> Any? {
  if anyMap.isBool(key: key) {
    return anyMap.getBoolean(key: key)
  }
  if anyMap.isArray(key: key) {
    return anyMap.getArray(key: key)
  }
  if anyMap.isDouble(key: key) {
    return anyMap.getDouble(key: key)
  }
  if anyMap.isObject(key: key) {
    return anyMap.getObject(key: key)
  }
  if anyMap.isString(key: key) {
    return anyMap.getString(key: key)
  }
  if anyMap.isInt64(key: key) {
    return anyMap.getInt64(key: key)
  }
  if anyMap.isNull(key: key) {
    return nil
  }
  return nil
}

/// Handles HealthKit's `errorNoData` by resuming the continuation with a fallback value if provided,
/// otherwise resumes with `nil` for Optional result types. For other errors, resumes by throwing.
/// - Parameters:
///   - error: The error returned by HealthKit.
///   - continuation: The continuation to resume.
///   - noDataFallback: Optional closure producing a fallback value to use when the error is `errorNoData`.
func handleHKNoDataOrThrow<T>(
  error: Error,
  continuation: CheckedContinuation<T, Error>,
  noDataFallback: (() -> T)
) {
  let nsError = error as NSError
  if nsError.domain == HKError.errorDomain,
    nsError.code == HKError.Code.errorNoData.rawValue {
    continuation.resume(returning: noDataFallback())
  } else {
    continuation.resume(throwing: error)
  }
}

class QuantityTypeModule: HybridQuantityTypeModuleSpec {
  func queryStatisticsForQuantitySeparateBySource(
    identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions],
    options: StatisticsQueryOptionsWithStringUnit?
  ) -> Promise<[QueryStatisticsResponseFromSingleSource]> {
    return Promise.async {
      let quantityType = try initializeQuantityType(identifier.stringValue)

      if let gottenStats = try await queryStatisticsForQuantityInternal(
        quantityType: quantityType,
        statistics: statistics,
        options: options
      ) {
        let unit = try await getUnitToUse(
          unitOverride: options?.unit,
          quantityType: quantityType
        )
        return serializeStatisticsPerSource(gottenStats: gottenStats, unit: unit)
      } else {
        return []
      }
    }
  }

  func queryStatisticsCollectionForQuantitySeparateBySource(
    identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions], anchorDate: Date,
    intervalComponents: IntervalComponents, options: StatisticsQueryOptionsWithStringUnit?
  ) -> Promise<[QueryStatisticsResponseFromSingleSource]> {
    return Promise.async {
      let quantityType = try initializeQuantityType(identifier.stringValue)

      if let statistics = try await queryStatisticsCollectionForQuantityInternal(
        quantityType: quantityType,
        statistics: statistics,
        anchorDate: anchorDate,
        intervalComponents: intervalComponents,
        options: options
      ) {

        let unit = try await getUnitToUse(
          unitOverride: options?.unit,
          quantityType: quantityType
        )

        return statistics.statistics().flatMap { statistics in
          return serializeStatisticsPerSource(gottenStats: statistics, unit: unit)
        }
      }
      return []
    }
  }

  func aggregationStyle(identifier: QuantityTypeIdentifier) throws -> AggregationStyle {
    let sampleType = try initializeQuantityType(identifier.stringValue)

    if let aggregationStyle = AggregationStyle(
      rawValue: Int32(sampleType.aggregationStyle.rawValue)) {
      return aggregationStyle
    }

    throw runtimeErrorWithPrefix(
      "Got unknown aggregation style value: \(sampleType.aggregationStyle.rawValue)")
  }

  func isQuantityCompatibleWithUnit(identifier: QuantityTypeIdentifier, unit: String) throws -> Bool {
    let sampleType = try initializeQuantityType(identifier.stringValue)

    let hkUnit = try parseUnitStringSafe(unit)

    return sampleType.is(compatibleWith: hkUnit)
  }

  func queryStatisticsForQuantity(
    identifier: QuantityTypeIdentifier,
    statistics: [StatisticsOptions],
    options: StatisticsQueryOptionsWithStringUnit?
  ) -> Promise<QueryStatisticsResponse> {
    return Promise.async {
      let quantityType = try initializeQuantityType(identifier.stringValue)

      if let gottenStats = try await queryStatisticsForQuantityInternal(
        quantityType: quantityType,
        statistics: statistics,
        options: options
      ) {
        let unit = try await getUnitToUse(
          unitOverride: options?.unit,
          quantityType: quantityType
        )

        return serializeStatistics(gottenStats: gottenStats, unit: unit)
      }

      return emptyStatisticsResponse(
        from: options?.filter?.date?.startDate,
        to: options?.filter?.date?.endDate
      )
    }
  }

  func queryStatisticsCollectionForQuantity(
    identifier: QuantityTypeIdentifier, statistics: [StatisticsOptions], anchorDate: Date,
    intervalComponents: IntervalComponents, options: StatisticsQueryOptionsWithStringUnit?
  ) -> Promise<[QueryStatisticsResponse]> {
    return Promise.async {
      let quantityType = try initializeQuantityType(identifier.stringValue)

      if let statistics = try await queryStatisticsCollectionForQuantityInternal(
        quantityType: quantityType,
        statistics: statistics,
        anchorDate: anchorDate,
        intervalComponents: intervalComponents,
        options: options
      ) {
        let unit = try await getUnitToUse(
          unitOverride: options?.unit,
          quantityType: quantityType
        )

        return statistics.statistics().map { statistics in
          return serializeStatistics(gottenStats: statistics, unit: unit)
        }
      }

      return []
    }
  }

  func queryQuantitySamplesWithAnchor(
    identifier: QuantityTypeIdentifier, options: QueryOptionsWithAnchorAndStringUnit
  ) -> Promise<QuantitySamplesWithAnchorResponse> {
    return Promise.async {
      let quantityType = try initializeQuantityType(identifier.stringValue)
      let predicate = createPredicateForSamples(options.filter)

      let unit = try await getUnitToUse(
        unitOverride: options.unit,
        quantityType: quantityType
      )

      let response = try await sampleAnchoredQueryAsync(
        sampleType: quantityType,
        limit: options.limit,
        queryAnchor: options.anchor,
        predicate: predicate
      )

      let quantitySamples = response.samples.compactMap { sample in
        if let quantitySample = sample as? HKQuantitySample {
          do {
            return try serializeQuantitySample(
              sample: quantitySample,
              unit: unit
            )
          } catch {
            warnWithPrefix("queryQuantitySamplesWithAnchor: \(error.localizedDescription)")
          }
        }
        return nil
      }

      return QuantitySamplesWithAnchorResponse(
        samples: quantitySamples,
        deletedSamples: response.deletedSamples,
        newAnchor: response.newAnchor
      )
    }
  }

  func saveQuantitySample(
    identifier: QuantityTypeIdentifierWriteable,
    unit: String,
    value: Double,
    start: Date,
    end: Date,
    metadata: AnyMap?
  ) -> Promise<QuantitySample?> {
    return Promise.async {
      let unit = try parseUnitStringSafe(unit)
      let quantity = HKQuantity.init(unit: unit, doubleValue: value)
      let typeIdentifier = HKQuantityType(
        HKQuantityTypeIdentifier(rawValue: identifier.stringValue)
      )
      let metadata = anyMapToDictionaryOptional(metadata)

      let sample = HKQuantitySample.init(
        type: typeIdentifier,
        quantity: quantity,
        start: start,
        end: end,
        metadata: metadata
      )

      let succeeded = try await saveAsync(sample: sample)

      return succeeded ? try serializeQuantitySample(sample: sample, unit: unit) : nil
    }
  }

  func queryQuantitySamples(
    identifier: QuantityTypeIdentifier, options: QueryOptionsWithSortOrderAndStringUnit
  ) -> Promise<[QuantitySample]> {
    return Promise.async {
      let quantityType = try initializeQuantityType(identifier.stringValue)
      let unit = try await getUnitToUse(unitOverride: options.unit, quantityType: quantityType)
      let samples = try await sampleQueryAsync(
        sampleType: quantityType,
        limit: options.limit,
        predicate: createPredicateForSamples(options.filter),
        sortDescriptors: getSortDescriptors(ascending: options.ascending)
      )

      return samples.compactMap({ sample in
        if let sample = sample as? HKQuantitySample {
          do {
            let serialized = try serializeQuantitySample(
              sample: sample,
              unit: unit
            )

            return serialized
          } catch {
            warnWithPrefix("Error serializing quantity sample: \(error.localizedDescription)")
          }
        }
        return nil
      })
    }
  }

}
