/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024, 2025, 2026 Taler Systems S.A.
 *
 * LibEuFin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3, or
 * (at your option) any later version.
 *
 * LibEuFin is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with LibEuFin; see the file COPYING.  If not, see
 * <http://www.gnu.org/licenses/>
 */

package tech.libeufin.common

import io.ktor.http.*
import io.ktor.server.plugins.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
import java.time.Instant
import java.time.Duration
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import java.nio.ByteBuffer
import java.nio.ByteOrder
import org.bouncycastle.math.ec.rfc8032.Ed25519
import io.github.smiley4.schemakenerator.core.annotations.Description
import kotlinx.io.bytestring.putByteString

sealed class CommonError(msg: String) : Exception(msg) {
    class AmountFormat(msg: String) : CommonError(msg)
    class AmountNumberTooBig(msg: String) : CommonError(msg)
    class Payto(msg: String) : CommonError(msg)
}

/**
 * Internal representation of relative times.  The
 * "forever" case is represented with Long.MAX_VALUE.
 */
@Description("Relative time duration, serialized as microseconds or 'forever'")
@JvmInline
@Serializable(with = RelativeTime.Serializer::class)
value class RelativeTime(val duration: Duration) {
    internal object Serializer : KSerializer<RelativeTime> {
        override val descriptor: SerialDescriptor =
            buildClassSerialDescriptor("RelativeTime") {
                element<JsonElement>("d_us")
            }

        override fun serialize(encoder: Encoder, value: RelativeTime) {
            val composite = encoder.beginStructure(descriptor)
            if (value.duration == ChronoUnit.FOREVER.duration) {
                composite.encodeStringElement(descriptor, 0, "forever")
            } else {
                composite.encodeLongElement(descriptor, 0, TimeUnit.MICROSECONDS.convert(value.duration))
            }
            composite.endStructure(descriptor)
        }

        override fun deserialize(decoder: Decoder): RelativeTime {
            val dec = decoder.beginStructure(descriptor)
            val jsonInput = dec as? JsonDecoder ?: error("Can be deserialized only by JSON")
            lateinit var maybeDUs: JsonPrimitive
            loop@ while (true) {
                when (val index = dec.decodeElementIndex(descriptor)) {
                    0 -> maybeDUs = jsonInput.decodeJsonElement().jsonPrimitive
                    CompositeDecoder.DECODE_DONE -> break@loop
                    else -> throw SerializationException("Unexpected index: $index")
                }
            }
            dec.endStructure(descriptor)
            if (maybeDUs.isString) {
                if (maybeDUs.content != "forever") throw badRequest("Only 'forever' allowed for d_us as string, but '${maybeDUs.content}' was found")
                return RelativeTime(ChronoUnit.FOREVER.duration)
            }
            val dUs: Long = maybeDUs.longOrNull
                ?: throw badRequest("Could not convert d_us: '${maybeDUs.content}' to a number")
            when {
                dUs < 0 -> throw badRequest("Negative duration specified.")
                dUs > MAX_SAFE_INTEGER -> throw badRequest("d_us value $dUs exceed cap (2^53-1)")
                else -> return RelativeTime(Duration.of(dUs, ChronoUnit.MICROS))
            }
        }
    }

    companion object {
        const val MAX_SAFE_INTEGER = 9007199254740991L // 2^53 - 1
    }
}

