package net.sergeych.uni

import com.icodici.crypto.PrivateKey
import com.icodici.crypto.PublicKey
import com.icodici.universa.ErrorRecord
import com.icodici.universa.HashId
import com.icodici.universa.node.ItemState
import com.icodici.universa.node2.network.Client
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.info
import net.sergeych.mp_logger.warning
import net.sergeych.mp_tools.decodeBase64
import java.io.File


/**
 * This is a rather simple connection pool for the threaded Universa clients
 * to use with Kotlin coroutines.
 *
 * Uni pool is needed as Universa's client is blocking old-school Java code, and to
 * use it with coroutines th pool is needed.
 *
 * Note that you can use Universa JAVA API directly. Also, indigo project uses multiplatform
 * library that works now on JS and JVM the same way. It will be made public on release.
 */
object Uni : LogTag("UNI") {

    // How many worker clients to use
    val Parallelism = 2

    // name of the topology file in resources/topologies, see source tree.
    // it must not differ too much from the network (small changes are ok)
    val NetworkName = "indigo_demo_2025"

    /**
     * The client's private key. We use "white" key that allows to execute paid operations
     * for free. This is usual for private networks.
     */
    private val key = PrivateKey(
        File("./white.private.unikey")
            .readBytes()
    )

    /**
     * The number of allocated clients
     */
    var allocated = 0
        private set

    // clients pool
    private val pool = Channel<Context>(Parallelism)

    // mutex to guard pool access
    private val access = Mutex()

    /**
     * Get a client from the pool or allocate a new one, and execute [f] in it's
     * context.
     */
    suspend fun <T> versa(f: suspend Context.() -> T): T {
        val x = access.withLock {
            pool.tryReceive().getOrNull()
                ?: run {
                    if (allocated < Parallelism) {
                        info { "Allocating new client, currenly there are $allocated" }
                        Context.connect(NetworkName, key).also { allocated++ }.also {
                            info { "Allocated new client, currenly there are $allocated" }
                        }
                    } else null
                }
        } ?: pool.receive()
        return try {
            x.f().also {
                access.withLock { pool.send(x) }
            }
        } catch (t: IllegalStateException) {
            warning { "Critical exception, will re-connect the client: $t" }
            allocated--
            throw t
        } catch (t: Throwable) {
            exception { "unexpected in UNI to t" to t }
            access.withLock { pool.send(x) }
            throw t
        }
    }

    /**
     * Kotlin wrapper for the Universa's java based registration result.
     */
    sealed class URegistrationResult {

        /**
         * true if the registration is approved by the network
         */
        @Suppress("unused")
        val approved by lazy {
            this is Approved
        }

        /**
         * Approved state, with extended [state] information
         */
        data class Approved(val state: ItemState) : URegistrationResult() {
            override fun toString(): String {
                return state.toString()
            }
        }

        /**
         * Rejected state, with extended [state] information and list of [errors]
         * as reported by the network node.
         */
        class Rejected(
            val errors: List<ErrorRecord>,
            val state: ItemState,
        ) : URegistrationResult() {
            override fun toString(): String {
                return "$state: ${errors.joinToString(", ")}"
            }
        }
    }

    /**
     * Context is a wrapper for the Universa's java based client to be used
     * with [Uni] pool and Kotlin coroutines.
     */
    class Context(private val c: Client) {

        data class TopologyInfo(
            val nodeNumber: Int,
            val ipUrl: String,
            val domainUrl: String,
            val keyAddress: String,
        )

        /**
         * Current network topology (configuration): urls, node numbers,
         * public keys, etc.
         */
        val topologyInfo by lazy {
            c.topology.map {
                TopologyInfo(
                    it.getIntOrThrow("number"),
                    it.getArray("direct_urls").first() as String,
                    it.getArray("domain_urls").first() as String,
                    PublicKey(it.getStringOrThrow("key").decodeBase64()).longAddress.toString()
                )
            }
        }

        /**
         * Register a Universa TransactionPack with one or more contracts.
         * It polls the network until the consensus will be reached.
         */
        @Suppress("unused")
        fun register(packed: ByteArray): URegistrationResult {
            while (true) {
                val ir = c.register(packed, 30_000)
                val state = ir.state
                if (state.isPending) continue
                if (state.isApproved) return URegistrationResult.Approved(state)
                else return URegistrationResult.Rejected(ir.errors, state)
            }
        }

        /**
         * Check that qualified majority of the network has approved the contract
         * with [contractId]
         * @param threshold is a percentage of the network size, default is 40%
         * @return true if the contract is approved by the network
         */
        fun isApprovedByNetwork(contractId: HashId, threshold: Double = 0.4): Boolean {
            return c.isApprovedByNetwork(contractId, threshold, 15_000)
        }

        companion object {
            /**
             * Create a new client and connect to the network. This is not used directly
             * mostly, but is used by [Uni] pool.
             */
            fun connect(networkName: String, key: PrivateKey): Context {
                val c = Client(networkName, null, key)
                return Context(c)
            }
        }
    }
}
