import Foundation

/// Parses a raw Valhalla `/route` response JSON — the exact string the SDK
/// hands back as `RouteResult.valhallaResponseJson` — into the bridge
/// contract's `Route` / `Maneuver` shapes (see `MapmapNav.types.ts`).
///
/// The SDK's `RouteResult` is a thin holder of the raw Valhalla response;
/// it exposes no structured geometry or maneuver list, so decoding the
/// Valhalla `trip` here is the only way to satisfy the contract's routing
/// surface. Valhalla's response schema and maneuver `type` codes are
/// stable and documented (valhalla/valhalla `odin` TripDirections), so
/// this parser is fully grounded — no SDK-private types involved.
enum ValhallaResponse {

  /// Decode every trip (primary + any `alternates`) into contract routes.
  /// - Throws: `BridgeError(.noRoute, …)` when the body carries a Valhalla
  ///   error or contains no usable trip.
  static func routes(fromResponseJson json: String) throws -> [[String: Any]] {
    guard
      let data = json.data(using: .utf8),
      let root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
    else {
      throw BridgeError(.internalError, "route response was not valid JSON")
    }

    // Valhalla error bodies look like { "error": "...", "error_code": N }.
    if let error = root["error"] as? String {
      let lower = error.lowercased()
      if lower.contains("no path") || lower.contains("no route") {
        throw BridgeError(.noRoute, error)
      }
      throw BridgeError(.noRoute, error)
    }

    var trips: [[String: Any]] = []
    if let trip = root["trip"] as? [String: Any] { trips.append(trip) }
    if let alternates = root["alternates"] as? [[String: Any]] {
      for alt in alternates {
        if let trip = alt["trip"] as? [String: Any] { trips.append(trip) }
      }
    }
    guard !trips.isEmpty else {
      throw BridgeError(.noRoute, "route response contained no trip")
    }

    return trips.enumerated().map { index, trip in route(fromTrip: trip, index: index) }
  }

  private static func route(fromTrip trip: [String: Any], index: Int) -> [String: Any] {
    let legs = trip["legs"] as? [[String: Any]] ?? []

    var geometry: [[String: Any]] = []
    var maneuvers: [[String: Any]] = []

    for leg in legs {
      let shapePoints: [(Double, Double)]
      if let encoded = leg["shape"] as? String {
        shapePoints = decodePolyline(encoded, precision: 1e6)
      } else {
        shapePoints = []
      }
      let legStart = geometry.count
      geometry.append(contentsOf: shapePoints.map {
        ["latitude": $0.0, "longitude": $0.1]
      })

      let legManeuvers = leg["maneuvers"] as? [[String: Any]] ?? []
      for m in legManeuvers {
        let typeCode = (m["type"] as? NSNumber)?.intValue ?? 0
        let beginIndex = (m["begin_shape_index"] as? NSNumber)?.intValue ?? 0
        let pointIndex = legStart + beginIndex
        let position: [String: Any]
        if pointIndex < geometry.count {
          position = geometry[pointIndex]
        } else if let last = geometry.last {
          position = last
        } else {
          position = ["latitude": 0.0, "longitude": 0.0]
        }
        let (maneuverType, roundaboutExit) = maneuverType(forValhalla: typeCode, raw: m)
        var maneuver: [String: Any] = [
          "type": maneuverType,
          "instruction": (m["instruction"] as? String) ?? "",
          // Valhalla reports leg length in km; contract wants metres.
          "distanceMeters": ((m["length"] as? NSNumber)?.doubleValue ?? 0) * 1000.0,
          "position": position,
        ]
        if let exit = roundaboutExit { maneuver["roundaboutExit"] = exit }
        maneuvers.append(maneuver)
      }
    }

    let summary = trip["summary"] as? [String: Any] ?? [:]
    let distanceMeters = ((summary["length"] as? NSNumber)?.doubleValue ?? 0) * 1000.0
    let durationSeconds = (summary["time"] as? NSNumber)?.doubleValue ?? 0

    return [
      "index": index,
      "geometry": geometry,
      "distanceMeters": distanceMeters,
      "durationSeconds": durationSeconds,
      "maneuvers": maneuvers,
    ]
  }

  /// Map a Valhalla maneuver `type` code to a contract `ManeuverType`,
  /// returning the roundabout exit number when the code is a roundabout.
  /// Codes per valhalla `odin/maneuver.h` (`DirectionsLeg_Maneuver_Type`).
  private static func maneuverType(
    forValhalla code: Int, raw: [String: Any]
  ) -> (String, Int?) {
    switch code {
    case 1, 2, 3: return ("depart", nil)          // start / start-right / start-left
    case 4, 5, 6: return ("arrive", nil)          // destination
    case 7, 8: return ("straight", nil)           // becomes / continue
    case 9: return ("slight-right", nil)
    case 10: return ("turn-right", nil)
    case 11: return ("sharp-right", nil)
    case 12, 13: return ("uturn", nil)
    case 14: return ("sharp-left", nil)
    case 15: return ("turn-left", nil)
    case 16: return ("slight-left", nil)
    case 17, 18, 19: return ("on-ramp", nil)      // ramp straight/right/left
    case 20, 21: return ("off-ramp", nil)         // exit right/left
    case 22: return ("straight", nil)             // stay straight
    case 23: return ("fork-right", nil)           // stay right
    case 24: return ("fork-left", nil)            // stay left
    case 25, 37, 38: return ("merge", nil)        // merge / merge-right / merge-left
    case 26, 27:                                  // roundabout enter / exit
      let exit = (raw["roundabout_exit_count"] as? NSNumber)?.intValue
      return ("roundabout", exit)
    default: return ("straight", nil)
    }
  }

  /// Decode an encoded polyline (Google algorithm) at the given precision
  /// (Valhalla uses 1e6). Returns (lat, lon) pairs.
  static func decodePolyline(_ encoded: String, precision: Double) -> [(Double, Double)] {
    var coordinates: [(Double, Double)] = []
    var index = encoded.startIndex
    let end = encoded.endIndex
    var lat = 0, lon = 0

    func nextValue() -> Int? {
      var shift = 0
      var result = 0
      while index < end {
        let byte = Int(encoded[index].asciiValue ?? 0) - 63
        index = encoded.index(after: index)
        result |= (byte & 0x1F) << shift
        shift += 5
        if byte < 0x20 { break }
      }
      let delta = (result & 1) != 0 ? ~(result >> 1) : (result >> 1)
      return delta
    }

    while index < end {
      guard let dLat = nextValue() else { break }
      lat += dLat
      guard let dLon = nextValue() else { break }
      lon += dLon
      coordinates.append((Double(lat) / precision, Double(lon) / precision))
    }
    return coordinates
  }
}
