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 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.LocalDateTime
import java.time.temporal.ChronoUnit

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

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

    @Value("\${fpsms.printer-monitor.offline-event-sample-sec:300}")
    private var offlineEventSampleSec: Long = 300

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

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

    @Transactional
    open fun checkAllPrinters(): Map<String, Any> {
        val printers = printerRepository.findAllByDeletedIsFalse()
        val results = printers.map { checkAndPersist(it) }
        return buildListResponse(results)
    }

    @Transactional(readOnly = true)
    open fun listStatus(): Map<String, Any> {
        val rows = jdbcDao.queryForList(
            """
                SELECT
                    id,
                    code,
                    name,
                    type,
                    brand,
                    description,
                    ip,
                    port,
                    dpi,
                    last_reachable AS lastReachable,
                    last_check_at AS lastCheckAt,
                    last_check_error AS lastCheckError,
                    last_check_latency_ms AS lastCheckLatencyMs
                FROM printer
                WHERE deleted = 0
                ORDER BY name, code, id
            """.trimIndent(),
        )
        val results = rows.map { rowToStatus(it) }
        return buildListResponse(results)
    }

    @Transactional(readOnly = true)
    open fun listHistory(
        from: LocalDateTime,
        to: LocalDateTime,
        printerId: Long?,
    ): 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 sql = buildString {
            append(
                """
                SELECT
                    id,
                    printer_id AS printerId,
                    printer_code AS printerCode,
                    printer_name AS printerName,
                    printer_type AS printerType,
                    brand,
                    ip,
                    port,
                    reachable,
                    error_message AS errorMessage,
                    latency_ms AS latencyMs,
                    recorded_at AS recordedAt
                FROM printer_connectivity_event
                WHERE recorded_at >= :from AND recorded_at <= :to
                  AND reachable = 0
                """.trimIndent(),
            )
            if (printerId != null) {
                append(" AND printer_id = :printerId")
            }
            append(" ORDER BY recorded_at DESC, id DESC")
        }
        val args = mutableMapOf<String, Any>("from" to from, "to" to to)
        if (printerId != null) {
            args["printerId"] = printerId
        }
        val events = jdbcDao.queryForList(sql, args)
        return mapOf(
            "events" to events,
            "summary" to mapOf(
                "total" to events.size,
                "from" to from.toString(),
                "to" to to.toString(),
            ),
        )
    }

    private fun checkAndPersist(printer: Printer): Map<String, Any?> {
        val probe = probePrinter(printer)
        val now = LocalDateTime.now()
        val printerId = printer.id ?: return emptyMap()

        val previousReachable = jdbcDao.queryForList(
            "SELECT last_reachable AS lastReachable FROM printer WHERE id = :id",
            mapOf("id" to printerId),
        ).firstOrNull()?.get("lastReachable")

        jdbcDao.executeUpdate(
            """
                UPDATE printer SET
                    last_reachable = :reachable,
                    last_check_at = :checkedAt,
                    last_check_error = :error,
                    last_check_latency_ms = :latencyMs
                WHERE id = :id
            """.trimIndent(),
            mapOf(
                "id" to printerId,
                "reachable" to probe.reachable?.let { if (it) 1 else 0 },
                "checkedAt" to now,
                "error" to probe.errorMessage?.take(255),
                "latencyMs" to probe.latencyMs,
            ),
        )

        maybeRecordOfflineEvent(printer, probe, previousReachable, now)

        return mapOf(
            "id" to printerId,
            "code" to printer.code,
            "name" to printer.name,
            "type" to printer.type,
            "brand" to printer.brand,
            "description" to printer.description,
            "ip" to printer.ip,
            "port" to (printer.port ?: defaultPort),
            "dpi" to printer.dpi,
            "status" to probe.status,
            "reachable" to probe.reachable,
            "latencyMs" to probe.latencyMs,
            "errorMessage" to probe.errorMessage,
            "lastCheckAt" to now,
        )
    }

    private fun rowToStatus(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"],
            "description" to row["description"],
            "ip" to row["ip"],
            "port" to (row["port"] ?: defaultPort),
            "dpi" to row["dpi"],
            "status" to status,
            "reachable" to reachable,
            "latencyMs" to row["lastCheckLatencyMs"],
            "errorMessage" to row["lastCheckError"],
            "lastCheckAt" to row["lastCheckAt"],
        )
    }

    private fun probePrinter(printer: Printer): ProbeResult {
        val ip = printer.ip?.trim().orEmpty()
        if (ip.isBlank()) {
            return ProbeResult(
                status = "unconfigured",
                reachable = null,
                latencyMs = null,
                errorMessage = "未設定 IP",
            )
        }
        val port = printer.port ?: defaultPort
        return probeTcp(ip, port)
    }

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

    private fun maybeRecordOfflineEvent(
        printer: Printer,
        probe: ProbeResult,
        previousReachable: Any?,
        now: LocalDateTime,
    ) {
        if (probe.reachable != false) return
        val printerId = printer.id ?: return

        val prev = when (previousReachable) {
            null -> null
            true, 1, 1L -> true
            else -> false
        }
        val lastEvent = jdbcDao.queryForList(
            """
                SELECT reachable, recorded_at AS recordedAt
                FROM printer_connectivity_event
                WHERE printer_id = :printerId
                ORDER BY id DESC
                LIMIT 1
            """.trimIndent(),
            mapOf("printerId" to printerId),
        ).firstOrNull()

        val lastReachable = lastEvent?.get("reachable")
        val lastWasOffline = lastReachable == false || lastReachable == 0 || lastReachable == 0L
        val lastAt = parseDateTime(lastEvent?.get("recordedAt"))
        val secondsSince = if (lastAt != null) ChronoUnit.SECONDS.between(lastAt, now) else Long.MAX_VALUE

        val shouldRecord = prev != false || !lastWasOffline || secondsSince >= offlineEventSampleSec
        if (!shouldRecord) return

        jdbcDao.executeUpdate(
            """
                INSERT INTO printer_connectivity_event (
                    printer_id, printer_code, printer_name, printer_type, brand,
                    ip, port, reachable, error_message, latency_ms, recorded_at
                ) VALUES (
                    :printerId, :code, :name, :type, :brand,
                    :ip, :port, 0, :error, :latencyMs, :recordedAt
                )
            """.trimIndent(),
            mapOf(
                "printerId" 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),
                "error" to probe.errorMessage?.take(255),
                "latencyMs" to probe.latencyMs,
                "recordedAt" to now,
            ),
        )
    }

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

    private fun parseDateTime(value: Any?): LocalDateTime? = when (value) {
        null -> null
        is LocalDateTime -> value
        is java.sql.Timestamp -> value.toLocalDateTime()
        else -> {
            val text = value.toString().trim()
            if (text.length >= 19) {
                runCatching { LocalDateTime.parse(text.replace(" ", "T").take(19)) }.getOrNull()
            } else null
        }
    }
}
