package zello

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 java.lang.Exception
import java.net.URI
import java.nio.ByteBuffer

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

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

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

    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"
    // default channel to join
    private var channels = arrayOf("GtcDev2025")
    // 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()
    fun Start(event: ZelloEvent){
        client = object : WebSocketClient(address) {
            override fun onOpen(handshakedata: ServerHandshake?) {
                logger.info("Connected to $address")
                seqID = 0
                inc_seqID()
                val lg = LogonCommand.create(seqID,channels, developerKey)
                val value = mapper.writeValueAsString(lg)
                logger.info("Sending LogonCommand: $value")
                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(data, 9, receivedbytes - 9)
                            logger.info("Audio stream received, stream_id=$str_id, packet_id=$packet_id, data length=${data.size}")

                            streamJob[str_id]?.pushAudioData(data)
                        }
                        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(data, 9, receivedbytes - 9)
                            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)}")
                        }
                    }

                }
            }


            override fun onMessage(message: String?) {
                logger.info("Message received: $message")
                val jsnode = mapper.readTree(message)
                if (jsnode["seq"] != null) {
                    val seq = jsnode["seq"].asInt()
                    if (seq == 1){
                        //logon reply
                        val lgreply = mapper.treeToValue(jsnode, LogonReply::class.java)
                        if (lgreply.success==true){
                            logger.info("successfully logged on, refresh_token=${lgreply.refresh_token}")
                            refresh_token = lgreply.refresh_token
                            event.onConnected()
                        } else {
                            logger.error("Failed to logon: ${lgreply.error ?: "Unknown error"}")
                        }
                    } else {
                        // other commands
                        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)
                            }
                            "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.put(streamstart.stream_id, ZelloAudioJob.fromEventOnStreamStart(streamstart))
                            }
                            "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.onAudioData(job.streamID, job.from, job.For?:"", job.channel, job.getAudioData())
                                }
                            }
                            "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.put(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()
                // try reconnecting after 10 seconds
                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()
    }

    fun Stop(){
        client?.close()
    }

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



}

