package expo.modules.mapmapreactnative

import org.json.JSONArray
import org.json.JSONObject
import java.io.File

/**
 * getTerritoryStyle(): return a complete MapLibre style JSON whose sources all
 * point at LOCAL data (renders with radios off).
 *
 * FINDING / CAVEAT: ai.mapmap:core 0.2.0 exposes no documented "offline style
 * for installed territory" API (the docs only show the web SDK's buildStyle()
 * and two pure JSON transforms, styleWithBuildings3d/styleForNavigation, that
 * assume you already have a style). Territory packages bundle base-map tiles,
 * so this resolver does the end-user-possible thing: locate a style JSON
 * inside the installed package on disk, pick the theme variant when present,
 * and absolutise relative refs into file:// URIs. If the package ships no
 * style JSON this rejects E_INTERNAL — reported for FINDINGS.md.
 */
object OfflineStyleResolver {

  fun resolve(rootDir: File, territoryId: String, theme: String): String {
    val territoryDir = findTerritoryDir(rootDir, territoryId)
      ?: throw BridgeException(
        E.INTERNAL,
        "installed territory \"$territoryId\" has no directory under ${rootDir.path} — package layout differs from expectation",
      )
    val styleFile = findStyleFile(territoryDir, theme)
      ?: throw BridgeException(
        E.INTERNAL,
        "no style JSON found in territory package \"$territoryId\" — offline style source undocumented (see FINDINGS)",
      )
    val style = JSONObject(styleFile.readText())
    rewriteLocalRefs(style, styleFile.parentFile ?: territoryDir)
    return style.toString()
  }

  /** Breadth-first search (bounded) for a directory named after the territory. */
  private fun findTerritoryDir(root: File, id: String): File? {
    val queue = ArrayDeque(listOf(root to 0))
    while (queue.isNotEmpty()) {
      val (dir, depth) = queue.removeFirst()
      if (!dir.isDirectory || depth > 4) continue
      if (dir.name == id || dir.name.startsWith("$id@")) return dir
      dir.listFiles()?.forEach { child ->
        if (child.isDirectory) queue.addLast(child to depth + 1)
      }
    }
    return null
  }

  /** Theme-specific style file first, generic style.json as fallback. */
  private fun findStyleFile(territoryDir: File, theme: String): File? {
    val preferred = listOf(
      "style-$theme.json",
      "style.$theme.json",
      "style_$theme.json",
      "$theme.json",
      "style.json",
    )
    val candidates = mutableMapOf<String, File>()
    territoryDir.walkTopDown().maxDepth(5).forEach { file ->
      if (file.isFile && file.name.endsWith(".json") && file.name !in candidates) {
        candidates[file.name] = file
      }
    }
    for (name in preferred) {
      candidates[name]?.let { return it }
    }
    return null
  }

  /**
   * Absolutise relative source/glyph/sprite refs against the style file's
   * directory. Scheme-qualified refs (http(s), file, mbtiles, asset) are left
   * as-is: a remaining http ref means the package expects gateway access and
   * true offline cannot be guaranteed — noted in FINDINGS rather than guessed at.
   */
  private fun rewriteLocalRefs(style: JSONObject, baseDir: File) {
    style.optJSONObject("sources")?.let { sources ->
      for (key in sources.keys()) {
        val source = sources.optJSONObject(key) ?: continue
        source.optString("url", "").takeIf { it.isNotEmpty() }?.let {
          source.put("url", localise(it, baseDir))
        }
        val tiles: JSONArray? = source.optJSONArray("tiles")
        if (tiles != null) {
          for (i in 0 until tiles.length()) {
            val tile = tiles.optString(i, "")
            if (tile.isNotEmpty()) tiles.put(i, localise(tile, baseDir))
          }
        }
      }
    }
    style.optString("glyphs", "").takeIf { it.isNotEmpty() }?.let {
      style.put("glyphs", localise(it, baseDir))
    }
    style.optString("sprite", "").takeIf { it.isNotEmpty() }?.let {
      style.put("sprite", localise(it, baseDir))
    }
  }

  private fun localise(ref: String, baseDir: File): String {
    if (":" in ref.substringBefore('/')) return ref // any scheme-qualified ref
    val cleaned = ref.removePrefix("./")
    return "file://${File(baseDir, cleaned).absolutePath}"
  }
}
