/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024-2025 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.ebics

import io.ktor.client.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.common.*

private val wsLog: Logger = LoggerFactory.getLogger("libeufin-ebics-ws")

@Serializable
data class WssParams(
    val URL: String,
    val TOKEN: String,
    val OTT: String,
    val VALIDITY: String,
    val PARTNERID: String,
    val USERID: String? = null,
)

@Serializable
data class WssNotificationClass(
    val NAME: String,
    val VERS: String,
    val TIMESTAMP: String,
)

@Serializable
data class WssNotificationBTF(
    val SERVICE: String,
    val SCOPE: String? = null,
    val OPTION: String? = null,
    val CONTTYPE: String? = null,
    val MSGNAME: String,
    val VARIANT: String? = null,
    val VERSION: String? = null,
    val FORMAT: String? = null,
)

@Serializable
data class WssNewData(
    val MCLASS: List<WssNotificationClass>,
    val PARTNERID: String,
    val USERID: String? = null,
    val BTF: List<WssNotificationBTF>,
    val ORDERTYPE: List<String>? = null
): WssNotification

@Serializable
data class WssInfo(
    val LANG: String,
    val FREE: String
)

@Serializable
data class WssGeneralInfo(
    val MCLASS: List<WssNotificationClass>,
    val INFO: List<WssInfo>
): WssNotification

@Serializable(with = WssNotification.Serializer::class)
sealed interface WssNotification {
    companion object Serializer : JsonContentPolymorphicSerializer<WssNotification>(WssNotification::class) {
        override fun selectDeserializer(element: JsonElement) = when {
            "INFO" in element.jsonObject -> WssGeneralInfo.serializer()
            else -> WssNewData.serializer()
        }
    }
}

/** Download EBICS real-time notifications websocket params */
suspend fun EbicsClient.wssParams(): WssParams =
    download(EbicsOrder.V3.WSS_PARAMS) { stream ->
        Json.decodeFromStream(stream)
    }

/** Receive a JSON message from a websocket session */
private suspend inline fun <reified T> DefaultClientWebSocketSession.receiveJson(): T {
    val frame = incoming.receive()
    val content = frame.readBytes()
    val msg = Json.decodeFromStream(kotlinx.serialization.serializer<T>(), content.inputStream())
    return msg
}

/** Connect to the EBICS real-time notifications websocket */
suspend fun WssParams.connect(client: HttpClient, lambda: suspend (WssNotification) -> Unit) {
    val client = client.config {
        install(WebSockets) {
            contentConverter = KotlinxWebsocketSerializationConverter(Json)
        }
    }
    // TODO check PARTNERID and USERID match conf ?
    val credentials = buildString {
        // Username
        append(PARTNERID)
        if (USERID != null) {
            append('_')
            append(USERID)
        }
        // Password
        append(':')
        append(TOKEN)
    }.encodeBase64()

    client.wss(URL.replace("https://", "wss://"), request = {
        headers {
            append(HttpHeaders.Authorization, "Basic $credentials")
        }
    }) {
        while (true) {
            wsLog.trace("wait for ws msg")
            // TODO use receiveDeserialized from ktor when it works
            val msg = receiveJson<WssNotification>()
            wsLog.trace("received: {}", msg)
            lambda(msg)
        }
    }
}

suspend fun listenForNotification(client: EbicsClient): ReceiveChannel<List<EbicsOrder>>? {
    val channel = Channel<List<EbicsOrder>>()
    val backoff = ExpoBackoffDecorr(
        30 * 1000,     // 30 seconds
        30 * 60 * 1000 // 30 min
    )
    kotlin.concurrent.thread(isDaemon = true) {
        runBlocking {
            while (true) {
                try {
                    // Try to get params
                    val params = try {
                        client.wssParams()
                    } catch (e: EbicsError) {
                        if (
                            // Expected EBICS error
                            (e is EbicsError.Code && e.technicalCode == EbicsReturnCode.EBICS_INVALID_ORDER_IDENTIFIER) || 
                            // Netzbon HTTP error
                            (e is EbicsError.HTTP && e.status == HttpStatusCode.BadRequest)
                        ) {
                            // Failure is expected if this wss is not supported
                            wsLog.info("Real-time EBICS notifications is not supported")
                            return@runBlocking
                        } else throw e
                    }
                    wsLog.info("Listening to real-time EBICS notifications")
                    wsLog.trace("{}", params)
                    params.connect(client.client) { msg ->
                        backoff.reset()
                        when (msg) {
                            is WssGeneralInfo -> {
                                for (info in msg.INFO) {
                                    wsLog.info("info: {}", info.FREE)
                                }
                            }
                            is WssNewData -> {
                                val orders = msg.BTF.map {
                                    EbicsOrder.V3(
                                        type = "BTD",
                                        service = it.SERVICE,
                                        scope = it.SCOPE,
                                        message = it.MSGNAME,
                                        version = it.VERSION,
                                        container = it.CONTTYPE,
                                        option = it.OPTION
                                    )
                                }
                                channel.send(orders)
                            }
                        }
                    }
                } catch (e: Exception) {
                    e.fmtLog(wsLog)
                    delay(backoff.next())
                }
            }
        }
    }
    return channel
}