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

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.user.entity.User
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit

@Service
open class ClientPresenceService(
    private val jdbcDao: JdbcDao,
) {
    @Value("\${fpsms.client-presence.offline-threshold-sec:90}")
    private var offlineThresholdSec: Long = 90

    @Value("\${fpsms.client-presence.idle-threshold-sec:300}")
    private var idleThresholdSec: Long = 300

    @Value("\${fpsms.client-presence.history-sample-sec:60}")
    private var historySampleSec: Long = 60

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

    /** Omit from live monitor and purge rows with no heartbeat for longer than this. */
    @Value("\${fpsms.client-presence.live-presence-retention-days:7}")
    private var livePresenceRetentionDays: Long = 7

    data class HeartbeatRequest(
        val deviceId: String,
        val displayName: String? = null,
        val currentPath: String? = null,
        val rttMs: Int? = null,
        val connectionQuality: String? = null,
        val navigatorOnline: Boolean? = true,
        val userAgent: String? = null,
        val clientType: String? = null,
        val clientIp: String? = null,
        val activityBump: Boolean = true,
    )

    @Transactional
    open fun upsertHeartbeat(user: User, req: HeartbeatRequest) {
        val deviceId = req.deviceId.trim().take(64)
        require(deviceId.isNotEmpty()) { "deviceId is required" }

        val quality = normalizeQuality(req.connectionQuality)
        val clientType = resolveClientType(req.userAgent, req.clientType)
        val path = req.currentPath?.trim()?.take(512)
        val displayName = req.displayName?.trim()?.take(128)?.ifBlank { null }
        val clientIp = req.clientIp?.trim()?.take(45)?.ifBlank { null }
        val now = LocalDateTime.now()

        val existing = jdbcDao.queryForList(
            """
                SELECT
                    device_id AS deviceId,
                    user_id AS userId,
                    username,
                    display_name AS displayName,
                    current_path AS currentPath,
                    client_type AS clientType,
                    rtt_ms AS rttMs,
                    connection_quality AS connectionQuality,
                    navigator_online AS navigatorOnline,
                    last_heartbeat AS lastHeartbeat,
                    last_activity AS lastActivity
                FROM client_presence
                WHERE device_id = :deviceId
            """.trimIndent(),
            mapOf("deviceId" to deviceId),
        )

        val statusBefore = existing.firstOrNull()?.let { deriveStatus(it, now) }

        if (existing.isEmpty()) {
            val insertSql = """
                INSERT INTO client_presence (
                    device_id, user_id, username, display_name, current_path, client_type,
                    user_agent, client_ip, rtt_ms, connection_quality, navigator_online,
                    last_heartbeat, last_activity
                ) VALUES (
                    :deviceId, :userId, :username, :displayName, :currentPath, :clientType,
                    :userAgent, :clientIp, :rttMs, :connectionQuality, :navigatorOnline,
                    :now, :lastActivity
                )
            """.trimIndent()
            jdbcDao.executeUpdate(
                insertSql,
                mapOf(
                    "deviceId" to deviceId,
                    "userId" to user.id,
                    "username" to user.username,
                    "displayName" to displayName,
                    "currentPath" to path,
                    "clientType" to clientType,
                    "userAgent" to req.userAgent?.take(512),
                    "clientIp" to clientIp,
                    "rttMs" to req.rttMs,
                    "connectionQuality" to quality,
                    "navigatorOnline" to if (req.navigatorOnline != false) 1 else 0,
                    "now" to now,
                    "lastActivity" to now,
                ),
            )
        } else {
            val lastActivity = if (req.activityBump) now else null
            val updateSql = if (lastActivity != null) {
                """
                    UPDATE client_presence SET
                        user_id = :userId,
                        username = :username,
                        display_name = COALESCE(:displayName, display_name),
                        current_path = :currentPath,
                        client_type = :clientType,
                        user_agent = :userAgent,
                        client_ip = :clientIp,
                        rtt_ms = :rttMs,
                        connection_quality = :connectionQuality,
                        navigator_online = :navigatorOnline,
                        last_heartbeat = :now,
                        last_activity = :lastActivity
                    WHERE device_id = :deviceId
                """.trimIndent()
            } else {
                """
                    UPDATE client_presence SET
                        user_id = :userId,
                        username = :username,
                        display_name = COALESCE(:displayName, display_name),
                        current_path = :currentPath,
                        client_type = :clientType,
                        user_agent = :userAgent,
                        client_ip = :clientIp,
                        rtt_ms = :rttMs,
                        connection_quality = :connectionQuality,
                        navigator_online = :navigatorOnline,
                        last_heartbeat = :now
                    WHERE device_id = :deviceId
                """.trimIndent()
            }

            val args = mutableMapOf<String, Any?>(
                "deviceId" to deviceId,
                "userId" to user.id,
                "username" to user.username,
                "displayName" to displayName,
                "currentPath" to path,
                "clientType" to clientType,
                "userAgent" to req.userAgent?.take(512),
                "clientIp" to clientIp,
                "rttMs" to req.rttMs,
                "connectionQuality" to quality,
                "navigatorOnline" to if (req.navigatorOnline != false) 1 else 0,
                "now" to now,
            )
            if (lastActivity != null) {
                args["lastActivity"] = lastActivity
            }
            jdbcDao.executeUpdate(updateSql, args)
        }

        val snapshot = mapOf<String, Any?>(
            "deviceId" to deviceId,
            "userId" to user.id,
            "username" to user.username,
            "displayName" to displayName,
            "currentPath" to path,
            "clientType" to clientType,
            "clientIp" to clientIp,
            "rttMs" to req.rttMs,
            "connectionQuality" to quality,
            "navigatorOnline" to if (req.navigatorOnline != false) 1 else 0,
            "lastHeartbeat" to now,
            "lastActivity" to if (req.activityBump) now else existing.firstOrNull()?.get("lastActivity"),
        )
        val statusAfter = deriveStatus(snapshot, now)
        maybeRecordEvent(snapshot, statusAfter, statusBefore, now)
    }

    @Transactional
    open fun recordStaleOfflineEvents() {
        val now = LocalDateTime.now()
        val cutoff = now.minusSeconds(offlineThresholdSec)
        val rows = jdbcDao.queryForList(
            """
                SELECT
                    device_id AS deviceId,
                    user_id AS userId,
                    username,
                    display_name AS displayName,
                    current_path AS currentPath,
                    client_type AS clientType,
                    client_ip AS clientIp,
                    rtt_ms AS rttMs,
                    connection_quality AS connectionQuality,
                    navigator_online AS navigatorOnline,
                    last_heartbeat AS lastHeartbeat,
                    last_activity AS lastActivity
                FROM client_presence
                WHERE last_heartbeat < :cutoff
            """.trimIndent(),
            mapOf("cutoff" to cutoff),
        )
        for (row in rows) {
            val lastHb = parseDateTime(row["lastHeartbeat"])
            val statusBeforeOffline = if (lastHb != null) deriveStatus(row, lastHb) else null
            maybeRecordEvent(row, "offline", statusBeforeOffline, now)
        }
    }

    @Transactional(readOnly = true)
    open fun listActive(clientTypeFilter: String?): List<Map<String, Any>> {
        val type = clientTypeFilter?.trim()?.lowercase()?.takeIf { it.isNotBlank() && it != "all" }
        val minHeartbeat = LocalDateTime.now().minusDays(livePresenceRetentionDays.coerceAtLeast(1))
        val sql = buildString {
            append(
                """
                SELECT
                    device_id AS deviceId,
                    user_id AS userId,
                    username,
                    display_name AS displayName,
                    current_path AS currentPath,
                    client_type AS clientType,
                    user_agent AS userAgent,
                    client_ip AS clientIp,
                    rtt_ms AS rttMs,
                    connection_quality AS connectionQuality,
                    navigator_online AS navigatorOnline,
                    last_heartbeat AS lastHeartbeat,
                    last_activity AS lastActivity
                FROM client_presence
                WHERE last_heartbeat >= :minHeartbeat
                """.trimIndent(),
            )
            if (type != null) {
                append(" AND client_type = :clientType")
            }
            append(" ORDER BY last_heartbeat DESC")
        }
        val args = mutableMapOf<String, Any>("minHeartbeat" to minHeartbeat)
        if (type != null) {
            args["clientType"] = type
        }
        val rows = jdbcDao.queryForList(sql, args)
        val now = LocalDateTime.now()
        return rows.map { row -> enrichWithStatus(row, now) }
    }

    @Transactional
    open fun purgeStalePresence(): Int {
        val days = livePresenceRetentionDays.coerceAtLeast(1)
        val cutoff = LocalDateTime.now().minusDays(days)
        return jdbcDao.executeUpdate(
            """
                DELETE FROM client_presence
                WHERE last_heartbeat < :cutoff
            """.trimIndent(),
            mapOf("cutoff" to cutoff),
        )
    }

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

        val type = clientTypeFilter?.trim()?.lowercase()?.takeIf { it.isNotBlank() && it != "all" }
        val deviceId = deviceIdFilter?.trim()?.takeIf { it.isNotBlank() }
        val statuses = (statusFilter?.map { it.trim().lowercase() }?.filter { it.isNotBlank() }
            ?: listOf("offline", "poor"))
            .distinct()
        require(statuses.isNotEmpty()) { "At least one status is required" }

        val sql = buildString {
            append(
                """
                SELECT
                    id,
                    device_id AS deviceId,
                    user_id AS userId,
                    username,
                    display_name AS displayName,
                    client_type AS clientType,
                    status,
                    connection_quality AS connectionQuality,
                    rtt_ms AS rttMs,
                    navigator_online AS navigatorOnline,
                    current_path AS currentPath,
                    client_ip AS clientIp,
                    recorded_at AS recordedAt
                FROM client_presence_event
                WHERE recorded_at >= :from AND recorded_at <= :to
                """.trimIndent(),
            )
            append(" AND status IN (:statuses)")
            if (type != null) {
                append(" AND client_type = :clientType")
            }
            if (deviceId != null) {
                append(" AND device_id = :deviceId")
            }
            append(" ORDER BY recorded_at DESC, id DESC")
        }

        val args = mutableMapOf<String, Any>(
            "from" to from,
            "to" to to,
            "statuses" to statuses,
        )
        if (type != null) {
            args["clientType"] = type
        }
        if (deviceId != null) {
            args["deviceId"] = deviceId
        }

        val events = jdbcDao.queryForList(sql, args)
        val offlineCount = events.count { it["status"] == "offline" }
        val poorCount = events.count { it["status"] == "poor" }
        val deviceCount = events.mapNotNull { it["deviceId"]?.toString() }.distinct().size

        return mapOf(
            "events" to events,
            "summary" to mapOf(
                "total" to events.size,
                "offline" to offlineCount,
                "poor" to poorCount,
                "deviceCount" to deviceCount,
                "from" to from.toString(),
                "to" to to.toString(),
            ),
        )
    }

    private fun maybeRecordEvent(
        row: Map<String, Any?>,
        status: String,
        previousStatus: String?,
        now: LocalDateTime,
    ) {
        val deviceId = row["deviceId"]?.toString()?.trim().orEmpty()
        if (deviceId.isEmpty()) return

        val last = jdbcDao.queryForList(
            """
                SELECT status, recorded_at AS recordedAt
                FROM client_presence_event
                WHERE device_id = :deviceId
                ORDER BY id DESC
                LIMIT 1
            """.trimIndent(),
            mapOf("deviceId" to deviceId),
        )

        val lastStatus = last.firstOrNull()?.get("status")?.toString()
        val lastAt = parseDateTime(last.firstOrNull()?.get("recordedAt"))
        val secondsSinceLast = if (lastAt != null) ChronoUnit.SECONDS.between(lastAt, now) else Long.MAX_VALUE

        val isIncident = status == "offline" || status == "poor"
        val statusChanged = lastStatus != status || previousStatus != status
        val shouldRecord = when {
            last.isEmpty() && isIncident -> true
            statusChanged -> true
            isIncident && secondsSinceLast >= historySampleSec -> true
            else -> false
        }
        if (!shouldRecord) return

        jdbcDao.executeUpdate(
            """
                INSERT INTO client_presence_event (
                    device_id, user_id, username, display_name, client_type,
                    status, connection_quality, rtt_ms, navigator_online, current_path, client_ip, recorded_at
                ) VALUES (
                    :deviceId, :userId, :username, :displayName, :clientType,
                    :status, :connectionQuality, :rttMs, :navigatorOnline, :currentPath, :clientIp, :recordedAt
                )
            """.trimIndent(),
            mapOf(
                "deviceId" to deviceId,
                "userId" to row["userId"],
                "username" to row["username"],
                "displayName" to row["displayName"],
                "clientType" to (row["clientType"]?.toString() ?: "unknown"),
                "status" to status,
                "connectionQuality" to row["connectionQuality"],
                "rttMs" to row["rttMs"],
                "navigatorOnline" to when (row["navigatorOnline"]) {
                    false, 0, 0L -> 0
                    else -> 1
                },
                "currentPath" to row["currentPath"],
                "clientIp" to row["clientIp"],
                "recordedAt" to now,
            ),
        )
    }

    private fun deriveStatus(row: Map<String, Any?>, now: LocalDateTime): String {
        val lastHb = parseDateTime(row["lastHeartbeat"])
        val lastAct = parseDateTime(row["lastActivity"]) ?: lastHb
        val quality = row["connectionQuality"]?.toString()?.lowercase() ?: "good"
        val navOnline = row["navigatorOnline"] == true ||
            row["navigatorOnline"] == 1 ||
            row["navigatorOnline"] == 1L

        val secondsSinceHb = if (lastHb != null) ChronoUnit.SECONDS.between(lastHb, now) else Long.MAX_VALUE
        val secondsSinceAct = if (lastAct != null) ChronoUnit.SECONDS.between(lastAct, now) else Long.MAX_VALUE

        return when {
            secondsSinceHb >= offlineThresholdSec -> "offline"
            !navOnline || quality == "poor" -> "poor"
            secondsSinceAct >= idleThresholdSec -> "idle"
            else -> "online"
        }
    }

    private fun enrichWithStatus(row: Map<String, Any>, now: LocalDateTime): Map<String, Any> {
        val lastHb = parseDateTime(row["lastHeartbeat"])
        val lastAct = parseDateTime(row["lastActivity"]) ?: lastHb
        val quality = row["connectionQuality"]?.toString()?.lowercase() ?: "good"
        val navOnline = row["navigatorOnline"] == true ||
            row["navigatorOnline"] == 1 ||
            row["navigatorOnline"] == 1L

        val secondsSinceHb = if (lastHb != null) ChronoUnit.SECONDS.between(lastHb, now) else Long.MAX_VALUE
        val secondsSinceAct = if (lastAct != null) ChronoUnit.SECONDS.between(lastAct, now) else Long.MAX_VALUE

        val status = when {
            secondsSinceHb >= offlineThresholdSec -> "offline"
            !navOnline || quality == "poor" -> "poor"
            secondsSinceAct >= idleThresholdSec -> "idle"
            else -> "online"
        }

        val result = LinkedHashMap<String, Any?>(row.size + 4)
        row.forEach { (k, v) -> result[k] = v }
        result["status"] = status
        result["secondsSinceHeartbeat"] = if (secondsSinceHb == Long.MAX_VALUE) null else secondsSinceHb
        result["secondsSinceActivity"] = if (secondsSinceAct == Long.MAX_VALUE) null else secondsSinceAct
        result["offlineThresholdSec"] = offlineThresholdSec
        result["idleThresholdSec"] = idleThresholdSec
        @Suppress("UNCHECKED_CAST")
        return result as Map<String, Any>
    }

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

    private fun normalizeQuality(raw: String?): String {
        val q = raw?.trim()?.lowercase() ?: return "good"
        return when (q) {
            "poor", "offline" -> q
            else -> "good"
        }
    }

    fun resolveClientType(userAgent: String?, clientReported: String?): String {
        val reported = clientReported?.trim()?.lowercase()?.takeIf {
            it in setOf("tablet", "desktop", "mobile", "unknown")
        }
        if (reported != null) return reported
        return detectClientType(userAgent)
    }

    fun detectClientType(userAgent: String?): String {
        if (userAgent.isNullOrBlank()) return "unknown"
        val ua = userAgent.lowercase()
        return when {
            ua.contains("ipad") || ua.contains("tablet") -> "tablet"
            ua.contains("iphone") || ua.contains("ipod") -> "mobile"
            // Warehouse clients are mostly Android slates; "Mobile" appears in tablet UAs too.
            ua.contains("android") -> "tablet"
            ua.contains("mobile") -> "mobile"
            else -> "desktop"
        }
    }
}