/** Timestamp containing the number of seconds since epoch */
@Description("Timestamp as seconds since Unix epoch, or 'never'")
@JvmInline
@Serializable(with = TalerTimestamp.Serializer::class)
value class TalerTimestamp constructor(val instant: Instant) {
    internal object Serializer : KSerializer<TalerTimestamp> {
        override val descriptor: SerialDescriptor =
            buildClassSerialDescriptor("Timestamp") {
                element<JsonElement>("t_s")
            }

        override fun serialize(encoder: Encoder, value: TalerTimestamp) {
            val composite = encoder.beginStructure(descriptor)
            if (value.instant == Instant.MAX) {
                composite.encodeStringElement(descriptor, 0, "never")
            } else {
                composite.encodeLongElement(descriptor, 0, value.instant.epochSecond)
            }
            composite.endStructure(descriptor)
        }

        override fun deserialize(decoder: Decoder): TalerTimestamp {
            val dec = decoder.beginStructure(descriptor)
            val jsonInput = dec as? JsonDecoder ?: error("Can be deserialized only by JSON")
            lateinit var maybeTs: JsonPrimitive
            loop@ while (true) {
                when (val index = dec.decodeElementIndex(descriptor)) {
                    0 -> maybeTs = jsonInput.decodeJsonElement().jsonPrimitive
                    CompositeDecoder.DECODE_DONE -> break@loop
                    else -> throw SerializationException("Unexpected index: $index")
                }
            }
            dec.endStructure(descriptor)
            if (maybeTs.isString) {
                if (maybeTs.content != "never") throw badRequest("Only 'never' allowed for t_s as string, but '${maybeTs.content}' was found")
                return TalerTimestamp(Instant.MAX)
            }
            val ts: Long = maybeTs.longOrNull
                ?: throw badRequest("Could not convert t_s '${maybeTs.content}' to a number")
            when {
                ts < 0 -> throw badRequest("Negative timestamp not allowed")
                ts > Instant.MAX.epochSecond -> throw badRequest("Timestamp $ts too big to be represented in Kotlin")
                else -> return TalerTimestamp(Instant.ofEpochSecond(ts))
            }
        }
    }

    companion object {
        fun never(): TalerTimestamp = TalerTimestamp(Instant.MAX)
    }
}

@Description("Base URL string ending with a trailing slash")
@JvmInline
@Serializable(with = BaseURL.Serializer::class)
value class BaseURL private constructor(val url: Url) {
    companion object {
        fun parse(raw: String): BaseURL {
            val url = URLBuilder(raw)
            if (url.protocolOrNull == null) {
                throw badRequest("missing protocol in baseURL got '${url}'")
            } else if (url.protocol.name !in setOf("http", "https")) {
                throw badRequest("only 'http' and 'https' are accepted for baseURL got '${url.protocol.name}'")
            } else if (url.host.isEmpty()) {
                throw badRequest("missing host in baseURL got '${url}'")
            } else if (!url.parameters.isEmpty()) {
                throw badRequest("require no query in baseURL got '${url.encodedParameters}'")
            } else if (url.fragment.isNotEmpty()) {
                throw badRequest("require no fragments in baseURL got '${url.fragment}'")
            } else if (!url.encodedPath.endsWith('/')) {
                throw badRequest("baseURL path must end with / got '${url.encodedPath}'")
            }
            return BaseURL(url.build())
        }
    }

    override fun toString(): String = url.toString()

    internal object Serializer : KSerializer<BaseURL> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("BaseURL", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: BaseURL) {
            encoder.encodeString(value.url.toString())
        }

        override fun deserialize(decoder: Decoder): BaseURL {
            return BaseURL.parse(decoder.decodeString())
        }
    }
}

@Serializable(with = DecimalNumber.Serializer::class)
class DecimalNumber {
    val value: Long
    val frac: Int

    constructor(value: Long, frac: Int) {
        this.value = value
        this.frac = frac
    }

    constructor(encoded: String) {
        val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format")
        val (value, frac) = match.destructured
        this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
        if (this.value > TalerAmount.MAX_VALUE)
            throw badRequest("Value specified in decimal number is too large")
        this.frac = if (frac.isEmpty()) {
            0
        } else {
            var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value")
            if (tmp > TalerAmount.FRACTION_BASE)
                throw badRequest("Fractional value specified in decimal number is too large")
            repeat(8 - frac.length) {
                tmp *= 10
            }
            tmp
        }
    }

    fun isZero(): Boolean = value == 0L && frac == 0

    override fun equals(other: Any?): Boolean {
        return other is DecimalNumber &&
                other.value == this.value &&
                other.frac == this.frac
    }

    override fun toString(): String {
        return if (frac == 0) {
            "$value"
        } else {
            "$value.${frac.toString().padStart(8, '0')}"
                .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
        }
    }

