package inn.ocsf.bee.freigeld.core.data

import inn.ocsf.bee.freigeld.core.calc.CoinStrategy
import inn.ocsf.bee.freigeld.core.calc.CoinUtils
import inn.ocsf.bee.freigeld.core.model.*
import inn.ocsf.bee.freigeld.core.service.TicketService
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.stream.Collectors
import javax.annotation.PostConstruct
import javax.inject.Inject

@Service
class CentralBankQueueLevel : CentralBankAccountLevel() {

    companion object {
        val bankTicketChannelName = Bank::class.java.name
        val bankExchangeBusName = "/topic/exchange"
    }

    @Autowired
    private lateinit var emitter: GlobalEmitter

    @Inject
    private lateinit var ticketService: TicketService

    @Inject
    private lateinit var messaging: SimpMessagingTemplate

    private val globalFutureMap = mutableMapOf<UUID, CompletableDeferred<Ticket>>()

    private val log = LoggerFactory.getLogger(javaClass)

    override fun exchange(to: BankAccount, from: BankAccount, amount: Long, details: BankExchangeDetails?): CompletableFuture<IExchangeTicket> {
        val e = ExchangeStartEvent(UUID.randomUUID(), to, from, amount, details)
        val ret = CompletableFuture<IExchangeTicket>()
        globalFutureMap.computeIfAbsent(e.id) { id ->
            ticketService.offerLast(bankTicketChannelName, e)
            val def = CompletableDeferred<Ticket>()
            def.invokeOnCompletion { t: Throwable? ->
                if (t == null) {
                    messaging.convertAndSendToUser(to.ownerId.toString(), bankExchangeBusName, e.id.toString())
                    messaging.convertAndSendToUser(from.ownerId.toString(), bankExchangeBusName, e.id.toString())
                    ret.complete(e)
                } else {
                    ret.completeExceptionally(t)
                }
            }
            def
        }
        return ret
    }

