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

import com.querydsl.core.types.dsl.Expressions
import inn.ocsf.bee.freigeld.core.calc.CoinUtils
import inn.ocsf.bee.freigeld.core.model.*
import inn.ocsf.bee.freigeld.core.model.data.BankAccountData
import inn.ocsf.bee.freigeld.core.model.data.CoinAccountLink
import inn.ocsf.bee.freigeld.core.model.data.PersonData
import inn.ocsf.bee.freigeld.core.model.data.QBankAccountData.bankAccountData
import inn.ocsf.bee.freigeld.core.model.data.StateObject
import inn.ocsf.bee.freigeld.core.repo.BankAccountRepository
import inn.ocsf.bee.freigeld.core.repo.CoinAccountLinkRepository
import inn.ocsf.bee.freigeld.core.repo.CoinRepository
import inn.ocsf.bee.freigeld.core.repo.StateRepository
import org.slf4j.LoggerFactory
import org.springframework.transaction.annotation.Transactional
import java.util.*
import java.util.stream.Stream
import java.util.stream.StreamSupport
import javax.annotation.PostConstruct
import javax.inject.Inject

abstract class CentralBankAccountLevel : CentralBank {

    @Inject
    private lateinit var world: GlobalWorld

    @Inject
    private lateinit var accountRepo: BankAccountRepository

    @Inject
    private lateinit var coinRepo: CoinRepository

    @Inject
    private lateinit var linkRepo: CoinAccountLinkRepository

    @Inject
    private lateinit var stateRepo: StateRepository

    @Inject
    private lateinit var emitter: GlobalEmitter

    protected val bankId = UUID.fromString("a8486777-834c-4267-9f10-69f03714c523")

    private val log = LoggerFactory.getLogger(javaClass)

    private fun delegateAccount(account: BankAccount): BankAccount {
        val delegate = object : WrappedBankAccountDelegate(account) {

            override fun getAccountOverall(account: BankAccount): Long {
                return getAccountCoins(account)?.map { it.current }?.sum() ?: 0L
            }

            override fun getAccountCoins(account: BankAccount): MutableCollection<Coin>? {
                val ids = linkRepo.findAllByAccountId(account.id).map { it.coinId }
                return coinRepo.findAllByCoinIdIn(ids).map { it.coin }.toMutableList()
            }

            override fun accountExtractOne(account: BankAccount, coinId: UUID): Coin? {
                val link = linkRepo.findOneByAccountIdAndCoinId(account.id, coinId).orElseThrow { RuntimeException("${account.id} has no such coin ${coinId}") }
                linkRepo.deleteById(link.id)
                return coinRepo.findOneByCoinId(link.coinId).map { it.coin }.orElse(null)
            }

            override fun accountAcceptOne(account: BankAccount, coin: Coin) {
                val link = CoinAccountLink()
                link.accountId = account.id
                link.coinId = coin.id
                link.ts = Date()
                linkRepo.save(link)
            }
        }
        return WrappedBankAccount(account as BankAccountData.BankAccountDefault, delegate)
    }

    @Transactional
    open fun <T> getCurrentState(fn: (CentralBankStateObject) -> T? = { null }): T? {
        return stateRepo.findById(bankId).orElseGet { stateRepo.save(CentralBankStateObject(bankId)) }.let {
            try {
                val ret = fn(it as CentralBankStateObject)
                stateRepo.save(it)
                ret
            } catch (e: Exception) {
                log.error("error while access state", e)
                null
            }
        }
    }

    @PostConstruct
    @Transactional
    open fun init() {
        world.getPerson(bankId) ?: world.addPerson(PersonData.BankPrivate(bankId))
        val thisAccounts = getAccounts(bankId)
        if (thisAccounts.isEmpty()) {
            val selfAcc = addAccount(bankId)
            emitter.emit(CoinUtils.makeSomeFuture(10_000_000))
            exchange(selfAcc, emitter, 5_000_000, null)
        }
        getCurrentState { }
    }

    override fun addAccount(personId: UUID): BankAccount {
        val ad = BankAccountData()
        ad.bankId = bankId
        ad.account = BankAccountData.BankAccountDefault(UUID.randomUUID(), personId)
        return delegateAccount(accountRepo.save(ad).account)
    }

    override fun getId(): UUID {
        return bankId
    }

    override fun getOwnerId(): UUID {
        return UUID.randomUUID()
    }

    override fun hasPerson(person: UUID): Boolean {
        return accountRepo.count(Expressions.allOf(bankAccountData.bankId.eq(bankId), bankAccountData.account.ownerId.eq(person))) > 0
    }

    override fun hasAccount(account: UUID): Boolean {
        return accountRepo.exists(Expressions.allOf(bankAccountData.bankId.eq(bankId), bankAccountData.account.id.eq(account)))
    }

    override fun getAccounts(person: UUID): MutableCollection<BankAccount> {
        return accountRepo.findAll(Expressions.allOf(bankAccountData.bankId.eq(bankId), bankAccountData.account.ownerId.eq(person))).map { delegateAccount(it.account) }.toMutableList()
    }

    override fun getAccounts(): Stream<BankAccount> {
        return StreamSupport.stream(accountRepo.findAll(bankAccountData.bankId.eq(bankId)).spliterator(), false).map { delegateAccount(it.account) }
    }

    override fun getAccount(accountId: UUID): BankAccount? {
        return accountRepo.findOne(Expressions.allOf(bankAccountData.bankId.eq(bankId), bankAccountData.account.id.eq(accountId))).map { delegateAccount(it.account) }.orElse(null)
    }
}

private abstract class WrappedBankAccountDelegate(var inner: BankAccount) : BankAccount {

    override fun getId(): UUID {
        TODO("Not yet implemented")
    }

    override fun getOwnerId(): UUID {
        TODO("Not yet implemented")
    }

    override fun getOverall(): Long {
        return getAccountOverall(inner)
    }

    abstract fun getAccountOverall(account: BankAccount): Long

    override fun getCoins(): MutableCollection<Coin>? {
        return getAccountCoins(inner)
    }

    abstract fun getAccountCoins(account: BankAccount): MutableCollection<Coin>?

    override fun extractOne(coinId: UUID): Coin? {
        return accountExtractOne(inner, coinId)
    }

    abstract fun accountExtractOne(account: BankAccount, coinId: UUID): Coin?

    override fun acceptOne(coin: Coin) {
        accountAcceptOne(inner, coin)
    }

    abstract fun accountAcceptOne(account: BankAccount, coin: Coin)
}

private class WrappedBankAccount(val inner: BankAccountData.BankAccountDefault, delegate: WrappedBankAccountDelegate) : BankAccount {

    init {
        inner.transientDelegate = delegate
    }

    override fun getId(): UUID {
        return inner.id
    }

    override fun getOwnerId(): UUID {
        return inner.ownerId
    }

    override fun getOverall(): Long {
        return inner.overall
    }

    override fun getCoins(): MutableCollection<Coin>? {
        return inner.coins
    }

    override fun extractOne(coinId: UUID): Coin? {
        return inner.extractOne(coinId)
    }

    override fun acceptOne(coin: Coin) {
        inner.acceptOne(coin)
    }

    override fun toString(): String {
        return inner.toString()
    }
}

enum class BankQueueState {
    open, recalc, closed;
}

class CentralBankStateObject() : StateObject() {

    var queueState: BankQueueState? = BankQueueState.open

    constructor(id: UUID) : this() {
        this.id = id
    }
}