package expo.modules.mapmapreactnative

import ai.mapmap.territory.LayerFetcher
import ai.mapmap.territory.TerritoryStore
import org.json.JSONArray
import org.json.JSONObject
import java.util.concurrent.atomic.AtomicLong

/**
 * Territory listing / install / removal on top of ai.mapmap.territory.TerritoryStore.
 *
 * installTerritory drives the SDK's signed differential update path
 * (checkForUpdate → applyUpdate against the channel index) with an app-supplied
 * fetcher carrying `Authorization: Bearer <key>` — the core verifies every
 * manifest signature and layer hash itself (CONTRACTS.md §Territory acquisition).
 */
class TerritoryService(
  private val store: TerritoryStore,
  private val gateway: GatewayClient,
  private val emitEvent: (name: String, body: Map<String, Any?>) -> Unit,
) {

  /* ---------- listing ---------- */

  fun listInstalled(): List<Map<String, Any?>> =
    store.territories.value.map {
      mapOf(
        "id" to it.id,
        "name" to it.name,
        "version" to it.version,
        "sizeOnDiskBytes" to it.sizeOnDiskBytes.toDouble(),
        "active" to it.active,
      )
    }

  fun listAvailable(): List<Map<String, Any?>> {
    val indexJson = gateway.getBytes("/territories")
    return ChannelIndex.parse(indexJson).map {
      mapOf(
        "id" to it.id,
        "name" to it.name,
        "sizeBytes" to it.sizeBytes.toDouble(),
        "version" to it.version,
      )
    }
  }

  fun isInstalled(territoryId: String): Boolean =
    store.territories.value.any { it.id == territoryId }

  fun hasActiveTerritory(): Boolean = store.territories.value.any { it.active }

  /* ---------- install ---------- */

  suspend fun install(territoryId: String) {
    val progress = ProgressEmitter(territoryId, emitEvent)
    val fetcher = ProgressFetcher(gateway) { done -> progress.downloading(done) }
    try {
      // Signed channel index + detached signature; the core, not us, decides
      // whether to trust them (device-side ed25519 verification).
      val indexJson = gateway.getBytes("/territories")
      val indexSig = gateway.getBytes("/territories/index.sig")

      // Best-effort early E_TERRITORY_NOT_FOUND with a clean message; if the
      // index format is unrecognised the SDK stays authoritative.
      val known = runCatching { ChannelIndex.parse(indexJson) }.getOrDefault(emptyList())
      if (known.isNotEmpty() && known.none { it.id == territoryId }) {
        throw BridgeException(E.TERRITORY_NOT_FOUND, "territory \"$territoryId\" is not in the channel index")
      }

      val planned = store.checkForUpdate(territoryId, indexJson, indexSig, fetcher)
      if (planned == null) {
        // Up to date. For a fresh install id this should not happen.
        if (isInstalled(territoryId)) {
          progress.done()
          return
        }
        throw BridgeException(
          E.INTERNAL,
          "channel reported no update for \"$territoryId\" but it is not installed",
        )
      }

      progress.totalBytes = planned.totalDownloadBytes
      progress.downloading(0)

      // applyUpdate downloads via our fetcher, verifies hashes/signatures and
      // atomically promotes (verify-then-promote). The verify/activate phases
      // happen inside this call, so we emit them on completion as UI hints.
      store.applyUpdate(territoryId, planned.manifestJson, planned.manifestSig, fetcher)

      progress.verifying()
      progress.activating()
      progress.done()
    } catch (t: Throwable) {
      rethrowIfCancellation(t)
      val error = mapInstallError(t, fetcher.lastError)
      progress.failed(error)
      throw error
    }
  }

  suspend fun remove(territoryId: String) {
    if (!isInstalled(territoryId)) {
      throw BridgeException(E.TERRITORY_NOT_FOUND, "territory \"$territoryId\" is not installed")
    }
    // NOTE: TerritoryStore.remove() is assumed — 0.2.0's removal API is not in
    // the public docs (see FINDINGS).
    store.remove(territoryId)
  }

  /**
   * Prefer the precise error our fetcher raised (the SDK may wrap it), then
   * classify verification failures, then fall back to E_INTERNAL/E_NETWORK.
   */
  private fun mapInstallError(t: Throwable, fetchError: BridgeException?): BridgeException {
    if (t is BridgeException) return t
    if (fetchError != null) return fetchError
    val message = (t.message ?: "").lowercase()
    val verifyHints = listOf("signature", "verif", "ed25519", "blake3", "hash", "digest", "tamper")
    if (verifyHints.any { message.contains(it) }) {
      return BridgeException(E.TERRITORY_VERIFY, t.message ?: "territory package verification failed", t)
    }
    return asBridgeException(t)
  }
}

/* ---------- progress plumbing ---------- */

/**
 * Emits contract TerritoryProgressEvent payloads; download events are
 * throttled to ~4/s, phase transitions always emitted.
 */
