package com.peterino.blastoise

import android.content.Context
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit

class MusicRoomClient(
    private val context: Context,
    private val listener: Listener,
) {
    interface Listener {
        fun onStatus(message: String)
        fun onDebug(message: String)
        fun onChannels(channels: List<ChannelInfo>)
        fun onLibrary(tracks: List<Track>)
        fun onPlaylists(playlists: PlaylistBundle)
        fun onPlaylistDetail(playlist: Playlist)
        fun onChannelState(state: ChannelState)
        fun onSwitched(channelId: String)
        fun onAuth(user: UserSession?)
        fun onError(message: String)
    }

    private val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(0, TimeUnit.SECONDS)
        .build()

    private var socket: WebSocket? = null

    fun close() {
        socket?.close(1000, "closing")
        socket = null
    }

    fun login(username: String, password: String) {
        listener.onDebug("POST /api/auth/login")
        val body = JSONObject()
            .put("username", username)
            .put("password", password)
            .toString()
            .toRequestBody("application/json".toMediaType())

        val request = request("/api/auth/login")
            .post(body)
            .build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("login failure: ${e.message ?: "network error"}")
                listener.onError("Login failed: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    listener.onDebug("login HTTP ${it.code}; cookie=${SessionStore.cookieHeader.isNotBlank()}")
                    if (!it.isSuccessful) {
                        listener.onError("Login failed: HTTP ${it.code}")
                        return
                    }
                    val session = JSONObject(it.body.string()).toUserSession()
                    listener.onAuth(session)
                    SessionStore.save(context)
                    listener.onStatus("Signed in")
                    loadMe()
                }
            }
        })
    }

    fun signup(username: String, password: String) {
        listener.onDebug("POST /api/auth/signup")
        val body = JSONObject()
            .put("username", username)
            .put("password", password)
            .toString()
            .toRequestBody("application/json".toMediaType())

        val request = request("/api/auth/signup")
            .post(body)
            .build()

        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("signup failure: ${e.message ?: "network error"}")
                listener.onError("Signup failed: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    listener.onDebug("signup HTTP ${it.code}; cookie=${SessionStore.cookieHeader.isNotBlank()}")
                    if (!it.isSuccessful) {
                        listener.onError("Signup failed: HTTP ${it.code}")
                        return
                    }
                    val session = JSONObject(it.body.string()).toUserSession()
                    listener.onAuth(session)
                    SessionStore.save(context)
                    listener.onStatus("Signed up")
                    loadMe()
                }
            }
        })
    }

    fun logout() {
        listener.onDebug("POST /api/auth/logout")
        val request = request("/api/auth/logout")
            .post("{}".toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("logout failure: ${e.message ?: "network error"}")
                SessionStore.cookieHeader = ""
                SessionStore.save(context)
                listener.onAuth(null)
                listener.onStatus("Signed out")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    SessionStore.cookieHeader = ""
                    SessionStore.save(context)
                    listener.onDebug("logout HTTP ${it.code}")
                    listener.onAuth(null)
                    listener.onStatus("Signed out")
                }
            }
        })
    }

    fun loadMe() {
        listener.onDebug("GET /api/auth/me")
        val request = request("/api/auth/me").build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("me failure: ${e.message ?: "network error"}")
                listener.onError("Auth check failed: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    listener.onDebug("me HTTP ${it.code}; cookie=${SessionStore.cookieHeader.isNotBlank()}")
                    if (!it.isSuccessful) {
                        SessionStore.cookieHeader = ""
                        SessionStore.save(context)
                        listener.onAuth(null)
                        listener.onStatus("Sign in required")
                        return
                    }
                    val body = JSONObject(it.body.string())
                    val user = body.optJSONObject("user")
                    if (user == null) {
                        SessionStore.cookieHeader = ""
                        listener.onAuth(null)
                        listener.onStatus("Sign in required")
                    } else {
                        listener.onAuth(body.toUserSession())
                    }
                    SessionStore.save(context)
                }
            }
        })
    }

    fun loadChannels() {
        listener.onDebug("GET /api/channels ${SessionStore.serverBaseUrl}")
        val request = request("/api/channels").build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("channels failure: ${e.message ?: "network error"}")
                listener.onError("Could not load channels: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    SessionStore.save(context)
                    listener.onDebug("channels HTTP ${it.code}; cookie=${SessionStore.cookieHeader.isNotBlank()}")
                    if (!it.isSuccessful) {
                        listener.onError("Could not load channels: HTTP ${it.code}")
                        return
                    }
                    val json = it.body.string()
                    val channels = JSONArray(json).toChannels()
                    listener.onDebug("channels parsed: ${channels.size}")
                    listener.onChannels(channels)
                    listener.onStatus("Loaded ${channels.size} channel(s)")
                }
            }
        })
    }

    fun loadLibrary() {
        listener.onDebug("GET /api/library")
        val request = request("/api/library").build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("library failure: ${e.message ?: "network error"}")
                listener.onError("Could not load library: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    SessionStore.save(context)
                    listener.onDebug("library HTTP ${it.code}; cookie=${SessionStore.cookieHeader.isNotBlank()}")
                    if (!it.isSuccessful) {
                        listener.onError("Could not load library: HTTP ${it.code}")
                        return
                    }
                    val tracks = JSONArray(it.body.string()).toTracks()
                    listener.onDebug("library parsed: ${tracks.size}")
                    listener.onLibrary(tracks)
                }
            }
        })
    }

    fun loadPlaylists() {
        listener.onDebug("GET /api/playlists")
        val request = request("/api/playlists").build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("playlists failure: ${e.message ?: "network error"}")
                listener.onError("Could not load playlists: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    SessionStore.save(context)
                    listener.onDebug("playlists HTTP ${it.code}; cookie=${SessionStore.cookieHeader.isNotBlank()}")
                    if (!it.isSuccessful) {
                        listener.onError("Could not load playlists: HTTP ${it.code}")
                        return
                    }
                    val json = JSONObject(it.body.string())
                    val bundle = PlaylistBundle(
                        mine = json.optJSONArray("mine").toPlaylists(),
                        shared = json.optJSONArray("shared").toPlaylists(),
                    )
                    listener.onDebug("playlists parsed: mine=${bundle.mine.size} shared=${bundle.shared.size}")
                    listener.onPlaylists(bundle)
                }
            }
        })
    }

    fun loadPlaylist(playlistId: String) {
        listener.onDebug("GET /api/playlists/$playlistId")
        val request = request("/api/playlists/$playlistId").build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("playlist detail failure: ${e.message ?: "network error"}")
                listener.onError("Could not load playlist: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    captureCookie(it)
                    SessionStore.save(context)
                    listener.onDebug("playlist detail HTTP ${it.code}")
                    if (!it.isSuccessful) {
                        listener.onError("Could not load playlist: HTTP ${it.code}")
                        return
                    }
                    listener.onPlaylistDetail(JSONObject(it.body.string()).toPlaylist())
                }
            }
        })
    }

    fun connectChannel(channelId: String) {
        socket?.close(1000, "switching")
        listener.onDebug("WS connecting channel=$channelId")
        val builder = Request.Builder()
            .url(SessionStore.wsUrl(channelId))
            .header("User-Agent", SessionStore.userAgent)
        if (SessionStore.cookieHeader.isNotBlank()) {
            builder.header("Cookie", SessionStore.cookieHeader)
        }

        socket = client.newWebSocket(builder.build(), object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                listener.onDebug("WS open HTTP ${response.code}")
                listener.onStatus("Connected")
                loadChannelState(channelId)
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                handleSocketMessage(text)
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                listener.onDebug("WS failure ${response?.code ?: "-"}: ${t.message ?: "websocket error"}")
                listener.onError("Disconnected: ${t.message ?: "websocket error"}")
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                listener.onDebug("WS closed $code $reason")
                listener.onStatus("Disconnected")
            }
        })
    }

    fun sendAction(action: String) {
        listener.onDebug("WS send action=$action")
        socket?.send(JSONObject().put("action", action).toString())
    }

    fun sendSeek(timestamp: Double) {
        listener.onDebug("WS send seek=${"%.1f".format(timestamp)}")
        socket?.send(
            JSONObject()
                .put("action", "seek")
                .put("timestamp", timestamp)
                .toString()
        )
    }

    fun sendJump(index: Int) {
        listener.onDebug("WS send jump=$index")
        socket?.send(
            JSONObject()
                .put("action", "jump")
                .put("index", index)
                .toString()
        )
    }

    fun setPlaybackMode(channelId: String, mode: String) {
        listener.onDebug("POST /api/channels/$channelId/mode $mode")
        val body = JSONObject()
            .put("mode", mode)
            .toString()
            .toRequestBody("application/json".toMediaType())
        val request = request("/api/channels/$channelId/mode")
            .post(body)
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("mode failure: ${e.message ?: "network error"}")
                listener.onError("Mode failed: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    listener.onDebug("mode HTTP ${it.code}")
                    if (it.code == 403) {
                        listener.onError("Mode denied")
                    } else if (!it.isSuccessful) {
                        listener.onError("Mode failed: HTTP ${it.code}")
                    } else {
                        listener.onStatus("Mode set")
                    }
                }
            }
        })
    }

    fun addTracksToQueue(channelId: String, trackIds: List<String>, insertAt: Int? = null) {
        if (trackIds.isEmpty()) return
        val insertLabel = insertAt?.let { " insertAt=$it" } ?: ""
        listener.onDebug("PATCH /api/channels/$channelId/queue add=${trackIds.size}$insertLabel")
        val bodyJson = JSONObject().put("add", JSONArray(trackIds))
        if (insertAt != null) bodyJson.put("insertAt", insertAt)
        val request = request("/api/channels/$channelId/queue")
            .patch(bodyJson.toString().toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("queue add failure: ${e.message ?: "network error"}")
                listener.onError("Queue failed: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    listener.onDebug("queue add HTTP ${it.code}")
                    if (it.code == 403) {
                        listener.onError("Queue denied")
                    } else if (!it.isSuccessful) {
                        listener.onError("Queue failed: HTTP ${it.code}")
                    } else {
                        listener.onStatus(if (insertAt == null) "Queued ${trackIds.size}" else "Play next queued")
                        currentChannelFromQueuePatch(channelId)
                    }
                }
            }
        })
    }

    fun removeTracksFromQueue(channelId: String, indices: List<Int>) {
        if (indices.isEmpty()) return
        listener.onDebug("PATCH /api/channels/$channelId/queue remove=${indices.size}")
        val request = request("/api/channels/$channelId/queue")
            .patch(
                JSONObject()
                    .put("remove", JSONArray(indices))
                    .toString()
                    .toRequestBody("application/json".toMediaType())
            )
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("queue remove failure: ${e.message ?: "network error"}")
                listener.onError("Queue remove failed: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    listener.onDebug("queue remove HTTP ${it.code}")
                    if (it.code == 403) {
                        listener.onError("Queue denied")
                    } else if (!it.isSuccessful) {
                        listener.onError("Queue remove failed: HTTP ${it.code}")
                    } else {
                        listener.onStatus("Removed from queue")
                        currentChannelFromQueuePatch(channelId)
                    }
                }
            }
        })
    }

    private fun loadChannelState(channelId: String) {
        listener.onDebug("GET /api/channels/$channelId")
        val request = request("/api/channels/$channelId").build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                listener.onDebug("channel state failure: ${e.message ?: "network error"}")
            }

            override fun onResponse(call: Call, response: Response) {
                response.use {
                    if (!it.isSuccessful) {
                        listener.onDebug("channel state HTTP ${it.code}")
                        return
                    }
                    val json = JSONObject(it.body.string())
                    listener.onDebug("channel state HTTP ${it.code}; queue=${json.optJSONArray("queue")?.length() ?: 0}")
                    listener.onChannelState(json.toChannelState())
                }
            }
        })
    }

    private fun currentChannelFromQueuePatch(channelId: String) {
        loadChannelState(channelId)
        loadChannels()
    }

    private fun handleSocketMessage(text: String) {
        val json = JSONObject(text)
        val type = json.optString("type", "")
        val summary = when {
            type.isNotBlank() -> "type=$type"
            json.has("track") -> "state track=${json.optJSONObject("track")?.optString("title", "unknown") ?: "none"}"
            else -> "state"
        }
        listener.onDebug("WS message $summary")

        when (type) {
            "" -> listener.onChannelState(json.toChannelState())
            "channel_list" -> {
                val channels = json.optJSONArray("channels")?.toChannels().orEmpty()
                listener.onChannels(channels)
            }
            "switched" -> listener.onSwitched(json.optString("channelId"))
            "kick" -> listener.onError("Disconnected: ${json.optString("reason", "kicked")}")
            "error" -> listener.onError(json.optString("message", "Server error"))
            else -> listener.onDebug("ignored server event type=$type")
        }
    }

    private fun request(path: String): Request.Builder {
        val builder = Request.Builder()
            .url(SessionStore.httpUrl(path))
            .header("User-Agent", SessionStore.userAgent)
        if (SessionStore.cookieHeader.isNotBlank()) {
            builder.header("Cookie", SessionStore.cookieHeader)
        }
        return builder
    }

    private fun captureCookie(response: Response) {
        response.headers("Set-Cookie")
            .map { it.substringBefore(";") }
            .firstOrNull { it.startsWith("blastoise_session=") }
            ?.let {
                SessionStore.cookieHeader = if (it == "blastoise_session=") "" else it
            }
    }
}
