import Foundation
import React
import TelematicsSDK
import UIKit

fileprivate enum Events: String, CaseIterable {
  case onLowPowerMode = "onLowPowerMode"
  case onLocationChanged = "onLocationChanged"
  case onTrackingStateChanged = "onTrackingStateChanged"
  case onWrongAccuracyAuthorization = "onWrongAccuracyAuthorization"
  case onRtldColectedData = "onRtldColectedData"
  case onSpeedViolation = "onSpeedViolation"
}

@objc(TelematicsSdk)
public class TelematicsSdk: RCTEventEmitter {

  private var hasListeners = false
  private var speedLimitKmH: Double = 0
  private var speedLimitTimeThreshold: TimeInterval = 0

  @objc override public static func requiresMainQueueSetup() -> Bool { true }

  override public func supportedEvents() -> [String]! {
    Events.allCases.map(\.rawValue)
  }

  override public func startObserving() {
    hasListeners = true
    RPEntry.instance.lowPowerModeDelegate = self
    RPEntry.instance.locationDelegate = self
    RPEntry.instance.trackingStateDelegate = self
    RPEntry.instance.accuracyAuthorizationDelegate = self
    RPEntry.instance.rtldDelegate = self
  }

  override public func stopObserving() {
    hasListeners = false
    RPEntry.instance.lowPowerModeDelegate = nil
    RPEntry.instance.locationDelegate = nil
    RPEntry.instance.trackingStateDelegate = nil
    RPEntry.instance.accuracyAuthorizationDelegate = nil
    RPEntry.instance.rtldDelegate = nil
  }

  // MARK: - Lifecycle

  @objc(initializeSdk:reject:)
  public func initializeSdk(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(nil)
  }