    internal object Serializer : KSerializer<DecimalNumber> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("DecimalNumber", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: DecimalNumber) {
            encoder.encodeString(value.toString())
        }

        override fun deserialize(decoder: Decoder): DecimalNumber {
            return DecimalNumber(decoder.decodeString())
        }
    }

    companion object {
        val ZERO = DecimalNumber(0, 0)
        private val PATTERN = Regex("([0-9]+)(?:\\.([0-9]{1,8}))?")
    }
}

@Serializable(with = TalerAmount.Serializer::class)
class TalerAmount : Comparable<TalerAmount> {
    val value: Long
    val frac: Int
    val currency: String

    constructor(value: Long, frac: Int, currency: String) {
        this.value = value
        this.frac = frac
        this.currency = currency
    }

    constructor(encoded: String) {
        val match = PATTERN.matchEntire(encoded) ?: throw CommonError.AmountFormat("Invalid amount format")
        val (currency, value, frac) = match.destructured
        this.currency = currency
        this.value = value.toLongOrNull() ?: throw CommonError.AmountFormat("Invalid value")
        if (this.value > MAX_VALUE)
            throw CommonError.AmountNumberTooBig("Value specified in amount is too large")
        this.frac = if (frac.isEmpty()) {
            0
        } else {
            var tmp = frac.toIntOrNull() ?: throw CommonError.AmountFormat("Invalid fractional value")
            if (tmp > FRACTION_BASE)
                throw CommonError.AmountFormat("Fractional value specified in amount is too large")
            repeat(8 - frac.length) {
                tmp *= 10
            }

            tmp
        }
    }

    fun number(): DecimalNumber = DecimalNumber(value, frac)

    /* Check if zero */
    fun isZero(): Boolean = value == 0L && frac == 0

    fun notZeroOrNull(): TalerAmount? = if (isZero()) null else this

    /* Check is amount has fractional amount < 0.01 */
    fun isSubCent(): Boolean = (frac % CENT_FRACTION) > 0

    /* Network bytes */
    fun nbo(): ByteArray = ByteBuffer.allocate(24).apply {
        order(ByteOrder.BIG_ENDIAN)
        putLong(value)
        putInt(frac)
        val curr = currency.encodeToByteArray()
        put(curr)
        repeat(12 - curr.size) {
            put(0)
        }
    }.array()

    override fun equals(other: Any?): Boolean {
        return other is TalerAmount &&
                other.value == this.value &&
                other.frac == this.frac &&
                other.currency == this.currency
    }

    override fun toString(): String {
        return if (frac == 0) {
            "$currency:$value"
        } else {
            "$currency:$value.${frac.toString().padStart(8, '0')}"
                .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
        }
    }

    fun normalize(): TalerAmount {
        val value = Math.addExact(this.value, (this.frac / FRACTION_BASE).toLong())
        val frac = this.frac % FRACTION_BASE
        if (value > MAX_VALUE) throw ArithmeticException("amount value overflowed")
        return TalerAmount(value, frac, currency)
    }

    override operator fun compareTo(other: TalerAmount) = compareValuesBy(this, other, { it.value }, { it.frac })

    operator fun plus(increment: TalerAmount): TalerAmount {
        require(this.currency == increment.currency) { "currency mismatch ${this.currency} != ${increment.currency}" }
        val value = Math.addExact(this.value, increment.value)
        val frac = Math.addExact(this.frac, increment.frac)
        return TalerAmount(value, frac, currency).normalize()
    }

    operator fun minus(decrement: TalerAmount): TalerAmount {
        require(this.currency == decrement.currency) { "currency mismatch ${this.currency} != ${decrement.currency}" }
        var frac = this.frac
        var value = this.value
        if (frac < decrement.frac) {
            if (value <= 0) {
                throw ArithmeticException("negative result")
            }
            frac += FRACTION_BASE
            value -= 1
        }
        if (value < decrement.value) {
            throw ArithmeticException("negative result")
        }
        return TalerAmount(value - decrement.value, frac - decrement.frac, currency).normalize()
    }

