package com.fmdxconnector.audio

import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.BaseDataSource
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import okio.ByteString.Companion.toByteString
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import kotlin.math.min

// 🔊 App-Logging
import com.fmdxconnector.LogCenter
import com.fmdxconnector.LogLevel

// --------- URL-Helfer ---------

private fun buildWebSocketAudioUrl(baseUrl: String): String {
    val trimmed = baseUrl.trim()

    // Scheme sicherstellen (http/https/ws/wss)
    val normalized = if (
        trimmed.startsWith("http://", true) ||
        trimmed.startsWith("https://", true) ||
        trimmed.startsWith("ws://", true) ||
        trimmed.startsWith("wss://", true)
    ) {
        trimmed
    } else {
        "http://$trimmed"
    }

    val uri = try {
        Uri.parse(normalized)
    } catch (_: Exception) {
        // Fallback: irgendwas Gültiges zurückgeben
        return "ws://localhost/audio"
    }

    // Schema → ws oder wss
    val scheme = if (normalized.startsWith("https://", true) ||
        normalized.startsWith("wss://", true)
    ) {
        "wss"
    } else {
        "ws"
    }

    val host = uri.host ?: "localhost"
    val portPart = if (uri.port != -1) ":${uri.port}" else ""

    // Grundpfad (z.B. "/hensbroek"), evtl. leer oder "/"
    var pathPrefix = uri.path?.trimEnd('/') ?: ""

    // Wenn kein echter Pfad, aber Fragment (#hensbroek) → als Pfad verwenden
    if ((pathPrefix.isEmpty() || pathPrefix == "/") && !uri.fragment.isNullOrBlank()) {
        pathPrefix = "/${uri.fragment!!.trim('/')}"
    }

    // finaler Pfad: [prefix]/audio
    val fullPath = buildString {
        if (pathPrefix.isNotEmpty() && pathPrefix != "/") {
            append(pathPrefix)
        }
        append("/audio")
    }

    return "$scheme://$host$portPart$fullPath"
}

// --------- MediaSource.Factory mit Low-Latency LiveConfig ---------

@UnstableApi
class WebSocketMediaSourceFactory(
    private val client: OkHttpClient,
    private val userAgent: String,
    private val networkBuffer: Int
) : MediaSource.Factory {

    override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory = this

    override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory = this

    override fun createMediaSource(mediaItem: MediaItem): MediaSource {
        val baseUrl = mediaItem.mediaId
        val wsUrl = buildWebSocketAudioUrl(baseUrl)

        val dataSourceFactory = DataSource.Factory {
            WebSocketStreamDataSource(client, wsUrl, userAgent, networkBuffer)
        }

        val richMediaItem = mediaItem.buildUpon()
            .setUri(wsUrl)
            .setMimeType(MimeTypes.AUDIO_MPEG)
            .setLiveConfiguration(
                MediaItem.LiveConfiguration.Builder()
                    .setTargetOffsetMs(50)
                    .build()
            )
            .build()

        return ProgressiveMediaSource.Factory(dataSourceFactory)
            .createMediaSource(richMediaItem)
    }

    override fun getSupportedTypes(): IntArray {
        return intArrayOf(C.CONTENT_TYPE_OTHER)
    }
}

