package com.erwan.revwatch.obd

import android.util.Log
import java.io.IOException
import kotlin.math.max
import kotlin.math.min

/**
 * Client ELM327 optimisé pour l'OBDLink LX Bluetooth.
 *
 * Optimisations vs implémentation générique :
 * - Séquence d'init spécifique OBDLink LX (ATS0 = pas d'espaces → moins de bytes)
 * - Timeout minimum à 80 ms (l'OBDLink LX répond en 20-60 ms sur CAN)
 * - [readBatchPids] : envoie jusqu'à 6 PIDs en une seule requête CAN → 5-6x plus rapide
 * - Calibration plus agressive (marge réduite à 1.4x)
 */
class Elm327Client(private val transport: ElmTransport) {

    // Buffer de lecture 4 Ko — réduit les appels read() sur les réponses longues
    private val readBuffer = ByteArray(4096)

    companion object {
        private const val TAG = "ELM327"
        private const val CALIBRATION_SAMPLES = 3
        private const val MIN_TIMEOUT_MS = 80    // OBDLink LX : très rapide sur CAN
        private const val MAX_TIMEOUT_MS = 4000
        private const val TIMEOUT_MARGIN = 1.4   // marge réduite : l'OBDLink LX est stable
        private val WHITESPACE_REGEX = Regex("\\s+")
        private val HEX_PAIR_REGEX = Regex("[0-9A-F]{2}")
        private val NON_HEX_REGEX = Regex("[^0-9A-F]")
        private val VOLTAGE_REGEX = Regex("([0-9]+\\.?[0-9]*)")
    }

    val recentLog = ArrayDeque<String>(50)

    // ── État des timeouts adaptatifs ──
    private var pidTimeoutMs = 1500
    private var atTimeoutMs = 600
    private val recentResponseTimes = ArrayDeque<Long>(20)
    private var consecutiveTimeouts = 0

    // ──────── Initialisation ────────

    fun initialize(): Boolean = try {
        initializeInternal()
    } catch (e: IOException) {
        log("INIT IOException: ${e.message}")
        ObdLogger.logInit("EXCEPTION", "${e.javaClass.simpleName}: ${e.message}")
        false
    }

    private fun initializeInternal(): Boolean {
        ObdLogger.logInit("START", "Initialisation OBDLink LX")

        // Reset — timeout généreux
        val resetResp = command("ATZ", 3000)
        log("ATZ -> ${resetResp.take(100)}")
        ObdLogger.logInit("ATZ", resetResp.replace("\r", "\\r").replace("\n", "\\n").take(100))
        drain()

        // Séquence d'init optimisée OBDLink LX :
        // ATE0   = Echo OFF        → réduit bytes reçus
        // ATL0   = Linefeeds OFF   → réduit bytes reçus
        // ATS0   = Spaces OFF      → réduit encore les bytes (parsing adapté)
        // ATH0   = Headers OFF     → réponses minimales
        // ATAL   = Adaptive Timing → active le timing adaptatif
        // ATAT1  = Auto1           → meilleur compromis vitesse/stabilité
        // ATSP0  = Auto-detect     → détecte CAN, K-Line, etc.
        val setupCmds = listOf("ATE0", "ATL0", "ATS0", "ATH0", "ATAL", "ATAT1", "ATSP0")
        for (atCmd in setupCmds) {
            val resp = command(atCmd, if (atCmd == "ATSP0") 1500 else 800)
            log("$atCmd -> ${resp.take(80)}")
            ObdLogger.logInit(atCmd, resp.replace("\r", "\\r").replace("\n", "\\n").take(80))
        }

        // Vérification de l'identité de l'adaptateur
        val id = command("ATI", 2000)
        log("ATI -> ${id.take(100)}")
        ObdLogger.logInit("ATI", id.replace("\r", "\\r").replace("\n", "\\n").take(100))

        val ok = id.contains("ELM", ignoreCase = true) ||
                id.contains("STN", ignoreCase = true) ||
                id.contains("OBD", ignoreCase = true) ||
                id.contains("v1", ignoreCase = true) ||
                id.contains("v2", ignoreCase = true)

        if (!ok) {
            Log.w(TAG, "Init échouée, ATI: ${id.take(80)}")
            ObdLogger.logInit("ATI_CHECK", "ECHEC — signature inconnue")
            return false
        }
        ObdLogger.logInit("ATI_CHECK", "OK — ${id.trim().take(60)}")

        // Warmup : première requête OBD (lente, détection protocole)
        val warmup = command("0100", 6000)
        log("WARMUP 0100 -> ${warmup.take(120)}")
        ObdLogger.logInit("WARMUP_0100", warmup.replace("\r", "\\r").replace("\n", "\\n").take(120))

        if (warmup.contains("NO DATA") || warmup.contains("UNABLE") || warmup.isBlank()) {
            ObdLogger.logInit("WARMUP_0100", "Echec, nouvelle tentative")
            val retry = command("0100", 6000)
            log("WARMUP RETRY -> ${retry.take(120)}")
            ObdLogger.logInit("WARMUP_RETRY", retry.replace("\r", "\\r").replace("\n", "\\n").take(120))
        }

        calibrate()

        ObdLogger.logInit("COMPLETE", "Initialisation réussie")
        return true
    }

