package com.omixlab.lckcontrol.p2p.webrtc

import android.content.Context
import android.util.Log
import com.omixlab.lckcontrol.R
import com.omixlab.lckcontrol.data.local.TokenStore
import com.omixlab.lckcontrol.p2p.discovery.NsdAdvertiser
import com.omixlab.lckcontrol.p2p.discovery.P2pPreferences
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.Socket
import java.security.KeyStore
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLServerSocket

@JsonClass(generateAdapter = true)
data class PairRequestBody(val token: String)

@JsonClass(generateAdapter = true)
data class PairResponseBody(val nonce: String, val deviceId: String, val deviceName: String)

@JsonClass(generateAdapter = true)
data class AuthPairResponseBody(val code: String, val deviceId: String, val deviceName: String)

@JsonClass(generateAdapter = true)
data class OfferRequestBody(
    val sdp: String,
    val type: String,
    val iceCandidates: List<IceCandidateDto>,
    val nonce: String,
)

@JsonClass(generateAdapter = true)
data class OfferResponseBody(
    val sdp: String,
    val type: String,
    val iceCandidates: List<IceCandidateDto>,
)

@JsonClass(generateAdapter = true)
data class IceCandidateDto(
    val sdpMid: String,
    val sdpMLineIndex: Int,
    val sdp: String,
)