    internal object Serializer : KSerializer<TalerAmount> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: TalerAmount) {
            encoder.encodeString(value.toString())
        }

        override fun deserialize(decoder: Decoder): TalerAmount =
            TalerAmount(decoder.decodeString())

    }

    companion object {
        const val FRACTION_BASE = 100000000
        const val CENT_FRACTION = 1000000
        const val MAX_VALUE = 4503599627370496L // 2^52
        private val PATTERN = Regex("([A-Z]{1,11}):([0-9]+)(?:\\.([0-9]{1,8}))?")

        fun zero(currency: String) = TalerAmount(0, 0, currency)
        fun max(currency: String) = TalerAmount(MAX_VALUE, FRACTION_BASE - 1, currency)
    }
}

@Serializable(with = Payto.Serializer::class)
sealed class Payto {
    abstract val parsed: Url
    abstract val canonical: String
    abstract val amount: TalerAmount?
    abstract val message: String?
    abstract val receiverName: String?

    /** Transform a payto URI to its bank form, using [name] as the receiver-name and the bank [ctx] */
    fun bank(name: String?, ctx: BankPaytoCtx): String = when (this) {
        is IbanPayto -> IbanPayto.build(iban.toString(), ctx.bic, name)
        is XTalerBankPayto -> {
            val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else ""
            "payto://x-taler-bank/${ctx.hostname}/$username$name"
        }
    }

    fun expectIbanFull(): IbanPayto {
        val payto = expectIban()
        if (payto.receiverName == null) {
            throw CommonError.Payto("expected a full IBAN payto got no receiver-name")
        }
        return payto
    }

    fun expectIban(): IbanPayto {
        return when (this) {
            is IbanPayto -> this
            else -> throw CommonError.Payto("expected an IBAN payto URI got '${parsed.host}'")
        }
    }

    fun expectXTalerBank(): XTalerBankPayto {
        return when (this) {
            is XTalerBankPayto -> this
            else -> throw CommonError.Payto("expected a x-taler-bank payto URI got '${parsed.host}'")
        }
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Payto) return false
        return this.parsed == other.parsed
    }

    internal object Serializer : KSerializer<Payto> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("Payto", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: Payto) {
            encoder.encodeString(value.toString())
        }

        override fun deserialize(decoder: Decoder): Payto {
            return parse(decoder.decodeString())
        }
    }

    companion object {
        private val HEX_PATTERN: Regex = Regex("%(?![0-9a-fA-F]{2})")
        fun parse(input: String): Payto {
            val raw = input.replace(HEX_PATTERN, "%25")
            val parsed = try {
                Url(raw)
            } catch (e: Exception) {
                throw CommonError.Payto("expected a valid URI")
            }
            if (parsed.protocol.name != "payto") throw CommonError.Payto("expect a payto URI got '${parsed.protocol.name}'")

            val amount = parsed.parameters["amount"]?.run { TalerAmount(this) }
            val message = parsed.parameters["message"]
            val receiverName = parsed.parameters["receiver-name"]

            return when (parsed.host) {
                "iban" -> {
                    val segments = parsed.segments
                    val (bic, rawIban) = when (segments.size) {
                        1 -> Pair(null, segments[0])
                        2 -> Pair(segments[0], segments[1])
                        else -> throw CommonError.Payto("too many path segments for an IBAN payto URI")
                    }
                    val iban = IBAN.parse(rawIban)
                    IbanPayto(
                        parsed,
                        "payto://iban/$iban",
                        amount,
                        message,
                        receiverName,
                        parsed.parameters["ch-qrr"],
                        bic,
                        iban,
                    )
                }

                "x-taler-bank" -> {
                    val segments = parsed.segments
                    if (segments.size != 2)
                        throw CommonError.Payto("bad number of path segments for a x-taler-bank payto URI")
                    val username = segments[1]
                    XTalerBankPayto(
                        parsed,
                        "payto://x-taler-bank/localhost/$username",
                        amount,
                        message,
                        receiverName,
                        username
                    )
                }

                else -> throw CommonError.Payto("unsupported payto URI kind '${parsed.host}'")
            }
        }
    }
}

