package io.synor.ibc

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.io.Closeable
import java.util.Base64

/**
 * Synor IBC SDK for Kotlin
 *
 * Inter-Blockchain Communication (IBC) protocol for cross-chain interoperability.
 */
class SynorIbc(private val config: IbcConfig) : Closeable {
    private val httpClient = HttpClient(CIO) {
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }
    }
    private var closed = false

    val clients = LightClientClient(this)
    val connections = ConnectionsClient(this)
    val channels = ChannelsClient(this)
    val transfer = TransferClient(this)
    val swaps = SwapsClient(this)

    val chainId: String get() = config.chainId

    suspend fun getChainInfo(): Map<String, Any> = get("/chain")

    @Suppress("UNCHECKED_CAST")
    suspend fun getHeight(): Height {
        val result = get("/chain/height")
        return Height(
            revisionNumber = (result["revision_number"] as? Number)?.toLong() ?: 0,
            revisionHeight = (result["revision_height"] as? Number)?.toLong() ?: 1
        )
    }

    suspend fun healthCheck(): Boolean = try {
        val result = get("/health")
        result["status"] == "healthy"
    } catch (_: Exception) {
        false
    }

    override fun close() {
        closed = true
        httpClient.close()
    }

    val isClosed: Boolean get() = closed

    // Internal HTTP methods
    @Suppress("UNCHECKED_CAST")
    internal suspend fun get(path: String): Map<String, Any> {
        checkClosed()
        val response = httpClient.get("${config.endpoint}$path") {
            headers {
                append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
                append(HttpHeaders.Authorization, "Bearer ${config.apiKey}")
                append("X-SDK-Version", "kotlin/0.1.0")
                append("X-Chain-Id", config.chainId)
            }
        }
        if (response.status.value >= 400) {
            val error = try { response.body<Map<String, Any>>() } catch (_: Exception) { null }
            throw IbcException(
                error?.get("message") as? String ?: "HTTP ${response.status.value}",
                error?.get("code") as? String,
                response.status.value
            )
        }
        return response.body()
    }

    @Suppress("UNCHECKED_CAST")
    internal suspend fun post(path: String, body: Map<String, Any>): Map<String, Any> {
        checkClosed()
        val response = httpClient.post("${config.endpoint}$path") {
            headers {
                append(HttpHeaders.ContentType, ContentType.Application.Json.toString())
                append(HttpHeaders.Authorization, "Bearer ${config.apiKey}")
                append("X-SDK-Version", "kotlin/0.1.0")
                append("X-Chain-Id", config.chainId)
            }
            setBody(body)
        }
        if (response.status.value >= 400) {
            val error = try { response.body<Map<String, Any>>() } catch (_: Exception) { null }
            throw IbcException(
                error?.get("message") as? String ?: "HTTP ${response.status.value}",
                error?.get("code") as? String,
                response.status.value
            )
        }
        return response.body()
    }

    private fun checkClosed() {
        if (closed) throw IbcException("Client has been closed", "CLIENT_CLOSED")
    }

    /**
     * Light client sub-client
     */
    class LightClientClient(private val ibc: SynorIbc) {
        suspend fun create(
            clientType: ClientType,
            clientState: ClientState,
            consensusState: Map<String, Any>
        ): ClientId {
            val result = ibc.post("/clients", mapOf(
                "client_type" to clientType.value,
                "client_state" to mapOf(
                    "chain_id" to clientState.chainId,
                    "trust_level" to mapOf(
                        "numerator" to clientState.trustLevel.numerator,
                        "denominator" to clientState.trustLevel.denominator
                    ),
                    "trusting_period" to clientState.trustingPeriod,
                    "unbonding_period" to clientState.unbondingPeriod,
                    "max_clock_drift" to clientState.maxClockDrift,
                    "latest_height" to mapOf(
                        "revision_number" to clientState.latestHeight.revisionNumber,
                        "revision_height" to clientState.latestHeight.revisionHeight
                    )
                ),
                "consensus_state" to consensusState
            ))
            return ClientId(result["client_id"] as String)
        }

        @Suppress("UNCHECKED_CAST")
        suspend fun getState(clientId: ClientId): ClientState {
            val result = ibc.get("/clients/${clientId.id}/state")
            val trustLevel = result["trust_level"] as Map<String, Any>
            val latestHeight = result["latest_height"] as Map<String, Any>
            val frozenHeight = result["frozen_height"] as? Map<String, Any>
            return ClientState(
                chainId = result["chain_id"] as String,
                trustLevel = TrustLevel(
                    numerator = (trustLevel["numerator"] as Number).toInt(),
                    denominator = (trustLevel["denominator"] as Number).toInt()
                ),
                trustingPeriod = (result["trusting_period"] as Number).toLong(),
                unbondingPeriod = (result["unbonding_period"] as Number).toLong(),
                maxClockDrift = (result["max_clock_drift"] as Number).toLong(),
                latestHeight = Height(
                    revisionNumber = (latestHeight["revision_number"] as Number).toLong(),
                    revisionHeight = (latestHeight["revision_height"] as Number).toLong()
                ),
                frozenHeight = frozenHeight?.let {
                    Height(
                        revisionNumber = (it["revision_number"] as Number).toLong(),
                        revisionHeight = (it["revision_height"] as Number).toLong()
                    )
                }
            )
        }

        @Suppress("UNCHECKED_CAST")
        suspend fun list(): List<Map<String, Any>> {
            val result = ibc.get("/clients")
            return result["clients"] as? List<Map<String, Any>> ?: emptyList()
        }
    }

    /**
     * Connections sub-client
     */
    class ConnectionsClient(private val ibc: SynorIbc) {
        suspend fun openInit(
            clientId: ClientId,
            counterpartyClientId: ClientId
        ): ConnectionId {
            val result = ibc.post("/connections/init", mapOf(
                "client_id" to clientId.id,
                "counterparty_client_id" to counterpartyClientId.id
            ))
            return ConnectionId(result["connection_id"] as String)
        }

        suspend fun get(connectionId: ConnectionId): Map<String, Any> =
            ibc.get("/connections/${connectionId.id}")

        @Suppress("UNCHECKED_CAST")
        suspend fun list(): List<Map<String, Any>> {
            val result = ibc.get("/connections")
            return result["connections"] as? List<Map<String, Any>> ?: emptyList()
        }
    }

    /**
     * Channels sub-client
     */
    class ChannelsClient(private val ibc: SynorIbc) {
        suspend fun bindPort(portId: PortId, module: String) {
            ibc.post("/ports/bind", mapOf("port_id" to portId.id, "module" to module))
        }

        suspend fun openInit(
            portId: PortId,
            ordering: ChannelOrder,
            connectionId: ConnectionId,
            counterpartyPort: PortId,
            version: String
        ): ChannelId {
            val result = ibc.post("/channels/init", mapOf(
                "port_id" to portId.id,
                "ordering" to ordering.name.lowercase(),
                "connection_id" to connectionId.id,
                "counterparty_port" to counterpartyPort.id,
                "version" to version
            ))
            return ChannelId(result["channel_id"] as String)
        }

        suspend fun get(portId: PortId, channelId: ChannelId): Map<String, Any> =
            ibc.get("/channels/${portId.id}/${channelId.id}")

        @Suppress("UNCHECKED_CAST")
        suspend fun list(): List<Map<String, Any>> {
            val result = ibc.get("/channels")
            return result["channels"] as? List<Map<String, Any>> ?: emptyList()
        }
    }

    /**
     * Transfer sub-client (ICS-20)
     */
    class TransferClient(private val ibc: SynorIbc) {
        suspend fun transfer(
            sourcePort: String,
            sourceChannel: String,
            denom: String,
            amount: String,
            sender: String,
            receiver: String,
            timeout: Timeout? = null,
            memo: String? = null
        ): Map<String, Any> {
            val body = mutableMapOf<String, Any>(
                "source_port" to sourcePort,
                "source_channel" to sourceChannel,
                "token" to mapOf("denom" to denom, "amount" to amount),
                "sender" to sender,
                "receiver" to receiver
            )
            timeout?.let {
                body["timeout_height"] = mapOf(
                    "revision_number" to it.height.revisionNumber,
                    "revision_height" to it.height.revisionHeight
                )
                body["timeout_timestamp"] = it.timestamp.toString()
            }
            memo?.let { body["memo"] = it }
            return ibc.post("/transfer", body)
        }

        suspend fun getDenomTrace(ibcDenom: String): Map<String, Any> =
            ibc.get("/transfer/denom_trace/$ibcDenom")
    }

    /**
     * Swaps sub-client (HTLC)
     */
    class SwapsClient(private val ibc: SynorIbc) {
        suspend fun initiate(
            responder: String,
            initiatorAsset: Map<String, Any>,
            responderAsset: Map<String, Any>
        ): Map<String, Any> = ibc.post("/swaps/initiate", mapOf(
            "responder" to responder,
            "initiator_asset" to initiatorAsset,
            "responder_asset" to responderAsset
        ))

        suspend fun lock(swapId: SwapId) {
            ibc.post("/swaps/${swapId.id}/lock", emptyMap())
        }

        suspend fun respond(swapId: SwapId, asset: Map<String, Any>): Map<String, Any> =
            ibc.post("/swaps/${swapId.id}/respond", mapOf("asset" to asset))

        suspend fun claim(swapId: SwapId, secret: ByteArray): Map<String, Any> =
            ibc.post("/swaps/${swapId.id}/claim", mapOf(
                "secret" to Base64.getEncoder().encodeToString(secret)
            ))

        suspend fun refund(swapId: SwapId): Map<String, Any> =
            ibc.post("/swaps/${swapId.id}/refund", emptyMap())

        suspend fun get(swapId: SwapId): AtomicSwap =
            AtomicSwap.fromJson(ibc.get("/swaps/${swapId.id}"))

        @Suppress("UNCHECKED_CAST")
        suspend fun listActive(): List<AtomicSwap> {
            val result = ibc.get("/swaps/active")
            val swaps = result["swaps"] as? List<Map<String, Any>> ?: emptyList()
            return swaps.map { AtomicSwap.fromJson(it) }
        }
    }
}