    private fun calibrate() {
        log("CALIBRATION...")
        ObdLogger.logInit("CALIBRATE", "Démarrage calibration ($CALIBRATION_SAMPLES échantillons)")

        val pidTimes = mutableListOf<Long>()
        repeat(CALIBRATION_SAMPLES) { i ->
            val t = measureResponseTime("010C")
            ObdLogger.logInit("CALIB_PID_$i", "010C: ${t}ms")
            if (t > 0) pidTimes.add(t)
        }

        val atTimes = mutableListOf<Long>()
        val t = measureResponseTime("ATRV")
        ObdLogger.logInit("CALIB_AT_0", "ATRV: ${t}ms")
        if (t > 0) atTimes.add(t)

        if (pidTimes.isNotEmpty()) {
            val maxPid = pidTimes.max()
            pidTimeoutMs = (maxPid * TIMEOUT_MARGIN).toInt().coerceIn(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS)
            log("Calibration PID: ${pidTimes}ms → timeout=${pidTimeoutMs}ms")
        } else {
            pidTimeoutMs = 1500
            log("Calibration PID échouée, défaut 1500ms")
            ObdLogger.logInit("CALIB_PID", "ECHEC — défaut 1500ms")
        }

        if (atTimes.isNotEmpty()) {
            val maxAt = atTimes.max()
            atTimeoutMs = (maxAt * TIMEOUT_MARGIN).toInt().coerceIn(MIN_TIMEOUT_MS, 2000)
        }

        log("Timeouts: PID=${pidTimeoutMs}ms AT=${atTimeoutMs}ms")
        ObdLogger.logCalibration(pidTimeoutMs, pidTimeoutMs, atTimeoutMs)
    }

    private fun measureResponseTime(cmd: String): Long {
        val payload = (cmd.trim() + "\r").toByteArray(Charsets.US_ASCII)
        val start = System.currentTimeMillis()
        return try {
            transport.write(payload)
            val resp = readUntilPrompt(MAX_TIMEOUT_MS)
            val elapsed = System.currentTimeMillis() - start
            if (resp.contains('>') && !resp.contains("NO DATA") && !resp.contains("ERROR")) elapsed
            else -1
        } catch (_: Exception) { -1 }
    }

    // ──────── API publique ────────

    fun setBusViaRts(msCan: Boolean): Boolean = transport.setRts(msCan)

    /**
     * Requête multi-PID : envoie jusqu'à 6 PIDs en une seule trame CAN.
     * Ex : readBatchPids(0x01, 0x0C, 0x0D, 0x05, 0x0B, 0x11)
     * → envoie "01 0C 0D 05 0B 11" et parse toutes les réponses d'un coup.
     *
     * Sur CAN ISO 15765-4 avec l'OBDLink LX, cette méthode est 4-6x plus rapide
     * qu'envoyer chaque PID individuellement, car on ne paie le RTT Bluetooth qu'une fois.
     */
    fun readBatchPids(mode: Int, vararg pids: Int): Map<Int, List<Int>> {
        if (pids.isEmpty()) return emptyMap()

        val cmd = "%02X %s".format(mode, pids.joinToString(" ") { "%02X".format(it) })
        val raw = timedCommand(cmd, pidTimeoutMs)

        val result = mutableMapOf<Int, List<Int>>()
        for (pid in pids) {
            val parsed = parseModePid(raw, mode, pid)
            if (parsed != null) result[pid] = parsed
        }
        return result
    }

