package com.wzp.engine

/**
 * Native VoIP engine wrapper. Delegates all work to libwzp_android.so via JNI.
 *
 * Lifecycle:
 * 1. Construct with a [WzpCallback]
 * 2. Call [init] to create the native engine
 * 3. Call [startCall] to begin a VoIP session
 * 4. Use [setMute], [setSpeaker], [getStats], [forceProfile] during the call
 * 5. Call [stopCall] to end the session
 * 6. Call [destroy] when the engine is no longer needed
 *
 * Thread safety: all methods must be called from the same thread (typically main).
 */
class WzpEngine(private val callback: WzpCallback) {

    /** Opaque pointer to the native EngineHandle. 0 means not initialised. */
    private var nativeHandle: Long = 0L

    /** Whether the engine has been initialised. */
    val isInitialized: Boolean get() = nativeHandle != 0L

    /** Create the native engine. Must be called before any other method. */
    fun init() {
        check(nativeHandle == 0L) { "Engine already initialized" }
        nativeHandle = nativeInit()
        check(nativeHandle != 0L) { "Native engine creation failed" }
    }

    /**
     * Start a call.
     *
     * @param relayAddr relay server address (host:port)
     * @param room      room identifier (used as QUIC SNI)
     * @param seedHex   64-char hex-encoded 32-byte identity seed (empty = random)
     * @param token     authentication token (empty = no auth)
     * @param alias     display name sent to relay for room participant list
     * @return 0 on success, negative error code on failure
     */
    /**
     * @param profile 0 = Opus GOOD, 1 = Opus DEGRADED, 2 = Codec2 CATASTROPHIC
     */
    fun startCall(relayAddr: String, room: String, seedHex: String = "", token: String = "", alias: String = "", profile: Int = 0): Int {
        check(nativeHandle != 0L) { "Engine not initialized" }
        val result = nativeStartCall(nativeHandle, relayAddr, room, seedHex, token, alias, profile)
        if (result == 0) {
            callback.onCallStateChanged(CallStateConstants.CONNECTING)
        } else {
            callback.onError(result, "Failed to start call")
        }
        return result
    }

    /** Stop the active call. Safe to call when no call is active. */
    @Synchronized
    fun stopCall() {
        if (nativeHandle != 0L) {
            nativeStopCall(nativeHandle)
            callback.onCallStateChanged(CallStateConstants.CLOSED)
        }
    }

    /** Mute or unmute the microphone. */
    fun setMute(muted: Boolean) {
        if (nativeHandle != 0L) nativeSetMute(nativeHandle, muted)
    }

    /** Enable or disable loudspeaker mode. */
    fun setSpeaker(speaker: Boolean) {
        if (nativeHandle != 0L) nativeSetSpeaker(nativeHandle, speaker)
    }


    /**
     * Get current call statistics as a JSON string.
     *
     * @return JSON-serialised [CallStats], or `"{}"` if the engine is not initialised.
     */
    @Synchronized
    fun getStats(): String {
        if (nativeHandle == 0L) return "{}"
        return try {
            nativeGetStats(nativeHandle) ?: "{}"
        } catch (_: Exception) {
            "{}"
        }
    }

    /**
     * Force a quality profile, overriding adaptive selection.
     *
     * @param profile 0 = GOOD, 1 = DEGRADED, 2 = CATASTROPHIC
     */
    fun forceProfile(profile: Int) {
        if (nativeHandle != 0L) nativeForceProfile(nativeHandle, profile)
    }

    /**
     * Signal a network transport change (e.g. WiFi → LTE handoff).
     *
     * @param networkType matches Rust `NetworkContext` ordinals:
     *   0=WiFi, 1=LTE, 2=5G, 3=3G, 4=Unknown, 5=None
     * @param bandwidthKbps reported downstream bandwidth in kbps
     */
    fun onNetworkChanged(networkType: Int, bandwidthKbps: Int) {
        if (nativeHandle != 0L) nativeOnNetworkChanged(nativeHandle, networkType, bandwidthKbps)
    }

    /** Destroy the native engine and free all resources. The instance must not be reused. */
    @Synchronized
    fun destroy() {
        if (nativeHandle != 0L) {
            nativeDestroy(nativeHandle)
            nativeHandle = 0L
        }
    }

    /**
     * Write captured PCM samples into the engine's capture ring buffer.
     * Called from the AudioRecord capture thread.
     */
    fun writeAudio(pcm: ShortArray): Int {
        if (nativeHandle == 0L) return 0
        return nativeWriteAudio(nativeHandle, pcm)
    }