    @Transactional
    fun pollLater(e: BankEvent): Boolean {
        return when (e) {
            is ExchangeStartEvent -> {
                val eFrom = e.from?.let { getAccount(it) } ?: emitter
                val eTo = e.to?.let { getAccount(it) }!!
                val eAmount = e.amount ?: 0L
                val selfIds = getAccounts(id).map { it.id }.toSet()
                CoinUtils.makeSomeStrategy(CoinStrategy(eAmount, if (e.from != emitter.id) eFrom.coins else null, if (!selfIds.contains(e.from)) eTo.coins else null)).let { st ->
                    if (st.res!!.isOk()) {
                        st
                    } else if (st.res!!.isErr()) {
                        ticketService.offerLast(bankTicketChannelName, ExchangeFailedEvent(UUID.randomUUID(), e, "strategy error ${st.res}"))
                        null
                    } else {
                        TODO()
                    }
                }?.let { st ->
                    log.info("${id} do ${st.res?.name} transaction: ${e.from} -> ${e.to} = ${e.amount}")
                    var fromAmount = eFrom.overall
                    var toAmount = eTo.overall

                    val leftExtracted = if (st.leftExtract != null) {
                        val old = eFrom.extract(st.leftExtract?.map { it.id }!!)
                        //emitter.accept(old) later
                        old
                    } else {
                        listOf()
                    }

                    if (st.leftEmit != null) {
                        val newIds = emitter.emit(st.leftEmit!!)
                        val newCoins = emitter.extract(newIds)
                        eFrom.accept(newCoins)
                        if (!selfIds.contains(e.from)) {
                            emitter.free(newCoins)
                        } else {
                            //do nothing
                        }
                    }

                    val rightExtracted = if (st.rightExtract != null) {
                        val old = eTo.extract(st.rightExtract?.map { it.id }!!)
                        //emitter.accept(old) later
                        old
                    } else {
                        listOf()
                    }

                    if (st.rightEmit != null) {
                        val newIds = emitter.emit(st.rightEmit!!)
                        if (e.from == emitter.id) {
                            fromAmount = eFrom.overall
                        }
                        val newCoins = emitter.extract(newIds)
                        eTo.accept(newCoins)
                        if (!selfIds.contains(e.to)) {
                            emitter.free(newCoins)
                        } else {
                            //do nothing
                        }
                    }

                    if (st.rightCashback != null) {
                        val localCoins = leftExtracted.map { it.id to it }.toMap().toMutableMap()
                        val externalCoins = emitter.extract(st.rightCashback?.filterNot { localCoins.containsKey(it.id) }?.map { it.id }!!)
                        val sameCoins = st.rightCashback?.filter { localCoins.containsKey(it.id) } ?: listOf()
                        val someCoins = sameCoins + externalCoins
                        eTo.accept(someCoins)
                        someCoins.forEach { localCoins.remove(it.id) }
                        emitter.accept(localCoins.values)
                        if (!selfIds.contains(e.to)) {
                            emitter.free(someCoins)
                        } else {
                            //do nothing
                        }
                    }

                    if (st.leftCashback != null) {
                        val localCoins = rightExtracted.map { it.id to it }.toMap().toMutableMap()
                        val externalCoins = emitter.extract(st.leftCashback?.filterNot { localCoins.containsKey(it.id) }?.map { it.id }!!)
                        val sameCoins = st.leftCashback?.filter { localCoins.containsKey(it.id) } ?: listOf()
                        val someCoins = sameCoins + externalCoins
                        eFrom.accept(someCoins)
                        someCoins.forEach { localCoins.remove(it.id) }
                        emitter.accept(localCoins.values)
                        if (!selfIds.contains(e.from)) {
                            emitter.free(someCoins)
                        } else {
                            //do nothing
                        }
                    }

                    val creditOk = (fromAmount - eAmount == eFrom.overall) || e.from == emitter.id
                    val debitOk = (toAmount + eAmount == eTo.overall)
                    val fromCoins = eFrom.coins
                    val toCoins = eTo.coins
                    val uniqOk = if (fromCoins != null) {
                        if (toCoins != null) {
                            fromCoins.map { it.id }.toSet().intersect(toCoins.map { it.id }.toSet()).isEmpty()
                        } else {
                            true
                        }
                    } else {
                        true
                    }
                    if (debitOk && creditOk && uniqOk) {
                        true
                    } else {
                        TODO()
                        false
                    }
                }?.let { ok ->
                    if (ok) {
                        ticketService.offerLast(bankTicketChannelName, ExchangeSuccessEvent(UUID.randomUUID(), e))
                    } else {
                        ticketService.offerLast(bankTicketChannelName, ExchangeFailedEvent(UUID.randomUUID(), e, "exchange strategy failed"))
                    }
                    ok
                } ?: true
            }
            is ExchangeSuccessEvent -> {
                e.parentEvent?.let { p ->
                    globalFutureMap.get(p.id)?.let {
                        globalFutureMap.remove(p.id)
                        it.complete(p)
                    }
                }
                true
            }
            is ExchangeFailedEvent -> {
                e.parentEvent?.let { p ->
                    globalFutureMap.get(p.id)?.let {
                        globalFutureMap.remove(e.parentEvent?.id)
                        it.completeExceptionally(ExchangeFailedException(e, e.reason))
                    }
                }
                true
            }
            is BankPauseOnRecalcEvent -> {
                getCurrentState { it.queueState = BankQueueState.recalc }
                true
            }
            is BankResumeAfterRecalcEvent -> {
                val ids = accounts.map { it to it.coins?.map { it.id }?.intersect(e.emitterEvent?.nullCoins ?: setOf()) }.filter { it.second?.isNotEmpty() ?: false }.map { ap ->
                    val nullCoins = ap.second?.map { ap.first.extractOne(it) }?.toSet() ?: setOf()
                    if (nullCoins.isNotEmpty()) {
                        emitter.accept(nullCoins)
                        ap.first
                    } else {
                        null
                    }
                }.map { it?.id }.filter { it != null }.collect(Collectors.toList()).filterNotNull().toSet()
                ticketService.offerLast(bankTicketChannelName, BankAffectAfterRecalcEvent(UUID.randomUUID(), e.emitterEvent?.nullCoins, ids, e))
                getCurrentState { it.queueState = BankQueueState.open }
                true
            }
            is BankAffectAfterRecalcEvent -> true
            else -> throw IllegalArgumentException("wrong event type ${e.javaClass.name}")
        }
    }

