import Foundation
#if canImport(MapMapKit)
import MapMapKit
#endif

/// Converts one SDK `NavigationState` into the contract's `GuidanceEvent`
/// dictionary (see `MapmapNav.types.ts`). Pure — no I/O, no SDK calls —
/// so it can be unit-tested against fixed states.
enum GuidanceMapping {

  /// - Returns: a JSON-serialisable `GuidanceEvent`.
  static func event(from state: NavigationState) -> [String: Any] {
    let update = state.update

    // Distances live only on the raw `.navigating` case. Positions 1 and 2
    // are confirmed from `GuidanceDisplay.summarise` in the SDK sources
    // (distanceToNextManeuverM, distanceRemainingM); the other associated
    // values are accessed through the typed `GuidanceDisplay` helpers.
    var distanceToManeuver = 0.0
    var remainingDistance = 0.0
    if case let .navigating(_, dToManeuver, dRemaining, _, _, _, _, _, _, _) = update {
      distanceToManeuver = dToManeuver
      remainingDistance = dRemaining
    }

    let stateString: String
    if state.isArrived {
      stateString = "arrived"
    } else if state.needsReroute {
      stateString = "rerouting"
    } else {
      stateString = "navigating"
    }

    // Location for the puck/chase camera. The SDK recommends the snapped
    // fix (`GuidanceDisplay.snappedFix`) over the raw fix, but `SnappedFix`'s
    // field names ship in the (missing) generated bindings, so we use the
    // raw sample here — all its fields are confirmed public. Swap to the
    // snapped position once the bindings are available (see FINDINGS).
    let fix = state.fix
    let location: [String: Any] = [
      "latitude": fix.lat,
      "longitude": fix.lon,
      "bearingDegrees": fix.bearingDeg ?? 0,
      "speedMps": fix.speedMps ?? 0,
    ]

    var event: [String: Any] = [
      "state": stateString,
      "location": location,
      "distanceToManeuverMeters": distanceToManeuver,
      "remainingDistanceMeters": remainingDistance,
      "isOffRoute": state.needsReroute,
    ]

    // Remaining duration + ETA are not surfaced by the SDK guidance state;
    // derive them from remaining distance and current speed (falling back
    // to a nominal 13.9 m/s ≈ 50 km/h when stationary). Documented as a
    // bridge-side estimate in FINDINGS.
    let speed = max(fix.speedMps ?? 0, 0)
    let effectiveSpeed = speed > 1.0 ? speed : 13.9
    let remainingDuration = remainingDistance / effectiveSpeed
    event["remainingDurationSeconds"] = remainingDuration
    event["etaEpochMs"] = Int64(Date().timeIntervalSince1970 * 1000)
      + Int64(remainingDuration * 1000)

    if let banner = banner(from: update) {
      event["banner"] = banner
    }
    return event
  }

  /// The banner instruction for the current update, if any. Uses the typed
  /// `GuidanceDisplay.instruction` (confirmed) for the primary line.
  ///
  /// `maneuverType` requires `VisualBanner`'s field layout, which ships in
  /// the missing generated bindings; until then it defaults to "straight".
  /// See FINDINGS — the visual banner's maneuver arrow cannot be mapped
  /// without that type.
  private static func banner(from update: GuidanceUpdate) -> [String: Any]? {
    guard let primary = GuidanceDisplay.instruction(update) else { return nil }
    return [
      "primary": primary,
      "maneuverType": "straight",
    ]
  }

  /// Voice state dict for the `voiceState` event.
  static func voiceState(muted: Bool, volume: Float, speaking: Bool) -> [String: Any] {
    ["muted": muted, "volume": Double(volume), "speaking": speaking]
  }
}
