Options.kt

package com.shengzhiai.yugu

import java.util.Random
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.roundToLong

/**
 * 鉴权方式,二选一。
 *
 * - [Token]:`Authorization: Bearer <jwt>`,实时评测握手用 `?token=`。
 * - [Signature]:appKey 加 secretKey 签名,REST 走 `X-App-Key`,`X-Timestamp`,`X-Nonce`,`X-Signature`
 *   请求头,实时评测握手走 query 参数。
 */
public sealed class Auth {
    /** JWT 鉴权。 */
    public class Token(public val token: String) : Auth() {
        init {
            require(token.isNotBlank()) { "token 不能为空" }
        }

        override fun toString(): String = "Auth.Token(***)"
    }

    /** appKey 加 secretKey 签名鉴权。secretKey 只用于本地计算签名,不会发送也不会写日志。 */
    public class Signature(public val appKey: String, public val secretKey: String) : Auth() {
        init {
            require(appKey.isNotBlank()) { "appKey 不能为空" }
            require(secretKey.isNotBlank()) { "secretKey 不能为空" }
        }

        override fun toString(): String = "Auth.Signature(appKey=${maskAppKey(appKey)})"
    }

    public companion object {
        /** JWT 鉴权。 */
        @JvmStatic
        public fun token(token: String): Auth = Token(token)

        /** appKey 加 secretKey 签名鉴权。 */
        @JvmStatic
        public fun appKey(appKey: String, secretKey: String): Auth = Signature(appKey, secretKey)

        internal fun maskAppKey(appKey: String): String = appKey.take(4) + "***"
    }
}

/**
 * REST 重试策略。第 n 次重试前的等待时长:
 *
 * ```
 * delay(n) = min(maxDelayMs, initialDelayMs * multiplier^(n-1)) * (1 + U(-jitter, +jitter))
 * ```
 *
 * 响应带 `Retry-After` 且 [respectRetryAfter] 为 true 时取
 * `max(delay, min(retryAfterMs, maxRetryAfterMs))`。默认值给出约 200 ms 与 400 ms 两次重试,各自上下浮动 30%。
 */
public data class RetryPolicy @JvmOverloads constructor(
    /** 最多重试次数,不含首次请求。默认 2,即最多 3 次尝试。 */
    val maxRetries: Int = 2,
    val initialDelayMs: Long = 200,
    val multiplier: Double = 2.0,
    val maxDelayMs: Long = 4000,
    /** 抖动比例,0.3 表示上下浮动 30%。 */
    val jitter: Double = 0.3,
    val respectRetryAfter: Boolean = true,
    val maxRetryAfterMs: Long = 30_000,
) {
    init {
        require(maxRetries >= 0) { "maxRetries 不能为负数" }
        require(initialDelayMs >= 0 && maxDelayMs >= 0) { "等待时长不能为负数" }
        require(multiplier >= 1.0) { "multiplier 不能小于 1" }
        require(jitter in 0.0..1.0) { "jitter 取值 0 到 1" }
        require(maxRetryAfterMs >= 0) { "maxRetryAfterMs 不能为负数" }
    }

    /** 第 [retry] 次重试的基础等待时长,不含抖动,[retry] 从 1 开始。 */
    public fun baseDelayMs(retry: Int): Long = Backoff.base(retry, initialDelayMs, multiplier, maxDelayMs)

    /** 第 [retry] 次重试的等待时长,含抖动与 `Retry-After`。 */
    @JvmOverloads
    public fun delayMs(retry: Int, random: Random, retryAfterMs: Long? = null): Long {
        var d = Backoff.jittered(baseDelayMs(retry), jitter, random)
        if (respectRetryAfter && retryAfterMs != null && retryAfterMs > 0) {
            d = maxOf(d, min(retryAfterMs, maxRetryAfterMs))
        }
        return d
    }

    public companion object {
        /** 默认策略。 */
        @JvmField
        public val DEFAULT: RetryPolicy = RetryPolicy()

        /** 不重试。 */
        @JvmField
        public val NONE: RetryPolicy = RetryPolicy(maxRetries = 0)
    }
}

/**
 * 实时评测的自动重连策略。等待时长公式与 [RetryPolicy] 相同,默认依次等待约 0.5,1,2,4,4,4,4,4 秒,
 * 合计约 23 秒,可以扛过 10 秒左右的断网。
 *
 * [maxAttempts] 为连续重连失败的次数上限,计满后会话以 90006 失败,重连成功后重新计数。
 * 为保证会话一定结束,一次会话的重连总次数另有上限,为 [maxAttempts] 的 3 倍,默认 24 次。
 */
public data class ReconnectPolicy @JvmOverloads constructor(
    val enabled: Boolean = true,
    val maxAttempts: Int = 8,
    val initialDelayMs: Long = 500,
    val multiplier: Double = 2.0,
    val maxDelayMs: Long = 4000,
    val jitter: Double = 0.3,
) {
    init {
        require(maxAttempts >= 0) { "maxAttempts 不能为负数" }
        require(initialDelayMs >= 0 && maxDelayMs >= 0) { "等待时长不能为负数" }
        require(multiplier >= 1.0) { "multiplier 不能小于 1" }
        require(jitter in 0.0..1.0) { "jitter 取值 0 到 1" }
    }

    /** 第 [attempt] 次重连的基础等待时长,不含抖动。 */
    public fun baseDelayMs(attempt: Int): Long = Backoff.base(attempt, initialDelayMs, multiplier, maxDelayMs)

    /** 第 [attempt] 次重连的等待时长,含抖动。 */
    public fun delayMs(attempt: Int, random: Random): Long = Backoff.jittered(baseDelayMs(attempt), jitter, random)

    /** 一次会话内重连总次数的上限,为 [maxAttempts] 的 3 倍。 */
    public val maxTotalAttempts: Int get() = maxAttempts * 3

    public companion object {
        /** 默认策略。 */
        @JvmField
        public val DEFAULT: ReconnectPolicy = ReconnectPolicy()

        /** 关闭自动重连。 */
        @JvmField
        public val DISABLED: ReconnectPolicy = ReconnectPolicy(enabled = false)
    }
}