  @objc(isInitializedSdk:reject:)
  public func isInitializedSdk(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.isInitialized())
  }

  // MARK: - Device token

  @objc(getDeviceId:reject:)
  public func getDeviceId(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.getDeviceId())
  }

  @objc(getDeviceIdRegistrationState:reject:)
  public func getDeviceIdRegistrationState(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    let state = RPEntry.instance.getDeviceIdRegistrationState()
    let checkedAtMillis: Int
    if state.checkedAt > 0 {
      checkedAtMillis = Int((state.checkedAt * 1000).rounded())
    } else {
      checkedAtMillis = 0
    }

    resolve([
      "status": deviceIdRegistrationStatusString(from: state.status),
      "checkedAtMillis": checkedAtMillis,
    ])
  }

  @objc(setDeviceId:resolve:reject:)
  public func setDeviceId(
    _ deviceId: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    do {
      try RPEntry.instance.setDeviceID(deviceId: deviceId)
      resolve(nil)
    } catch {
      reject("ERROR", error.localizedDescription, error)
    }
  }

  @objc(logout:reject:)
  public func logout(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.logout()
    resolve(nil)
  }

  // MARK: - Permissions & tracking

  @objc(isAllRequiredPermissionsAndSensorsGranted:reject:)
  public func isAllRequiredPermissionsAndSensorsGranted(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      resolve(RPEntry.instance.isAllRequiredPermissionsAndSensorsGranted())
    }
  }

  @objc(isSdkEnabled:reject:)
  public func isSdkEnabled(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isSDKEnabled())
  }

  @objc(isTracking:reject:)
  public func isTracking(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isTracking())
  }

  @objc(setEnableSdk:resolve:reject:)
  public func setEnableSdk(
    _ enable: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.setEnableSdk(enable)
      resolve(nil)
    }
  }

  @objc(startManualTracking:reject:)
  public func startManualTracking(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.startTracking()
      resolve(nil)
    }
  }

  @objc(startTrackAsPersistent:reject:)
  public func startTrackAsPersistent(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.startTrackAsPersistent()
      resolve(nil)
    }
  }

  @objc(stopManualTracking:reject:)
  public func stopManualTracking(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.stopTracking()
      resolve(nil)
    }
  }

  @objc(setMaxPersistentTrackingInterval:resolve:reject:)
  public func setMaxPersistentTrackingInterval(
    _ minutes: Double,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    do {
      try RPEntry.instance.setMaxPersistentTrackingInterval(minutes: Int(minutes))
      resolve(nil)
    } catch {
      reject("INVALID_ARGUMENT", error.localizedDescription, error)
    }
  }

  @objc(getMaxPersistentTrackingInterval:reject:)
  public func getMaxPersistentTrackingInterval(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.getMaxPersistentTrackingInterval())
  }

  @objc(setTrackingMode:resolve:reject:)
  public func setTrackingMode(
    _ trackingMode: Double,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    guard let mode = RPTrackingMode(rawValue: Int(trackingMode)) else {
      reject("INVALID_ARGUMENT", "trackingMode is invalid", nil)
      return
    }

    RPEntry.instance.setTrackingMode(mode)
    resolve(nil)
  }

  @objc(getTrackingMode:reject:)
  public func getTrackingMode(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.getTrackingMode().rawValue)
  }

  @objc(getTrackingState:reject:)
  public func getTrackingState(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.getTrackingState { [weak self] state in
      resolve([
        "automaticTrackingStatus": self?.trackingStatusString(from: state.automaticTrackingStatus) ?? "UNKNOWN",
        "manualTrackingStatus": self?.trackingStatusString(from: state.manualTrackingStatus) ?? "UNKNOWN",
      ])
    }
  }

  // MARK: - Upload

  @objc(uploadUnsentTrips:reject:)
  public func uploadUnsentTrips(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.uploadUnsentTrips()
      resolve(nil)
    }
  }

  @objc(getUnsentTripCount:reject:)
  public func getUnsentTripCount(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.getUnsentTripCount { unsentTripsCount in
        resolve(unsentTripsCount)
      }
    }
  }

  // MARK: - Heartbeats

  @objc(sendCustomHeartbeats:resolve:reject:)
  public func sendCustomHeartbeats(
    _ reason: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.sendCustomHeartbeat(reason)
      resolve(nil)
    }
  }

  // MARK: - Wizard

  @objc(showPermissionWizardWithOptions:blockEarlyExit:skipWizardPages:resolve:reject:)
  public func showPermissionWizardWithOptions(
    _ themeMode: String,
    blockEarlyExit: Bool,
    skipWizardPages: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      if RPEntry.instance.isAllRequiredPermissionsAndSensorsGranted() {
        resolve(true)
        return
      }

      RPPermissionsWizard.instance.launch { _ in
        resolve(RPEntry.instance.isAllRequiredPermissionsAndSensorsGranted())
      }
    }
  }

  @objc(configureIosPermissionWizard:resolve:reject:)
  public func configureIosPermissionWizard(
    _ configurationJson: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    do {
      let values = try jsonDictionary(from: configurationJson)
      let defaults = RPPermissionsWizardConfiguration.defaultConfiguration()
      let configuration = RPPermissionsWizardConfiguration(
        locationWhenInUse: wizardPage(
          dictionary(from: values["locationWhenInUse"]),
          fallback: defaults.locationWhenInUse
        ),
        locationAlways: wizardPage(
          dictionary(from: values["locationAlways"]),
          fallback: defaults.locationAlways
        ),
        motion: wizardPage(dictionary(from: values["motion"]), fallback: defaults.motion),
        status: wizardStatus(dictionary(from: values["status"]), fallback: defaults.status),
        lightTheme: wizardTheme(dictionary(from: values["lightTheme"]), fallback: defaults.lightTheme),
        darkTheme: wizardTheme(dictionary(from: values["darkTheme"]), fallback: defaults.darkTheme)
      )
      runOnMain {
        RPPermissionsWizard.instance.configure(configuration)
        resolve(nil)
      }
    } catch {
      reject("INVALID_ARGUMENT", error.localizedDescription, error)
    }
  }

  @objc(configureIosMissingPermissionsAlert:resolve:reject:)
  public func configureIosMissingPermissionsAlert(
    _ configurationJson: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    do {
      let values = try jsonDictionary(from: configurationJson)
      let defaults = RPPermissionsWizardMissingPermissionsAlertConfiguration.defaultConfiguration()
      let configuration = RPPermissionsWizardMissingPermissionsAlertConfiguration(
        title: string(values, "title", fallback: defaults.title),
        body: string(values, "body", fallback: defaults.body),
        locationTitle: string(values, "locationTitle", fallback: defaults.locationTitle),
        motionTitle: string(values, "motionTitle", fallback: defaults.motionTitle),
        locationEnabledText: string(values, "locationEnabledText", fallback: defaults.locationEnabledText),
        locationAlwaysRequiredText: string(values, "locationAlwaysRequiredText", fallback: defaults.locationAlwaysRequiredText),
        locationPreciseRequiredText: string(values, "locationPreciseRequiredText", fallback: defaults.locationPreciseRequiredText),
        locationActionNeededText: string(values, "locationActionNeededText", fallback: defaults.locationActionNeededText),
        motionEnabledText: string(values, "motionEnabledText", fallback: defaults.motionEnabledText),
        motionActionNeededText: string(values, "motionActionNeededText", fallback: defaults.motionActionNeededText),
        fixInSettingsButtonTitle: string(values, "fixInSettingsButtonTitle", fallback: defaults.fixInSettingsButtonTitle),
        isBlocking: bool(values, "isBlocking", fallback: defaults.isBlocking),
        skipButtonTitle: string(values, "skipButtonTitle", fallback: defaults.skipButtonTitle),
        lightTheme: wizardTheme(dictionary(from: values["lightTheme"]), fallback: defaults.lightTheme),
        darkTheme: wizardTheme(dictionary(from: values["darkTheme"]), fallback: defaults.darkTheme)
      )
      runOnMain {
        RPPermissionsWizard.instance.configureMissingPermissionsAlert(configuration)
        resolve(nil)
      }
    } catch {
      reject("INVALID_ARGUMENT", error.localizedDescription, error)
    }
  }

  @objc(setIosMissingPermissionsAlertEnabled:resolve:reject:)
  public func setIosMissingPermissionsAlertEnabled(
    _ enabled: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPPermissionsWizard.instance.setMissingPermissionsAlertEnabled(enabled)
      resolve(nil)
    }
  }

  // MARK: - Properties, sub-units, activity log

  @objc(setProperties:resolve:reject:)
  public func setProperties(
    _ propertiesJson: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain { [weak self] in
      do {
        let dict = try self?.stringDictionary(from: propertiesJson)
        try RPEntry.instance.setProperties(dict: dict ?? [:])
        resolve(nil)
      } catch {
        reject("INVALID_ARGUMENT", error.localizedDescription, error)
      }
    }
  }

  @objc(getProperties:reject:)
  public func getProperties(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain { [weak self] in
      let jsonString = self?.jsonString(from: RPEntry.instance.getProperties(), reject: reject)
      resolve(jsonString)
    }
  }

  @objc(clearProperties:reject:)
  public func clearProperties(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.clearProperties()
      resolve(nil)
    }
  }

  @objc(setSubUnits:resolve:reject:)
  public func setSubUnits(
    _ subUnitsJson: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain { [weak self] in
      do {
        let dict = try self?.stringDictionary(from: subUnitsJson)
        try RPEntry.instance.setSubUnits(dict: dict ?? [:])
        resolve(nil)
      } catch {
        reject("INVALID_ARGUMENT", error.localizedDescription, error)
      }
    }
  }

  @objc(getSubUnits:reject:)
  public func getSubUnits(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain { [weak self] in
      let jsonString = self?.jsonString(from: RPEntry.instance.getSubUnits(), reject: reject)
      resolve(jsonString)
    }
  }

  @objc(clearSubUnits:reject:)
  public func clearSubUnits(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.clearSubUnits()
      resolve(nil)
    }
  }

  @objc(addActivityLog:dataJson:resolve:reject:)
  public func addActivityLog(
    _ text: String,
    dataJson: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain { [weak self] in
      do {
        let dict = try self?.stringDictionary(from: dataJson)
        try RPEntry.instance.addActivityLog(text: text, data: dict ?? [:])
        resolve(nil)
      } catch {
        reject("INVALID_ARGUMENT", error.localizedDescription, error)
      }
    }
  }

  // MARK: - Accidents / RTLD

  @objc(setAccidentDetectionSensitivity:resolve:reject:)
  public func setAccidentDetectionSensitivity(
    _ value: Double,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    guard let sensitivity = RPAccidentDetectionSensitivity(rawValue: Int(value)) else {
      reject("INVALID_PARAMS", "Invalid accidentDetectionSensitivity value", nil)
      return
    }
    RPEntry.instance.setAccidentDetectionSensitivity(sensitivity: sensitivity)
    resolve(nil)
  }

  @objc(isRTLDEnabled:reject:)
  public func isRTLDEnabled(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isRTLDEnabled())
  }

  @objc(setAccidentDetectionEnabled:resolve:reject:)
  public func setAccidentDetectionEnabled(
    _ enable: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.setAccidentDetectionEnabled(enable)
    resolve(nil)
  }

  @objc(isAccidentDetectionEnabled:reject:)
  public func isAccidentDetectionEnabled(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isAccidentDetectionEnabled())
  }

  // MARK: - Tags API

  @objc(getFutureTrackTags:reject:)
  public func getFutureTrackTags(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.api.getFutureTrackTag { status, tags in
      let tagsList = tags.map { self.tagPayload(tag: $0.tag, source: $0.source) }
      let result: [String: Any] = ["status": self.parseTagStatus(status: status), "tags": tagsList]
      resolve(result)
    }
  }

  @objc(addFutureTrackTag:source:resolve:reject:)
  public func addFutureTrackTag(
    _ tag: String,
    source: String?,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    let futureTag = RPFutureTag(tag: tag, source: source)
    RPEntry.instance.api.addFutureTrackTag(futureTag) { status, error in
      if let err = error {
        reject("ERROR", err.localizedDescription, err)
      } else {
        let result: [String: Any] = [
          "status": self.parseTagStatus(status: status),
          "tag": self.tagPayload(tag: tag, source: source),
        ]
        resolve(result)
      }
    }
  }

  @objc(removeFutureTrackTag:source:resolve:reject:)
  public func removeFutureTrackTag(
    _ tag: String,
    source: String?,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    let futureTag = RPFutureTag(tag: tag, source: source)
    RPEntry.instance.api.removeFutureTrackTag(futureTag) { status, error in
      if let err = error {
        reject("ERROR", err.localizedDescription, err)
      } else {
        let result: [String: Any] = [
          "status": self.parseTagStatus(status: status),
          "tag": self.tagPayload(tag: tag, source: source),
        ]
        resolve(result)
      }
    }
  }

  @objc(removeAllFutureTrackTags:reject:)
  public func removeAllFutureTrackTags(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.api.removeAllFutureTrackTags { status, error in
      if let err = error {
        reject("ERROR", err.localizedDescription, err)
      } else {
        resolve(self.parseTagStatus(status: status))
      }
    }
  }

  // MARK: - Speed violations (flattened params)

  @objc(registerSpeedViolations:speedLimitTimeout:resolve:reject:)
  public func registerSpeedViolations(
    _ speedLimitKmH: Double,
    speedLimitTimeout: Double,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    self.speedLimitKmH = speedLimitKmH
    self.speedLimitTimeThreshold = speedLimitTimeout

    RPEntry.instance.speedLimitDelegate = self
    resolve(nil)
  }

  // MARK: - iOS-only

  @objc(isAggressiveHeartbeats:reject:)
  public func isAggressiveHeartbeats(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isAggressiveHeartbeats())
  }

  @objc(setAggressiveHeartbeats:resolve:reject:)
  public func setAggressiveHeartbeats(
    _ enable: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.setAggressiveHeartbeats(enable)
    resolve(nil)
  }

  @objc(setDisableTracking:resolve:reject:)
  public func setDisableTracking(
    _ value: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    RPEntry.instance.setDisableTracking(disableTracking: value)
    resolve(nil)
  }

  @objc(isDisableTracking:reject:)
  public func isDisableTracking(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isDisableTracking())
  }

  @objc(isWrongAccuracyState:reject:)
  public func isWrongAccuracyState(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    resolve(RPEntry.instance.isWrongAccuracyState())
  }

  @objc(requestIOSLocationAlwaysPermission:reject:)
  public func requestIOSLocationAlwaysPermission(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.requestLocationAlwaysPermission()
      resolve(nil)
    }
  }

  @objc(requestIOSMotionPermission:reject:)
  public func requestIOSMotionPermission(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    runOnMain {
      RPEntry.instance.requestMotionPermission()
      resolve(nil)
    }
  }

  @objc(getApiLanguage:reject:)
  public func getApiLanguage(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    switch RPEntry.instance.getApiLanguage() {
    case .none:
      resolve("None")
    case .english:
      resolve("English")
    case .russian:
      resolve("Russian")
    case .portuguese:
      resolve("Portuguese")
    case .spanish:
      resolve("Spanish")
    @unknown default:
      resolve("English")
    }
  }

  @objc(setApiLanguage:resolve:reject:)
  public func setApiLanguage(
    _ language: String,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    switch language {
    case "None":
      RPEntry.instance.setApiLanguage(apiLanguage: .none)
    case "English":
      RPEntry.instance.setApiLanguage(apiLanguage: .english)
    case "Russian":
      RPEntry.instance.setApiLanguage(apiLanguage: .russian)
    case "Portuguese":
      RPEntry.instance.setApiLanguage(apiLanguage: .portuguese)
    case "Spanish":
      RPEntry.instance.setApiLanguage(apiLanguage: .spanish)
    default:
      reject("INVALID_PARAMS", "Unsupported language: \(language)", nil)
      return
    }
    resolve(nil)
  }

  // MARK: - Android-only stubs (no-op on iOS, required by codegen protocol)

  @objc(setAndroidAutoStartEnabled:permanent:resolve:reject:)
  public func setAndroidAutoStartEnabled(
    _ enable: Bool,
    permanent: Bool,
    resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    reject("PLATFORM_ERROR", "setAndroidAutoStartEnabled is not available on iOS", nil)
  }

  @objc(isAndroidAutoStartEnabled:reject:)
  public func isAndroidAutoStartEnabled(
    _ resolve: @escaping RCTPromiseResolveBlock,
    reject: @escaping RCTPromiseRejectBlock
  ) {
    reject("PLATFORM_ERROR", "isAndroidAutoStartEnabled is not available on iOS", nil)
  }

  // MARK: - Helpers

  private func jsonDictionary(from json: String) throws -> [String: Any] {
    let data = Data(json.utf8)
    guard let values = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
      throw NSError(
        domain: "TelematicsSdk",
        code: 1,
        userInfo: [NSLocalizedDescriptionKey: "Expected a JSON object"]
      )
    }
    return values
  }

  private func stringDictionary(from json: String) throws -> [String: String] {
    let values = try jsonDictionary(from: json)
    var result = [String: String](minimumCapacity: values.count)
    for (key, value) in values {
      guard let stringValue = value as? String else {
        throw NSError(
          domain: "TelematicsSdk",
          code: 2,
          userInfo: [NSLocalizedDescriptionKey: "Dictionary values must be strings"]
        )
      }
      result[key] = stringValue
    }
    return result
  }

  private func jsonString(
    from values: [String: String],
    reject: @escaping RCTPromiseRejectBlock
  ) -> String? {
    do {
      let data = try JSONSerialization.data(withJSONObject: values)
      return String(decoding: data, as: UTF8.self)
    } catch {
      reject("SERIALIZATION_ERROR", error.localizedDescription, error)
      return nil
    }
  }

  private func dictionary(from value: Any?) -> [String: Any] {
    value as? [String: Any] ?? [:]
  }

  private func string(_ values: [String: Any], _ key: String, fallback: String) -> String {
    values[key] as? String ?? fallback
  }

  private func bool(_ values: [String: Any], _ key: String, fallback: Bool) -> Bool {
    values[key] as? Bool ?? fallback
  }

  private func wizardPage(
    _ values: [String: Any],
    fallback: RPPermissionsWizardPageConfiguration
  ) -> RPPermissionsWizardPageConfiguration {
    RPPermissionsWizardPageConfiguration(
      title: string(values, "title", fallback: fallback.title),
      body: string(values, "body", fallback: fallback.body),
      primaryButtonTitle: string(values, "primaryButtonTitle", fallback: fallback.primaryButtonTitle),
      hintLead: string(values, "hintLead", fallback: fallback.hintLead),
      permissionHint: string(values, "permissionHint", fallback: fallback.permissionHint)
    )
  }

  private func wizardStatus(
    _ values: [String: Any],
    fallback: RPPermissionsWizardStatusConfiguration
  ) -> RPPermissionsWizardStatusConfiguration {
    RPPermissionsWizardStatusConfiguration(
      title: string(values, "title", fallback: fallback.title),
      body: string(values, "body", fallback: fallback.body),
      locationTitle: string(values, "locationTitle", fallback: fallback.locationTitle),
      motionTitle: string(values, "motionTitle", fallback: fallback.motionTitle),
      locationEnabledText: string(values, "locationEnabledText", fallback: fallback.locationEnabledText),
      locationAlwaysRequiredText: string(values, "locationAlwaysRequiredText", fallback: fallback.locationAlwaysRequiredText),
      locationPreciseRequiredText: string(values, "locationPreciseRequiredText", fallback: fallback.locationPreciseRequiredText),
      locationActionNeededText: string(values, "locationActionNeededText", fallback: fallback.locationActionNeededText),
      motionEnabledText: string(values, "motionEnabledText", fallback: fallback.motionEnabledText),
      motionActionNeededText: string(values, "motionActionNeededText", fallback: fallback.motionActionNeededText),
      fixInSettingsButtonTitle: string(values, "fixInSettingsButtonTitle", fallback: fallback.fixInSettingsButtonTitle),
      skipButtonTitle: string(values, "skipButtonTitle", fallback: fallback.skipButtonTitle)
    )
  }

  private func wizardTheme(
    _ values: [String: Any],
    fallback: RPPermissionsWizardTheme
  ) -> RPPermissionsWizardTheme {
    RPPermissionsWizardTheme(
      backgroundColor: color(values["backgroundColor"], fallback: fallback.backgroundColor),
      gradientStartColor: color(values["gradientStartColor"], fallback: fallback.gradientStartColor),
      gradientEndColor: color(values["gradientEndColor"], fallback: fallback.gradientEndColor),
      titleTextColor: color(values["titleTextColor"], fallback: fallback.titleTextColor),
      bodyTextColor: color(values["bodyTextColor"], fallback: fallback.bodyTextColor),
      primaryElementColor: color(values["primaryElementColor"], fallback: fallback.primaryElementColor),
      secondaryElementColor: color(values["secondaryElementColor"], fallback: fallback.secondaryElementColor),
      buttonTextColor: color(values["buttonTextColor"], fallback: fallback.buttonTextColor),
      cardBackgroundColor: color(values["cardBackgroundColor"], fallback: fallback.cardBackgroundColor),
      successElementColor: color(values["successElementColor"], fallback: fallback.successElementColor),
      warningElementColor: color(values["warningElementColor"], fallback: fallback.warningElementColor),
      secondaryButtonTextColor: color(values["secondaryButtonTextColor"], fallback: fallback.secondaryButtonTextColor),
      secondaryButtonBackgroundColor: color(values["secondaryButtonBackgroundColor"], fallback: fallback.secondaryButtonBackgroundColor),
      statusIndicatorTextColor: color(values["statusIndicatorTextColor"], fallback: fallback.statusIndicatorTextColor),
      modalScrimColor: color(values["modalScrimColor"], fallback: fallback.modalScrimColor)
    )
  }

  private func color(_ value: Any?, fallback: UIColor) -> UIColor {
    guard let stringValue = value as? String, stringValue.first == "#" else { return fallback }
    let hex = String(stringValue.dropFirst())
    guard (hex.count == 6 || hex.count == 8), let parsed = UInt64(hex, radix: 16) else {
      return fallback
    }
    let value = hex.count == 6 ? parsed | 0xFF000000 : parsed
    return UIColor(
      red: CGFloat((value >> 16) & 0xFF) / 255,
      green: CGFloat((value >> 8) & 0xFF) / 255,
      blue: CGFloat(value & 0xFF) / 255,
      alpha: CGFloat((value >> 24) & 0xFF) / 255
    )
  }

  private func runOnMain(_ work: @escaping () -> Void) {
    if Thread.isMainThread {
      work()
    } else {
      DispatchQueue.main.async(execute: work)
    }
  }

  private func deviceIdRegistrationStatusString(
    from status: RPDeviceIdRegistrationStatus
  ) -> String {
    switch status {
    case .notSet:
      return "NOT_SET"
    case .unknown:
      return "UNKNOWN"
    case .registered:
      return "REGISTERED"
    case .notRegistered:
      return "NOT_REGISTERED"
    @unknown default:
      return "UNKNOWN"
    }
  }

  private func trackingStatusString(from status: RPTrackingStatus) -> String {
    switch status {
    case .enabled:
      return "ENABLED"
    case .deviceIdNotSet:
      return "DEVICE_ID_NOT_SET"
    case .sdkDisabled:
      return "SDK_DISABLED"
    case .disabledBySettings:
      return "DISABLED_BY_SETTINGS"
    case .disabledByServer:
      return "DISABLED_BY_SERVER"
    case .disabledBySchedule:
      return "DISABLED_BY_SCHEDULE"
    @unknown default:
      return "UNKNOWN"
    }
  }

  private func parseTagStatus(status: RPTagStatus) -> String {
    switch status {
    case .success:
      return "Success"
    case .offline:
      return "Offline"
    case .errorTagOperation:
      return "Wrong tag operation"
    case .invalidDeviceId:
      return "Invalid device token"
    @unknown default:
      return "Unknown error"
    }
  }

  private func tagPayload(tag: String, source: String?) -> [String: Any] {
    [
      "tag": tag,
      "source": source as Any? ?? NSNull(),
    ]
  }
}

