package net.buzzert.kordophone.backend

import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.buzzert.kordophone.backend.model.Conversation
import net.buzzert.kordophone.backend.model.GUID
import net.buzzert.kordophone.backend.model.Message
import net.buzzert.kordophone.backend.model.UpdateItem
import net.buzzert.kordophone.backend.server.APIClient
import net.buzzert.kordophone.backend.server.APIInterface
import net.buzzert.kordophone.backend.server.AuthenticationRequest
import net.buzzert.kordophone.backend.server.AuthenticationResponse
import net.buzzert.kordophone.backend.server.SendMessageRequest
import net.buzzert.kordophone.backend.server.SendMessageResponse
import net.buzzert.kordophone.backend.server.UploadAttachmentResponse
import net.buzzert.kordophone.backend.server.authenticatedWebSocketURL
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import retrofit2.Response
import java.util.Date
import java.util.UUID

@OptIn(ExperimentalStdlibApi::class)
class MockServer {
    val version = "Kordophone-2.0"
    val conversations: MutableList<Conversation> = mutableListOf()
    val updateFlow: Flow<UpdateItem> get() = _updateFlow
    var updateMessageSequence: Int = 0

    private val messages: MutableMap<String, MutableList<Message>> = mutableMapOf()
    private val _updateFlow: MutableSharedFlow<UpdateItem> = MutableSharedFlow()

    private val client = MockServerClient(this)

    companion object {
        fun generateMessage(parentConversation: Conversation): Message {
            return Message(
                date = Date(),
                text = "This is a test!",
                guid = UUID.randomUUID().toString(),
                sender = null,
                conversation = parentConversation,
                attachmentGUIDs = null,
            )
        }

        fun generateConversation(): Conversation {
            return Conversation(
                date = Date(),
                participants = listOf("james@magahern.com"),
                displayName = null,
                unreadCount = 0,
                lastMessagePreview = null,
                lastMessage = null,
                guid = UUID.randomUUID().toString(),
                lastFetchedMessageGUID = null,
            )
        }
    }

    fun getServer(): MockWebServer = MockWebServer()
    fun getClient(): MockServerClient = client
    fun getAPIInterface(): APIInterface = MockServerClient(this).getAPIInterface()

    fun addConversation(conversation: Conversation) {
        conversations.add(conversation)
        messages[conversation.guid] = mutableListOf()

        runBlocking {
            _updateFlow.emit(UpdateItem(
                sequence = updateMessageSequence++,
                conversationChanged = conversation
            ))
        }
    }

    fun updateConversation(conversation: Conversation) {
        conversations.removeAll { it.guid == conversation.guid }
        addConversation(conversation)
    }

    fun addMessagesToConversation(conversation: Conversation, messages: List<Message>) {
        val guid = conversation.guid
        this.messages[guid]?.addAll(messages)
        conversation.lastMessage = messages.last()
        conversation.lastMessagePreview = messages.last().text

        runBlocking {
            for (message in messages) {
                _updateFlow.emit(
                    UpdateItem(
                        sequence = updateMessageSequence++,
                        conversationChanged = conversation,
                        messageAdded = message
                    )
                )
            }
        }
    }

    fun addTestConversations(count: Int): List<Conversation> {
        val testConversations = ArrayList<Conversation>()
        for (i in 0..<count) {
            val conversation = MockServer.generateConversation()
            testConversations.add(conversation)
            addConversation(conversation)
        }

        return testConversations
    }

    fun addTestMessages(count: Int, conversation: Conversation): List<Message> {
        val testMessages = ArrayList<Message>()
        for (i in 0..<count) {
            val message = MockServer.generateMessage(conversation)
            testMessages.add(message)
        }

        addMessagesToConversation(conversation, testMessages)
        return testMessages
    }

    fun markConversationAsRead(guid: GUID) {
        val conversation = conversations.first { it.guid == guid }
        conversation.unreadCount = 0

        updateConversation(conversation)
    }

    internal fun getMessagesForConversationGUID(guid: GUID): List<Message>? {
        return messages[guid]?.toList()
    }