    /**
     * Read decoded PCM samples from the engine's playout ring buffer.
     * Called from the AudioTrack playout thread.
     */
    fun readAudio(pcm: ShortArray): Int {
        if (nativeHandle == 0L) return 0
        return nativeReadAudio(nativeHandle, pcm)
    }

    /**
     * Write captured PCM from a DirectByteBuffer — zero JNI array copy.
     * The buffer must be a direct ByteBuffer with native byte order containing i16 samples.
     * Called from the AudioRecord capture thread.
     */
    fun writeAudioDirect(buffer: java.nio.ByteBuffer, sampleCount: Int): Int {
        if (nativeHandle == 0L) return 0
        return nativeWriteAudioDirect(nativeHandle, buffer, sampleCount)
    }

    /**
     * Read decoded PCM into a DirectByteBuffer — zero JNI array copy.
     * The buffer must be a direct ByteBuffer with native byte order.
     * Called from the AudioTrack playout thread.
     */
    fun readAudioDirect(buffer: java.nio.ByteBuffer, maxSamples: Int): Int {
        if (nativeHandle == 0L) return 0
        return nativeReadAudioDirect(nativeHandle, buffer, maxSamples)
    }

    // -- JNI native methods --------------------------------------------------

    private external fun nativeInit(): Long
    private external fun nativeStartCall(
        handle: Long, relay: String, room: String, seed: String, token: String, alias: String, profile: Int
    ): Int
    private external fun nativeStopCall(handle: Long)
    private external fun nativeSetMute(handle: Long, muted: Boolean)
    private external fun nativeSetSpeaker(handle: Long, speaker: Boolean)
    private external fun nativeGetStats(handle: Long): String?
    private external fun nativeForceProfile(handle: Long, profile: Int)
    private external fun nativeWriteAudio(handle: Long, pcm: ShortArray): Int
    private external fun nativeReadAudio(handle: Long, pcm: ShortArray): Int
    private external fun nativeWriteAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, sampleCount: Int): Int
    private external fun nativeReadAudioDirect(handle: Long, buffer: java.nio.ByteBuffer, maxSamples: Int): Int
    private external fun nativeDestroy(handle: Long)
    private external fun nativePingRelay(handle: Long, relay: String): String?
    private external fun nativeStartSignaling(handle: Long, relay: String, seed: String, token: String, alias: String): Int
    private external fun nativePlaceCall(handle: Long, targetFp: String): Int
    private external fun nativeAnswerCall(handle: Long, callId: String, mode: Int): Int
    private external fun nativeOnNetworkChanged(handle: Long, networkType: Int, bandwidthKbps: Int)

    /**
     * Ping a relay server. Requires engine to be initialized.
     * Returns JSON `{"rtt_ms":N,"server_fingerprint":"hex"}` or null.
     */
    fun pingRelay(address: String): String? {
        if (nativeHandle == 0L) return null
        return nativePingRelay(nativeHandle, address)
    }

    /**
     * Start persistent signaling connection for direct 1:1 calls.
     * The engine registers on the relay and listens for incoming calls.
     * Call state updates are available via [getStats].
     *
     * @return 0 on success, -1 on error
     */
    fun startSignaling(relay: String, seed: String = "", token: String = "", alias: String = ""): Int {
        check(nativeHandle != 0L) { "Engine not initialized" }
        return nativeStartSignaling(nativeHandle, relay, seed, token, alias)
    }

    /**
     * Place a direct call to a peer by fingerprint.
     * Requires [startSignaling] to have been called first.
     *
     * @return 0 on success, -1 on error
     */
    fun placeCall(targetFingerprint: String): Int {
        check(nativeHandle != 0L) { "Engine not initialized" }
        return nativePlaceCall(nativeHandle, targetFingerprint)
    }

    /**
     * Answer an incoming direct call.
     *
     * @param callId The call ID from the incoming call (available in stats.incoming_call_id)
     * @param mode 0=Reject, 1=AcceptTrusted (P2P in Phase 2), 2=AcceptGeneric (relay-mediated)
     * @return 0 on success, -1 on error
     */
    fun answerCall(callId: String, mode: Int = 2): Int {
        check(nativeHandle != 0L) { "Engine not initialized" }
        return nativeAnswerCall(nativeHandle, callId, mode)
    }

    companion object {
        init {
            System.loadLibrary("wzp_android")
        }
    }
}

/** Integer constants matching the Rust [CallState] enum ordinals. */
object CallStateConstants {
    const val IDLE = 0
    const val CONNECTING = 1
    const val ACTIVE = 2
    const val RECONNECTING = 3
    const val CLOSED = 4
}
