package zello

import com.fasterxml.jackson.module.kotlin.contains
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import somecodes.Codes
import java.lang.Exception
import java.net.URI
import java.nio.ByteBuffer
import java.util.function.BiConsumer

@Suppress("unused")
/**
 * ZelloClient is a WebSocket client for connecting to Zello services.
 * [Source](https://github.com/zelloptt/zello-channel-api/blob/master/API.md)
 * @param address the WebSocket address of the Zello server, e.g. "wss://zello.io/ws"
 * @param username the username for Zello authentication
 * @param password the password for Zello authentication
 * @param channel the default channel to join after connecting
 *
 */
class ZelloClient(val address : URI, val username: String, val password: String, val channel: String="GtcDev2025") {
    private val streamJob = HashMap<Int, ZelloAudioJob>()
    private val imageJob = HashMap<Int, ZelloImageJob>()
    private val commandJob = HashMap<Int, Any>()
    companion object {
        fun fromConsumerZello(username : String, password: String, channel: String) : ZelloClient {
            return ZelloClient(URI.create("wss://zello.io/ws"), username, password, channel)
        }

        fun fromZelloWork(username: String, password: String, channel: String, networkName : String) : ZelloClient{
            return ZelloClient(URI.create("wss://zellowork.io/ws/$networkName"), username, password, channel)
        }

        fun fromZelloEnterpriseServer(username: String, password: String, channel: String, serverDomain: String) : ZelloClient{
            return ZelloClient(URI.create("wss://$serverDomain/ws/mesh"), username, password, channel)
        }
    }

    private var client : WebSocketClient? = null
    private var seqID : Int = 0
    // this key is temporary, valid only 30 days from 2025-07-29
    // if need to create, from https://developers.zello.com/keys
    private val developerKey : String  = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJXa002Y21ScllYSjBiMjV2T2pFLi1yYjJ2THFRbUhYV3dKY2I2azl2TDdUMEtzRWZMRjcxZm5jcktTZ0s2ZE0iLCJleHAiOjE3NTY0MzIyMTIsImF6cCI6ImRldiJ9.ANK7BIS6WVVWsQRjcZXyGWrV2RodCUQD4WXWaA6E4Dlyy8bBCMFdbiKN2D7B_x729HQULailnfRhbXF4Avfg14qONdc1XE_0iGiPUO1kfUSgdd11QylOzjxy6FTKSeZmHOh65JZq2dIWxobCcva-RPvbR8TA656upHh32xrWv9zlU0N707FTca04kze0Iq-q-uC5EL82yK10FEvOPDX88MYy71QRYi8Qh_KbSyMcYAhe2bTsiyjm51ZH9ntkRHd0HNiaijNZI6-qXkkp5Soqmzh-bTtbbgmbX4BT3Qpz_IP3epaX3jl_Aq5DHxXwCsJ9FThif9um5D0TWVGQteR0cQ"
    // refresh token for the session
    // this is set after the first LogonReply
    private var refresh_token: String? = null

    private val logger : Logger = LoggerFactory.getLogger(ZelloClient::class.java)
    private val mapper = jacksonObjectMapper()

    var currentChannel: String? = null ; private set
    var isOnline: Boolean = false ; private set
    var isReceivingStreaming: Boolean = false
    var receivingFrom: String? = null; private set
    var bytesReceived: Long = 0; private set

