package com.jsjdesigns.poker.game

import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

/**
 * A [PlayerAgent] driven by the UI rather than by code.
 *
 * When it is this player's turn, [act] publishes an immutable [DecisionOffer] and
 * suspends until [submit] delivers a choice. Cancelling the coroutine running the
 * hand — a screen closing, a game abandoned — completes the pending wait
 * exceptionally rather than leaving it parked forever.
 *
 * [offer] is a `StateFlow` rather than a plain property because it is written by
 * the game coroutine and read by the UI thread: a bare `var` would be both a data
 * race and invisible to Compose.
 */
class HumanAgent : PlayerAgent {

    private val lock = Mutex()
    private var pending: CompletableDeferred<Action>? = null
    private var pendingToken = 0L

    private val _offer = MutableStateFlow<DecisionOffer?>(null)

    /** The decision awaiting input, or null when it is not this player's turn. */
    val offer: StateFlow<DecisionOffer?> = _offer.asStateFlow()

    val isAwaitingInput: Boolean get() = _offer.value != null

    override suspend fun act(ctx: DecisionContext): Action {
        val deferred = CompletableDeferred<Action>()
        // The engine owns decision identity, so the offer and the snapshot that
        // announced this turn carry the same token.
        val token = ctx.decisionToken
        lock.withLock {
            check(pending == null) { "already awaiting a decision for this agent" }
            pending = deferred
            pendingToken = token
        }
        // Published after the deferred is installed, so a UI that reacts instantly
        // to the offer always finds something able to receive its submission.
        _offer.value = ctx.toOffer(token)

        return try {
            deferred.await()
        } finally {
            // Runs on normal completion AND on cancellation, so the agent is never
            // left believing it is still waiting.
            _offer.value = null
            lock.withLock { pending = null }
        }
    }

    /**
     * Supplies the player's choice for the decision identified by [token].
     *
     * Returns false when nothing is waiting or when [token] is stale. Requiring
     * the token is what stops a double tap, or a tap that lands just after the
     * turn moved on, from being applied to the *next* decision — which could be a
     * different street or an entirely different hand.
     */
    suspend fun submit(token: Long, action: Action): Boolean = lock.withLock {
        val deferred = pending ?: return@withLock false
        if (token != pendingToken) return@withLock false
        deferred.complete(action)
    }

    /** Abandons the pending decision, releasing [act] with [cause]. */
    suspend fun cancel(cause: Throwable = IllegalStateException("hand abandoned")): Boolean =
        lock.withLock {
            val deferred = pending ?: return@withLock false
            deferred.completeExceptionally(cause)
        }
}
