package com.ffii.fpsms.modules.monitoring.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.master.entity.Printer
import com.ffii.fpsms.modules.master.entity.PrinterRepository
import com.ffii.fpsms.modules.monitoring.zebra.ZebraLinkOsClient
import com.ffii.fpsms.py.PyPrintChannel
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.InetSocketAddress
import java.net.Socket
import java.time.LocalDate
import java.time.LocalDateTime

@Service
open class LabelPrinterMonitorService(
    private val printerRepository: PrinterRepository,
    private val jdbcDao: JdbcDao,
) {
    @Value("\${fpsms.label-printer-monitor.connect-timeout-ms:3000}")
    private var connectTimeoutMs: Int = 3000

    @Value("\${fpsms.label-printer-monitor.zebra-query-timeout-ms:2500}")
    private var zebraQueryTimeoutMs: Int = 2500

    @Value("\${fpsms.label-printer-monitor.default-port:9100}")
    private var defaultPort: Int = 9100

    @Value("\${fpsms.label-printer-monitor.zebra-odometer-enabled:true}")
    private var zebraOdometerEnabled: Boolean = true

    @Value("\${fpsms.label-printer-monitor.history-max-range-days:30}")
    private var historyMaxRangeDays: Long = 30

    data class ProbeResult(
        val reachable: Boolean,
        val latencyMs: Int?,
        val errorMessage: String?,
    )

    @Transactional
    open fun checkAllLabelPrinters(): Map<String, Any> {
        val printers = labelPrinters()
        val results = printers.map { checkAndPersist(it) }
        val today = LocalDate.now()
        val stats = labelPrintStats(today.atStartOfDay(), LocalDateTime.now())
        return mapOf(
            "printers" to results,
            "labelStats" to stats,
            "summary" to buildSummary(results),
        )
    }

    @Transactional(readOnly = true)
    open fun listStatus(): Map<String, Any> {
        val rows = jdbcDao.queryForList(
            """
                SELECT
                    p.id,
                    p.code,
                    p.name,
                    p.type,
                    p.brand,
                    p.ip,
                    p.port,
                    p.last_label_odometer AS lastLabelOdometer,
                    p.last_label_odometer_at AS lastLabelOdometerAt,
                    log.odometer_total AS odometerTotal,
                    log.delta_since_previous AS deltaSincePrevious,
                    log.reachable AS lastReachable,
                    log.latency_ms AS latencyMs,
                    log.error_message AS errorMessage,
                    log.host_status_snippet AS hostStatusSnippet,
                    log.recorded_at AS lastCheckAt
                FROM printer p
                LEFT JOIN printer_label_odometer_log log ON log.id = (
                    SELECT l2.id
                    FROM printer_label_odometer_log l2
                    WHERE l2.printer_id = p.id
                    ORDER BY l2.id DESC
                    LIMIT 1
                )
                WHERE p.deleted = 0
                  AND p.type = 'Label'
                ORDER BY p.name, p.code, p.id
            """.trimIndent(),
        )
        val results = rows.map { rowToPrinterStatus(it) }
        val today = LocalDate.now()
        val stats = labelPrintStats(today.atStartOfDay(), LocalDateTime.now())
        return mapOf(
            "printers" to results,
            "labelStats" to stats,
            "summary" to buildSummary(results),
        )
    }

    @Transactional(readOnly = true)
    open fun labelPrintStats(from: LocalDateTime, to: LocalDateTime): Map<String, Any> {
        require(!from.isAfter(to)) { "fromDateTime must be before toDateTime" }
        require(!to.isAfter(from.plusDays(historyMaxRangeDays))) {
            "Date range must not exceed $historyMaxRangeDays days"
        }

        val totalRow = jdbcDao.queryForList(
            """
                SELECT COALESCE(SUM(qty), 0) AS total
                FROM py_job_order_print_submit
                WHERE deleted = 0
                  AND print_channel = :channel
                  AND created >= :from
                  AND created <= :to
            """.trimIndent(),
            mapOf(
                "channel" to PyPrintChannel.LABEL,
                "from" to from,
                "to" to to,
            ),
        ).firstOrNull()

        val todayStart = LocalDate.now().atStartOfDay()
        val todayRow = jdbcDao.queryForList(
            """
                SELECT COALESCE(SUM(qty), 0) AS total
                FROM py_job_order_print_submit
                WHERE deleted = 0
                  AND print_channel = :channel
                  AND created >= :from
                  AND created <= :to
            """.trimIndent(),
            mapOf(
                "channel" to PyPrintChannel.LABEL,
                "from" to todayStart,
                "to" to LocalDateTime.now(),
            ),
        ).firstOrNull()

        val recent = jdbcDao.queryForList(
            """
                SELECT
                    s.id,
                    s.job_order_id AS jobOrderId,
                    s.qty,
                    s.created,
                    jo.code AS jobCode
                FROM py_job_order_print_submit s
                LEFT JOIN job_order jo ON jo.id = s.job_order_id
                WHERE s.deleted = 0
                  AND s.print_channel = :channel
                  AND s.created >= :from
                  AND s.created <= :to
                ORDER BY s.created DESC, s.id DESC
                LIMIT 50
            """.trimIndent(),
            mapOf(
                "channel" to PyPrintChannel.LABEL,
                "from" to from,
                "to" to to,
            ),
        )

        return mapOf(
            "todayTotal" to (numberValue(todayRow?.get("total")) ?: 0L),
            "rangeTotal" to (numberValue(totalRow?.get("total")) ?: 0L),
            "from" to from.toString(),
            "to" to to.toString(),
            "recentSubmits" to recent,
        )
    }

    @Transactional(readOnly = true)
    open fun odometerRangeStats(from: LocalDateTime, to: LocalDateTime): Map<String, Any> {
        require(!from.isAfter(to)) { "fromDateTime must be before toDateTime" }
        require(!to.isAfter(from.plusDays(historyMaxRangeDays))) {
            "Date range must not exceed $historyMaxRangeDays days"
        }

        val zebraPrinters = labelPrinters().filter { ZebraLinkOsClient.isZebraBrand(it.brand) }
        val results = zebraPrinters.mapNotNull { p ->
            val printerId = p.id ?: return@mapNotNull null
            val startRow = jdbcDao.queryForList(
                """
                    SELECT odometer_total AS odometerTotal, recorded_at AS recordedAt
                    FROM printer_label_odometer_log
                    WHERE printer_id = :printerId
                      AND odometer_total IS NOT NULL
                      AND recorded_at >= :from
                      AND recorded_at <= :to
                    ORDER BY recorded_at ASC, id ASC
                    LIMIT 1
                """.trimIndent(),
                mapOf(
                    "printerId" to printerId,
                    "from" to from,
                    "to" to to,
                ),
            ).firstOrNull()
            val endRow = jdbcDao.queryForList(
                """
                    SELECT odometer_total AS odometerTotal, recorded_at AS recordedAt
                    FROM printer_label_odometer_log
                    WHERE printer_id = :printerId
                      AND odometer_total IS NOT NULL
                      AND recorded_at >= :from
                      AND recorded_at <= :to
                    ORDER BY recorded_at DESC, id DESC
                    LIMIT 1
                """.trimIndent(),
                mapOf(
                    "printerId" to printerId,
                    "from" to from,
                    "to" to to,
                ),
            ).firstOrNull()

            val startOdo = numberValue(startRow?.get("odometerTotal"))
            val endOdo = numberValue(endRow?.get("odometerTotal"))
            val delta = if (startOdo != null && endOdo != null && endOdo >= startOdo) endOdo - startOdo else null

            mapOf(
                "printerId" to printerId,
                "code" to p.code,
                "name" to p.name,
                "brand" to p.brand,
                "startAt" to startRow?.get("recordedAt")?.toString(),
                "startOdometer" to startOdo,
                "endAt" to endRow?.get("recordedAt")?.toString(),
                "endOdometer" to endOdo,
                "delta" to delta,
            )
        }

        return mapOf(
            "from" to from.toString(),
            "to" to to.toString(),
            "printers" to results,
        )
    }

    private fun labelPrinters(): List<Printer> =
        printerRepository.findAllByDeletedIsFalse().filter { it.type == "Label" }

    private fun checkAndPersist(printer: Printer): Map<String, Any?> {
        val printerId = printer.id ?: return emptyMap()
        val now = LocalDateTime.now()
        val probe = probeTcp(printer.ip?.trim().orEmpty(), printer.port ?: defaultPort)
        val status = connectivityStatus(printer.ip, probe)

        var odometerTotal: Long? = null
        var deltaSincePrevious: Long? = null
        var hostStatusSnippet: String? = null
        var odometerError: String? = null

        val previousOdometer = loadPreviousOdometer(printerId)

        if (
            probe.reachable &&
            zebraOdometerEnabled &&
            ZebraLinkOsClient.isZebraBrand(printer.brand)
        ) {
            val ip = printer.ip?.trim().orEmpty()
            val port = printer.port ?: defaultPort
            val zebra = ZebraLinkOsClient.queryOdometer(
                ip = ip,
                port = port,
                connectTimeoutMs = connectTimeoutMs,
                queryTimeoutMs = zebraQueryTimeoutMs,
            )
            odometerTotal = zebra.totalLabelCount
            hostStatusSnippet = zebra.hostStatusSnippet
            odometerError = zebra.errorMessage
            if (odometerTotal != null && previousOdometer != null && odometerTotal >= previousOdometer) {
                deltaSincePrevious = odometerTotal - previousOdometer
            }
        }

        val combinedError = when {
            !probe.reachable -> probe.errorMessage
            odometerError != null && ZebraLinkOsClient.isZebraBrand(printer.brand) && zebraOdometerEnabled ->
                odometerError
            else -> null
        }

        jdbcDao.executeUpdate(
            """
                INSERT INTO printer_label_odometer_log (
                    printer_id, odometer_total, delta_since_previous,
                    reachable, latency_ms, error_message, host_status_snippet, recorded_at
                ) VALUES (
                    :printerId, :odometerTotal, :deltaSincePrevious,
                    :reachable, :latencyMs, :error, :hostStatus, :recordedAt
                )
            """.trimIndent(),
            mapOf(
                "printerId" to printerId,
                "odometerTotal" to odometerTotal,
                "deltaSincePrevious" to deltaSincePrevious,
                "reachable" to if (probe.reachable) 1 else 0,
                "latencyMs" to probe.latencyMs,
                "error" to combinedError?.take(255),
                "hostStatus" to hostStatusSnippet?.take(500),
                "recordedAt" to now,
            ),
        )

        if (odometerTotal != null) {
            jdbcDao.executeUpdate(
                """
                    UPDATE printer SET
                        last_label_odometer = :odometer,
                        last_label_odometer_at = :checkedAt
                    WHERE id = :id
                """.trimIndent(),
                mapOf(
                    "id" to printerId,
                    "odometer" to odometerTotal,
                    "checkedAt" to now,
                ),
            )
        }

        return mapOf(
            "id" to printerId,
            "code" to printer.code,
            "name" to printer.name,
            "type" to printer.type,
            "brand" to printer.brand,
            "ip" to printer.ip,
            "port" to (printer.port ?: defaultPort),
            "status" to status,
            "reachable" to probe.reachable,
            "latencyMs" to probe.latencyMs,
            "errorMessage" to combinedError,
            "odometerTotal" to odometerTotal,
            "deltaSincePrevious" to deltaSincePrevious,
            "lastLabelOdometer" to (odometerTotal ?: previousOdometer),
            "hostStatusSnippet" to hostStatusSnippet,
            "zebraOdometerEnabled" to zebraOdometerEnabled,
            "lastCheckAt" to now,
        )
    }

    private fun loadPreviousOdometer(printerId: Long): Long? {
        val row = jdbcDao.queryForList(
            "SELECT last_label_odometer AS v FROM printer WHERE id = :id",
            mapOf("id" to printerId),
        ).firstOrNull() ?: return null
        return numberValue(row["v"])
    }

    private fun rowToPrinterStatus(row: Map<String, Any>): Map<String, Any?> {
        val ip = row["ip"]?.toString()?.trim().orEmpty()
        val lastReachable = row["lastReachable"]
        val reachable = when (lastReachable) {
            null -> null
            true, 1, 1L -> true
            else -> false
        }
        val status = when {
            ip.isBlank() -> "unconfigured"
            reachable == null -> "unchecked"
            reachable == true -> "online"
            else -> "offline"
        }
        return mapOf(
            "id" to row["id"],
            "code" to row["code"],
            "name" to row["name"],
            "type" to row["type"],
            "brand" to row["brand"],
            "ip" to row["ip"],
            "port" to (row["port"] ?: defaultPort),
            "status" to status,
            "reachable" to reachable,
            "latencyMs" to row["latencyMs"],
            "errorMessage" to row["errorMessage"],
            "odometerTotal" to numberValue(row["odometerTotal"] ?: row["lastLabelOdometer"]),
            "deltaSincePrevious" to numberValue(row["deltaSincePrevious"]),
            "lastLabelOdometerAt" to row["lastLabelOdometerAt"],
            "hostStatusSnippet" to row["hostStatusSnippet"],
            "zebraOdometerEnabled" to zebraOdometerEnabled,
            "lastCheckAt" to row["lastCheckAt"],
        )
    }

    private fun connectivityStatus(ip: String?, probe: ProbeResult): String {
        val trimmed = ip?.trim().orEmpty()
        return when {
            trimmed.isBlank() -> "unconfigured"
            probe.reachable -> "online"
            else -> "offline"
        }
    }

    private fun probeTcp(ip: String, port: Int): ProbeResult {
        if (ip.isBlank()) {
            return ProbeResult(reachable = false, latencyMs = null, errorMessage = "未設定 IP")
        }
        val start = System.currentTimeMillis()
        return try {
            Socket().use { socket ->
                socket.connect(InetSocketAddress(ip, port), connectTimeoutMs)
            }
            ProbeResult(
                reachable = true,
                latencyMs = (System.currentTimeMillis() - start).toInt(),
                errorMessage = null,
            )
        } catch (e: Exception) {
            ProbeResult(
                reachable = false,
                latencyMs = null,
                errorMessage = e.message?.take(200) ?: e.javaClass.simpleName,
            )
        }
    }

    private fun buildSummary(results: List<Map<String, Any?>>): Map<String, Any> {
        return mapOf(
            "total" to results.size,
            "online" to results.count { it["status"] == "online" },
            "offline" to results.count { it["status"] == "offline" },
            "unconfigured" to results.count { it["status"] == "unconfigured" },
            "unchecked" to results.count { it["status"] == "unchecked" },
        )
    }

    private fun numberValue(value: Any?): Long? = when (value) {
        null -> null
        is Number -> value.toLong()
        else -> value.toString().trim().toLongOrNull()
    }
}
