package com.ffii.fpsms.modules.monitoring.zebra

import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket
import java.nio.charset.StandardCharsets

data class ZebraOdometerResult(
    val totalLabelCount: Long?,
    val hostStatusSnippet: String?,
    val errorMessage: String?,
)

object ZebraLinkOsClient {

    private const val ODOMETER_VAR = "odometer.total_label_count"

    fun isZebraBrand(brand: String?): Boolean =
        brand?.contains("zebra", ignoreCase = true) == true

    fun queryOdometer(
        ip: String,
        port: Int,
        connectTimeoutMs: Int,
        queryTimeoutMs: Int,
        includeHostStatus: Boolean = true,
    ): ZebraOdometerResult {
        val safeIp = ip.trim()
        if (safeIp.isEmpty()) {
            return ZebraOdometerResult(null, null, "IP is blank")
        }
        return try {
            Socket().use { socket ->
                socket.tcpNoDelay = true
                socket.soTimeout = queryTimeoutMs.coerceAtLeast(500)
                socket.connect(InetSocketAddress(safeIp, port), connectTimeoutMs.coerceAtLeast(500))
                val out = socket.getOutputStream()
                val cmd = """! U1 getvar "$ODOMETER_VAR"""".toByteArray(StandardCharsets.US_ASCII) + "\r\n".toByteArray()
                out.write(cmd)
                out.flush()
                val odometerBytes = readAvailable(socket, queryTimeoutMs)
                val odometerText = String(odometerBytes, StandardCharsets.UTF_8).trim()
                val total = parseOdometerValue(odometerText)

                val hostSnippet = if (includeHostStatus) {
                    out.write("~HS\r\n".toByteArray(StandardCharsets.US_ASCII))
                    out.flush()
                    val hsBytes = readAvailable(socket, queryTimeoutMs)
                    val hs = String(hsBytes, StandardCharsets.UTF_8).trim()
                    hs.take(500).ifBlank { null }
                } else {
                    null
                }

                if (total == null) {
                    ZebraOdometerResult(
                        totalLabelCount = null,
                        hostStatusSnippet = hostSnippet,
                        errorMessage = odometerText.ifBlank { "No odometer response" }.take(255),
                    )
                } else {
                    ZebraOdometerResult(
                        totalLabelCount = total,
                        hostStatusSnippet = hostSnippet,
                        errorMessage = null,
                    )
                }
            }
        } catch (e: IOException) {
            ZebraOdometerResult(null, null, e.message?.take(255) ?: e.javaClass.simpleName)
        } catch (e: Exception) {
            ZebraOdometerResult(null, null, e.message?.take(255) ?: e.javaClass.simpleName)
        }
    }

    private fun readAvailable(socket: Socket, waitMs: Int): ByteArray {
        val input = socket.getInputStream()
        val buffer = ByteArray(4096)
        val chunks = ArrayList<ByteArray>()
        var total = 0
        val deadline = System.currentTimeMillis() + waitMs.coerceAtLeast(300)
        while (System.currentTimeMillis() < deadline && total < 16384) {
            val available = try {
                input.available()
            } catch (_: IOException) {
                break
            }
            if (available > 0) {
                val n = input.read(buffer, 0, minOf(buffer.size, available))
                if (n <= 0) break
                chunks.add(buffer.copyOf(n))
                total += n
                continue
            }
            if (total > 0) break
            try {
                Thread.sleep(50)
            } catch (_: InterruptedException) {
                Thread.currentThread().interrupt()
                break
            }
        }
        if (chunks.isEmpty()) return ByteArray(0)
        val merged = ByteArray(total)
        var offset = 0
        for (chunk in chunks) {
            System.arraycopy(chunk, 0, merged, offset, chunk.size)
            offset += chunk.size
        }
        return merged
    }

    internal fun parseOdometerValue(raw: String): Long? {
        val text = raw.trim()
        if (text.isEmpty()) return null
        val unquoted = text.trim('"', '\'', ' ')
        val digits = unquoted.filter { it.isDigit() }
        if (digits.isEmpty()) return null
        return digits.toLongOrNull()
    }
}
