import audio.AudioFileInfo
import barix.BarixConnection
import codes.Result_GetSoundbankFiles
import codes.Somecodes.Companion.Get_ANN_ID
import codes.Somecodes.Companion.IsAlphabethic
import codes.Somecodes.Companion.IsNumber
import codes.Somecodes.Companion.Make_WAV_FileName
import codes.Somecodes.Companion.SoundbankResult_directory
import codes.Somecodes.Companion.ValidFile
import codes.Somecodes.Companion.ValidIPV4
import codes.Somecodes.Companion.ValidString
import codes.Somecodes.Companion.dateformat1
import codes.Somecodes.Companion.datetimeformat1
import codes.Somecodes.Companion.timeformat2
import content.Category
import content.Language
import content.ScheduleDay
import database.BroadcastZones
import database.Messagebank
import database.QueueTable
import database.Soundbank
import org.tinylog.Logger
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime

/**
 * MainExtension01 contains additional functions for the main application.
 * Main responsibilities are :
 * 1. Reading and processing Queue_Paging
 * 2. Reading and processing Queue_Table
 * 3. Reading and processing Schedule
 * This class is separated to keep the main.kt file cleaner and more organized.
 */
class MainExtension01 {

    /**
     * Fungsi untuk cek apakah semua broadcast zone valid
     * Valid berarti nama broadcast zone ada di tabel BroadcastZones, dan SoundChannel-nya ada di tabel SoundChannel, dan IP-nya valid
     * @param bz List of broadcast zone (SoundChannel)
     * @return true jika semua valid, false jika ada yang tidak valid
     */
    fun AllBroadcastZonesValid(bz: List<String>): Boolean {
        if (bz.isNotEmpty()) {
            val validbz = mutableListOf<BroadcastZones>()
            db.broadcastDB.List.forEach { xx ->
                if (ValidString(xx.description) && ValidString(xx.SoundChannel)){
                    if (xx.description in bz) {
                        db.soundchannelDB.List.forEach { sc ->
                            if (sc.channel == xx.SoundChannel){
                                if (ValidIPV4(sc.ip)){
                                    validbz.add(xx)
                                }
                            }
                        }
                    }
                }

            }
            if (validbz.size == bz.size) {
                return true
            } else Logger.error { "Some requested broadcast zones are not registered in BroadcastZone table" }
        } else Logger.error { "No Broadcast Zones checked for validity" }
        return false
    }

    /**
     * Fungsi untuk cek apakah semua Streamer Output idle
     * @param bz List of ip address
     * @return true jika semua idle, false jika ada yang tidak idle
     */
    fun AllStreamerOutputIdle(bz: List<String>): Boolean {
        if (bz.isNotEmpty()) {
            if (StreamerOutputs.isNotEmpty()){
                val idlebz = mutableListOf<String>()
                bz.forEach { z1 ->
                    val so = StreamerOutputs.values.find { it.ipaddress == z1 }
                    if (so != null) {
                        if (so.isIdle()) {
                            idlebz.add(z1)
                        }
                    }
                }
                if (idlebz.size == bz.size) {
                    return true
                }
            }
        }
        return false
    }

    /**
     * Find BarixConnection by BroadcastZones description
     * @param zonename BroadcastZones description
     * @return BarixConnection if found, null if not found or invalid
     */
    fun Get_Barix_Connection_by_ZoneName(zonename: String) : BarixConnection? {
        if (ValidString(zonename)){
            val bz = db.broadcastDB.List.find{ it.description == zonename }
            val sc = if (bz!=null) db.soundchannelDB.List.find { it.channel == bz.SoundChannel } else null
            val ip = sc?.ip ?: ""
            if (ValidIPV4(ip)){
                // ketemu ip-nya
                return StreamerOutputs[ip]

            }
        }
        return null
    }

    /**
     * find SoundChannel IP from BroadcastZones description
     * @param bz List of BroadcastZones description
     * @return List of SoundChannel IP
     */
    fun BroadcastZones_to_SoundChannel_IP(bz: List<String>) : List<String>{
        val result = mutableListOf<String>()
        if (bz.isNotEmpty()) {
            val l1 = db.broadcastDB.List.filter { bz.contains(it.description) }
            if (l1.size==bz.size){
                l1.forEach { xx ->
                    val yy = db.soundchannelDB.List.find { it.channel == xx.SoundChannel }
                    if (yy!=null) result.add(yy.ip)
                }
            } else Logger.error { "Some requested broadcast zones are not registered in BroadcastZone table" }
        } else Logger.error { "No Broadcast Zones to convert to SoundChannel IP" }
        return result
    }

    /**
     * Fungsi untuk ambil messagebank berdasarkan ANN_ID, diurutkan berdasarkan urutan bahasa di urutan_bahasa
     * @param id ANN_ID dari messagebank
     * @param languages List of language yang diinginkan, default urutan_bahasa
     * @return List of Messagebank
     */
    fun Get_MessageBank_by_id(id: Int, languages: List<String> = urutan_bahasa): ArrayList<Messagebank> {
        val mb_list = ArrayList<Messagebank>()
        languages.forEach { lang ->
            db.messageDB.List.find { mb -> mb.ANN_ID == id.toUInt() && mb.Language.equals(lang, true) && mb.Voice_Type.equals(selected_voice, true) }
                ?.let {
                    mb_list.add(it)
                }
        }
        return mb_list
    }