@Serializable(with = IbanPayto.Serializer::class)
class IbanPayto internal constructor(
    override val parsed: Url,
    override val canonical: String,
    override val amount: TalerAmount?,
    override val message: String?,
    override val receiverName: String?,
    val chQrr: String?,
    val bic: String?,
    val iban: IBAN
) : Payto() {
    override fun toString(): String = parsed.toString()

    /** Format an IbanPayto in a more human readable way */
    fun fmt(): String = buildString {
        append('(')
        append(iban)
        if (bic != null) {
            append(' ')
            append(bic)
        }
        if (receiverName != null) {
            append(' ')
            append(receiverName)
        }
        append(')')
    }

    /** Transform an IBAN payto URI to its simple form without any query */
    fun simple(): String = build(iban.toString(), bic, null)

    /** Transform an IBAN payto URI to its full form, using [name] as its receiver-name */
    fun full(name: String): String = build(iban.toString(), bic, name)

    internal object Serializer : KSerializer<IbanPayto> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("IbanPayto", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: IbanPayto) {
            encoder.encodeString(value.toString())
        }

        override fun deserialize(decoder: Decoder): IbanPayto {
            return parse(decoder.decodeString()).expectIban()
        }
    }

    companion object {
        fun build(iban: String, bic: String?, name: String?): String {
            val bic = if (bic != null) "$bic/" else ""
            val name = if (name != null) "?receiver-name=${name.encodeURLParameter()}" else ""
            return "payto://iban/$bic$iban$name"
        }

        fun rand(name: String? = null, country: Country = Country.DE): IbanPayto = parse(
            "payto://iban/${IBAN.rand(country)}${
                if (name != null) {
                    "?receiver-name=${name.encodeURLParameter()}"
                } else {
                    ""
                }
            }"
        ).expectIban()
    }
}

class XTalerBankPayto internal constructor(
    override val parsed: Url,
    override val canonical: String,
    override val amount: TalerAmount?,
    override val message: String?,
    override val receiverName: String?,
    val username: String
) : Payto() {
    override fun toString(): String = parsed.toString()

    companion object {
        fun forUsername(username: String): XTalerBankPayto {
            return parse("payto://x-taler-bank/hostname/$username").expectXTalerBank()
        }
    }
}

/** Context specific data necessary to create a bank payto URI from a canonical payto URI */
data class BankPaytoCtx(
    val bic: String?,
    val hostname: String
)


/** 16-byte Crockford's Base32 encoded data */
@Serializable(with = Base32Crockford16B.Serializer::class)
class Base32Crockford16B {
    private var encoded: String? = null
    val raw: ByteArray

    constructor(encoded: String) {
        val decoded = try {
            Base32Crockford.decode(encoded)
        } catch (e: IllegalArgumentException) {
            null
        }
        require(decoded != null && decoded.size == 16) {
            "expected 16 bytes encoded in Crockford's base32"
        }
        this.raw = decoded
        this.encoded = encoded
    }

    constructor(raw: ByteArray) {
        require(raw.size == 16) {
            "encoded data should be 16 bytes long"
        }
        this.raw = raw
    }

    fun encoded(): String {
        val tmp = encoded ?: Base32Crockford.encode(raw)
        encoded = tmp
        return tmp
    }

    override fun toString(): String {
        return encoded()
    }

    override fun equals(other: Any?) = (other is Base32Crockford16B) && raw.contentEquals(other.raw)

    internal object Serializer : KSerializer<Base32Crockford16B> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("Base32Crockford16B", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: Base32Crockford16B) {
            encoder.encodeString(value.encoded())
        }

        override fun deserialize(decoder: Decoder): Base32Crockford16B {
            return Base32Crockford16B(decoder.decodeString())
        }
    }

    companion object {
        fun rand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).rand())
        fun secureRand(): Base32Crockford16B = Base32Crockford16B(ByteArray(16).secureRand())
    }
}