@UnstableApi
private class WebSocketStreamDataSource(
    private val client: OkHttpClient,
    private val url: String,
    private val userAgent: String,
    networkBuffer: Int
) : BaseDataSource(true) {

    // 🔽 kleinerer Netzwerk-Puffer = weniger Latenz (z.B. 16)
    private val queue = LinkedBlockingQueue<ByteArray>(networkBuffer)
    private val endMarker = ByteArray(0)
    private var currentBuffer: ByteArray? = null
    private var bufferPosition = 0
    private var webSocket: WebSocket? = null
    private var closed = false
    private var failure: Throwable? = null
    private var dataSpec: DataSpec? = null

    override fun open(dataSpec: DataSpec): Long {
        this.dataSpec = dataSpec
        transferInitializing(dataSpec)
        transferStarted(dataSpec)

        Log.d("WebSocketStreamDataSource", "Opening Audio WebSocket: $url")
        LogCenter.log("Opening Audio WebSocket: $url", LogLevel.INFO)

        val request = Request.Builder()
            .url(url)
            .header("User-Agent", "$userAgent (audio)")
            .build()

        webSocket = client.newWebSocket(
            request,
            object : WebSocketListener() {
                override fun onOpen(webSocket: WebSocket, response: Response) {
                    // Wichtig: Fallback-Handshake
                    webSocket.send(
                        "{\"type\":\"fallback\",\"data\":\"mp3\"}"
                            .encodeToByteArray()
                            .toByteString()
                    )
                    Log.d("WebSocketStreamDataSource", "Audio WebSocket opened, fallback sent")
                    LogCenter.log("Audio WebSocket opened", LogLevel.SUCCESS)
                }

                override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                    val packet = bytes.toByteArray()
                    while (!queue.offer(packet)) {
                        queue.poll()
                    }
                }

                override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                    Log.d("WebSocketStreamDataSource", "Audio WebSocket closed: $code $reason")
                    LogCenter.log("Audio WebSocket closed", LogLevel.WARNING)
                    while (!queue.offer(endMarker)) {
                        queue.poll()
                    }
                }

                override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                    Log.e("WebSocketStreamDataSource", "Audio WebSocket failure", t)
                    LogCenter.log("Audio WebSocket error", LogLevel.ERROR)
                    failure = t
                    while (!queue.offer(endMarker)) {
                        queue.poll()
                    }
                }
            }
        )

        return C.LENGTH_UNSET.toLong()
    }

    override fun getUri(): Uri? = dataSpec?.uri

    override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int {
        while (true) {
            val data = currentBuffer
            if (data != null && bufferPosition < data.size) {
                val toCopy = min(readLength, data.size - bufferPosition)
                System.arraycopy(data, bufferPosition, buffer, offset, toCopy)
                bufferPosition += toCopy
                bytesTransferred(toCopy)
                return toCopy
            }

            // 🔽 kürzeres Poll-Timeout macht das Ende etwas reaktiver,
            // hat aber wenig Einfluss auf hörbare Latenz
            val next = queue.poll(100, TimeUnit.MILLISECONDS) ?: continue
            if (next === endMarker) {
                failure?.let { throw it }
                return C.RESULT_END_OF_INPUT
            }
            currentBuffer = next
            bufferPosition = 0
        }
    }

    override fun close() {
        if (closed) return
        closed = true
        try {
            webSocket?.close(1000, null)
        } catch (_: Exception) {}
        while (!queue.offer(endMarker)) {
            queue.poll()
        }
        currentBuffer = null
        bufferPosition = 0
        transferEnded()

        Log.d("WebSocketStreamDataSource", "Audio DataSource closed for $url")
    }
}

// --------- Player-Singleton mit Low-Latency LoadControl ---------

@UnstableApi
object AudioWebSocketPlayer {

    private var player: ExoPlayer? = null

    private val client: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .readTimeout(0, TimeUnit.MILLISECONDS)
            .build()
    }

    @Synchronized
    fun start(context: Context, baseUrl: String) {
        stop()

        val appContext = context.applicationContext
        val wsUrl = buildWebSocketAudioUrl(baseUrl)

        Log.d("AudioWebSocketPlayer", "Audio starting for baseUrl=$baseUrl, wsUrl=$wsUrl")
        LogCenter.log("Audio started", LogLevel.SUCCESS)

        // 🔽 kleinerer Netzwerk-Puffer (z.B. 16)
        val factory = WebSocketMediaSourceFactory(
            client = client,
            userAgent = "FMDXConnector",
            networkBuffer = 16
        )

        val mediaItem = MediaItem.Builder()
            .setMediaId(baseUrl)
            .build()

        val mediaSource = factory.createMediaSource(mediaItem)

        // 🔽 Low-Latency LoadControl
        val loadControl = DefaultLoadControl.Builder()
            .setBufferDurationsMs(
                /* minBufferMs = */ 150,
                /* maxBufferMs = */ 300,
                /* bufferForPlaybackMs = */ 50,
                /* bufferForPlaybackAfterRebufferMs = */ 100
            )
            .build()

        val exo = ExoPlayer.Builder(appContext)
            .setLoadControl(loadControl)
            .build()
            .apply {
                repeatMode = Player.REPEAT_MODE_OFF
                setMediaSource(mediaSource)
                prepare()
                playWhenReady = true
            }

        player = exo
        Log.d("AudioWebSocketPlayer", "Audio started for baseUrl=$baseUrl")
    }

    @Synchronized
    fun stop() {
        if (player != null) {
            LogCenter.log("Audio stopped", LogLevel.WARNING)
        }
        player?.run {
            try { stop() } catch (_: Exception) {}
            try { release() } catch (_: Exception) {}
        }
        player = null
        Log.d("AudioWebSocketPlayer", "Audio stopped")
    }
}