    /**
     * Find Soundbank path for AlphabetNumeric category based on value
     * @param sb List of Soundbank to search
     * @param value String value to search, can be combination of letters and numbers, e.g. A1, B2, 3C, 12, etc.
     * @return Soundbank path if found and valid, null if not found or invalid
     */
    fun Get_Soundbank_AlpabethNumeric(sb: List<Soundbank>, value: String): List<String>? {
        val result = mutableListOf<String>()
        if (ValidString(value)) {
            if (sb.isNotEmpty()) {
                val regex = Regex("[A-Z0-9]")
                val match = regex.findAll(value)
                match.forEach {
                    mm ->
                    if (IsNumber(mm.value)) {
                        val num =sb.firstOrNull { s1 -> s1.Category == Category.AlphabetNumeric.name && s1.TAG == "N${mm.value}" }
                        if (num!=null){
                            if (ValidFile(num.Path)){
                                result.add(num.Path)
                            }
                        }
                    } else if (IsAlphabethic(mm.value)) {
                        val alp =
                            sb.firstOrNull { s1 -> s1.Category == Category.AlphabetNumeric.name && s1.TAG == mm.value }
                        if (alp != null) {
                            if (ValidFile(alp.Path)) {
                                result.add(alp.Path)
                            }
                        }
                    }
                }
                return result

            }
        }
        return null
    }

    fun Get_Soundbank_Hour_Minute(sb: List<Soundbank>, value: Int) : String {
        if (value in 0..59){
            val tag = if (value<10) "N0${value}" else "N${value}"
            val hm = sb.firstOrNull { it.Category == Category.AlphabetNumeric.name && it.TAG == tag }
            if (hm!=null){
                if (ValidFile(hm.Path)) return hm.Path
            }
        }
        return ""
    }

    val SoundbankKeywords = listOf("ANN_ID","AL","FLNUM","A_D","I_D","ORIGIN","CITY","SHALAT","PLACES","DESTINATION","ETAD","STANDCODE","GATECODE","REMARK","BCB","PLATNOMOR","REASON","PROCEDURE")

    /**
     * Parse soundbank data from string value in format "KEY:VALUE KEY:VALUE ..."
     * @param value String value to parse
     * @return Map of key-value pairs if valid, null if invalid
     */
    fun Get_Soundbank_Data(value: String) : Map<String, String>? {
        if (ValidString(value)){
            //println("Parsing soundbank data from value: $value")
            val values = value.split(" ").map { it.trim() }.filter { ValidString(it) }
            //println("Split into values: $values")
            if (values.isNotEmpty()){
                val result = mutableMapOf<String, String>()
                values.forEach {
                    val separatorindex = it.indexOf(":")
                    if (separatorindex!=-1){
                        // ada separator
                        val key = it.substring(0, separatorindex).trim().uppercase()
                        val value = it.substring(separatorindex+1).trim().uppercase()
                        //println("Got $key : $value")
                        if (ValidString(key) && ValidString(value)){
                            if (SoundbankKeywords.contains(key)) result[key] = value
                        }
                    }

                }
                if (result.isNotEmpty()) return result
            }
        }
        return null
    }

