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.CoinData
import inn.ocsf.bee.freigeld.core.model.data.CoinStatus
import inn.ocsf.bee.freigeld.core.model.data.QCoinData.coinData
import inn.ocsf.bee.freigeld.core.repo.CoinRepository
import inn.ocsf.bee.freigeld.core.service.TicketService
import inn.ocsf.bee.freigeld.utils.KeyValueBucket
import inn.ocsf.bee.freigeld.utils.KeyValueStorage
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Lazy
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.ZoneId
import java.time.temporal.TemporalAdjusters
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.locks.ReentrantLock
import java.util.function.Consumer
import javax.annotation.PostConstruct
import javax.inject.Inject
import kotlin.concurrent.withLock

@Service
class GlobalEmitterImpl : GlobalEmitter {

    companion object {
        val emitterTicketChannel = GlobalEmitter::class.java.name
    }

    @Inject
    private lateinit var coinRepo: CoinRepository

    @Inject
    private lateinit var ticketService: TicketService

    @Inject
    private lateinit var kvStore: KeyValueStorage

    @Inject
    @Lazy
    private var centralBank: CentralBank? = null

    private val globalId = UUID.fromString("a671cdca-782a-4caf-8aad-056f6b62d822")

    private val listeners = mutableListOf<Consumer<EmitterEvent>>()

    private val deferredLock = ReentrantLock()

    private val log = LoggerFactory.getLogger(javaClass)

    @Scheduled(fixedDelay = 1000L)
    @Transactional
    fun initcalc() {
        val now = LocalDate.now()
        val mark = kvStore.get<String>(KeyValueBucket.emitterToPlan, now.year)
        if (mark == null) {
            val start = LocalDate.of(now.year, 1, 1)
            val end = LocalDate.now().with(TemporalAdjusters.lastDayOfYear())
            //val leap = now.isLeapYear
            var dt = start
            val planList = mutableListOf<LocalDate>()
            while (!dt.isAfter(end)) {
                if (dt.dayOfWeek == DayOfWeek.SATURDAY) {
                    planList.add(dt)
                }
                dt = dt.plusDays(1)
            }
            val ids = planList.filter { it.isAfter(now) }.map { it.atStartOfDay().plusHours(3) }.map { Date.from(it.atZone(ZoneId.systemDefault()).toInstant()) }.map {
                ticketService.offerAfter(emitterTicketChannel, it, EmitterRecalculationEvent(UUID.randomUUID(), it))
            }.joinToString(",")
            kvStore.put(KeyValueBucket.emitterToPlan, now.year, ids)
        }
    }

    @PostConstruct
    fun init() {
        GlobalScope.launch {
            delay(10 * 1000L)
            if (centralBank?.id == null) throw RuntimeException("bank not ready")
            ticketService.listen(emitterTicketChannel) { e ->
                when (e) {
                    is EmitterRecalculationEvent -> {
                        calc()
                        log.info("emitter recalculated ${e.dt}")
                        true
                    }
                    else -> throw RuntimeException("wrong event type ${e::class.java}")
                }
            }
        }
    }

    private fun computeIfAbsent(future: Coin? = null, fn: () -> CoinData): Coin {
        val ret: Coin? = if (future != null) {
            coinRepo.findAll(Expressions.allOf(coinData.status.eq(CoinStatus.indoor), coinData.coin.value.eq(future.value), coinData.coin.era.eq(future.era))).map { it.coin }.firstOrNull()
        } else {
            null
        }
        return if (ret == null) {
            var coin = fn()
            coin = coinRepo.save(coin)
            coin.coin
        } else {
            log.debug("emitter reuse coin ${CoinUtils.sum(listOf(ret))}")
            ret
        }
    }

    override fun listen(fn: Consumer<EmitterEvent>) {
        listeners.add(fn)
    }

    protected fun calcLater(): Set<UUID> {
        val ret = mutableSetOf<UUID>()
        coinRepo.findAllByStatus(CoinStatus.free).peek {
            it.coin.incEra()
            if (it.coin.era == CoinValue.MAX_ERA.toInt()) {
                it.status = CoinStatus.nil
                ret.add(it.coin.id)
            }
        }.forEach { coinRepo.save(it) }
        return ret
    }

    @Transactional
    override fun calc(): CompletableFuture<EmitterEvent> {
        val ret = CompletableFuture<EmitterEvent>()
        listeners.forEach { it.accept(EmitterStartRecalculationEvent()) }
        deferredLock.withLock {
            val deferred = CompletableDeferred<EmitterEvent>()
            GlobalScope.launch {
                try {
                    val nullCoins = calcLater()
                    val ev = EmitterStopRecalculationEvent(nullCoins)
                    deferred.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(ev) else ret.completeExceptionally(t) }
                    deferred.complete(ev)
                    listeners.forEach { it.accept(ev) }
                } catch (t: Throwable) {
                    deferred.completeExceptionally(t)
                }
            }
        }
        return ret
    }

    override fun emitOne(coin: Coin): UUID {
        return computeIfAbsent(coin) {
            val new = coinRepo.create(coin.value, coin.era)
            log.debug("emitter emit coin ${CoinUtils.sum(listOf(new.coin))}")
            new
        }.id
    }

    override fun emit(count: Long, value: CoinValue, era: Int) {
        (0 until count).forEach {
            computeIfAbsent { coinRepo.create(value, era) }
        }
        log.debug("emitter emit ${count} of ${value}-${era}")
    }

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

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

    override fun getOwnerId(): UUID {
        return globalId
    }

    override fun getId(): UUID {
        return globalId
    }

    private fun setStatus(coinId: UUID, status: CoinStatus, expectStatus: Set<CoinStatus>? = null): CoinData {
        val coin = coinRepo.findOneByCoinId(coinId).orElseThrow { IllegalArgumentException("coin ${coinId} must be real") }
        if (coin.status != status && expectStatus != null && expectStatus.isNotEmpty() && !expectStatus.contains(coin.status)) {
            throw RuntimeException("unexpected old status")
        }
        return if (coin.status != status && coin.status != CoinStatus.nil) {
            coin.status = status
            coinRepo.save(coin)
        } else {
            coin
        }
    }

    override fun acceptOne(coin: Coin) {
        setStatus(coin.id, CoinStatus.indoor, setOf(CoinStatus.nil, CoinStatus.free, CoinStatus.outdoor))
    }

    override fun free(c: Coin) {
        setStatus(c.id, CoinStatus.free, setOf(CoinStatus.indoor, CoinStatus.outdoor))
    }

    override fun extractOne(coinId: UUID): Coin? {
        return setStatus(coinId, CoinStatus.outdoor, setOf(CoinStatus.indoor)).coin
    }
}

class EmitterRecalculationEvent() : EmitterEvent() {

    var dt: Date? = null

    constructor(eventId: UUID, dt: Date) : this() {
        this.id = eventId
        this.dt = dt
    }
}