    fun readPid(mode: Int, pid: Int): List<Int>? {
        val cmd = "%02X%02X".format(mode, pid)
        val raw = timedCommand(cmd, pidTimeoutMs)
        val result = parseModePid(raw, mode, pid)
        if (result == null && raw.isNotBlank()) log("PARSE FAIL $cmd -> ${raw.take(100)}")
        ObdLogger.logParse(cmd, raw, result,
            if (result != null) "bytes=${result.joinToString(",") { "%02X".format(it) }}"
            else if (raw.isBlank()) "empty_response" else "parse_failed")
        return result
    }

    fun readPidRetry(mode: Int, pid: Int): List<Int>? {
        val result = readPid(mode, pid)
        if (result != null) return result
        ObdLogger.log("ELM", "Retry ${"%02X%02X".format(mode, pid)}")
        return readPid(mode, pid)
    }

    fun readMode22(pid: Int): List<Int>? {
        val cmd = "22%04X".format(pid)
        val raw = timedCommand(cmd, pidTimeoutMs)
        val result = parseMode22(raw, pid)
        if (result == null && raw.isNotBlank()) log("PARSE FAIL $cmd -> ${raw.take(100)}")
        ObdLogger.logParse(cmd, raw, result,
            if (result != null) "bytes=${result.joinToString(",") { "%02X".format(it) }}"
            else if (raw.isBlank()) "empty_response" else "parse_failed")
        return result
    }

    fun readMode22Retry(pid: Int): List<Int>? {
        val result = readMode22(pid)
        if (result != null) return result
        ObdLogger.log("ELM", "Retry 22${"%04X".format(pid)}")
        return readMode22(pid)
    }

    fun setHeader(header: String) {
        val resp = command("ATSH$header", atTimeoutMs)
        log("ATSH$header -> ${resp.take(80)}")
        ObdLogger.log("ELM", "Set header: $header")
    }

    fun readRaw(command: String): String = timedCommand(command, pidTimeoutMs)

    fun readVoltage(): Double? {
        val raw = timedCommand("ATRV", atTimeoutMs)
        val result = VOLTAGE_REGEX.find(raw)?.groupValues?.get(1)?.toDoubleOrNull()
        ObdLogger.logParse("ATRV", raw, null,
            if (result != null) "voltage=${result}V" else "parse_failed")
        return result
    }

    fun close() = transport.close()

    // ──────── Couche basse ────────

    private fun timedCommand(cmd: String, baseTimeoutMs: Int): String {
        val start = System.currentTimeMillis()
        val resp = command(cmd, baseTimeoutMs)
        val elapsed = System.currentTimeMillis() - start
        val timedOut = !resp.contains('>')
        ObdLogger.logCommand(cmd, resp, elapsed, timedOut)

        if (!timedOut) {
            consecutiveTimeouts = 0
            synchronized(recentResponseTimes) {
                if (recentResponseTimes.size >= 20) recentResponseTimes.removeFirst()
                recentResponseTimes.addLast(elapsed)

                if (recentResponseTimes.size >= 8) {
                    val maxRecent = recentResponseTimes.max()
                    val ideal = (maxRecent * TIMEOUT_MARGIN).toInt().coerceIn(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS)
                    val old = pidTimeoutMs
                    pidTimeoutMs = pidTimeoutMs + ((ideal - pidTimeoutMs) * 0.3).toInt()
                    pidTimeoutMs = pidTimeoutMs.coerceIn(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS)
                    if (pidTimeoutMs != old) {
                        ObdLogger.logAdaptiveTimeout(pidTimeoutMs, pidTimeoutMs,
                            "adapt max=${maxRecent}ms")
                    }
                }
            }
        } else {
            consecutiveTimeouts++
            if (consecutiveTimeouts >= 2) {
                val old = pidTimeoutMs
                pidTimeoutMs = min((pidTimeoutMs * 1.5).toInt(), MAX_TIMEOUT_MS)
                log("Timeout x$consecutiveTimeouts → PID=${pidTimeoutMs}ms")
                ObdLogger.logAdaptiveTimeout(pidTimeoutMs, pidTimeoutMs,
                    "timeout x$consecutiveTimeouts was ${old}ms")
            }
        }
        return resp
    }