    fun Start(event: ZelloEvent){
        client = object : WebSocketClient(address) {
            override fun onOpen(handshakedata: ServerHandshake?) {
                //logger.info("Connected to $address")
                isOnline = false
                currentChannel = null
                isReceivingStreaming = false
                seqID = 0
                inc_seqID()
                val lg = LogonCommand.create(seqID,arrayOf(channel), developerKey, username, password)
                val value = mapper.writeValueAsString(lg)
                send(value)
            }


            override fun onMessage(bytes: ByteBuffer?) {
                val receivedbytes = bytes?.capacity() ?: 0
                //logger.info("Binary message received, length: $receivedbytes")
                if (receivedbytes>0){
                    when(bytes?.get(0)){
                        0x01.toByte() -> {
                            // audio stream
                            // format {type(8) = 0x01, stream_id(32), packet_id(32), data[]}
                            val str_id = bytes.getInt(1)
                            //val packet_id = bytes.getInt(5)
                            val data = ByteArray(receivedbytes - 9)
                            bytes.get(9,data )
                            val job = streamJob[str_id]
                            if (job!=null){
                                job.pushAudioData(data)
                                event.onStreamingData(job.from, job.For?:"", job.channel, data)
                            }
                            bytesReceived += data.size

                        }
                        0x02.toByte() ->{
                            // image stream
                            // tumbnail packet : format {type(8) = 0x02, image_id(32), image_type(32) = 0x02, data[]}
                            // full image packet : format {type(8) = 0x02, image_id(32), image_type(32) = 0x01, data[]}
                            val image_id = bytes.getInt(1)
                            val image_type = bytes.getInt(5)
                            val data = ByteArray(receivedbytes - 9)
                            bytes.get(9, data)
                            logger.info("Image stream received, image_id=$image_id, image_type=$image_type, data length=${data.size}")
                            // Here you can process the image data
                            // image_type 0x01 is full image, 0x02 is thumbnail
                            when (image_type) {
                                0x01 -> {
                                    // full image
                                    imageJob[image_id]?.pushFull(data)
                                }
                                0x02 -> {
                                    // thumbnail image
                                    imageJob[image_id]?.pushThumbnail(data)
                                }
                                else -> {
                                    logger.warn("Unknown image type: $image_type")
                                }
                            }
                        }
                        else -> {
                            logger.warn("Unknown binary message type: ${bytes?.get(0)}")
                        }
                    }

                }
            }

            @Suppress("UNCHECKED_CAST")
            override fun onMessage(message: String?) {
                logger.info("Message received: $message")
                val jsnode = mapper.readTree(message)
                if (jsnode["seq"] != null) {
                    when(val seq = jsnode.get("seq").asInt()){
                        1 ->{
                            //logon reply
                            val lgreply = mapper.treeToValue(jsnode, LogonReply::class.java)
                            if (lgreply.success==true){
                                refresh_token = lgreply.refresh_token
                                event.onConnected()
                            } else {
                                event.onError("Failed to logon: ${lgreply.error ?: "Unknown error"}")
                            }
                        }
                        else ->{
                            // other sequence reply
                            val j = commandJob.remove(seq)
                            if (j is BiConsumer<*, *>) {
                                val sucess = jsnode["success"]?.asBoolean() ?: false
                                if (jsnode.contains("stream_id")){
                                    // start stream reply
                                    val job = j as BiConsumer<Boolean, Int>
                                    job.accept(sucess, jsnode["stream_id"].asInt())
                                } else if (jsnode.contains("image_id")){
                                    // send image reply
                                    val job = j as BiConsumer<Boolean, Int>
                                    job.accept(sucess, jsnode["image_id"].asInt())
                                } else {
                                    // normal success / fail reply
                                    val job = j as BiConsumer<Boolean, Int>
                                    job.accept(sucess,0)
                                }



                            }
                        }
                    }
                } else {
                    // event, tidak ada seq
                    when (jsnode["command"]?.asText()){
                        "on_channel_status" -> {
                            val channelstatus = mapper.treeToValue(jsnode, Event_OnChannelStatus::class.java)
                            event.onChannelStatus(channelstatus.channel, channelstatus.status, channelstatus.users_online, channelstatus.error, channelstatus.error_type)
                            currentChannel = channelstatus.channel
                            isOnline = channelstatus.status== "online"
                        }
                        "on_error" -> {
                            val error = mapper.treeToValue(jsnode, Event_OnError::class.java)
                            event.onError(error.error)
                        }
                        "on_stream_start" ->{
                            val streamstart = mapper.treeToValue(jsnode, Event_OnStreamStart::class.java)
                            logger.info("Stream started on channel ${streamstart.channel} from ${streamstart.from} for ${streamstart.For}")
                            streamJob[streamstart.stream_id] = ZelloAudioJob.fromEventOnStreamStart(streamstart)
                            event.onStartStreaming(streamstart.from, streamstart.For, streamstart.channel)
                            isReceivingStreaming = true
                            receivingFrom = streamstart.from
                            bytesReceived = 0 // reset bytes received
                        }
                        "on_stream_stop" -> {
                            val streamstop = mapper.treeToValue(jsnode, Event_OnStreamStop::class.java)
                            logger.info("Stream stopped on channel ${streamstop.stream_id}")

                            val job = streamJob.remove(streamstop.stream_id)

                            if (job!=null){
                                event.onStopStreaming(job.from, job.For?:"", job.channel)
                                event.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
                            }
                            isReceivingStreaming = false
                            receivingFrom = null
                        }
                        "on_image" ->{
                            val image = mapper.treeToValue(jsnode, Event_OnImage::class.java)
                            logger.info("Image received, image_id=${image.message_id}, from=${image.from}, For=${image.For}, channel=${image.channel}")
                            imageJob[image.message_id] = ZelloImageJob(image.channel,image.message_id,image.height,image.width,image.source,image.from,image.For)
                        }
                        "on_text_message" ->{
                            val textmessage = mapper.treeToValue(jsnode, Event_OnTextMessage::class.java)
                            event.onTextMessage(textmessage.message_id, textmessage.from, textmessage.For, textmessage.channel, textmessage.text, System.currentTimeMillis())
                        }
                        "on_location" -> {
                            val location = mapper.treeToValue(jsnode, Event_OnLocation::class.java)
                            event.onLocation(location.message_id, location.from, location.For, location.channel, location.latitude, location.longitude, location.formatted_address, location.accuracy, System.currentTimeMillis())
                        }
                        else ->{
                            // handle other commands
                            logger.warn("Unknown command: ${jsnode["command"]?.asText()}")
                        }
                    }
                }

            }

            override fun onClose(code: Int, reason: String?, remote: Boolean) {
                logger.info("Closed from ${if (remote) "Remote side" else "Local side"}, Code=$code, Reason=$reason")
                event.onDisconnected(reason?: "Unknown reason")
                isOnline = false
                currentChannel = null


                //Revisi 06/08/2025 : Change to Coroutines
//                val thread = Thread {
//                    try {
//                        Thread.sleep(10000)
//                        connect()
//                    } catch (e: InterruptedException) {
//                        logger.error("Reconnection interrupted: ${e.message}")
//                    }
//                }
//                thread.name= "ZelloClient-ReconnectThread"
//                thread.isDaemon = true
//                thread.start()
            }

            override fun onError(ex: Exception?) {
                logger.error("Error occurred: ${ex?.message}")
            }
        }
        client?.connect()
    }

