package xyz.cottongin.radio247.audio

import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InputStream
import java.net.Socket
import java.net.URL
import java.time.Duration

class ConnectionFailed(message: String, cause: Throwable? = null) : Exception(message, cause)

data class StreamInfo(
    val bitrate: Int?,
    val ssl: Boolean,
    val contentType: String?,
    val sampleRate: Int? = null,
    val channelCount: Int? = null
)

class StreamConnection(private val url: String) {
    private val client = OkHttpClient.Builder()
        .protocols(listOf(Protocol.HTTP_1_1))
        .readTimeout(Duration.ofSeconds(30))
        .build()

    var metaint: Int? = null
        private set
    var inputStream: InputStream? = null
        private set
    var streamInfo: StreamInfo? = null
        private set
    private var response: Response? = null
    private var rawSocket: Socket? = null

    fun open() {
        try {
            openWithOkHttp()
        } catch (e: IOException) {
            if (looksLikeIcyProtocolError(e)) {
                Log.i(TAG, "OkHttp rejected response (likely ICY protocol), falling back to raw socket")
                openWithRawSocket()
            } else {
                throw ConnectionFailed("Network error", e)
            }
        }
    }

    private fun openWithOkHttp() {
        val request = Request.Builder()
            .url(url)
            .header("Icy-MetaData", "1")
            .header("User-Agent", "Radio247/1.0")
            .build()

        val resp = client.newCall(request).execute()
        if (!resp.isSuccessful) {
            resp.close()
            throw ConnectionFailed("HTTP ${resp.code}")
        }
        response = resp
        metaint = resp.header("icy-metaint")?.toIntOrNull()
        streamInfo = StreamInfo(
            bitrate = resp.header("icy-br")?.toIntOrNull(),
            ssl = url.startsWith("https", ignoreCase = true),
            contentType = resp.header("Content-Type")
        )
        inputStream = resp.body?.byteStream()
            ?: throw ConnectionFailed("Empty response body")
    }

    private fun openWithRawSocket() {
        try {
            val parsed = URL(url)
            val host = parsed.host ?: throw ConnectionFailed("No host in URL")
            val port = if (parsed.port != -1) parsed.port else parsed.defaultPort
            val path = if (parsed.path.isNullOrEmpty()) "/" else parsed.path

            val socket = Socket(host, port)
            socket.soTimeout = 30_000
            rawSocket = socket

            val out = socket.getOutputStream()
            val requestStr = "GET $path HTTP/1.0\r\n" +
                "Host: $host\r\n" +
                "User-Agent: Radio247/1.0\r\n" +
                "Icy-MetaData: 1\r\n" +
                "Accept: */*\r\n" +
                "\r\n"
            out.write(requestStr.toByteArray(Charsets.US_ASCII))
            out.flush()

            val buffered = BufferedInputStream(socket.getInputStream(), 16384)
            val headers = parseIcyHeaders(buffered)

            metaint = headers["icy-metaint"]?.toIntOrNull()
            streamInfo = StreamInfo(
                bitrate = headers["icy-br"]?.toIntOrNull(),
                ssl = url.startsWith("https", ignoreCase = true),
                contentType = headers["content-type"]
            )
            inputStream = buffered
        } catch (e: ConnectionFailed) {
            closeRawSocket()
            throw e
        } catch (e: IOException) {
            closeRawSocket()
            throw ConnectionFailed("Network error (raw socket)", e)
        }
    }

    fun close() {
        try {
            inputStream?.close()
        } catch (_: IOException) {}
        try {
            response?.close()
        } catch (_: IOException) {}
        closeRawSocket()
        response = null
        inputStream = null
        metaint = null
        streamInfo = null
    }

    private fun closeRawSocket() {
        try {
            rawSocket?.close()
        } catch (_: IOException) {}
        rawSocket = null
    }

    companion object {
        private const val TAG = "StreamConnection"

        /**
         * Shoutcast v1 servers respond with `ICY 200 OK` instead of a valid HTTP
         * status line. OkHttp surfaces this as a ProtocolException.
         */
        internal fun looksLikeIcyProtocolError(e: IOException): Boolean {
            val msg = e.message ?: return false
            return msg.contains("unexpected status line", ignoreCase = true) ||
                msg.contains("http/0.9", ignoreCase = true) ||
                e.javaClass.simpleName == "ProtocolException"
        }

        /**
         * Reads headers line-by-line from an ICY response until the blank line.
         * Returns a map of lowercased header names to values. Throws
         * [ConnectionFailed] if the status line is not `ICY 200 OK` or `HTTP/1.x 200`.
         */
        internal fun parseIcyHeaders(input: InputStream): Map<String, String> {
            val statusLine = readLine(input)
            if (!statusLine.startsWith("ICY ", ignoreCase = true) &&
                !statusLine.startsWith("HTTP/", ignoreCase = true)
            ) {
                throw ConnectionFailed("Unexpected status line: $statusLine")
            }

            val headers = mutableMapOf<String, String>()
            while (true) {
                val line = readLine(input)
                if (line.isEmpty()) break
                val colon = line.indexOf(':')
                if (colon > 0) {
                    headers[line.substring(0, colon).trim().lowercase()] =
                        line.substring(colon + 1).trim()
                }
            }
            return headers
        }

        private fun readLine(input: InputStream): String {
            val sb = StringBuilder(128)
            while (true) {
                val b = input.read()
                if (b == -1) break
                if (b == '\n'.code) break
                if (b == '\r'.code) {
                    input.mark(1)
                    if (input.read() != '\n'.code) input.reset()
                    break
                }
                sb.append(b.toChar())
            }
            return sb.toString()
        }
    }
}
