package de.duffy.screencast

import android.util.Log
import java.io.BufferedOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.InetSocketAddress
import java.net.ServerSocket
import java.net.Socket
import java.security.MessageDigest
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeUnit

/**
 * Kleiner HTTP-Server mit WebSocket-Unterstuetzung - ohne Fremdbibliothek,
 * damit die App schlank bleibt.
 *
 *   /            Player-Seite fuer den Fernseher-Browser
 *   /ws          Live-Stream als fragmentiertes MP4 ueber WebSocket
 *   /stream.ts   derselbe Stream als MPEG-TS fuer VLC, mpv oder Kodi
 */
class WebServer(
    /** Laedt eine Datei aus den App-Assets - injiziert, damit testbar. */
    private val assetLoader: (String) -> ByteArray,
    private val hub: StreamHub,
    private val preferredPort: Int = 8080
) {
    var port: Int = preferredPort
        private set

    private var serverSocket: ServerSocket? = null
    private var acceptThread: Thread? = null
    @Volatile private var running = false

    fun start() {
        val socket = bind() ?: throw IOException("Kein freier Port gefunden")
        serverSocket = socket
        port = socket.localPort
        running = true
        acceptThread = Thread({
            while (running) {
                val client = try {
                    socket.accept()
                } catch (e: IOException) {
                    if (running) Log.w(TAG, "accept fehlgeschlagen", e)
                    break
                }
                Thread({ handle(client) }, "http-client").apply { isDaemon = true }.start()
            }
        }, "http-accept").also { it.isDaemon = true; it.start() }
        Log.i(TAG, "Server laeuft auf Port $port")
    }

    private fun bind(): ServerSocket? {
        for (candidate in preferredPort..(preferredPort + 20)) {
            try {
                return ServerSocket().apply {
                    reuseAddress = true
                    bind(InetSocketAddress(candidate))
                }
            } catch (_: IOException) { /* naechsten Port probieren */ }
        }
        return null
    }

    fun stop() {
        running = false
        try { serverSocket?.close() } catch (_: IOException) {}
        serverSocket = null
        acceptThread = null
    }

    // ------------------------------------------------------------------ HTTP

    private fun handle(socket: Socket) {
        try {
            socket.tcpNoDelay = true
            socket.soTimeout = 15_000
            val input = socket.getInputStream()
            val request = readRequest(input) ?: run { socket.close(); return }
            val path = request.path.substringBefore('?')

            when {
                request.headers["upgrade"]?.lowercase() == "websocket" -> {
                    socket.soTimeout = 0
                    serveWebSocket(socket, input, request)
                }
                path == "/stream.ts" -> {
                    socket.soTimeout = 0
                    serveTs(socket)
                }
                path == "/" || path == "/index.html" ->
                    serveAsset(socket, "player.html", "text/html; charset=utf-8")
                path == "/favicon.ico" -> { respond(socket, 204, "No Content", "", ""); socket.close() }
                else -> { respond(socket, 404, "Not Found", "text/plain", "Nicht gefunden"); socket.close() }
            }
        } catch (e: Exception) {
            Log.d(TAG, "Verbindung beendet: ${e.message}")
            try { socket.close() } catch (_: IOException) {}
        }
    }

    private class Request(val method: String, val path: String, val headers: Map<String, String>)

    private fun readRequest(input: InputStream): Request? {
        val line = StringBuilder()
        val lines = ArrayList<String>()
        var previous = -1
        while (true) {
            val b = input.read()
            if (b < 0) return null
            if (b == '\n'.code) {
                val text = line.toString().trimEnd('\r')
                if (text.isEmpty()) break
                lines.add(text)
                line.setLength(0)
            } else if (b != '\r'.code || previous == -1) {
                line.append(b.toChar())
            }
            previous = b
            if (lines.size > 60) return null
        }
        if (lines.isEmpty()) return null
        val parts = lines[0].split(' ')
        if (parts.size < 2) return null
        val headers = HashMap<String, String>()
        for (i in 1 until lines.size) {
            val idx = lines[i].indexOf(':')
            if (idx > 0) headers[lines[i].substring(0, idx).trim().lowercase()] =
                lines[i].substring(idx + 1).trim()
        }
        return Request(parts[0], parts[1], headers)
    }

    private fun respond(socket: Socket, code: Int, status: String, contentType: String, body: String) {
        val bytes = body.toByteArray(Charsets.UTF_8)
        val out = socket.getOutputStream()
        val header = StringBuilder("HTTP/1.1 $code $status\r\n")
        if (contentType.isNotEmpty()) header.append("Content-Type: $contentType\r\n")
        header.append("Content-Length: ${bytes.size}\r\n")
        header.append("Connection: close\r\n\r\n")
        out.write(header.toString().toByteArray(Charsets.US_ASCII))
        out.write(bytes)
        out.flush()
    }

    private fun serveAsset(socket: Socket, name: String, contentType: String) {
        val bytes = assetLoader(name)
        val out = socket.getOutputStream()
        out.write(
            ("HTTP/1.1 200 OK\r\nContent-Type: $contentType\r\n" +
                "Content-Length: ${bytes.size}\r\nCache-Control: no-store\r\n" +
                "Connection: close\r\n\r\n").toByteArray(Charsets.US_ASCII)
        )
        out.write(bytes)
        out.flush()
        socket.close()
    }

    // ------------------------------------------------------------- MPEG-TS

    private fun serveTs(socket: Socket) {
        val out = BufferedOutputStream(socket.getOutputStream(), 64 * 1024)
        out.write(
            ("HTTP/1.1 200 OK\r\nContent-Type: video/mp2t\r\n" +
                "Cache-Control: no-store\r\nConnection: close\r\n\r\n").toByteArray(Charsets.US_ASCII)
        )
        out.flush()
        val client = QueuedClient(socket, out, null)
        if (!hub.addTsClient(client)) {
            socket.close(); return
        }
        client.pump()
        hub.removeClient(client)
    }

    // ----------------------------------------------------------- WebSocket

    private fun serveWebSocket(socket: Socket, input: InputStream, request: Request) {
        val key = request.headers["sec-websocket-key"] ?: run { socket.close(); return }
        val accept = MessageDigest.getInstance("SHA-1")
            .digest((key + WS_GUID).toByteArray(Charsets.US_ASCII))
            .let { base64(it) }

        val out = BufferedOutputStream(socket.getOutputStream(), 256 * 1024)
        out.write(
            ("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n" +
                "Connection: Upgrade\r\nSec-WebSocket-Accept: $accept\r\n\r\n")
                .toByteArray(Charsets.US_ASCII)
        )
        out.flush()

        val client = QueuedClient(socket, out, WsFramer())
        // Eingehende Frames nur mitlesen, damit ein Close sauber erkannt wird.
        Thread({ drainIncoming(input, client) }, "ws-reader").apply { isDaemon = true }.start()
        hub.addBrowserClient(client)
        client.pump()
        hub.removeClient(client)
    }

    private fun drainIncoming(input: InputStream, client: QueuedClient) {
        try {
            while (true) {
                val b0 = input.read()
                if (b0 < 0) break
                val opcode = b0 and 0x0F
                val b1 = input.read()
                if (b1 < 0) break
                val masked = b1 and 0x80 != 0
                var length = (b1 and 0x7F).toLong()
                if (length == 126L) {
                    length = ((input.read() shl 8) or input.read()).toLong()
                } else if (length == 127L) {
                    length = 0
                    repeat(8) { length = (length shl 8) or input.read().toLong() }
                }
                if (masked) repeat(4) { input.read() }
                var skipped = 0L
                while (skipped < length) {
                    val n = input.skip(length - skipped)
                    if (n <= 0) break
                    skipped += n
                }
                if (opcode == 0x8) break // Close
            }
        } catch (_: Exception) {
        } finally {
            client.close()
        }
    }

    /** Baut WebSocket-Rahmen um die Nutzdaten (Server sendet unmaskiert). */
    private class WsFramer {
        fun frame(payload: ByteArray, opcode: Int): ByteArray {
            val header = when {
                payload.size < 126 -> byteArrayOf((0x80 or opcode).toByte(), payload.size.toByte())
                payload.size < 65536 -> byteArrayOf(
                    (0x80 or opcode).toByte(), 126,
                    (payload.size shr 8).toByte(), payload.size.toByte()
                )
                else -> ByteArray(10).also {
                    it[0] = (0x80 or opcode).toByte(); it[1] = 127
                    for (i in 0 until 8) it[9 - i] = ((payload.size.toLong() shr (8 * i)) and 0xFF).toByte()
                }
            }
            return header + payload
        }
    }

    /**
     * Ein Zuschauer mit eigener Sende-Warteschlange. Ein langsamer Fernseher
     * bremst so niemals den Encoder oder die anderen Geraete aus.
     */
    private class QueuedClient(
        private val socket: Socket,
        private val out: OutputStream,
        private val framer: WsFramer?
    ) : StreamHub.MediaClient {

        private val queue = ArrayBlockingQueue<ByteArray>(90)
        @Volatile private var open = true

        override fun enqueue(data: ByteArray): Boolean {
            if (!open) return false
            val payload = framer?.frame(data, 0x2) ?: data
            if (!queue.offer(payload)) {
                // Client haengt hinterher: Rueckstand verwerfen statt aufstauen.
                queue.clear()
                queue.offer(payload)
                return false
            }
            return true
        }

        override fun sendStreamInfo(codec: String, width: Int, height: Int) {
            sendText("""{"type":"init","codec":"$codec","width":$width,"height":$height}""")
        }

        override fun sendReset() = sendText("""{"type":"reset"}""")

        private fun sendText(json: String) {
            val f = framer ?: return
            if (!open) return
            queue.offer(f.frame(json.toByteArray(Charsets.UTF_8), 0x1))
        }

        /** Blockiert, bis die Verbindung endet. */
        fun pump() {
            try {
                while (open) {
                    val chunk = queue.poll(1, TimeUnit.SECONDS) ?: continue
                    out.write(chunk)
                    // Alles direkt hinterherschieben, was schon bereitliegt.
                    while (true) {
                        val more = queue.poll() ?: break
                        out.write(more)
                    }
                    out.flush()
                }
            } catch (e: Exception) {
                Log.d(TAG, "Client weg: ${e.message}")
            } finally {
                close()
            }
        }

        override fun close() {
            open = false
            try { socket.close() } catch (_: IOException) {}
        }
    }

    companion object {
        private const val TAG = "WebServer"
        private const val WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
        private const val B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

        /** Base64 ohne Android-Abhaengigkeit - so bleibt der Handshake testbar. */
        fun base64(data: ByteArray): String {
            val sb = StringBuilder()
            var i = 0
            while (i < data.size) {
                val b0 = data[i].toInt() and 0xFF
                val b1 = if (i + 1 < data.size) data[i + 1].toInt() and 0xFF else 0
                val b2 = if (i + 2 < data.size) data[i + 2].toInt() and 0xFF else 0
                sb.append(B64[b0 shr 2])
                sb.append(B64[((b0 and 0x03) shl 4) or (b1 shr 4)])
                sb.append(if (i + 1 < data.size) B64[((b1 and 0x0F) shl 2) or (b2 shr 6)] else '=')
                sb.append(if (i + 2 < data.size) B64[b2 and 0x3F] else '=')
                i += 3
            }
            return sb.toString()
        }
    }
}