/** 32-byte Crockford's Base32 encoded data */
@Description("32-byte Crockford Base32 encoded data")
@Serializable(with = Base32Crockford32B.Serializer::class)
class Base32Crockford32B {
    private var encoded: String? = null
    val raw: ByteArray

    constructor(encoded: String) {
        val decoded = try {
            Base32Crockford.decode(encoded)
        } catch (e: IllegalArgumentException) {
            null
        }
        require(decoded != null && decoded.size == 32) {
            "expected 32 bytes encoded in Crockford's base32"
        }
        this.raw = decoded
        this.encoded = encoded
    }

    constructor(raw: ByteArray) {
        require(raw.size == 32) {
            "encoded data should be 32 bytes long"
        }
        this.raw = raw
    }

    fun encoded(): String {
        val tmp = encoded ?: Base32Crockford.encode(raw)
        encoded = tmp
        return tmp
    }

    override fun toString(): String {
        return encoded()
    }

    override fun equals(other: Any?) = (other is Base32Crockford32B) && raw.contentEquals(other.raw)

    internal object Serializer : KSerializer<Base32Crockford32B> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("Base32Crockford32B", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: Base32Crockford32B) {
            encoder.encodeString(value.encoded())
        }

        override fun deserialize(decoder: Decoder): Base32Crockford32B {
            return Base32Crockford32B(decoder.decodeString())
        }
    }

    companion object {
        fun rand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).rand())
        fun secureRand(): Base32Crockford32B = Base32Crockford32B(ByteArray(32).secureRand())
        fun randEdsaKey(): EddsaPublicKey = randEdsaKeyPair().second
        fun randEdsaKeyPair(): Pair<ByteArray, EddsaPublicKey> {
            val secretKey = ByteArray(32)
            Ed25519.generatePrivateKey(SECURE_RNG.get(), secretKey)
            val publicKey = ByteArray(32)
            Ed25519.generatePublicKey(secretKey, 0, publicKey, 0)
            return Pair(secretKey, Base32Crockford32B(publicKey))
        }
    }
}

/** 64-byte Crockford's Base32 encoded data */
@Description("64-byte Crockford Base32 encoded data")
@Serializable(with = Base32Crockford64B.Serializer::class)
class Base32Crockford64B {
    private var encoded: String? = null
    val raw: ByteArray

    constructor(encoded: String) {
        val decoded = try {
            Base32Crockford.decode(encoded)
        } catch (e: IllegalArgumentException) {
            null
        }

        require(decoded != null && decoded.size == 64) {
            "expected 64 bytes encoded in Crockford's base32"
        }
        this.raw = decoded
        this.encoded = encoded
    }

    constructor(raw: ByteArray) {
        require(raw.size == 64) {
            "encoded data should be 64 bytes long"
        }
        this.raw = raw
    }

    fun encoded(): String {
        val tmp = encoded ?: Base32Crockford.encode(raw)
        encoded = tmp
        return tmp
    }

    override fun toString(): String {
        return encoded()
    }

    override fun equals(other: Any?) = (other is Base32Crockford64B) && raw.contentEquals(other.raw)

    internal object Serializer : KSerializer<Base32Crockford64B> {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("Base32Crockford64B", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: Base32Crockford64B) {
            encoder.encodeString(value.encoded())
        }

        override fun deserialize(decoder: Decoder): Base32Crockford64B {
            return Base32Crockford64B(decoder.decodeString())
        }
    }

    companion object {
        fun rand(): Base32Crockford64B = Base32Crockford64B(ByteArray(64).rand())
    }
}

/** 32-byte hash code */
typealias ShortHashCode = Base32Crockford32B
/** 64-byte hash code */
typealias HashCode = Base32Crockford64B

typealias EddsaSignature = Base32Crockford64B
/**
 * EdDSA and ECDHE public keys always point on Curve25519
 * and represented  using the standard 256 bits Ed25519 compact format,
 * converted to Crockford Base32.
 */
typealias EddsaPublicKey = Base32Crockford32B