    internal fun sendMessage(body: String, toConversationGUID: GUID): Message {
        val conversation = conversations.first { it.guid == toConversationGUID }

        val message = Message(
            text = body,
            date = Date(),
            guid = UUID.randomUUID().toString(),
            sender = null, // me
            conversation = conversation,
            attachmentGUIDs = null,
        )

        addMessagesToConversation(conversation, listOf(message))
        return message
    }
}

class MockServerClient(private val server: MockServer): APIClient, WebSocketListener() {
    private var updateWebSocket: WebSocket? = null
    private var updateWatchJob: Job? = null
    private val gson: Gson = Gson()

    override val isConfigured: Boolean = true

    override fun getAPIInterface(): APIInterface {
        return MockServerInterface(server)
    }

    override fun getWebSocketClient(
        serverPath: String,
        queryParams: Map<String, String>?,
        listener: WebSocketListener
    ): WebSocket {
        val webServer = server.getServer()

        val params = queryParams ?: mapOf()
        val baseHTTPURL: HttpUrl = webServer.url("/")
        val baseURL = baseHTTPURL.toUrl()
        val requestURL = baseURL.authenticatedWebSocketURL(serverPath, params)
        val request = Request.Builder()
            .url(requestURL)
            .build()

        webServer.enqueue(MockResponse().withWebSocketUpgrade(this))

        if (this.updateWatchJob == null) {
            CoroutineScope(Job()).launch {
                startWatchingForUpdates(this)
            }
        }

        return OkHttpClient().newWebSocket(request, listener)
    }

    private fun startWatchingForUpdates(scope: CoroutineScope) {
        this.updateWatchJob = scope.launch {
            server.updateFlow.collect {
                println("Mock WebSocket is sending a message")

                // Encode to JSON and send to websocket
                val updateItems = listOf(it)
                val encodedUpdateItem = gson.toJson(updateItems)
                updateWebSocket?.send(encodedUpdateItem)
            }
        }
    }

    fun stopWatchingForUpdates() = runBlocking {
        updateWatchJob?.cancelAndJoin()
    }

    override fun onOpen(webSocket: WebSocket, response: okhttp3.Response) {
        super.onOpen(webSocket, response)

        println("Mock WebSocket opened.")
        this.updateWebSocket = webSocket
    }

    override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
        super.onClosed(webSocket, code, reason)

        println("Mock WebSocket closed.")
        this.updateWebSocket = null
    }
}

class MockServerInterface(private val server: MockServer): APIInterface {
    override suspend fun getVersion(): ResponseBody {
        return server.version.toResponseBody("text/plain".toMediaType())
    }

    override suspend fun getConversations(): Response<List<Conversation>> {
        return Response.success(server.conversations)
    }

    override suspend fun getMessages(
        conversationGUID: String,
        limit: Int?,
        beforeMessageGUID: GUID?,
        afterMessageGUID: GUID?
    ): Response<List<Message>> {
        val messages = server.getMessagesForConversationGUID(conversationGUID)

        return if (messages != null) {
            Response.success(messages)
        } else {
            Response.error(500, "GUID not found".toResponseBody())
        }
    }

    override suspend fun sendMessage(request: SendMessageRequest): Response<SendMessageResponse> {
        val message = server.sendMessage(request.body, request.conversationGUID)

        val response = SendMessageResponse(message.guid)
        return Response.success(response)
    }

    override suspend fun markConversation(conversationGUID: String): Response<Void> {
        server.markConversationAsRead(conversationGUID)
        return Response.success(null)
    }

    override suspend fun fetchAttachment(guid: String, preview: Boolean): ResponseBody                                           {
        TODO("Not yet implemented")
    }

    override suspend fun uploadAttachment(
        filename: String,
        body: RequestBody
    ): Response<UploadAttachmentResponse> {
        TODO("Not yet implemented")
    }

    override suspend fun authenticate(request: AuthenticationRequest): Response<AuthenticationResponse> {
        // Anything goes!
        val response = AuthenticationResponse(
                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
                "eyJ1c2VybmFtZSI6InRlc3QiLCJleHAiOjE3MDk3OTQ5NjB9." +
                "82UcI1gB4eARmgrKwAY6JnbEdWLXou1GWp29scnUhi8"
        )

        return Response.success(response)
    }
}