package de.ypchat.android.data.api

import io.socket.client.IO
import io.socket.client.Socket
import io.socket.engineio.client.transports.Polling
import io.socket.engineio.client.transports.WebSocket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import de.ypchat.android.data.model.ChatMessageDto
import de.ypchat.android.data.model.HistoryItemDto
import de.ypchat.android.data.model.InboxItemDto
import de.ypchat.android.data.model.SocketEvent
import de.ypchat.android.data.model.UserDto
import okhttp3.OkHttpClient
import org.json.JSONArray
import org.json.JSONObject

class SocketClient(
    private val baseUrl: String,
    private val okHttpClient: OkHttpClient
) {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    private val _events = MutableSharedFlow<SocketEvent>(extraBufferCapacity = 64)
    val events: SharedFlow<SocketEvent> = _events

    private var socket: Socket? = null
    private var pendingExpressSessionId: String? = null

    val isConnected: Boolean
        get() = socket?.connected() == true

    fun connect() {
        disconnect()

        val options = IO.Options().apply {
            transports = arrayOf(WebSocket.NAME, Polling.NAME)
            reconnection = true
            reconnectionAttempts = Int.MAX_VALUE
            reconnectionDelay = 1_000
            timeout = 10_000
            callFactory = okHttpClient
            webSocketFactory = okHttpClient
        }

        socket = IO.socket(baseUrl, options).also { s ->
            s.on(Socket.EVENT_CONNECT) {
                pendingExpressSessionId?.let { sessionId ->
                    s.emit("setSessionId", JSONObject().put("expressSessionId", sessionId))
                }
                emit(SocketEvent.ConnectionChanged(true))
            }
            s.on(Socket.EVENT_DISCONNECT) { args ->
                emit(SocketEvent.ConnectionChanged(false, args.firstOrNull()?.toString()))
            }
            s.on(Socket.EVENT_CONNECT_ERROR) { args ->
                emit(SocketEvent.Error("Socket-Verbindung fehlgeschlagen: ${args.firstOrNull()?.toString().orEmpty()}"))
            }

            s.on("connected") { args ->
                args.firstJson()?.let { json ->
                    emit(
                        SocketEvent.Connected(
                            sessionId = json.optStringOrNull("sessionId"),
                            loggedIn = json.optBoolean("loggedIn", false),
                            user = json.optJSONObject("user")?.toUserDto()
                        )
                    )
                }
            }
            s.on("loginSuccess") { args ->
                args.firstJson()?.let { json ->
                    emit(SocketEvent.LoginSuccess(json.optStringOrNull("sessionId"), json.optJSONObject("user")?.toUserDto()))
                }
            }
            s.on("userList") { args ->
                args.firstJson()?.optJSONArray("users")?.let { users ->
                    emit(SocketEvent.UserList(users.toObjectList { it.toUserDto() }))
                }
            }
            s.on("message") { args ->
                args.firstJson()?.let { emit(SocketEvent.IncomingMessage(it.toMessageDto())) }
            }
            s.on("messageSent") { args ->
                args.firstJson()?.let { json ->
                    emit(SocketEvent.MessageSent(json.optStringOrNull("messageId"), json.optStringOrNull("to")))
                }
            }
            s.on("conversation") { args ->
                args.firstJson()?.let { json ->
                    val messages = json.optJSONArray("messages")?.toObjectList { it.toMessageDto() }.orEmpty()
                    emit(SocketEvent.Conversation(json.optString("with"), messages))
                }
            }
            s.on("searchResults") { args ->
                args.firstJson()?.optJSONArray("results")?.let { results ->
                    emit(SocketEvent.SearchResults(results.toObjectList { it.toUserDto() }))
                }
            }
            s.on("historyResults") { args ->
                args.firstJson()?.optJSONArray("results")?.let { results ->
                    emit(SocketEvent.HistoryResults(results.toObjectList { it.toHistoryItemDto() }))
                }
            }
            s.on("inboxResults") { args ->
                args.firstJson()?.optJSONArray("results")?.let { results ->
                    emit(SocketEvent.InboxResults(results.toObjectList { it.toInboxItemDto() }))
                }
            }
            s.on("unreadChats") { args ->
                args.firstJson()?.let { emit(SocketEvent.UnreadChats(it.optInt("count", 0))) }
            }
            s.on("userBlocked") { args ->
                args.firstJson()?.let { emit(SocketEvent.UserBlocked(it.optString("userName"))) }
            }
            s.on("userUnblocked") { args ->
                args.firstJson()?.let { emit(SocketEvent.UserUnblocked(it.optString("userName"))) }
            }
            s.on("commandResult") { args ->
                args.firstJson()?.let { json ->
                    emit(SocketEvent.CommandResult(json.optJSONArray("lines").toStringList(), json.optString("kind", "info")))
                }
            }
            s.on("commandTable") { args ->
                args.firstJson()?.let { json ->
                    emit(
                        SocketEvent.CommandTable(
                            title = json.optString("title", "Ausgabe"),
                            columns = json.optJSONArray("columns").toStringList(),
                            rows = json.optJSONArray("rows").toNestedStringList()
                        )
                    )
                }
            }
            s.on("error") { args ->
                val message = args.firstJson()?.optString("message") ?: args.firstOrNull()?.toString() ?: "Unbekannter Socket-Fehler"
                emit(SocketEvent.Error(message))
            }
            s.connect()
        }
    }

    fun disconnect() {
        socket?.disconnect()
        socket?.off()
        socket = null
    }

    fun setSessionId(expressSessionId: String) {
        pendingExpressSessionId = expressSessionId
        socket?.takeIf { it.connected() }?.emit("setSessionId", JSONObject().put("expressSessionId", expressSessionId))
    }

    fun login(userName: String, gender: String, age: Int, country: String, expressSessionId: String?) {
        socket?.emit(
            "login",
            JSONObject()
                .put("userName", userName)
                .put("gender", gender)
                .put("age", age)
                .put("country", country)
                .put("expressSessionId", expressSessionId)
        )
    }

    fun sendMessage(toUserName: String?, message: String, messageId: String = System.currentTimeMillis().toString()) {
        val payload = JSONObject()
            .put("message", message.trim())
            .put("messageId", messageId)

        if (!toUserName.isNullOrBlank()) {
            payload.put("toUserName", toUserName)
        }

        socket?.emit("message", payload)
    }

    fun sendImage(toUserName: String, imageCode: String, imageUrl: String, messageId: String = System.currentTimeMillis().toString()) {
        socket?.emit(
            "message",
            JSONObject()
                .put("toUserName", toUserName)
                .put("message", imageCode)
                .put("messageId", messageId)
                .put("isImage", true)
                .put("imageUrl", imageUrl)
        )
    }

    fun requestConversation(withUserName: String) {
        socket?.emit("requestConversation", JSONObject().put("withUserName", withUserName))
    }

    fun userSearch(nameIncludes: String?, minAge: Int?, maxAge: Int?, countries: List<String>, genders: List<String>) {
        socket?.emit(
            "userSearch",
            JSONObject()
                .put("nameIncludes", nameIncludes)
                .put("minAge", minAge)
                .put("maxAge", maxAge)
                .put("countries", JSONArray(countries))
                .put("genders", JSONArray(genders))
        )
    }

    fun requestHistory() = socket?.emit("requestHistory")
    fun requestOpenConversations() = socket?.emit("requestOpenConversations")
    fun blockUser(userName: String) = socket?.emit("blockUser", JSONObject().put("userName", userName))
    fun unblockUser(userName: String) = socket?.emit("unblockUser", JSONObject().put("userName", userName))

    private fun emit(event: SocketEvent) {
        scope.launch { _events.emit(event) }
    }
}

