package com.soulstream.mediasession

import android.content.Context
import android.util.Log
import org.json.JSONObject

/**
 * The door between the Rust engine and this process when there is no
 * WebView — and the reason a service-started process has a native library
 * loaded at all.
 *
 * `System.loadLibrary` appears in exactly one other place in the whole
 * mobile stack: wry's generated `com.soulstream.app.Rust`, whose static
 * initializer runs when the Activity is created. Nothing touches it without
 * an Activity, so in a process Android Auto started by binding this service
 * the .so would never be loaded, `JNI_OnLoad` would never run, `ndk-context`
 * would never be filled and rodio could not open an output stream. This
 * object's `init` is what changes that, and it is modelled byte for byte on
 * wry's own file: an `object`, a `loadLibrary` in `init`, and `@JvmStatic
 * external fun`s — which is what makes the natives static and named
 * `Java_com_soulstream_mediasession_NativeAuto_<name>`.
 *
 * The symbol lives in `libapp_lib.so`, the APP crate, because that is the
 * crate with the `library` and `player` dependencies. Names link at runtime,
 * so this module's Rust needs no dependency on the app crate at all.
 *
 * A second `loadLibrary("app_lib")` from the Activity path is a no-op — the
 * VM tracks loaded libraries per class loader and both classes share the
 * app's — and `JNI_OnLoad` still runs exactly once.
 *
 * REACHED BY STRING, from both sides: Rust finds this class with
 * `FindClass` and its two callbacks with `GetStaticMethodID`, so R8
 * renaming any of it breaks the car at RUNTIME with nothing in any log.
 * See consumer-rules.pro.
 */
object NativeAuto {
    init {
        try {
            System.loadLibrary("app_lib")
        } catch (e: Throwable) {
            /* The one failure worth a loud line: nothing below this can work
               and the car will simply do nothing. */
            Log.e("SoulStream", "auto native library would not load: $e")
        }
    }

    @Volatile private var appCtx: Context? = null

    /** Remember an application context for the callbacks. Called from
     *  `MediaService.onCreate` and from `AutoPlay`, so whichever of the two
     *  the car reaches first has already done it. */
    fun attach(ctx: Context) {
        if (appCtx == null) appCtx = ctx.applicationContext
    }

    /**
     * Resolve media ids against the library and play them. Answers
     * "headless" (this process is driving), "webview" (the app is up and was
     * handed the rows), "empty" (nothing resolved).
     *
     * `dataDir` is `ctx.dataDir` — NOT `filesDir`, which is one level too
     * deep and would find no database at all. BLOCKING: it opens SQLite and
     * starts a decoder, so never call it on the main thread; `AutoPlay` owns
     * the worker that does.
     */
    @JvmStatic external fun play(dataDir: String, ids: Array<String>, startIndex: Int, startAtMs: Long): String

    /**
     * A transport button with no WebView to give it to. True means the
     * headless side took it; false means there is a WebView (or nothing
     * playing) and `MediaHub.onAction` is the right road.
     */
    @JvmStatic external fun transport(action: String, positionSecs: Double): Boolean

    /**
     * Rust pushing what is playing. The SAME `NowPlayingArgs` the WebView
     * pushes, through the SAME `MediaHub.update`, so `SoulStreamPlayer`, the
     * card, the notification and audio focus all keep working by
     * construction — there is exactly one writer at a time and nothing in
     * round forty-eight's file needs a second reading path.
     *
     * An EMPTY string means the bar emptied.
     *
     * Called from Rust's `player-events` thread, which is a pthread attached
     * as a daemon — not the main looper. That is fine: `MediaHub.update` is
     * what `MediaSessionPlugin.update` already calls from a binder thread,
     * and it posts everything that touches media3 to the main looper itself.
     */
    @JvmStatic
    fun onHeadlessState(json: String) {
        val ctx = appCtx ?: return
        try {
            if (json.isEmpty()) {
                MediaHub.clear(ctx)
                return
            }
            MediaHub.update(ctx, parse(JSONObject(json)))
        } catch (e: Throwable) {
            Log.w("SoulStream", "auto state would not parse: $e")
        }
    }

    /** Rust's only way onto the device's log. `tauri_plugin_log` is
     *  registered inside Tauri's `.setup()`, so in a service-started process
     *  `log::info!` reaches nowhere `adb logcat` can be pointed at.
     *
     *  The prefix is `auto ` and the tag is `SoulStream`, which is the tag
     *  tests/mobile/smoke.mjs already greps. No line from here may contain
     *  the substring "media window " — `mediaWindow()` filters on exactly
     *  that and would swallow ours into the `queue-window` assertion. */
    @JvmStatic
    fun log(msg: String) {
        Log.i("SoulStream", "auto $msg")
    }

    /* org.json rather than Jackson: this is the framework's own parser, it
       is in every process for free, and the plugin's Jackson path belongs to
       Tauri's @InvokeArg machinery, which does not exist here. Field for
       field and camelCase, like both hops of the real bridge. */
    /** A string field, or null. `isNull` FIRST and an empty string as the
     *  fallback, both deliberately: `JSONObject.optString` renders a JSON
     *  `null` as the four-letter string "null" — serde writes `null` for
     *  every `Option::None` on this bridge — and Kotlin has no business
     *  handing a platform-typed null default around. */
    private fun str(o: JSONObject, key: String): String? {
        if (o.isNull(key)) return null
        val v = o.optString(key, "")
        return if (v.isEmpty()) null else v
    }

    private fun num(o: JSONObject, key: String): Double? =
        if (o.isNull(key)) null else o.optDouble(key)

    private fun parse(o: JSONObject): NowPlayingArgs {
        val np = NowPlayingArgs()
        np.title = str(o, "title")
        np.artist = str(o, "artist")
        np.album = str(o, "album")
        np.playing = o.optBoolean("playing", false)
        np.position = o.optDouble("position", 0.0)
        np.duration = num(o, "duration")
        np.artKey = str(o, "artKey")
        np.art = str(o, "art")
        np.hasNext = o.optBoolean("hasNext", true)
        np.hasPrev = o.optBoolean("hasPrev", true)
        np.queueIndex = if (o.isNull("queueIndex")) null else o.optInt("queueIndex")
        np.queueBase = if (o.isNull("queueBase")) null else o.optInt("queueBase")
        np.repeat = o.optInt("repeat", 0)
        val arr = o.optJSONArray("queue")
        if (arr != null) {
            val rows = ArrayList<QueueItemArgs>(arr.length())
            for (i in 0 until arr.length()) {
                val q = arr.optJSONObject(i) ?: continue
                val item = QueueItemArgs()
                item.id = str(q, "id")
                item.title = str(q, "title")
                item.artist = str(q, "artist")
                item.album = str(q, "album")
                item.duration = num(q, "duration")
                item.artKey = str(q, "artKey")
                rows.add(item)
            }
            np.queue = rows
        }
        return np
    }
}