class ProgressEmitter(
  private val territoryId: String,
  private val emitEvent: (name: String, body: Map<String, Any?>) -> Unit,
) {
  @Volatile var totalBytes: Long = 0
  private val lastEmitMs = AtomicLong(0)
  private val bytesDone = AtomicLong(0)

  fun downloading(done: Long) {
    bytesDone.set(done)
    val now = System.currentTimeMillis()
    val last = lastEmitMs.get()
    if (now - last < 250) return
    if (!lastEmitMs.compareAndSet(last, now)) return
    emit("downloading")
  }

  fun verifying() = emit("verifying")
  fun activating() = emit("activating")
  fun done() = emit("done")

  fun failed(error: BridgeException) {
    emitEvent(
      "territoryProgress",
      base("failed") + mapOf("error" to error.toEventMap()),
    )
  }

  private fun emit(phase: String) = emitEvent("territoryProgress", base(phase))

  private fun base(phase: String): Map<String, Any?> = mapOf(
    "territoryId" to territoryId,
    "phase" to phase,
    "bytesDone" to bytesDone.get().toDouble(),
    "bytesTotal" to totalBytes.toDouble(),
  )
}

/**
 * App-supplied LayerFetcher: gateway HTTP with Bearer auth + byte accounting.
 * Remembers the last precise failure so install() can surface E_AUTH /
 * E_TERRITORY_ALLOWANCE / E_NETWORK even if the SDK wraps the exception.
 */
class ProgressFetcher(
  private val gateway: GatewayClient,
  private val onBytes: (done: Long) -> Unit,
) : LayerFetcher {
  private val bytesDone = AtomicLong(0)

  @Volatile
  var lastError: BridgeException? = null
    private set

  override fun fetch(path: String): ByteArray {
    try {
      return gateway.getBytes(path) { chunk ->
        onBytes(bytesDone.addAndGet(chunk.toLong()))
      }
    } catch (e: BridgeException) {
      lastError = e
      throw e
    }
  }
}

/* ---------- channel index parsing ---------- */

/**
 * Tolerant parser for the signed channel index. FINDING: the index JSON schema
 * is not published in the docs, so this accepts the plausible shapes (top-level
 * array, or an entry array/map under "territories"/"packages"/"entries") and
 * common field spellings. The SDK does its own authoritative parsing — this
 * only feeds listAvailableTerritories() and a friendlier not-found error.
 */
object ChannelIndex {
  data class Entry(val id: String, val name: String, val version: String, val sizeBytes: Long)

  fun parse(bytes: ByteArray): List<Entry> {
    val text = bytes.toString(Charsets.UTF_8).trim()
    val entries = mutableListOf<Entry>()
    if (text.startsWith("[")) {
      collectArray(JSONArray(text), entries)
      return entries
    }
    val root = JSONObject(text)
    for (key in listOf("territories", "packages", "entries")) {
      val array = root.optJSONArray(key)
      if (array != null) {
        collectArray(array, entries)
        return entries
      }
      val obj = root.optJSONObject(key)
      if (obj != null) {
        for (id in obj.keys()) {
          obj.optJSONObject(id)?.let { entries.add(entry(it, fallbackId = id)) }
        }
        return entries
      }
    }
    throw BridgeException(E.INTERNAL, "unrecognised channel index format")
  }

  private fun collectArray(array: JSONArray, out: MutableList<Entry>) {
    for (i in 0 until array.length()) {
      array.optJSONObject(i)?.let { out.add(entry(it, fallbackId = null)) }
    }
  }

  private fun entry(obj: JSONObject, fallbackId: String?): Entry {
    val id = firstString(obj, "id", "territory", "territory_id", "territoryId")
      ?: fallbackId
      ?: throw BridgeException(E.INTERNAL, "channel index entry without an id")
    val name = firstString(obj, "name", "display_name", "displayName")
      ?: id.replaceFirstChar { it.uppercase() }
    val version = firstString(obj, "version", "latest_version", "latestVersion", "latest", "current")
      ?: ""
    var size = firstLong(obj, "size_bytes", "sizeBytes", "total_bytes", "totalBytes", "download_bytes", "downloadBytes", "size")
    if (size == 0L) {
      val layers = obj.optJSONArray("layers")
      if (layers != null) {
        for (i in 0 until layers.length()) {
          val layer = layers.optJSONObject(i) ?: continue
          size += firstLong(layer, "size_bytes", "sizeBytes", "size")
        }
      }
    }
    return Entry(id, name, version, size)
  }

  private fun firstString(obj: JSONObject, vararg keys: String): String? {
    for (key in keys) {
      val value = obj.optString(key, "")
      if (value.isNotEmpty()) return value
    }
    return null
  }

  private fun firstLong(obj: JSONObject, vararg keys: String): Long {
    for (key in keys) {
      val value = obj.optLong(key, 0L)
      if (value != 0L) return value
    }
    return 0L
  }
}
