package expo.modules.mapmapreactnative

import ai.mapmap.AndroidLocationProvider
import ai.mapmap.LocationProvider
import ai.mapmap.ReplayLocationProvider
import ai.mapmap.nav.NavigationEngine
import ai.mapmap.nav.NavigationState
import ai.mapmap.nav.VoiceGuidance
import ai.mapmap.replay.DriveCorpus
import ai.mapmap.routing.Route
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File

/**
 * One guidance session at a time: NavigationEngine's Flow<NavigationState> →
 * contract `guidance` events, with VoiceGuidance fed every update.
 *
 * Event rate: the contract requires ≥1Hz plus one event per state transition.
 * The engine's flow is expected to tick with the location provider (~1Hz); a
 * keepalive re-emits the last state whenever >1s passes without one, so the
 * guarantee holds even if the provider goes quiet (e.g. GNSS blackout).
 */
class GuidanceService(
  private val context: Context,
  private val voice: VoiceGuidance,
  private val scope: CoroutineScope,
  private val emitEvent: (name: String, body: Map<String, Any?>) -> Unit,
) {
  private var sessionJob: Job? = null

  val isActive: Boolean
    get() = sessionJob?.isActive == true

  fun start(route: Route, source: LocationSourceRecord) {
    if (isActive) {
      throw BridgeException(E.GUIDANCE_ACTIVE, "a guidance session is already running — call stopGuidance() first")
    }
    val provider = buildProvider(source)
    val engine = NavigationEngine(route)

    sessionJob = scope.launch {
      try {
        coroutineScope {
          var last: NavigationState? = null
          var lastEmitMs = 0L

          // ≥1Hz keepalive (checked at 4Hz).
          launch {
            while (isActive) {
              delay(250)
              val state = last ?: continue
              if (System.currentTimeMillis() - lastEmitMs >= 1000) {
                lastEmitMs = System.currentTimeMillis()
                emitEvent("guidance", ContractMapping.guidanceEvent(state))
              }
            }
          }

          engine.session(provider).collect { state ->
            last = state
            lastEmitMs = System.currentTimeMillis()
            voice.handle(state)
            emitEvent("guidance", ContractMapping.guidanceEvent(state))
          }
          // Flow completed (e.g. replay corpus exhausted): the keepalive keeps
          // re-emitting the final state until stopGuidance() cancels us.
        }
      } catch (t: Throwable) {
        rethrowIfCancellation(t)
        val error = asBridgeException(t)
        emitEvent("bridgeError", error.toEventMap())
      }
    }
  }

  suspend fun stop() {
    val job = sessionJob ?: throw BridgeException(E.NO_GUIDANCE, "no guidance session is running")
    sessionJob = null
    job.cancel()
    job.join()
    voice.stop()
  }

  /** Best-effort teardown for OnDestroy — never throws. */
  fun shutdown() {
    sessionJob?.cancel()
    sessionJob = null
    runCatching { voice.stop() }
  }

  private fun buildProvider(source: LocationSourceRecord): LocationProvider = when (source.kind) {
    "live" -> {
      val granted =
        context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
          context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
      if (!granted) {
        // No dedicated contract code for permissions; E_INTERNAL with a
        // precise message per the error model ("message carries detail").
        throw BridgeException(
          E.INTERNAL,
          "location permission not granted — request ACCESS_FINE_LOCATION before starting live guidance",
        )
      }
      AndroidLocationProvider(context)
    }

    "replay" -> {
      val path = source.corpusPath
        ?: throw BridgeException(E.REPLAY_CORPUS, "replay source requires corpusPath")
      val file = File(path)
      if (!file.isFile || !file.canRead()) {
        throw BridgeException(E.REPLAY_CORPUS, "replay corpus not found or unreadable: $path")
      }
      val corpus = try {
        DriveCorpus.load(file)
      } catch (t: Throwable) {
        rethrowIfCancellation(t)
        throw BridgeException(E.REPLAY_CORPUS, "malformed .drive.jsonl corpus: ${t.message}", t)
      }
      ReplayLocationProvider(corpus, source.speedMultiplier ?: 1.0)
    }

    else -> throw BridgeException(E.INTERNAL, "unknown location source kind \"${source.kind}\"")
  }
}
