package com.correx.core.events.serialization

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive

private object AnySerializer : KSerializer<Any> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("AnyValue")

    override fun serialize(encoder: Encoder, value: Any) {
        val jsonEncoder = encoder as? JsonEncoder
            ?: throw SerializationException("AnySerializer requires a JSON encoder")
        jsonEncoder.encodeJsonElement(value.toJsonElement())
    }

    override fun deserialize(decoder: Decoder): Any {
        val jsonDecoder = decoder as? JsonDecoder
            ?: throw SerializationException("AnySerializer requires a JSON decoder")
        return jsonDecoder.decodeJsonElement().toAnyOrNull()
            ?: throw SerializationException("null values are not supported in Map<String, Any>")
    }
}

// Null values are dropped, not written: a Map<String, Any> can't hold a Kotlin null, so a null
// (e.g. a tool arg the model emitted as JSON null) carries no information. Serialize and deserialize
// must stay symmetric — the event log has to round-trip anything it writes, or a single null-valued
// map key makes the whole session's events undecodable (500 on /events, unreplayable).
private fun Any?.toJsonElement(): JsonElement = when (this) {
    null -> JsonNull
    is String -> JsonPrimitive(this)
    is Number -> JsonPrimitive(this)
    is Boolean -> JsonPrimitive(this)
    is Map<*, *> -> JsonObject(entries.mapNotNull { (k, v) -> v?.let { k.toString() to it.toJsonElement() } }.toMap())
    is List<*> -> JsonArray(mapNotNull { it?.toJsonElement() })
    else -> throw SerializationException("Unsupported type: ${this::class.simpleName}")
}

// Returns null for JSON null (and the literal "null" scalar); callers drop those entries so a
// null value never reaches the non-null Map<String, Any>.
private fun JsonElement.toAnyOrNull(): Any? = when (this) {
    is JsonNull -> null
    is JsonPrimitive -> {
        val content = this.content
        when {
            content == "true" || content == "false" -> content == "true"
            content == "null" -> null
            content.toDoubleOrNull() != null && content.contains('.') -> content.toDouble()
            content.toLongOrNull() != null -> content.toLong()
            else -> content // fallback to string
        }
    }
    is JsonObject -> entries.mapNotNull { (k, v) -> v.toAnyOrNull()?.let { k to it } }.toMap()
    is JsonArray -> mapNotNull { it.toAnyOrNull() }
}

object AnyMapSerializer : KSerializer<Map<String, Any>> {
    private val delegate = MapSerializer(String.serializer(), AnySerializer)
    override val descriptor: SerialDescriptor = delegate.descriptor

    override fun deserialize(decoder: Decoder): Map<String, Any> {
        // Decode the object directly and drop null-valued keys, rather than delegating per-value
        // (which throws on a null before the map can skip it). Keeps a null-containing map readable.
        val jsonDecoder = decoder as? JsonDecoder
            ?: return delegate.deserialize(decoder)
        val obj = jsonDecoder.decodeJsonElement() as? JsonObject
            ?: throw SerializationException("expected a JSON object for Map<String, Any>")
        return obj.entries.mapNotNull { (k, v) -> v.toAnyOrNull()?.let { k to it } }.toMap()
    }

    override fun serialize(encoder: Encoder, value: Map<String, Any>) =
        delegate.serialize(encoder, value)
}