@Singleton
class LanSignalingServer @Inject constructor(
    @ApplicationContext private val context: Context,
    private val tokenStore: TokenStore,
    private val p2pPreferences: P2pPreferences,
    private val moshi: Moshi,
    private val httpClient: OkHttpClient,
) {
    companion object {
        private const val TAG = "LanSignalingServer"
        private const val BASE_URL = "https://lck.omigame.dev"
        private const val KEYSTORE_PASSWORD = "lckcontrol"
    }

    private var serverSocket: SSLServerSocket? = null
    private var serverJob: Job? = null
    private val validNonces = mutableMapOf<String, Long>() // nonce -> expiry timestamp

    var onOfferReceived: ((String, List<IceCandidateDto>, String) -> OfferResponseBody?)? = null

    private fun createSslContext(): SSLContext {
        val keyStore = KeyStore.getInstance("PKCS12")
        context.resources.openRawResource(R.raw.lck_lan).use { stream ->
            keyStore.load(stream, KEYSTORE_PASSWORD.toCharArray())
        }
        val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
        kmf.init(keyStore, KEYSTORE_PASSWORD.toCharArray())
        val sslContext = SSLContext.getInstance("TLS")
        sslContext.init(kmf.keyManagers, null, null)
        return sslContext
    }

    fun start(scope: CoroutineScope) {
        if (serverJob != null) return

        serverJob = scope.launch(Dispatchers.IO) {
            try {
                val sslContext = createSslContext()
                serverSocket = sslContext.serverSocketFactory
                    .createServerSocket(NsdAdvertiser.PORT) as SSLServerSocket
                Log.d(TAG, "LAN TLS server started on port ${NsdAdvertiser.PORT}")

                while (isActive) {
                    val socket = serverSocket?.accept() ?: break
                    launch { handleConnection(socket) }
                }
            } catch (e: Exception) {
                if (isActive) Log.e(TAG, "Server error", e)
            }
        }
    }

    fun stop() {
        serverJob?.cancel()
        serverJob = null
        serverSocket?.close()
        serverSocket = null
        validNonces.clear()
    }

    private fun handleConnection(socket: Socket) {
        try {
            val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
            val writer = OutputStreamWriter(socket.getOutputStream())

            // Read HTTP request
            val requestLine = reader.readLine() ?: return
            val headers = mutableMapOf<String, String>()
            var line = reader.readLine()
            while (line != null && line.isNotEmpty()) {
                val colonIdx = line.indexOf(':')
                if (colonIdx > 0) {
                    headers[line.substring(0, colonIdx).trim().lowercase()] =
                        line.substring(colonIdx + 1).trim()
                }
                line = reader.readLine()
            }

            val contentLength = headers["content-length"]?.toIntOrNull() ?: 0
            val body = if (contentLength > 0) {
                val chars = CharArray(contentLength)
                reader.read(chars)
                String(chars)
            } else ""

            val parts = requestLine.split(" ")
            val method = parts.getOrNull(0) ?: ""
            val path = parts.getOrNull(1) ?: ""

            val (status, responseBody) = when {
                method == "POST" && path == "/pair" -> handlePair(body)
                method == "POST" && path == "/auth-pair" -> handleAuthPair()
                method == "POST" && path == "/offer" -> handleOffer(body)
                method == "GET" && path == "/status" -> 200 to """{"status":"ok","deviceId":"${p2pPreferences.stableDeviceId}"}"""
                else -> 404 to """{"error":"not found"}"""
            }

            writer.write("HTTP/1.1 $status OK\r\n")
            writer.write("Content-Type: application/json\r\n")
            writer.write("Content-Length: ${responseBody.length}\r\n")
            writer.write("\r\n")
            writer.write(responseBody)
            writer.flush()
        } catch (e: Exception) {
            Log.e(TAG, "Connection error", e)
        } finally {
            socket.close()
        }
    }

    private fun handleAuthPair(): Pair<Int, String> {
        return try {
            if (!tokenStore.isLoggedIn()) {
                return 401 to """{"error":"quest not authenticated"}"""
            }

            // Use OkHttp (has AuthInterceptor for auto-refresh) to generate pairing code
            val request = Request.Builder()
                .url("$BASE_URL/auth/pairing/generate")
                .post("{}".toRequestBody("application/json".toMediaType()))
                .build()

            val resp = httpClient.newCall(request).execute()
            val respBody = resp.body?.string() ?: ""

            if (!resp.isSuccessful) {
                Log.e(TAG, "Backend pairing generate failed: ${resp.code} $respBody")
                return 502 to """{"error":"failed to generate code"}"""
            }

            // Parse code from backend response {"code":"123456","expiresAt":"..."}
            val codeMatch = Regex(""""code"\s*:\s*"(\d+)"""").find(respBody)
            val code = codeMatch?.groupValues?.get(1)
                ?: return 500 to """{"error":"could not parse code"}"""

            val response = AuthPairResponseBody(
                code = code,
                deviceId = p2pPreferences.stableDeviceId,
                deviceName = android.os.Build.MODEL,
            )
            200 to moshi.adapter(AuthPairResponseBody::class.java).toJson(response)
        } catch (e: Exception) {
            Log.e(TAG, "Auth-pair error", e)
            500 to """{"error":"internal error"}"""
        }
    }

    private fun handlePair(body: String): Pair<Int, String> {
        return try {
            val request = moshi.adapter(PairRequestBody::class.java).fromJson(body)
                ?: return 400 to """{"error":"invalid body"}"""

            // Validate that the JWT belongs to the same user
            // For now, accept any valid-looking JWT
            if (request.token.split(".").size != 3) {
                return 401 to """{"error":"invalid token"}"""
            }

            val nonce = UUID.randomUUID().toString()
            validNonces[nonce] = System.currentTimeMillis() + 300_000 // 5 min expiry

            val response = PairResponseBody(
                nonce = nonce,
                deviceId = p2pPreferences.stableDeviceId,
                deviceName = android.os.Build.MODEL,
            )
            200 to moshi.adapter(PairResponseBody::class.java).toJson(response)
        } catch (e: Exception) {
            Log.e(TAG, "Pair error", e)
            500 to """{"error":"internal error"}"""
        }
    }

    private fun handleOffer(body: String): Pair<Int, String> {
        return try {
            val request = moshi.adapter(OfferRequestBody::class.java).fromJson(body)
                ?: return 400 to """{"error":"invalid body"}"""

            // Validate nonce
            val expiry = validNonces[request.nonce]
            if (expiry == null || System.currentTimeMillis() > expiry) {
                return 401 to """{"error":"invalid or expired nonce"}"""
            }
            validNonces.remove(request.nonce)

            val response = onOfferReceived?.invoke(request.sdp, request.iceCandidates, request.nonce)
                ?: return 500 to """{"error":"peer not ready"}"""

            200 to moshi.adapter(OfferResponseBody::class.java).toJson(response)
        } catch (e: Exception) {
            Log.e(TAG, "Offer error", e)
            500 to """{"error":"internal error"}"""
        }
    }
}
