package com.amz.genie.activities

import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.amz.genie.R
import com.amz.genie.adapters.ChatAdapter
import com.amz.genie.helpers.EmojiPickerBottomSheet
import com.amz.genie.helpers.Preferences
import com.amz.genie.helpers.Utils.forceLogoutAndGoLogin
import com.amz.genie.models.ChatItem
import com.amz.genie.models.GeneralThreadItem
import com.amz.genie.models.InboxThreadResponse
import com.amz.genie.models.Message
import com.amz.genie.models.Pengguna
import com.amz.genie.models.RawMessage
import com.amz.genie.models.ReaksiPost
import com.amz.genie.models.ReaksiResponse
import com.amz.genie.services.APIMain
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.JsonParser
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class GeneralSubDetailActivity : BaseActivity() {

    private lateinit var ivBack: ImageView
    private lateinit var tvDescription: TextView
    private lateinit var tvName: TextView
    private lateinit var tvJobDesk: TextView
    private lateinit var ibEmoji: ImageButton
    private lateinit var ibAttach: ImageButton
    private lateinit var ibCamera: ImageButton
    private lateinit var ibMic: ImageButton
    private lateinit var tietMessage: TextInputEditText
    private lateinit var rvChat: RecyclerView
    private lateinit var chatAdapter: ChatAdapter
    private lateinit var lm: LinearLayoutManager
    private lateinit var tvNewMsg: TextView

    private var counterpartKode: String? = null
    private var aksiId: Int? = null
    private var reaksiId: Int? = null
    private var tentangId: String = ""

    private var isLoading = false
    private var hasMore = true
    private var currentPage = 1
    private val perPage = 30
    private var isSendMode = false
    private var newestMessageId: String? = null

    private var userNearBottom = true
    private var pendingNewCount = 0

    // ✅ FIX READ PAYLOAD
    private var readedId: Int? = null
    private var readedTipe: String = "AKSI"
    private var hasScheduledMark = false
    private var hasPostedMark = false
    private val markHandler = Handler(Looper.getMainLooper())
    private var markRunnable: Runnable? = null

    private var displayTipe: String = "AKSI"
    private var myKodePegawai: String = ""

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_inbox_sub_detail)

        initUI()
        setupActions()

        when {
            intent.hasExtra("data") -> initData()
            intent.hasExtra("aksi_id") && intent.hasExtra("counterpart_kode") -> initDataFromNotif()
            else -> {
                showSnack("Data notifikasi tidak lengkap")
                finish()
            }
        }
    }

    private fun initDataFromNotif() {
        counterpartKode = intent.getStringExtra("counterpart_kode")
        aksiId = intent.getStringExtra("aksi_id")?.toIntOrNull()
        tentangId = intent.getStringExtra("tentang_id").orEmpty()

        if (counterpartKode.isNullOrBlank() || aksiId == null) {
            showSnack("Data thread tidak lengkap")
            finish()
            return
        }

        val userData = Gson().fromJson(Preferences.getUserData(this), Pengguna::class.java)
        myKodePegawai = userData.pegawai?.kode.orEmpty()

        displayTipe = (intent.getStringExtra("tipe") ?: "AKSI").uppercase()
        readedTipe = "AKSI"
        readedId = aksiId

        currentPage = 1
        hasMore = true
        newestMessageId = null
        pendingNewCount = 0
        tvNewMsg.isVisible = false
        chatAdapter.submitList(emptyList())

        applyHeader(name = counterpartKode, jobDesk = "Memuat...", description = "")
        loadOlder(1, scrollToBottom = true)
    }

    private fun initUI() {
        ivBack = findViewById(R.id.ib_back_inbox_sub_detail)
        tvDescription = findViewById(R.id.tv_description_inbox_sub_detail)
        tvName = findViewById(R.id.tv_name_inbox_sub_detail)
        tvJobDesk = findViewById(R.id.tv_jobdesk_inbox_sub_detail)
        ibEmoji = findViewById(R.id.ib_emoji_inbox_sub_detail)
        ibAttach = findViewById(R.id.ib_attach_inbox_sub_detail)
        ibCamera = findViewById(R.id.ib_camera_inbox_sub_detail)
        ibMic = findViewById(R.id.ib_mic_inbox_sub_detail)
        tietMessage = findViewById(R.id.tiet_message_inbox_sub_detail)
        rvChat = findViewById(R.id.rv_inbox_sub_detail)

        tvNewMsg = findViewById(R.id.tv_new_message_indicator)
        tvNewMsg.isVisible = false

        chatAdapter = ChatAdapter()
        lm = LinearLayoutManager(this).apply { stackFromEnd = true }

        rvChat.layoutManager = lm
        rvChat.adapter = chatAdapter
        rvChat.setHasFixedSize(true)

        tietMessage.addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                updateMicButtonByMessage()
            }
            override fun afterTextChanged(s: Editable?) {}
        })

        updateMicButtonByMessage()
    }

    private fun setupActions() {
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() = handleBackPress(0)
        })

        ivBack.setOnClickListener { handleBackPress(0) }

        ibEmoji.setOnClickListener {
            EmojiPickerBottomSheet { emoji -> insertEmojiToMessage(emoji) }
                .show(supportFragmentManager, "emoji_picker")
        }

        ibAttach.setOnClickListener { /* TODO */ }
        ibCamera.setOnClickListener { /* TODO */ }

        ibMic.setOnClickListener {
            if (isSendMode) {
                val msg = tietMessage.text?.toString()?.trim().orEmpty()
                if (msg.isNotEmpty()) showSendOptionsDialog()
            } else {
                // TODO
            }
        }

        rvChat.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)

                userNearBottom = isUserNearBottom()

                if (userNearBottom && tvNewMsg.isVisible) {
                    pendingNewCount = 0
                    tvNewMsg.isVisible = false
                }

                val firstVisible = lm.findFirstVisibleItemPosition()
                if (!isLoading && hasMore && firstVisible <= 3) {
                    loadOlder(currentPage + 1)
                }
            }
        })

        tvNewMsg.setOnClickListener {
            pendingNewCount = 0
            tvNewMsg.isVisible = false
            rvChat.scrollToPosition(chatAdapter.itemCount - 1)
        }
    }

    private fun initData() {
        val intentDataJson = intent.getStringExtra("data") ?: return
        val data = Gson().fromJson(intentDataJson, GeneralThreadItem::class.java)

        counterpartKode = intent.getStringExtra("counterpart")
        displayTipe = data.tipe.uppercase()

        val aksi = data.aksi
        val reaksi = data.reaksi

        aksiId = aksi.id
        tentangId = aksi.tentang.id
        reaksiId = if (displayTipe == "REAKSI" && reaksi != null) reaksi.id else null

        readedTipe = displayTipe
        readedId = if (readedTipe == "REAKSI") reaksiId else aksiId

        if (displayTipe == "REAKSI" && reaksi != null) {
            tvName.text = reaksi.pembuat.nama
            tvJobDesk.text = "${reaksi.pembuat.jabatan?.nama} - ${reaksi.pembuat.outlet?.nama}"
            tvDescription.text = reaksi.uraian
        } else {
            tvName.text = aksi.pembuat.nama
            tvJobDesk.text = "${aksi.pembuat.jabatan?.nama} - ${aksi.pembuat.outlet?.nama}"
            tvDescription.text = aksi.uraian
        }

        val userData = Gson().fromJson(Preferences.getUserData(this), Pengguna::class.java)
        myKodePegawai = userData.pegawai?.kode.orEmpty()

        resetThreadStateAndLoad(scrollToBottom = true)
    }

    private fun loadOlder(page: Int, scrollToBottom: Boolean = false) {
        val cp = counterpartKode ?: return
        val idAksi = aksiId ?: return
        if (isLoading) return
        if (!hasMore && page != 1) return

        isLoading = true
        showProgressDialog(true)

        APIMain.require().generalServices
            .threadDetail(
                Preferences.getAccessToken(this@GeneralSubDetailActivity),
                cp, idAksi, page, perPage
            )
            .enqueue(object : Callback<InboxThreadResponse> {
                override fun onResponse(call: Call<InboxThreadResponse>, response: Response<InboxThreadResponse>) {
                    showProgressDialog(false)
                    isLoading = false

                    if (!response.isSuccessful) {
                        val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
                        val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)

                        val message = when {
                            (response.code() == 401 || response.code() == 500) && expired -> {
                                forceLogoutAndGoLogin(this@GeneralSubDetailActivity)
                                "Session expired. Please login again."
                            }
                            response.code() == 400 -> runCatching {
                                JsonParser.parseString(raw).asJsonObject["message"].asString
                            }.getOrDefault("Bad request")
                            else -> "${response.code()}, ${response.message()}"
                        }
                        showSnack(message)
                        hasPostedMark = false
                        return
                    }

                    val body = response.body() ?: return

                    currentPage = body.meta?.page ?: page
                    hasMore = body.meta?.has_more ?: false

                    val raw = body.items.orEmpty() // newest -> oldest
                    if (page == 1) newestMessageId = raw.firstOrNull()?.id

                    val display = raw.asReversed() // oldest -> newest

                    if (page == 1) {
                        applyHeaderFromThread(display)
                    }

                    val chatItems = mapToChatItems(display, myKodePegawai)

                    if (page == 1) {
                        chatAdapter.submitList(chatItems)
                        if (scrollToBottom) rvChat.scrollToPosition(chatAdapter.itemCount - 1)
                    } else {
                        val beforeCount = chatAdapter.itemCount
                        val firstPos = lm.findFirstVisibleItemPosition()
                        val topView = rvChat.getChildAt(0)
                        val topOffset = topView?.top ?: 0

                        chatAdapter.prepend(chatItems)
                        lm.scrollToPositionWithOffset(
                            firstPos + (chatAdapter.itemCount - beforeCount),
                            topOffset
                        )
                    }

                    if (!hasMore) scheduleMarkAfterFullyLoaded()
                }

                override fun onFailure(call: Call<InboxThreadResponse>, t: Throwable) {
                    showProgressDialog(false)
                    isLoading = false
                    showSnack(t.message ?: "Terjadi kesalahan")
                }
            })
    }

    private fun mapToChatItems(display: List<RawMessage>, myKode: String): List<ChatItem> {
        val out = ArrayList<ChatItem>()
        var prevSender: String? = null

        for (m in display) {
            val senderKode = m.sender_kode.trim()
            if (senderKode.isEmpty()) continue

            val sameAsPrev = prevSender != null && prevSender == senderKode
            val isMine = senderKode == myKode

            out.add(
                ChatItem(
                    id = m.id.orEmpty(),
                    senderKode = senderKode,
                    senderName = m.sender?.nama,
                    senderJob = m.sender?.jabatan?.nama,
                    senderOutlet = m.sender?.outlet?.nama,
                    message = m.message,
                    timeText = m.waktu_buat.orEmpty(),
                    isMine = isMine,
                    isSameSenderAsPrev = sameAsPrev
                )
            )

            prevSender = senderKode
        }
        return out
    }

    private fun updateMicButtonByMessage() {
        val hasText = !tietMessage.text.isNullOrBlank()
        isSendMode = hasText

        if (hasText) {
            ibMic.setImageResource(R.drawable.send_24px)
            ibMic.contentDescription = "send"
        } else {
            ibMic.setImageResource(R.drawable.mic_24px)
            ibMic.contentDescription = "mic"
        }
    }

    private fun insertEmojiToMessage(emoji: String) {
        val editable = tietMessage.text ?: return
        val start = tietMessage.selectionStart.coerceAtLeast(0)
        val end = tietMessage.selectionEnd.coerceAtLeast(0)

        val minPos = minOf(start, end)
        val maxPos = maxOf(start, end)

        editable.replace(minPos, maxPos, emoji)
        tietMessage.setSelection(minPos + emoji.length)
        tietMessage.requestFocus()
    }

    private fun showSnack(message: String) {
        Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show()
    }

    private fun isUserNearBottom(threshold: Int = 2): Boolean {
        val total = chatAdapter.itemCount
        if (total == 0) return true
        val lastVisible = lm.findLastVisibleItemPosition()
        return lastVisible >= total - 1 - threshold
    }

    private fun scheduleMarkAfterFullyLoaded() {
        if (hasScheduledMark || hasPostedMark) return
        if (readedId == null) return

        hasScheduledMark = true
        markRunnable = Runnable {
            postMarkToApi()
            hasScheduledMark = false
        }
        markHandler.postDelayed(markRunnable!!, 1200L)
    }

    private fun postMarkToApi() {
        if (hasPostedMark) return

        val id = readedId ?: return
        val tipeKirim = readedTipe

        hasPostedMark = true

        APIMain.require().generalServices.readed(
            Preferences.getAccessToken(this),
            id,
            tipeKirim
        ).enqueue(object : Callback<Message> {

            override fun onResponse(call: Call<Message>, response: Response<Message>) {
                if (!response.isSuccessful) {
                    hasPostedMark = false
                    showSnack("${response.code()} ${response.message()}")
                    return
                }
                setResult(RESULT_OK, Intent().putExtra("needs_refresh", true))
            }

            override fun onFailure(call: Call<Message>, t: Throwable) {
                hasPostedMark = false
                showSnack(t.message ?: "Terjadi kesalahan")
            }
        })
    }

    override fun onPause() {
        super.onPause()
        markRunnable?.let { markHandler.removeCallbacks(it) }
        hasScheduledMark = false
    }

    override fun onDestroy() {
        super.onDestroy()
        markRunnable?.let { markHandler.removeCallbacks(it) }
        hasScheduledMark = false
    }

    private fun resetThreadStateAndLoad(scrollToBottom: Boolean = true) {
        currentPage = 1
        hasMore = true
        newestMessageId = null
        pendingNewCount = 0
        tvNewMsg.isVisible = false
        chatAdapter.submitList(emptyList())

        hasPostedMark = false
        hasScheduledMark = false

        loadOlder(1, scrollToBottom = scrollToBottom)
    }

    private fun applyHeader(name: String?, jobDesk: String?, description: String?) {
        tvName.text = name ?: ""
        tvJobDesk.text = jobDesk ?: ""
        tvDescription.text = description ?: ""
    }

    private fun applyHeaderFromThread(display: List<RawMessage>) {
        if (display.isEmpty()) return

        val partnerMsg = display.firstOrNull { it.sender_kode.trim() != myKodePegawai }
            ?: display.lastOrNull { it.sender_kode.trim() != myKodePegawai }

        val partner = partnerMsg?.sender
        val partnerName = partner?.nama ?: (counterpartKode ?: "")
        val partnerJob = listOfNotNull(partner?.jabatan?.nama, partner?.outlet?.nama).joinToString(" - ")

        // ✅ ambil uraian kegiatan dari payload AKSI (message json), bukan dari text header aja
        val desc = extractUraianKegiatanFromMsg(partnerMsg?.message)

        tvName.text = partnerName
        tvJobDesk.text = partnerJob
        if (!desc.isNullOrBlank()) tvDescription.text = desc
    }

    // =============================
    // SEND REAKSI (tetap)
    // =============================
    private fun showSendOptionsDialog() {
        val msg = tietMessage.text?.toString()?.trim().orEmpty()
        if (msg.isEmpty()) return

        val options = listOf("Bertanya", "Informasi", "Laporan", "Pengajuan", "Penugasan")

        val view = LayoutInflater.from(this).inflate(R.layout.dialog_send_type, null)
        val til = view.findViewById<TextInputLayout>(R.id.til_type)
        val actv = view.findViewById<AutoCompleteTextView>(R.id.actv_type_dialog_send_type)

        val adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, options)
        actv.setAdapter(adapter)
        actv.setText(options[0], false)

        actv.keyListener = null
        actv.isCursorVisible = false
        actv.isFocusable = true
        actv.isFocusableInTouchMode = true
        actv.showSoftInputOnFocus = false
        actv.setOnClickListener { actv.showDropDown() }

        val dialog = MaterialAlertDialogBuilder(this)
            .setView(view)
            .setNegativeButton("Batal", null)
            .setPositiveButton("Kirim", null)
            .create()

        dialog.setOnShowListener {
            dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener {
                val selected = actv.text?.toString()?.trim().orEmpty()
                if (selected.isEmpty()) {
                    til.error = "Pilih tipe komunikasi"
                    return@setOnClickListener
                }

                showProgressDialog(true)

                val topic = when (selected) {
                    "Bertanya" -> "A"
                    "Informasi" -> "I"
                    "Laporan" -> "L"
                    "Approval" -> "P"
                    "Pengajuan" -> "R"
                    "Penugasan" -> "T"
                    else -> ""
                }

                val reaksi = ReaksiPost(
                    aksiId!!, "R",
                    reaksiId,
                    tentangId, topic, tietMessage.text.toString()
                )

                val gson = GsonBuilder().serializeNulls().create()
                val json = gson.toJson(reaksi)
                val body = json.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())

                APIMain.require().reaksiServices.add(
                    Preferences.getAccessToken(this@GeneralSubDetailActivity),
                    body, emptyList()
                ).enqueue(object : Callback<ReaksiResponse> {
                    override fun onResponse(call: Call<ReaksiResponse>, response: Response<ReaksiResponse>) {
                        showProgressDialog(false)

                        if (!response.isSuccessful) {
                            val raw = runCatching { response.errorBody()?.string().orEmpty() }.getOrDefault("")
                            val expired = raw.contains("Signature has expired", true) || raw.contains("token_expired", true)

                            val message = when {
                                (response.code() == 401 || response.code() == 500) && expired -> {
                                    forceLogoutAndGoLogin(this@GeneralSubDetailActivity)
                                    "Session expired. Please login again."
                                }
                                response.code() == 400 -> runCatching {
                                    JsonParser.parseString(raw).asJsonObject["message"].asString
                                }.getOrDefault("Bad request")
                                else -> "${response.code()}, ${response.message()}"
                            }
                            showSnack(message)
                            hasPostedMark = false
                            return
                        }

                        val body = response.body()
                        val rawItems = body?.thread?.items
                        if (rawItems.isNullOrEmpty()) {
                            showSnack("Reaksi terkirim. Refresh thread...")
                            loadOlder(1, scrollToBottom = true)
                            til.error = null
                            tietMessage.setText("")
                            dialog.dismiss()
                            return
                        }

                        val display = rawItems.asReversed()
                        val chatItems = mapToChatItems(display, myKodePegawai)
                        chatAdapter.submitList(chatItems)
                        rvChat.scrollToPosition(chatAdapter.itemCount - 1)

                        til.error = null
                        tietMessage.setText("")
                        dialog.dismiss()
                    }

                    override fun onFailure(call: Call<ReaksiResponse>, t: Throwable) {
                        showProgressDialog(false)
                        showSnack(t.message ?: "Terjadi kesalahan")
                        hasPostedMark = false
                    }
                })
            }
        }

        dialog.show()
    }

    private fun extractUraianKegiatanFromMsg(msg: com.google.gson.JsonObject?): String? {
        if (msg == null) return null

        // -------------------------
        // Safe helpers
        // -------------------------
        fun JsonElement?.asStringSafeNullable(): String? {
            if (this == null || this.isJsonNull) return null
            return try {
                when {
                    this.isJsonPrimitive -> this.asString
                    this.isJsonArray -> this.asJsonArray.joinToString("\n") { it.asStringSafeNullable().orEmpty() }
                    this.isJsonObject -> {
                        // common payload {nilai: "..."} / {value:"..."}
                        val o = this.asJsonObject
                        val pick = listOf("nilai", "value", "teks", "string", "uraian", "isi", "jawaban")
                            .firstNotNullOfOrNull { k -> o.get(k)?.takeIf { !it.isJsonNull } }
                        pick?.asStringSafeNullable() ?: o.toString()
                    }
                    else -> this.toString()
                }
            } catch (_: Exception) {
                null
            }
        }

        fun getObjAtPath(root: com.google.gson.JsonObject, path: String): JsonElement? {
            var cur: JsonElement = root
            for (seg in path.split(".")) {
                if (!cur.isJsonObject) return null
                val o = cur.asJsonObject
                if (!o.has(seg)) return null
                cur = o.get(seg)
            }
            return cur
        }

        fun arrFrom(vararg paths: String): com.google.gson.JsonArray? {
            for (p in paths) {
                val el = getObjAtPath(msg, p)
                if (el != null && el.isJsonArray) return el.asJsonArray
            }
            return null
        }

        fun rootOrValuesGet(key: String): JsonElement? {
            if (msg.has(key) && !msg.get(key).isJsonNull) return msg.get(key)
            val values = msg.get("values")?.takeIf { it.isJsonObject }?.asJsonObject
            if (values != null && values.has(key) && !values.get(key).isJsonNull) return values.get(key)

            val dv = msg.get("detail_values")?.takeIf { it.isJsonObject }?.asJsonObject
            if (dv != null && dv.has(key) && !dv.get(key).isJsonNull) return dv.get(key)

            return null
        }

        // -------------------------
        // 1) cari definisi field "Uraian Kegiatan"
        // -------------------------
        val detailArr = arrFrom(
            "komunikasi.komunikasi_detail",
            "komunikasi_detail",
            "aksi.komunikasi.komunikasi_detail",
            "aksi.komunikasi.komunikasi_detail" // keep
        ) ?: return null

        // target field = "Uraian Kegiatan" (kode detail 143) dan jenis teks (id_jenis_isian=0)
        var kodeUraian: Int? = null
        for (i in 0 until detailArr.size()) {
            val el = detailArr[i]
            if (!el.isJsonObject) continue
            val d = el.asJsonObject

            val kode = d.get("kode")?.takeIf { it.isJsonPrimitive }?.asInt
            val jenisId =
                d.get("id_jenis_isian")?.takeIf { it.isJsonPrimitive }?.asInt
                    ?: d.get("jenis_isian")?.takeIf { it.isJsonObject }?.asJsonObject?.get("id")
                        ?.takeIf { it.isJsonPrimitive }?.asInt
                    ?: 0

            val label = d.get("isian")?.asStringSafeNullable()?.trim().orEmpty()

            if (jenisId == ChatAdapter.JENIS_TEKS) {
                // paling ideal ketemu kode 143
                if (kode == 143) {
                    kodeUraian = 143
                    break
                }
                // fallback: label matching
                if (kode != null && label.contains("uraian", true) && label.contains("kegiatan", true)) {
                    kodeUraian = kode
                }
            }
        }

        // kalau tetap gak ketemu, fallback langsung 143
        val kd = kodeUraian ?: 143

        // -------------------------
        // 2) ambil value dari tempat yang benar
        // -------------------------

        // A) jika backend sudah kirim map detail_values/values by kode detail
        rootOrValuesGet(kd.toString())?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
        rootOrValuesGet("nilai_$kd")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
        rootOrValuesGet("isian_$kd")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }

        // B) pola sekarang: aksi_komunikasi_teks adalah object {id, aksi_id, nilai:"..."} TANPA kode_detail
        // jadi ambil "nilai" dari aksi_komunikasi_teks kalau ada (karena untuk RK Harian biasanya cuma 1 teks yaitu uraian)
        msg.get("aksi_komunikasi_teks")
            ?.asStringSafeNullable()
            ?.trim()
            ?.takeIf { it.isNotBlank() }
            ?.let { return it }

        val teksObj = msg.get("aksi_komunikasi_teks")?.takeIf { it.isJsonObject }?.asJsonObject
        teksObj?.get("nilai")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }

        // fallback lain
        msg.get("aksi_komunikasi_string")?.takeIf { it.isJsonObject }?.asJsonObject
            ?.get("nilai")?.asStringSafeNullable()?.trim()?.takeIf { it.isNotBlank() }?.let { return it }

        return null
    }

    // =========================
    // Helpers Json Safe (single source)
    // =========================
    private fun JsonElement?.asStringSafeNullable(): String? {
        if (this == null || this.isJsonNull) return null
        return try {
            when {
                this.isJsonPrimitive -> this.asString
                this.isJsonArray -> this.asJsonArray.joinToString("\n") { it.asStringSafeNullable().orEmpty() }
                this.isJsonObject -> this.toString()
                else -> this.toString()
            }
        } catch (_: Exception) {
            null
        }
    }
}