    /**
     * Stops the Zello client and closes the WebSocket connection.
     */
    fun Stop(){
        client?.close()
    }

    /**
     * Starts a stream on the specified channel with the given sampling rate.
     * will raise a callback <Boolean, Int> where Boolean is true if the stream started successfully, and Int is the stream ID.
     * Stream ID will be used to stop the stream later.
     * @param chanel which channel to stream to
     * @param samplingrate the sampling rate for the audio stream
     * @param For optional parameter to specify the recipient of the stream
     * @param callback a callback to handle the result of the stream start operation
     */
    fun StartStream(chanel: String, samplingrate: Int, For: String? = null, callback: BiConsumer<Boolean, Int>){
        val seqID = inc_seqID()
        val x = StartStreamCommand(
            seqID,
            chanel,
            20,
            Codes.toCodecHeader(samplingrate,1,60),
            For)
        client?.send(mapper.writeValueAsString(x))
        // put here, because require a response in onMessage
        commandJob[seqID] = callback
    }

    /**
     * Stops a stream on the specified channel with the given stream ID.
     * @param channel which channel to stop the stream on
     * @param streamID the ID of the stream to stop
     */
    fun StopStream(channel: String, streamID: Int){
        val seqID = inc_seqID()
        val x = StopStreamCommand(seqID, streamID, channel)
        client?.send(mapper.writeValueAsString(x))

    }

    private fun inc_seqID(): Int{
        if (seqID < Int.MAX_VALUE) {
            seqID++
        } else {
            seqID = 0
        }
        return seqID
    }



}