    @PostConstruct
    fun start() {
        GlobalScope.launch {
            emitter.listen { e ->
                log.debug("emitter event ${e.javaClass.name}")
                when (e) {
                    is EmitterStartRecalculationEvent -> ticketService.offerLast(bankTicketChannelName, BankPauseOnRecalcEvent(UUID.randomUUID(), e))
                    is EmitterStopRecalculationEvent -> ticketService.offerLast(bankTicketChannelName, BankResumeAfterRecalcEvent(UUID.randomUUID(), e))
                    else -> throw RuntimeException("unknown emitter event ${e.javaClass.name}")
                }
            }
            ticketService.listen(bankTicketChannelName) { e ->
                val qst = getCurrentState { it.queueState ?: BankQueueState.open }
                log.info("poll event ${e::class.java} on state ${qst}")
                if (e is AbstractBankEvent && qst != BankQueueState.closed) {
                    if ((qst == BankQueueState.open && !(e is BankResumeAfterRecalcEvent)) || (qst == BankQueueState.recalc && e is BankResumeAfterRecalcEvent)) {
                        pollLater(e)
                    } else {
                        false
                    }
                } else {
                    null
                }
            }
        }
    }
}

abstract class AbstractBankEvent : BankEvent()

abstract class ExchangeBankEvent : AbstractBankEvent()

class ExchangeSuccessEvent() : ExchangeBankEvent() {

    var parentEvent: ExchangeStartEvent? = null

    constructor(id: UUID, parentEvent: ExchangeStartEvent) : this() {
        this.id = id
        this.parentEvent = parentEvent
    }
}

class ExchangeFailedEvent() : ExchangeBankEvent() {

    var reason: String? = null
    var parentEvent: ExchangeStartEvent? = null

    constructor(id: UUID, parentEvent: ExchangeStartEvent, reason: String) : this() {
        this.id = id
        this.parentEvent = parentEvent
        this.reason = reason
    }
}

class ExchangeFailedException(val event: ExchangeBankEvent, message: String? = "exchange failed") : Exception(message)

class ExchangeStartEvent() : ExchangeBankEvent(), IExchangeTicket {
    var to: UUID? = null
    var from: UUID? = null
    var amount: Long? = null
    var _details: BankExchangeDetails? = null

    constructor(id: UUID, to: BankAccount, from: BankAccount, amount: Long, details: BankExchangeDetails?) : this() {
        this.id = id
        this.to = to.id
        this.from = from.id
        this.amount = amount
        this._details = details
    }

    override fun getDetails(): BankExchangeDetails? {
        return _details
    }

}

class BankPauseOnRecalcEvent() : AbstractBankEvent() {

    var emitterEvent: EmitterStartRecalculationEvent? = null

    constructor(id: UUID, emitterEvent: EmitterStartRecalculationEvent) : this() {
        this.id = id
        this.emitterEvent = emitterEvent
    }
}

class BankResumeAfterRecalcEvent() : AbstractBankEvent() {
    var emitterEvent: EmitterStopRecalculationEvent? = null

    constructor(id: UUID, emitterEvent: EmitterStopRecalculationEvent) : this() {
        this.id = id
        this.emitterEvent = emitterEvent
    }
}

class BankAffectAfterRecalcEvent() : AbstractBankEvent() {
    var nullCoins: Set<UUID>? = null
    var nullAccounts: Set<UUID>? = null
    var parentEvent: BankResumeAfterRecalcEvent? = null

    constructor(id: UUID, nullCoins: Set<UUID>?, nullAccounts: Set<UUID>?, parent: BankResumeAfterRecalcEvent) : this() {
        this.id = id
        this.nullAccounts = nullAccounts
        this.nullCoins = nullCoins
        this.parentEvent = parent
    }
}
