package io.synor.rpc

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json
import java.io.Closeable
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap

/**
 * Synor RPC SDK client for Kotlin.
 *
 * Provides blockchain queries, transaction submission, and real-time
 * subscriptions via WebSocket.
 *
 * Example:
 * ```kotlin
 * val rpc = SynorRpc(RpcConfig.builder("your-api-key").build())
 *
 * // Get latest block
 * val block = rpc.getLatestBlock()
 * println("Latest block: ${block.height}")
 *
 * // Subscribe to new blocks
 * val scope = CoroutineScope(Dispatchers.Default)
 * val subscription = rpc.subscribeBlocks(scope) { block ->
 *     println("New block: ${block.height}")
 * }
 *
 * // Later: cancel subscription
 * subscription.cancel()
 * rpc.close()
 * ```
 */
class SynorRpc(private val config: RpcConfig) : Closeable {

    private val json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        encodeDefaults = true
    }

    private val client = HttpClient(CIO) {
        install(ContentNegotiation) {
            json(json)
        }

        install(WebSockets)

        install(HttpTimeout) {
            requestTimeoutMillis = config.timeout
            connectTimeoutMillis = config.timeout
        }

        defaultRequest {
            header("Authorization", "Bearer ${config.apiKey}")
            header("Content-Type", "application/json")
        }
    }

    private var wsSession: WebSocketSession? = null
    private val subscriptions = ConcurrentHashMap<String, (String) -> Unit>()
    private var wsJob: Job? = null

    // Block operations

    /**
     * Get the latest block.
     */
    suspend fun getLatestBlock(): Block {
        return get("/blocks/latest")
    }

    /**
     * Get a block by hash or height.
     */
    suspend fun getBlock(hashOrHeight: String): Block {
        return get("/blocks/$hashOrHeight")
    }

    /**
     * Get a block header by hash or height.
     */
    suspend fun getBlockHeader(hashOrHeight: String): BlockHeader {
        return get("/blocks/$hashOrHeight/header")
    }

    /**
     * Get blocks in a range.
     */
    suspend fun getBlocks(startHeight: Long, endHeight: Long): List<Block> {
        return get("/blocks?start=$startHeight&end=$endHeight")
    }

    // Transaction operations

    /**
     * Get a transaction by ID.
     */
    suspend fun getTransaction(txid: String): Transaction {
        return get("/transactions/$txid")
    }

    /**
     * Get raw transaction hex.
     */
    suspend fun getRawTransaction(txid: String): String {
        val response: Map<String, String> = get("/transactions/$txid/raw")
        return response["hex"] ?: throw RpcException("Missing hex in response")
    }

    /**
     * Send a raw transaction.
     */
    suspend fun sendRawTransaction(hex: String): SubmitResult {
        return post("/transactions/send", mapOf("hex" to hex))
    }

    /**
     * Decode a raw transaction without broadcasting.
     */
    suspend fun decodeRawTransaction(hex: String): Transaction {
        return post("/transactions/decode", mapOf("hex" to hex))
    }

    /**
     * Get transactions for an address.
     */
    suspend fun getAddressTransactions(
        address: String,
        limit: Int = 50,
        offset: Int = 0
    ): List<Transaction> {
        return get("/addresses/$address/transactions?limit=$limit&offset=$offset")
    }

    // Fee estimation

    /**
     * Estimate fee for a given priority.
     */
    suspend fun estimateFee(priority: Priority = Priority.MEDIUM): FeeEstimate {
        return get("/fees/estimate?priority=${priority.name.lowercase()}")
    }

    /**
     * Get all fee estimates.
     */
    suspend fun getAllFeeEstimates(): Map<Priority, FeeEstimate> {
        val estimates: List<FeeEstimate> = get("/fees/estimates")
        return estimates.associateBy { it.priority }
    }

    // Chain information

    /**
     * Get chain information.
     */
    suspend fun getChainInfo(): ChainInfo {
        return get("/chain/info")
    }

    /**
     * Get mempool information.
     */
    suspend fun getMempoolInfo(): MempoolInfo {
        return get("/mempool/info")
    }

    /**
     * Get mempool transactions.
     */
    suspend fun getMempoolTransactions(limit: Int = 100): List<String> {
        return get("/mempool/transactions?limit=$limit")
    }

    // WebSocket subscriptions

    /**
     * Subscribe to new blocks.
     */
    suspend fun subscribeBlocks(
        scope: CoroutineScope,
        callback: (Block) -> Unit
    ): Subscription {
        return subscribe(scope, "blocks", null) { data ->
            val block = json.decodeFromString<Block>(data)
            callback(block)
        }
    }

    /**
     * Subscribe to transactions for a specific address.
     */
    suspend fun subscribeAddress(
        scope: CoroutineScope,
        address: String,
        callback: (Transaction) -> Unit
    ): Subscription {
        return subscribe(scope, "address", address) { data ->
            val tx = json.decodeFromString<Transaction>(data)
            callback(tx)
        }
    }

    /**
     * Subscribe to mempool transactions.
     */
    suspend fun subscribeMempool(
        scope: CoroutineScope,
        callback: (Transaction) -> Unit
    ): Subscription {
        return subscribe(scope, "mempool", null) { data ->
            val tx = json.decodeFromString<Transaction>(data)
            callback(tx)
        }
    }

    private suspend fun subscribe(
        scope: CoroutineScope,
        channel: String,
        filter: String?,
        callback: (String) -> Unit
    ): Subscription {
        ensureWebSocketConnection(scope)

        val subscriptionId = UUID.randomUUID().toString()
        subscriptions[subscriptionId] = callback

        val message = WsMessage(
            type = "subscribe",
            channel = channel,
            filter = filter
        )
        wsSession?.send(Frame.Text(json.encodeToString(WsMessage.serializer(), message)))

        return Subscription(
            id = subscriptionId,
            channel = channel,
            onCancel = {
                subscriptions.remove(subscriptionId)
                scope.launch {
                    val unsubMsg = WsMessage(type = "unsubscribe", channel = channel)
                    wsSession?.send(Frame.Text(json.encodeToString(WsMessage.serializer(), unsubMsg)))
                }
            }
        )
    }

    private suspend fun ensureWebSocketConnection(scope: CoroutineScope) {
        if (wsSession != null && wsSession?.isActive == true) return

        wsSession = client.webSocketSession("${config.wsEndpoint}?token=${config.apiKey}")

        wsJob = scope.launch {
            try {
                wsSession?.incoming?.consumeAsFlow()?.collect { frame ->
                    when (frame) {
                        is Frame.Text -> {
                            val text = frame.readText()
                            try {
                                val message = json.decodeFromString<WsMessage>(text)
                                if (message.type == "data" && message.data != null) {
                                    subscriptions.values.forEach { it(message.data) }
                                }
                            } catch (e: Exception) {
                                if (config.debug) {
                                    println("Failed to parse WS message: ${e.message}")
                                }
                            }
                        }
                        else -> {}
                    }
                }
            } catch (e: Exception) {
                if (config.debug) {
                    println("WebSocket error: ${e.message}")
                }
            }
        }
    }

    private suspend inline fun <reified T> get(path: String): T {
        return executeWithRetry {
            val response = client.get("${config.endpoint}$path")
            handleResponse(response)
        }
    }

    private suspend inline fun <reified T, reified R> post(path: String, body: R): T {
        return executeWithRetry {
            val response = client.post("${config.endpoint}$path") {
                setBody(body)
            }
            handleResponse(response)
        }
    }

    private suspend inline fun <reified T> handleResponse(response: HttpResponse): T {
        if (response.status.isSuccess()) {
            return response.body()
        }

        val errorBody = try {
            response.bodyAsText()
        } catch (e: Exception) {
            "Unknown error"
        }

        throw RpcException(
            message = "RPC API error: $errorBody",
            statusCode = response.status.value
        )
    }

    private suspend inline fun <T> executeWithRetry(block: () -> T): T {
        var lastException: Exception? = null

        repeat(config.retries) { attempt ->
            try {
                return block()
            } catch (e: Exception) {
                lastException = e
                if (config.debug) {
                    println("Attempt ${attempt + 1} failed: ${e.message}")
                }
                if (attempt < config.retries - 1) {
                    delay(1000L * (attempt + 1))
                }
            }
        }

        throw lastException ?: RpcException("Unknown error after ${config.retries} retries")
    }

    override fun close() {
        wsJob?.cancel()
        runBlocking {
            wsSession?.close()
        }
        client.close()
    }
}