internal object Backoff {
    fun base(n: Int, initial: Long, multiplier: Double, max: Long): Long {
        require(n >= 1) { "retry index starts at 1" }
        val raw = initial.toDouble() * multiplier.pow((n - 1).toDouble())
        return if (raw >= max.toDouble()) max else raw.roundToLong()
    }

    /** base * (1 + U(-jitter, +jitter)) */
    fun jittered(base: Long, jitter: Double, random: Random): Long {
        if (jitter == 0.0 || base == 0L) return base
        val u = (random.nextDouble() * 2.0 - 1.0) * jitter
        return (base * (1.0 + u)).roundToLong().coerceAtLeast(0)
    }
}

/** 实时评测断线期间音频的处理方式。 */
public enum class AudioBufferPolicy {
    /**
     * 默认。保留本次会话已发送的全部音频,上限 10 MB。重连后用同一组参数与同一幂等键开新会话,
     * 重发开始帧,重放缓冲,补发断线期间排队的音频,已调用过 `end()` 时再发结束帧。
     * 缓冲超过上限后,下一次需要重连时会话以 90008 失败。
     */
    REPLAY,

    /** 不缓冲。断线期间送入的音频丢弃,新会话只评测重连后送入的音频。 */
    DROP,

    /** 不重连。传输层故障直接进入 FAILED。 */
    FAIL,
}

/** 本地音频预检方式。 */
public enum class AudioPrecheckMode {
    /** 不预检。 */
    OFF,

    /** 预检结果只作为警告写进结果的 `localWarnings`。 */
    WARN,

    /** 时长过短,过长,全程静音,格式不支持时在上传前抛 [AudioQualityException],音量过低仍只警告。 */
    REJECT,
}

/** 日志级别,默认 [WARN]。 */
public enum class LogLevel {
    OFF, ERROR, WARN, INFO, DEBUG;

    internal fun allows(level: LogLevel): Boolean = this != OFF && level != OFF && level.ordinal <= ordinal
}

/**
 * 取消令牌。传给 [RequestOptions] 后,在任何线程调用 [cancel] 都会中止对应调用,
 * 调用方得到 [RequestCancelledException],错误码 90003。
 */
public class CancellationToken {
    private val lock = Any()
    private val latch = CountDownLatch(1)
    private val listeners = ArrayList<() -> Unit>()

    /** 是否已取消。 */
    public val isCancelled: Boolean get() = latch.count == 0L

    /** 取消,可重复调用。 */
    public fun cancel() {
        val toRun: List<() -> Unit>
        synchronized(lock) {
            if (latch.count == 0L) return
            latch.countDown()
            toRun = ArrayList(listeners)
            listeners.clear()
        }
        for (l in toRun) {
            try {
                l()
            } catch (_: Throwable) {
                // listeners are internal and must not break cancellation
            }
        }
    }

    internal fun register(listener: () -> Unit): () -> Unit {
        var runNow = false
        synchronized(lock) {
            if (latch.count == 0L) runNow = true else listeners.add(listener)
        }
        if (runNow) listener()
        return { synchronized(lock) { listeners.remove(listener) } }
    }

    /** Waits up to [ms]. Returns true when cancelled before or during the wait. */
    internal fun await(ms: Long): Boolean = if (ms <= 0) isCancelled else latch.await(ms, TimeUnit.MILLISECONDS)
}

/**
 * 单次 REST 调用的选项。
 *
 * - [idempotencyKey]:调用方指定的幂等键,须匹配 `^[\x21-\x7E]{1,200}$`,否则在发请求前抛 90010。
 *   不指定时 SDK 自动生成 32 位小写十六进制键,同一次调用的全部重试复用同一个键。
 * - [totalTimeoutMs]:覆盖客户端的总时限,含全部重试与等待。
 * - [readTimeoutMs]:覆盖客户端的单次读取超时。
 * - [retryPolicy]:覆盖客户端的重试策略。
 * - [cancellationToken]:取消令牌。
 */
public class RequestOptions @JvmOverloads constructor(
    public val idempotencyKey: String? = null,
    public val totalTimeoutMs: Long? = null,
    public val readTimeoutMs: Long? = null,
    public val retryPolicy: RetryPolicy? = null,
    public val cancellationToken: CancellationToken? = null,
) {
    public companion object {
        /** 全部取客户端默认值。 */
        @JvmField
        public val DEFAULT: RequestOptions = RequestOptions()
    }
}

/**
 * 单个实时评测会话的选项,未指定的项取客户端默认值。
 *
 * [extraQuery] 为额外的握手 query 参数,会一并参与签名。
 */
public class SessionOptions @JvmOverloads constructor(
    public val idempotencyKey: String? = null,
    public val reconnectPolicy: ReconnectPolicy? = null,
    public val audioBufferPolicy: AudioBufferPolicy? = null,
    public val resultTimeoutMs: Long? = null,
    public val extraQuery: Map<String, String> = emptyMap(),
) {
    public companion object {
        /** 全部取客户端默认值。 */
        @JvmField
        public val DEFAULT: SessionOptions = SessionOptions()
    }
}