    private fun command(command: String, timeoutMs: Int): String {
        val payload = (command.trim() + "\r").toByteArray(Charsets.US_ASCII)
        transport.write(payload)
        return try {
            readUntilPrompt(timeoutMs)
        } catch (_: Exception) { "" }
    }

    private fun readUntilPrompt(timeoutMs: Int): String {
        val deadline = System.currentTimeMillis() + max(timeoutMs, 100)
        val out = StringBuilder()

        while (System.currentTimeMillis() < deadline) {
            val remaining = (deadline - System.currentTimeMillis()).toInt().coerceIn(1, 300)
            val read = try {
                transport.read(readBuffer, remaining)
            } catch (_: Exception) { 0 }

            if (read > 0) {
                out.append(String(readBuffer, 0, read, Charsets.US_ASCII))
                if (out.contains('>')) break
            }
        }
        return out.toString()
    }

    private fun drain() {
        val end = System.currentTimeMillis() + 300
        while (System.currentTimeMillis() < end) {
            try {
                val n = transport.read(readBuffer, 60)
                if (n <= 0) break
            } catch (_: Exception) { break }
        }
    }

    // ──────── Parsing ────────
    //
    // Avec ATS0 (pas d'espaces), la réponse multi-PID ressemble à :
    //   "410C1AF0\r410D3C\r41055A\r>"
    // On concatène tout, on coupe en paires hex, et on cherche les marqueurs 41 xx.
    //
    // Avec ATS1 (espaces), les tokens "41 0C 1A F0" sont directement parsables.
    // Les deux formats sont gérés par extractHexTokens.

    private fun extractHexTokens(raw: String): List<Int> {
        val upper = raw.uppercase()
        val cleaned = upper
            .replace('>', ' ')
            .replace("SEARCHING...", "")
            .replace("SEARCHING", "")
            .replace("NO DATA", "")
            .replace("STOPPED", "")
            .replace("BUS INIT:", "")
            .replace("BUS INIT", "")
            .replace("UNABLE TO CONNECT", "")
            .replace("CAN ERROR", "")
            .replace("FB ERROR", "")
            .replace("DATA ERROR", "")
            .replace("BUFFER FULL", "")
            .replace("ACT ALERT", "")
            .replace("LV RESET", "")
            .replace("ERROR", "")
            .replace("?", "")
            .replace("OK", " ")
            .replace("\r", " ")
            .replace("\n", " ")

        // Priorité 1 : tokens séparés par espaces (ATS1)
        val spacedTokens = cleaned
            .split(WHITESPACE_REGEX)
            .filter { it.matches(HEX_PAIR_REGEX) }
            .mapNotNull { it.toIntOrNull(16) }

        if (spacedTokens.isNotEmpty()) return spacedTokens

        // Fallback : bytes concaténés sans espace (ATS0)
        val hexOnly = cleaned.replace(NON_HEX_REGEX, "")
        if (hexOnly.length >= 4 && hexOnly.length % 2 == 0) {
            return hexOnly.chunked(2).mapNotNull { it.toIntOrNull(16) }
        }

        return emptyList()
    }

    private fun parseModePid(raw: String, mode: Int, pid: Int): List<Int>? {
        val tokens = extractHexTokens(raw)
        if (tokens.isEmpty()) return null

        val modeResp = mode + 0x40
        for (i in 0 until tokens.size - 1) {
            if (tokens[i] == modeResp && tokens[i + 1] == pid) {
                if (i + 2 >= tokens.size) return emptyList()
                return tokens.subList(i + 2, tokens.size)
            }
        }
        return null
    }

    private fun parseMode22(raw: String, pid: Int): List<Int>? {
        val tokens = extractHexTokens(raw)
        if (tokens.isEmpty()) return null

        val pidHi = (pid shr 8) and 0xFF
        val pidLo = pid and 0xFF
        for (i in 0 until tokens.size - 2) {
            if (tokens[i] == 0x62 && tokens[i + 1] == pidHi && tokens[i + 2] == pidLo) {
                if (i + 3 >= tokens.size) return emptyList()
                return tokens.subList(i + 3, tokens.size)
            }
        }
        return null
    }

    private fun log(msg: String) {
        Log.d(TAG, msg)
        synchronized(recentLog) {
            if (recentLog.size >= 50) recentLog.removeFirst()
            recentLog.addLast(msg)
        }
    }
}