// MARK: - Event delegates

extension TelematicsSdk: RPLowPowerModeDelegate {
  public func lowPowerMode(_ state: Bool) {
    guard hasListeners else { return }
    sendEvent(withName: Events.onLowPowerMode.rawValue, body: ["enabled": state])
  }
}

extension TelematicsSdk: RPLocationDelegate {
  public func onLocationChanged(_ location: CLLocation) {
    guard hasListeners else { return }
    let coordinate: [String: Any] = [
      "latitude": location.coordinate.latitude,
      "longitude": location.coordinate.longitude,
    ]
    sendEvent(withName: Events.onLocationChanged.rawValue, body: coordinate)
  }

  public func onNewEvents(_ events: [TelematicsSDK.RPEventPoint]) {}
}

extension TelematicsSdk: RPTrackingStateListenerDelegate {
  public func trackingStateChanged(_ state: Bool) {
    guard hasListeners else { return }
    sendEvent(withName: Events.onTrackingStateChanged.rawValue, body: state)
  }
}

extension TelematicsSdk: RPAccuracyAuthorizationDelegate {
  public func wrongAccuracyAuthorization() {
    guard hasListeners else { return }
    sendEvent(withName: Events.onWrongAccuracyAuthorization.rawValue, body: nil)
  }
}

extension TelematicsSdk: RPRTLDDelegate {
  public func rtldColectedData() {
    guard hasListeners else { return }
    sendEvent(withName: Events.onRtldColectedData.rawValue, body: nil)
  }
}

extension TelematicsSdk: RPSpeedLimitDelegate {
  public var timeThreshold: TimeInterval { speedLimitTimeThreshold }
  public var speedLimit: Double { speedLimitKmH }

  public func speedLimitNotification(
    _ speedLimit: Double,
    speed: Double,
    latitude: Double,
    longitude: Double,
    date: Date
  ) {
    guard hasListeners else { return }
    let payload: [String: Any] = [
      "date": Int(date.timeIntervalSince1970),
      "latitude": latitude,
      "longitude": longitude,
      "speed": speed,
      "speedLimit": speedLimit,
    ]
    sendEvent(withName: Events.onSpeedViolation.rawValue, body: payload)
  }
}
