package com.wisp.app.nostr

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import androidx.compose.runtime.Immutable
import java.security.MessageDigest

@Immutable
@Serializable
data class NostrEvent(
    val id: String,
    val pubkey: String,
    @Serializable(with = LongAsStringSerializer::class)
    val created_at: Long,
    val kind: Int,
    val tags: List<List<String>>,
    val content: String,
    val sig: String
) {
    companion object {
        private val json = Json { ignoreUnknownKeys = true }
        private val sha256Local = ThreadLocal.withInitial { MessageDigest.getInstance("SHA-256") }

        fun create(
            privkey: ByteArray,
            pubkey: ByteArray,
            kind: Int,
            content: String,
            tags: List<List<String>> = emptyList(),
            createdAt: Long = System.currentTimeMillis() / 1000
        ): NostrEvent {
            val pubkeyHex = pubkey.toHex()
            val id = computeId(pubkeyHex, createdAt, kind, tags, content)
            val idBytes = id.hexToByteArray()
            val sig = Keys.sign(privkey, idBytes).toHex()
            return NostrEvent(
                id = id,
                pubkey = pubkeyHex,
                created_at = createdAt,
                kind = kind,
                tags = tags,
                content = content,
                sig = sig
            )
        }

        fun computeId(
            pubkey: String,
            createdAt: Long,
            kind: Int,
            tags: List<List<String>>,
            content: String
        ): String {
            val serialized = buildString(512) {
                append("[0,\"")
                append(pubkey)
                append("\",")
                append(createdAt)
                append(',')
                append(kind)
                append(",[")
                for (i in tags.indices) {
                    if (i > 0) append(',')
                    val tag = tags[i]
                    append('[')
                    for (j in tag.indices) {
                        if (j > 0) append(',')
                        append('"')
                        appendJsonEscaped(tag[j])
                        append('"')
                    }
                    append(']')
                }
                append("],")
                append('"')
                appendJsonEscaped(content)
                append('"')
                append(']')
            }
            val digest = sha256Local.get().apply { reset() }
            return digest.digest(serialized.toByteArray(Charsets.UTF_8)).toHex()
        }

        fun createUnsigned(
            pubkeyHex: String,
            kind: Int,
            content: String,
            tags: List<List<String>> = emptyList(),
            createdAt: Long = System.currentTimeMillis() / 1000
        ): NostrEvent {
            val id = computeId(pubkeyHex, createdAt, kind, tags, content)
            return NostrEvent(
                id = id,
                pubkey = pubkeyHex,
                created_at = createdAt,
                kind = kind,
                tags = tags,
                content = content,
                sig = ""
            )
        }

        fun fromJson(jsonStr: String): NostrEvent = json.decodeFromString(jsonStr)

        fun fromJsonArray(array: JsonArray): NostrEvent {
            return NostrEvent(
                id = array.jsonArray[0].let {
                    (it as? JsonPrimitive)?.content ?: throw IllegalArgumentException("Missing id")
                },
                pubkey = (array.jsonArray[1] as JsonPrimitive).content,
                created_at = (array.jsonArray[2] as JsonPrimitive).content.toLong(),
                kind = (array.jsonArray[3] as JsonPrimitive).content.toInt(),
                tags = array.jsonArray[4].jsonArray.map { tagArr ->
                    tagArr.jsonArray.map { it.jsonPrimitive.content }
                },
                content = (array.jsonArray[5] as JsonPrimitive).content,
                sig = (array.jsonArray[6] as JsonPrimitive).content
            )
        }
    }

    fun toJson(): String = json.encodeToString(serializer(), this)

    fun withSignature(sig: String): NostrEvent = copy(sig = sig)

    /**
     * Verify the Schnorr signature of this event.
     * Recomputes the event ID and checks the sig against the pubkey.
     * Returns false if the ID doesn't match or the signature is invalid.
     */
    fun verifySignature(): Boolean {
        if (sig.length != 128 || pubkey.length != 64) return false
        val expectedId = computeId(pubkey, created_at, kind, tags, content)
        if (id != expectedId) return false
        return try {
            Keys.verifySchnorr(sig.hexToByteArray(), id.hexToByteArray(), pubkey.hexToByteArray())
        } catch (_: Exception) {
            false
        }
    }
}

private object LongAsStringSerializer : KSerializer<Long> {
    override val descriptor = PrimitiveSerialDescriptor("LongAsString", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: Long) = encoder.encodeLong(value)
    override fun deserialize(decoder: Decoder): Long = decoder.decodeLong()
}

private fun StringBuilder.appendJsonEscaped(s: String) {
    for (c in s) {
        when (c) {
            '\\' -> append("\\\\")
            '"' -> append("\\\"")
            '\b' -> append("\\b")
            '\u000C' -> append("\\f")
            '\n' -> append("\\n")
            '\r' -> append("\\r")
            '\t' -> append("\\t")
            else -> if (c < ' ') {
                append("\\u00")
                append(HEX_CHARS[c.code ushr 4])
                append(HEX_CHARS[c.code and 0xF])
            } else {
                append(c)
            }
        }
    }
}

private const val HEX_CHARS = "0123456789abcdef"

fun ByteArray.toHex(): String {
    val result = CharArray(size * 2)
    for (i in indices) {
        val v = this[i].toInt() and 0xFF
        result[i * 2] = HEX_CHARS[v ushr 4]
        result[i * 2 + 1] = HEX_CHARS[v and 0x0F]
    }
    return String(result)
}

fun String.hexToByteArray(): ByteArray {
    require(length % 2 == 0) { "Hex string must have even length" }
    return ByteArray(length / 2) { i ->
        ((Character.digit(this[i * 2], 16) shl 4) + Character.digit(this[i * 2 + 1], 16)).toByte()
    }
}

/** Encodes a 32-byte hex pubkey as a bech32 npub string. */
fun String.toNpub(): String = Nip19.npubEncode(hexToByteArray())