private fun Array<out Any>.firstJson(): JSONObject? = firstOrNull() as? JSONObject

private fun JSONObject.optStringOrNull(name: String): String? = if (has(name) && !isNull(name)) optString(name) else null

private fun JSONObject.toUserDto(): UserDto = UserDto(
    sessionId = optStringOrNull("sessionId"),
    userName = optString("userName"),
    gender = optString("gender"),
    age = optInt("age", 0),
    country = optString("country"),
    isoCountryCode = optString("isoCountryCode")
)

private fun JSONObject.toMessageDto(): ChatMessageDto = ChatMessageDto(
    from = optString("from"),
    to = optStringOrNull("to"),
    message = optString("message"),
    messageId = optStringOrNull("messageId"),
    timestamp = optString("timestamp"),
    read = optBoolean("read", false),
    isImage = optBoolean("isImage", false),
    imageType = optStringOrNull("imageType"),
    imageUrl = optStringOrNull("imageUrl"),
    imageCode = optStringOrNull("imageCode")
)

private fun JSONObject.toHistoryItemDto(): HistoryItemDto = HistoryItemDto(
    userName = optString("userName"),
    lastMessage = optJSONObject("lastMessage")?.toMessageDto()
)

private fun JSONObject.toInboxItemDto(): InboxItemDto = InboxItemDto(
    userName = optString("userName"),
    unreadCount = optInt("unreadCount", 0)
)

private fun JSONArray?.toStringList(): List<String> {
    if (this == null) return emptyList()
    return List(length()) { index -> opt(index)?.toString().orEmpty() }
}

private fun JSONArray?.toNestedStringList(): List<List<String>> {
    if (this == null) return emptyList()
    return List(length()) { index -> optJSONArray(index).toStringList() }
}

private fun <T> JSONArray.toObjectList(mapper: (JSONObject) -> T): List<T> = buildList {
    for (index in 0 until length()) {
        optJSONObject(index)?.let { add(mapper(it)) }
    }
}