    /**
     * Find soundbank files from messagebank tags, filtered by VoiceType and Language
     * @param mb Messagebank object
     * @param variables Map of variables to replace in tags.
     * @return Result_GetSoundbankFiles object containing success status, message, and list of soundbank file paths.
     */
    fun Get_Soundbank_Files(
        mb: Messagebank,
        variables: Map<String, String>

    ) : Result_GetSoundbankFiles {
        val tags = mb.Message_TAGS.split(" ")
        if (tags.isEmpty()) {
            return Result_GetSoundbankFiles(false, "No tags found in messagebank id ${mb.ANN_ID}")

        }
        // dapatkan soundbank array berdasarkan VoiceType dan Language
        val sb = db.soundDB.List
            .filter { it.VoiceType.equals(mb.Voice_Type, true) }
            .filter { it.Language.equals(mb.Language, true)  }
        if (sb.isEmpty()) {
            return Result_GetSoundbankFiles(false, "No soundbank found for voice type ${mb.Voice_Type} and language ${mb.Language}")

        }

        val files = mutableListOf<String>()

        tags.forEach { tag ->
            when (val _tag = tag.trim()) {
                "[AIRPLANE_NAME]" -> {
                    val value = variables["AL"].orEmpty()
                    if (ValidString(value)) {
                        val airplane =
                            sb.firstOrNull { it.Category == Category.Airplane_Name.name && it.TAG == value }
                        if (airplane != null) {
                            if (ValidFile(airplane.Path)) {
                                files.add(airplane.Path)
                            } else {
                                return Result_GetSoundbankFiles(false, "Invalid soundbank file ${airplane.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                            }
                        } else {
                            return Result_GetSoundbankFiles(false, "No Airplane_Name found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }
                    } else {
                        return Result_GetSoundbankFiles(false, "AIRPLANE_NAME variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                "[FLIGHT_NUMBER]" -> {
                    val alcode = variables["AL"].orEmpty()
                    val fncode = variables["FLNUM"].orEmpty()
                    if (ValidString(alcode) && ValidString(fncode)) {
                        val val1 = sb.firstOrNull { it.Category == Category.Airline_Code.name && it.TAG == alcode }
                        val val2 = Get_Soundbank_AlpabethNumeric(sb, fncode)
                        if (val1 != null) {
                            if (ValidFile(val1.Path)) {
                                files.add(val1.Path)
                            } else {
                                return Result_GetSoundbankFiles(false, "Invalid soundbank file ${val1.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                            }
                        } else {

                            return Result_GetSoundbankFiles(false, "No Airline_Code found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
                        }
                        if (val2 != null && val2.isNotEmpty()) {
                            files.addAll(val2)
                        } else {
                            return Result_GetSoundbankFiles(false, "No valid soundbank files found for FLIGHT_NUMBER value '$fncode' for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }

                    } else {
                        return Result_GetSoundbankFiles(false, "FLIGHT_NUMBER or AIRLINE_CODE variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                "[PLATNOMOR]" -> {
                    // plat nomor bisa huruf dan angka, atau huruf angka huruf, misalnya B123CD, AB1234EF, RI1
                    val value = variables["PLATNOMOR"].orEmpty()
                    if (ValidString(value)) {
                        val regex = Regex("([A-Z]+)(\\d+)([A-Z]*)")
                        val match = regex.find(value)
                        if (match != null) {
                            val depan = match.groups[1]?.value   // huruf depan
                            val tengah = match.groups[2]?.value  // angka
                            val belakang = match.groups[3]?.value // huruf belakang, bisa kosong
                            // ambilin per huruf
                            depan?.forEach {
                                val dep = Get_Soundbank_AlpabethNumeric(sb, it.toString())
                                if (dep != null) {
                                    files.addAll(dep)
                                }
                            }
                            // ambilin per angka
                            tengah?.forEach {
                                val tgh = Get_Soundbank_AlpabethNumeric(sb, it.toString())
                                if (tgh != null) {
                                    files.addAll(tgh)
                                }
                            }
                            // ambilin per huruf
                            belakang?.forEach {
                                val blk = Get_Soundbank_AlpabethNumeric(sb, it.toString())
                                if (blk != null) {
                                    files.addAll(blk)
                                }
                            }
                        } else {
                            return Result_GetSoundbankFiles(false, "PLATNOMOR variable has invalid format for value '$value' in messagebank id ${mb.ANN_ID}")

                        }
                    } else {
                        return Result_GetSoundbankFiles(false, "PLATNOMOR variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }
                }

                "[CITY]" -> {
                    val values = variables["CITY"].orEmpty().split(";").map { it.trim() }.filter { ValidString(it) }
                    if (values.isNotEmpty()) {
                        values.forEach { vv ->
                            val city = sb.firstOrNull { it.Category == Category.City.name && it.TAG == vv }
                            if (city != null) {
                                if (ValidFile(city.Path)) {
                                    files.add(city.Path)
                                } else {
                                        return Result_GetSoundbankFiles(false, "Invalid soundbank file ${city.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
                                }
                            } else {
                                    return Result_GetSoundbankFiles(false, "No City found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")
                            }
                        }
                    } else {
                            return Result_GetSoundbankFiles(false, "CITY variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }
                }

                "[PLACES]" -> {
                    val value = variables["PLACES"].orEmpty()
                    if (ValidString(value)) {
                        val places = sb.firstOrNull { it.Category == Category.Places.name && it.TAG == value }
                        if (places != null) {
                            if (ValidFile(places.Path)) {
                                files.add(places.Path)
                            } else {
                                    return Result_GetSoundbankFiles(false, "Invalid soundbank file ${places.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                            }
                        } else {
                                return Result_GetSoundbankFiles(false, "No Places found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }
                    } else {
                            return Result_GetSoundbankFiles(false, "PLACES variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                "[ETAD]" -> {
                    val values = variables["ETAD"].orEmpty().split(":").map { it.trim() }.filter { IsNumber(it) }
                    println("ETAD values: $values")
                    if (values.size == 2) {
                        if (IsNumber(values[0]) && IsNumber(values[1])) {
                            val _h = values[0].toInt()
                            val _m = values[1].toInt()
                            if (_h in 0..23 && _m in 0..59) {
                                // valid
                                val hh = Get_Soundbank_Hour_Minute(sb, _h)
                                val mm = Get_Soundbank_Hour_Minute(sb, _m)
                                if (hh.isNotEmpty() && mm.isNotEmpty()){
                                    files.add(hh)
                                    files.add(mm)
                                } else {
                                    return Result_GetSoundbankFiles(false, "ETAD variable has invalid soundbank for hour or minute for tag $_tag in messagebank id ${mb.ANN_ID}")

                                }

                            } else {
                                return Result_GetSoundbankFiles(false, "ETAD variable has invalid hour or minute for tag $_tag in messagebank id ${mb.ANN_ID}, hour must be 0-23 and minute must be 0-59")

                            }
                        }

                    } else {
                        return Result_GetSoundbankFiles(false, "ETAD variable has invalid format for tag $_tag in messagebank id ${mb.ANN_ID}, expected format HH:MM")

                    }

                }

                "[SHALAT]" -> {
                    val value = variables["SHALAT"].orEmpty()
                    if (ValidString(value)) {
                        val shalat = sb.firstOrNull { it.Category == Category.Shalat.name && it.TAG == value }
                        if (shalat != null) {
                            if (ValidFile(shalat.Path)) {
                                files.add(shalat.Path)
                            } else {
                                    return Result_GetSoundbankFiles(false, "Invalid soundbank file ${shalat.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                            }
                        } else {
                                return Result_GetSoundbankFiles(false, "No Shalat found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }
                    } else {
                        return Result_GetSoundbankFiles(false, "SHALAT variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                "[BCB]" -> {
                    // BCB bisa angka saja, misalnya 1,2,3
                    // atau huruf dan angka, misalnya A1, B2, C3, 1A, 2B, 3C
                    val value = variables["BCB"].orEmpty()
                    val path = Get_Soundbank_AlpabethNumeric(sb, value)
                    if (path != null) {
                        files.addAll(path)
                    } else {
                        return Result_GetSoundbankFiles(false, "BCB variable doesn't have valid soundbank for value '$value' in messagebank id ${mb.ANN_ID}")

                    }
                }

                "[GATENUMBER]" -> {
                    // gate number bisa angka saja, misalnya 1,2,3
                    // atau huruf dan angka, misalnya A1, B2, C3, 1A, 2B, 3C
                    val value = variables["GATECODE"].orEmpty()
                    if (ValidString(value)) {
                        val values = value.split(",").map { it.trim() }.filter { ValidString(it) }
                        if (values.isNotEmpty()) {
                            values.forEach { vv ->
                                val path = Get_Soundbank_AlpabethNumeric(sb, vv)
                                if (path != null) {
                                    files.addAll(path)
                                } else {
                                    return Result_GetSoundbankFiles(false, "GATENUMBER variable doesn't have valid soundbank for value '$vv' in messagebank id ${mb.ANN_ID}")

                                }
                            }
                        } else {
                            return Result_GetSoundbankFiles(false, "GATENUMBER variable is empty after split for tag $_tag in messagebank id ${mb.ANN_ID}")

                        }
                    } else {
                        return Result_GetSoundbankFiles(false, "GATENUMBER variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                "[REASON]" -> {
                    val value = variables["REASON"].orEmpty()
                    if (ValidString(value)) {
                        val reason = sb.firstOrNull { it.Category == Category.Reason.name && it.TAG == value }
                        if (reason != null) {
                            if (ValidFile(reason.Path)) {
                                files.add(reason.Path)
                            } else {
                                    return Result_GetSoundbankFiles(false, "Invalid soundbank file ${reason.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                            }
                        } else {
                                return Result_GetSoundbankFiles(false, "No Reason found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }
                    } else {
                            return Result_GetSoundbankFiles(false, "REASON variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                "[PROCEDURE]" -> {
                    val value = variables["PROCEDURE"].orEmpty()
                    if (ValidString(value)) {
                        val procedure = sb.firstOrNull { it.Category == Category.Procedure.name && it.TAG == value }
                        if (procedure != null) {
                            if (ValidFile(procedure.Path)) {
                                files.add(procedure.Path)
                            } else {
                                    return Result_GetSoundbankFiles(false, "Invalid soundbank file ${procedure.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                            }
                        } else {
                                    return Result_GetSoundbankFiles(false, "No Procedure found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }
                    } else {
                        return Result_GetSoundbankFiles(false,"PROCEDURE variable is missing or empty for tag $_tag in messagebank id ${mb.ANN_ID}")

                    }

                }

                else -> {
                    // Phrase
                    val phrase = sb.firstOrNull { it.Category == Category.Phrase.name && it.TAG == _tag }
                    if (phrase != null) {
                        if (ValidFile(phrase.Path)) {
                            files.add(phrase.Path)
                        } else {
                                return Result_GetSoundbankFiles(false, "Invalid soundbank file ${phrase.Path} for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                        }
                    } else {
                            return Result_GetSoundbankFiles(false, "No Phrase found for tag=$_tag voicetype=${mb.Voice_Type} language=${mb.Language} ANN_ID=${mb.ANN_ID}")

                    }

                }

            }
        }
        // all tags processed, return files
        return Result_GetSoundbankFiles(true, "Success", files)
    }

    fun Read_Queue_Paging() : Boolean{
        db.queuepagingDB.Get()
        val list = db.queuepagingDB.List
            .filter { it.Type=="PAGING" }

        list.forEach { qp ->
            //println("Processing $qp")
            if (qp.BroadcastZones.isNotBlank()){
                if (ValidFile(qp.Message)){
                    val zz = qp.BroadcastZones.split(";")
                    if (AllBroadcastZonesValid(zz)){
                        val ips = BroadcastZones_to_SoundChannel_IP(zz)
                        if (AllStreamerOutputIdle(ips)){
                            val afi = audioPlayer.LoadAudioFile(qp.Message)
                            if (afi.isValid()){
                                // file bisa di load, kirim ke masing-masing Streamer Output by IP address
                                Activate_Relays(zz)
                                ips.forEach { ip ->
                                    // send byte array to streamer output
                                    StreamerOutputs[ip]?.SendData(afi.bytes,
                                        {
                                            Deactivate_Relays(zz)
                                            db.Add_Log("AAS", it) },
                                        {
                                            Deactivate_Relays(zz)
                                            db.Add_Log("AAS", it) })
                                }

                                val logmessage =
                                    "Broadcast started PAGING with Filename '${qp.Message}' to zones: ${qp.BroadcastZones}"
                                Logger.info { logmessage }
                                db.Add_Log("AAS", logmessage)
                                db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                                db.queuepagingDB.Resort()
                                return true
                            } else {
                                // file tidak valid, delete from queue paging
                                db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                                db.queuepagingDB.Resort()
                                db.Add_Log(
                                    "AAS",
                                    "Cancelled paging message with index ${qp.index} due to invalid audio file"
                                )
                            }
                        }   // kalau enggak semua streamer idle, skip dulu
                    } else {
                        // ada broadcast zone yang tidak valid, delete from queue paging
                        db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                        db.queuepagingDB.Resort()
                        db.Add_Log("AAS", "Cancelled paging message with index ${qp.index} due to invalid broadcast zone")
                    }
                } else {
                    // file tidak valid, delete from queue paging
                    db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                    db.queuepagingDB.Resort()
                    db.Add_Log(
                        "AAS",
                        "Cancelled paging message with index ${qp.index} due to invalid audio file"
                    )
                }
            } else {
                // invalid broadcast zone, delete from queue paging
                db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                db.queuepagingDB.Resort()
                db.Add_Log("AAS", "Cancelled paging message with index ${qp.index} due to empty broadcast zone")
            }
        }
        return false
    }

    fun Read_Queue_Shalat() : Boolean{
        db.queuepagingDB.Get()
        val list = db.queuepagingDB.List
            .filter { it.Type=="SHALAT" }
        list.forEach { qp ->
            println("Processing $qp")
            if (qp.BroadcastZones.isNotBlank()){
                val zz = qp.BroadcastZones.split(";")
                if (AllBroadcastZonesValid(zz)){
                    val ann_id = Get_ANN_ID(qp.Message)
                    if (ann_id>0){
                        val ips = BroadcastZones_to_SoundChannel_IP(zz)
                        if (AllStreamerOutputIdle(ips)){
                            val mblist = Get_MessageBank_by_id(ann_id, listOf(Language.INDONESIA.name))
                            if (mblist.isNotEmpty()) {
                                mblist.forEach { mb ->
                                    val result = Get_Soundbank_Files(mb, emptyMap())
                                    if (result.success){
                                        val listafi = mutableListOf<AudioFileInfo>()
                                        result.files.forEach { filenya ->
                                            val afi = contentCache.getAudioFile(filenya)
                                            if (afi!=null && afi.isValid()){
                                                listafi.add(afi)
                                            } else {
                                                val afi = audioPlayer.LoadAudioFile(filenya)
                                                if (afi.isValid()) {
                                                    listafi.add(afi)
                                                    contentCache.addAudioFile(filenya, afi)
                                                }
                                            }


                                        }
                                        val targetfile = SoundbankResult_directory.resolve(
                                            Make_WAV_FileName(
                                                "Shalat",
                                                ""
                                            )
                                        ).toString()

                                        val result =audioPlayer.WavWriter(
                                            listafi,
                                            targetfile, true,
                                        )
                                        db.Add_Log("AAS", result.message)
                                        if (result.success) {
                                            // file siap broadcast
                                            val targetafi = audioPlayer.LoadAudioFile(targetfile)
                                            if (targetafi.isValid()) {
                                                // activate relays from broadcast zone
                                                Activate_Relays(zz)
                                                ips.forEach { ip ->
                                                    // send byte array to streamer output
                                                    StreamerOutputs[ip]?.SendData(targetafi.bytes,
                                                        {
                                                            Deactivate_Relays(zz)
                                                            db.Add_Log("AAS", it) },
                                                        {
                                                            Deactivate_Relays(zz)
                                                            db.Add_Log("AAS", it) })
                                                }

                                                val logmsg =
                                                    "Broadcast started SHALAT message with generated file '$targetfile' to zones: ${qp.BroadcastZones}"
                                                Logger.info { logmsg }
                                                db.Add_Log("AAS", logmsg)
                                                db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                                                db.queuepagingDB.Resort()
                                                return true
                                            } else {
                                                db.Add_Log(
                                                    "AAS",
                                                    "Failed to load generated Shalat WAV file $targetfile"
                                                )
                                            }
                                        }
                                    } else{
                                        db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                                        db.queuepagingDB.Resort()
                                        db.Add_Log("AAS", result.message)
                                    }
                                }


                            } else {
                                // tidak ada messagebank dengan ann_id ini, delete from queue paging
                                db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                                db.queuepagingDB.Resort()
                                db.Add_Log(
                                    "AAS",
                                    "Cancelled Shalat message with index ${qp.index} due to ANN_ID $ann_id not found in Messagebank"
                                )
                            }
                        } // kalau enggak semua streamer idle, skip dulu

                    } else {
                        // invalid ann_id, delete from queue paging
                        db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                        db.queuepagingDB.Resort()
                        db.Add_Log("AAS", "Cancelled shalat message with index ${qp.index} due to invalid ANN_ID")
                    }
                } else {
                    // ada broadcast zone yang tidak valid, delete from queue paging
                    db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                    db.queuepagingDB.Resort()
                    db.Add_Log("AAS", "Cancelled shalat message with index ${qp.index} due to invalid broadcast zone")
                }
            } else {
                // invalid broadcast zone, delete from queue paging
                db.queuepagingDB.DeleteByIndex(qp.index.toInt())
                db.queuepagingDB.Resort()
                db.Add_Log("AAS", "Cancelled shalat message with index ${qp.index} due to empty broadcast zone")
            }
        }
        return false
    }

    fun Read_Queue_Timer() : Boolean{
        db.queuetableDB.Get()
        val list = db.queuetableDB.List.filter { it.Type=="TIMER" }
        list.forEach { qa ->
            println("Processing $qa")
            if (qa.BroadcastZones.isNotEmpty()){
                val zz = qa.BroadcastZones.split(";")
                if (AllBroadcastZonesValid(zz)){
                    val ips = BroadcastZones_to_SoundChannel_IP(zz)
                    val ann_id = Get_ANN_ID(qa.Message)
                    if (ann_id>0){
                        if (AllStreamerOutputIdle(ips)){
                            val mblist = Get_MessageBank_by_id(ann_id, qa.Language.split(";"))
                            if (mblist.isNotEmpty()) {
                                mblist.forEach {
                                        mb ->
                                    val result = Get_Soundbank_Files(mb, emptyMap())
                                    if (result.success){
                                        val listafi = mutableListOf<AudioFileInfo>()
                                        result.files.forEach { filenya ->
                                            val afi = contentCache.getAudioFile(filenya)
                                            if (afi!=null && afi.isValid()){
                                                listafi.add(afi)
                                            } else {
                                                val afi = audioPlayer.LoadAudioFile(filenya)
                                                if (afi.isValid()) {
                                                    listafi.add(afi)
                                                    contentCache.addAudioFile(filenya, afi)
                                                }
                                            }

                                        }
                                        val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("Timer","")).toString()
                                        val result = audioPlayer.WavWriter(listafi, targetfile, true)
                                        db.Add_Log("AAS", result.message)
                                        if (result.success){
                                            // file siap broadcast
                                            val targetafi = audioPlayer.LoadAudioFile(targetfile)
                                            if (targetafi.isValid()) {
                                                // activate relays from broadcast zone
                                                Activate_Relays(zz)
                                                ips.forEach { ip ->
                                                    // send byte array to streamer output
                                                    StreamerOutputs[ip]?.SendData(targetafi.bytes,
                                                        {
                                                            Deactivate_Relays(zz)
                                                            db.Add_Log("AAS", it) },
                                                        {
                                                            Deactivate_Relays(zz)
                                                            db.Add_Log("AAS", it) })
                                                }

                                                val logmsg =
                                                    "Broadcast started TIMER message with generated file '$targetfile' to zones: ${qa.BroadcastZones}"
                                                Logger.info { logmsg }
                                                db.Add_Log("AAS", logmsg)
                                                db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                                db.queuetableDB.Resort()
                                                return true
                                            }
                                        } else{
                                            db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                            db.queuetableDB.Resort()
                                            db.Add_Log("AAS", result.message)
                                        }
                                    } else {
                                        db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                        db.queuetableDB.Resort()
                                        db.Add_Log("AAS", result.message)
                                    }

                                }

                            } else {
                                // tidak ada messagebank dengan ann_id ini, delete from queue table
                                db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                db.queuetableDB.Resort()
                                db.Add_Log(
                                    "AAS",
                                    "Cancelled TIMER with index ${qa.index} due to ANN_ID $ann_id not found in Messagebank"
                                )
                            }
                        } // kalau enggak semua streamer idle, skip dulu

                    } else {
                        // invalid ann_id, delete from queue table
                        db.queuetableDB.DeleteByIndex(qa.index.toInt())
                        db.queuetableDB.Resort()
                        db.Add_Log("AAS", "Cancelled TIMER message with index ${qa.index} due to invalid ANN_ID")
                    }
                } else {
                    // ada broadcast zone yang tidak valid, delete from queue table
                    db.queuetableDB.DeleteByIndex(qa.index.toInt())
                    db.queuetableDB.Resort()
                    db.Add_Log("AAS", "Cancelled TIMER message with index ${qa.index} due to invalid broadcast zone")
                }
            } else {
                // invalid broadcast zone, delete from queue table
                db.queuetableDB.DeleteByIndex(qa.index.toInt())
                db.queuetableDB.Resort()
                db.Add_Log("AAS", "Cancelled TIMER message with index ${qa.index} due to empty broadcast zone")
            }
        }
        return false
    }

    fun Activate_Relays(zz: List<String>){
        zz.forEach { zonename ->
            val bz = db.broadcastDB.List.find { it.description == zonename }
            if (bz!=null){
                val bc = Get_Barix_Connection_by_ZoneName(zonename)
                if (bc!=null){
                    // ada barix connection
                    val relays = bz.bp.split(",")
                        .map { it.trim()}
                        .filter { it.isNotBlank() }
                        .filter { IsNumber(it) && it.toInt() in 1..8 }
                        .map{ it.toInt() }
                    if (relays.isNotEmpty()) bc.ActivateRelay(relays)

                }
            }

        }
    }

    fun Deactivate_Relays(zz: List<String>){
        zz.forEach { zonename ->
            val bz = db.broadcastDB.List.find { it.description == zonename }
            if (bz!=null){
                val bc = Get_Barix_Connection_by_ZoneName(zonename)
                bc?.DeactivateRelay()
            }

        }
    }

    fun Read_Queue_Soundbank() : Boolean{
        db.queuetableDB.Get()
        val list = db.queuetableDB.List.filter { it.Type=="SOUNDBANK" }
        list.forEach { qa ->
            //println("Processing $qa")
            if (qa.BroadcastZones.isNotEmpty()){
                val zz = qa.BroadcastZones.split(";")
                if (AllBroadcastZonesValid(zz)){
                    val ips = BroadcastZones_to_SoundChannel_IP(zz)
                    if (AllStreamerOutputIdle(ips)) {
                        val variables = Get_Soundbank_Data(qa.SB_TAGS)
                        val languages = qa.Language.split(";")
                        // cek apakah ANN_ID ada di SB_TAGS
                        val ann_id = variables?.get("ANN_ID")?.toIntOrNull() ?: 0
                        if (ann_id==0){
                            // not available from variables, try to get from Message column
                            // ada ini, karena protokol FIS dulu tidak ada ANN_ID tapi pake Remark
                            val remark = variables?.get("REMARK").orEmpty()
                            when(remark){
                                "GOP" -> {
                                    //TODO Combobox First_Call_Message_Chooser
                                }
                                "GBD" ->{
                                    // TODO Combobox Second_Call_Message_Chooser
                                }
                                "GFC" ->{
                                    // TODO Combobox Final_Call_Message_Chooser
                                }
                                "FLD" ->{
                                    // TODO Combobox Landed_Message_Chooser
                                }
                            }
                        }

                        // recheck again
                        if (ann_id == 0) {
                            db.Add_Log(
                                "AAS",
                                "Cancelled SOUNDBANK message with index ${qa.index} due to missing or invalid ANN_ID in SB_TAGS"
                            )
                            db.queuetableDB.DeleteByIndex(qa.index.toInt())
                            db.queuetableDB.Resort()
                            return@forEach
                        }
                        // sampe sini punya ann_id valid

                        val mblist = Get_MessageBank_by_id(ann_id, languages)
                        if (mblist.isNotEmpty()) {
                            val listafi = mutableListOf<AudioFileInfo>()

                            mblist.forEach { mb ->
                                val result = Get_Soundbank_Files(mb, variables ?: emptyMap())
                                if (result.success){
                                    result.files.forEach { filenya ->
                                        val afi = contentCache.getAudioFile(filenya)
                                        if (afi!=null && afi.isValid()){
                                            listafi.add(afi)
                                        } else {
                                            val afi = audioPlayer.LoadAudioFile(filenya)
                                            if (afi.isValid()) {
                                                listafi.add(afi)
                                                contentCache.addAudioFile(filenya, afi)
                                            }
                                        }

                                    }
                                } else {
                                    db.Add_Log("AAS", result.message)
                                    db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                    db.queuetableDB.Resort()
                                }

                            }

                            if (listafi.isNotEmpty()){
                                db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                db.queuetableDB.Resort()
                                // bikin nama file ada postifx nya dari SB_TAGS, tapi spasi diganti underscore
                                // dan titik dua diganti dash
                                val postfix = qa.SB_TAGS.replace(" ","_").replace(":","-")
                                val targetfile = SoundbankResult_directory.resolve(Make_WAV_FileName("${qa.Source}_${qa.Type}",postfix)).toString()
                                //println("Writing to target WAV file: $targetfile")
                                val result = audioPlayer.WavWriter(listafi, targetfile, true)
                                if (result.success){
                                    // file siap broadcast
                                    //println("Successfully wrote WAV file: $targetfile")
                                    val targetafi = audioPlayer.LoadAudioFile(targetfile)
                                    if (targetafi.isValid()) {
                                        // activate relays from broadcast zone
                                        Activate_Relays(zz)
                                        ips.forEach { ip ->
                                            // send byte array to streamer output
                                            StreamerOutputs[ip]?.SendData(targetafi.bytes,
                                                {
                                                    Deactivate_Relays(zz)
                                                    db.Add_Log("AAS", it)

                                                }, {
                                                    Deactivate_Relays(zz)
                                                    db.Add_Log("AAS", it)
                                                })
                                        }

                                        val logmsg =
                                            "Broadcast started SOUNDBANK message with generated file '$targetfile' to zones: ${qa.BroadcastZones}"
                                        Logger.info { logmsg }
                                        db.Add_Log("AAS", logmsg)

                                        return true
                                    }
                                } else {
                                    println("Failed to write WAV file: ${result.message}")
                                    db.queuetableDB.DeleteByIndex(qa.index.toInt())
                                    db.queuetableDB.Resort()
                                    db.Add_Log("AAS", result.message)
                                }

                            }
                        } else {
                            // tidak ada messagebank dengan ann_id ini, delete from queue table
                            db.queuetableDB.DeleteByIndex(qa.index.toInt())
                            db.queuetableDB.Resort()
                            db.Add_Log(
                                "AAS",
                                "Cancelled SOUNDBANK message with index ${qa.index} due to ANN_ID $ann_id not found in Messagebank"
                            )
                        }

                    }

                } else {
                    // ada broadcast zone yang tidak valid, delete from queue table
                    db.queuetableDB.DeleteByIndex(qa.index.toInt())
                    db.queuetableDB.Resort()
                    db.Add_Log("AAS", "Cancelled SOUNDBANK message with index ${qa.index} due to invalid broadcast zone")
                }
            } else {
                // invalid broadcast zone, delete from queue table
                db.queuetableDB.DeleteByIndex(qa.index.toInt())
                db.queuetableDB.Resort()
                db.Add_Log("AAS", "Cancelled SOUNDBANK message with index ${qa.index} due to empty broadcast zone")
            }
        }
        return false
    }





    /**
     * Read and process Schedule_Table table.
     * This function is called every minute when second=00.
     * It checks for schedules that match the current time and day,
     * and adds them to the Queue_Table for processing.
     */
    fun Read_Schedule_Table(){
        val localtime = LocalTime.now()
        // detik harus 00
        if (localtime.second != 0) return
        val timestring = timeformat2.format(localtime)
        val sch = db.scheduleDB.List.filter {
            it.Time == timestring && it.Enable
        }
        // tidak ada schedule dengan time sekarang dan enable=true
        if (sch.isEmpty()) return

        val localdate = LocalDate.now()
        val ddmmyyyy = dateformat1.format(localdate)
        // check special date dulu
        val specialdate = sch.find {
            it.Day == ddmmyyyy
        }
        if (specialdate != null) {
            val qt = QueueTable(
                0u,
                LocalDateTime.now().format(datetimeformat1),
                "AAS",
                "TIMER",
                specialdate.Description,
                specialdate.Soundpath,
                specialdate.BroadcastZones,
                1.toUInt(),
                specialdate.Language
            )
            db.queuetableDB.Add(qt)
            return

        }
        // cek weekly schedule
        val weekly = sch.find {
            it.Day == when (localdate.dayOfWeek) {
                DayOfWeek.MONDAY -> ScheduleDay.Monday.name
                DayOfWeek.TUESDAY -> ScheduleDay.Tuesday.name
                DayOfWeek.WEDNESDAY -> ScheduleDay.Wednesday.name
                DayOfWeek.THURSDAY -> ScheduleDay.Thursday.name
                DayOfWeek.FRIDAY -> ScheduleDay.Friday.name
                DayOfWeek.SATURDAY -> ScheduleDay.Saturday.name
                DayOfWeek.SUNDAY -> ScheduleDay.Sunday.name
            }
        }
        if (weekly != null) {
            val qt = QueueTable(
                0u,
                LocalDateTime.now().format(datetimeformat1),
                "AAS",
                "TIMER",
                weekly.Description,
                weekly.Soundpath,
                weekly.BroadcastZones,
                1.toUInt(),
                weekly.Language
            )
            db.queuetableDB.Add(qt)
            return

        }
        // check daily schedule
        val daily = sch.find {
            it.Day == ScheduleDay.Everyday.name
        }
        if (daily != null) {
            val qt = QueueTable(
                0u,
                LocalDateTime.now().format(datetimeformat1),
                "AAS",
                "TIMER",
                daily.Description,
                daily.Soundpath,
                daily.BroadcastZones,
                1.toUInt(),
                daily.Language
            )
            db.queuetableDB.Add(qt)
            return

        }
    }
}