package com.correx.apps.server.narration

import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.StageCompletedEvent
import com.correx.core.events.events.StageFailedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.events.WorkflowCompletedEvent
import com.correx.core.events.events.WorkflowFailedEvent
import com.correx.core.events.events.WorkflowStartedEvent
import com.correx.core.events.stores.EventStore
import com.correx.core.events.types.SessionId
import com.correx.core.router.RouterFacade
import com.correx.core.router.model.NarrationTrigger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import java.util.concurrent.ConcurrentHashMap

class NarrationSubscriber(
    private val eventStore: EventStore,
    private val routerFacade: RouterFacade,
    private val scope: CoroutineScope,
    private val maxPerRun: Int = DEFAULT_MAX_PER_RUN,
) {
    private data class SessionLane(val channel: Channel<NarrationTrigger>, var used: Int)

    // Latest approval awaiting a decision per session, so a pause narration can name the
    // tool and show the proposed change instead of the bare "APPROVAL_PENDING" reason.
    private data class PendingApproval(val toolName: String, val tier: String, val preview: String)

    private val lanes = ConcurrentHashMap<String, SessionLane>()
    private val pendingApprovals = ConcurrentHashMap<String, PendingApproval>()

    fun start() {
        eventStore.subscribeAll().onEach { handle(it) }.launchIn(scope)
    }

    private fun handle(event: StoredEvent) {
        val sid = event.metadata.sessionId
        when (val p = event.payload) {
            is WorkflowStartedEvent -> lanes[sid.value]?.let { it.used = 0 }
            is ApprovalRequestedEvent -> pendingApprovals[sid.value] = PendingApproval(
                toolName = p.toolName ?: "a tool",
                tier = p.tier.toString(),
                preview = p.preview.orEmpty(),
            )
            is StageCompletedEvent -> enqueue(
                sid,
                NarrationTrigger(kind = "stage_completed", instruction = "Stage ${p.stageId.value} completed. Summarise what was accomplished."),
            )
            is StageFailedEvent -> enqueue(
                sid,
                NarrationTrigger(kind = "stage_failed", instruction = "Stage ${p.stageId.value} failed: ${p.reason}. Explain what went wrong."),
            )
            is WorkflowCompletedEvent -> enqueue(
                sid,
                NarrationTrigger(kind = "workflow_completed", instruction = "The workflow finished successfully. Provide a brief summary."),
            )
            is WorkflowFailedEvent -> enqueue(
                sid,
                NarrationTrigger(kind = "workflow_failed", instruction = "The workflow failed: ${p.reason}. Explain the failure to the user."),
            )
            is OrchestrationPausedEvent -> enqueue(
                sid,
                NarrationTrigger(kind = "paused", instruction = pauseInstruction(sid, p)),
            )
            else -> Unit
        }
    }

    private fun pauseInstruction(sessionId: SessionId, event: OrchestrationPausedEvent): String {
        val pending = pendingApprovals[sessionId.value]
        if (!event.reason.contains("APPROVAL", ignoreCase = true) || pending == null) {
            return "Orchestration paused in stage ${event.stageId.value}: ${event.reason}. Inform the user."
        }
        return buildString {
            append("The workflow paused in stage ${event.stageId.value} awaiting your approval of the ")
            append("'${pending.toolName}' tool (tier ${pending.tier}). ")
            append("Tell the operator what it is about to do and why it needs sign-off.")
            if (pending.preview.isNotBlank()) {
                append("\n\nProposed change:\n")
                append(pending.preview.take(MAX_PREVIEW_CHARS))
            }
        }
    }

    private fun enqueue(sessionId: SessionId, trigger: NarrationTrigger) {
        val lane = lanes.computeIfAbsent(sessionId.value) { startLane(sessionId) }
        if (lane.used >= maxPerRun) {
            log.debug("narration budget reached for session {}; skipping {}", sessionId.value, trigger.kind)
            return
        }
        lane.used += 1
        lane.channel.trySend(trigger)
    }

    private fun startLane(sessionId: SessionId): SessionLane {
        val channel = Channel<NarrationTrigger>(capacity = Channel.UNLIMITED)
        scope.launch {
            for (trigger in channel) {
                runCatching { routerFacade.narrate(sessionId, trigger) }
                    .onFailure { e ->
                        if (e is CancellationException) throw e
                        log.warn(
                            "narration failed for session {} trigger {}: {}",
                            sessionId.value,
                            trigger.kind,
                            e.message,
                        )
                    }
            }
        }
        return SessionLane(channel, used = 0)
    }

    companion object {
        private const val DEFAULT_MAX_PER_RUN = 100
        private const val MAX_PREVIEW_CHARS = 800
        private val log = LoggerFactory.getLogger(NarrationSubscriber::class.java)
    }
}
