package com.correx.apps.server.narration

import com.correx.core.events.events.ApprovalDecisionResolvedEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ExecutionPlanRejectedEvent
import com.correx.core.events.events.InitialIntentEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.ReviewFindingsRaisedEvent
import com.correx.core.events.events.ReviewVerdict
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.talkie.TalkieFacade
import com.correx.core.talkie.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.Collections
import java.util.concurrent.ConcurrentHashMap

class NarrationSubscriber(
    private val eventStore: EventStore,
    private val routerFacade: TalkieFacade,
    private val scope: CoroutineScope,
    private val maxPerRun: Int = DEFAULT_MAX_PER_RUN,
) {
    /**
     * A queued narration plus the pause-trigger bookkeeping needed to drop it if the
     * pause it described resolves before the lane worker delivers it.
     *
     * [pauseKey] is non-null only for pause-trigger narrations. It is the resolving signal's
     * scope: the originating approval request id when known (per-request supersession), or a
     * session-wide sentinel for non-approval pauses (per-session supersession).
     */
    private data class QueuedNarration(val trigger: NarrationTrigger, val pauseKey: String?)

    /**
     * Per-session lane. [pendingPauses] holds the pause keys still queued (not yet delivered
     * by the worker). When an approval resolves or the orchestration resumes, the matching
     * keys are removed so the worker skips the now-stale pause narration instead of blending
     * it with the current status.
     */
    private data class SessionLane(
        val channel: Channel<QueuedNarration>,
        var used: Int,
        val pendingPauses: MutableSet<String> = Collections.newSetFromMap(ConcurrentHashMap()),
    )

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

    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 }
            // Derive a human title from the opening intent, off the narration budget/lane (it's not
            // a narration and must not consume a run's narration allowance). Launched so a slow name
            // inference never stalls the event stream; failure is non-fatal (session keeps its id).
            is InitialIntentEvent -> scope.launch {
                runCatching { routerFacade.nameSession(sid, p.intent) }
                    .onFailure { e ->
                        if (e is CancellationException) throw e
                        log.warn("session naming failed for {}: {}", sid.value, e.message)
                    }
            }
            // Approval pauses narrate off this event, not OrchestrationPaused: it carries the
            // tool/tier/preview facts directly, where the paused event forced a racy lookup
            // against state the next event had not yet written.
            is ApprovalRequestedEvent -> enqueue(
                sid,
                NarrationTrigger(
                    kind = "paused",
                    instruction = approvalInstruction(p),
                    stageId = p.stageId?.value,
                ),
                pauseKey = pauseKeyForRequest(p.requestId.value),
            )
            is StageCompletedEvent -> enqueue(
                sid,
                NarrationTrigger(
                    kind = "stage_completed",
                    instruction = "Stage ${p.stageId.value} completed. Summarise what was accomplished.",
                    stageId = p.stageId.value,
                ),
            )
            is StageFailedEvent -> enqueue(
                sid,
                NarrationTrigger(
                    kind = "stage_failed",
                    instruction = "Stage ${p.stageId.value} failed: ${p.reason}. Explain what went wrong.",
                    stageId = p.stageId.value,
                ),
            )
            is WorkflowCompletedEvent -> {
                enqueue(
                    sid,
                    NarrationTrigger(
                        kind = "workflow_completed",
                        instruction = "The workflow finished successfully. Provide a brief summary.",
                        stageId = p.terminalStageId.value,
                    ),
                )
                closeLane(sid)
            }
            is WorkflowFailedEvent -> {
                val instruction = "The workflow failed in stage ${p.stageId.value}: ${p.reason}. " +
                    "Explain the failure to the user."
                enqueue(
                    sid,
                    NarrationTrigger(
                        kind = "workflow_failed",
                        instruction = instruction,
                        stageId = p.stageId.value,
                    ),
                )
                closeLane(sid)
            }
            // Surface the semantic reviewer's verdict conversationally instead of a raw findings
            // dump: the narrator turns "FAIL, 2 findings" into a plain-language explanation the
            // operator can act on. Only narrate when there's something to say (non-PASS or findings).
            is ReviewFindingsRaisedEvent -> if (p.verdict != ReviewVerdict.PASS || p.findings.isNotEmpty()) {
                enqueue(
                    sid,
                    NarrationTrigger(
                        kind = "review",
                        instruction = reviewInstruction(p),
                        stageId = p.stageId.value,
                    ),
                )
            }
            is ExecutionPlanRejectedEvent -> {
                val instruction = "The execution plan was rejected (${p.source}): ${p.reason}. " +
                    "Explain to the user what happened and what they can do next."
                enqueue(
                    sid,
                    NarrationTrigger(
                        kind = "plan_rejected",
                        instruction = instruction,
                    ),
                )
            }
            is OrchestrationPausedEvent -> {
                // Approval pauses are narrated from ApprovalRequestedEvent above.
                if (!p.reason.contains("APPROVAL", ignoreCase = true)) {
                    val instruction = "Orchestration paused in stage ${p.stageId.value}: ${p.reason}. " +
                        "Inform the user."
                    enqueue(
                        sid,
                        NarrationTrigger(
                            kind = "paused",
                            instruction = instruction,
                            stageId = p.stageId.value,
                        ),
                        pauseKey = SESSION_PAUSE_KEY,
                    )
                }
            }
            // An approval resolving makes its queued pause narration stale: drop it (scoped to
            // that request) so a resolved pause never narrates as if still pending.
            is ApprovalDecisionResolvedEvent -> dropPendingPause(sid, pauseKeyForRequest(p.requestId.value))
            // Resuming supersedes every pause still queued for the session (approval-pending or
            // not): the lane is live again, so any pending "paused" narration is stale.
            is OrchestrationResumedEvent -> dropAllPendingPauses(sid)
            else -> Unit
        }
    }

    private fun approvalInstruction(event: ApprovalRequestedEvent): String {
        val stage = event.stageId?.value ?: "the current stage"
        val subject = event.toolName?.let { "of the '$it' tool (tier ${event.tier})" }
            ?: "(tier ${event.tier})"
        return buildString {
            append("The workflow paused in stage $stage awaiting your approval $subject. ")
            append("Tell the operator what is proposed and why it needs sign-off.")
            val preview = event.preview.orEmpty()
            if (preview.isNotBlank()) {
                append("\n\nProposed change:\n")
                append(preview.take(MAX_PREVIEW_CHARS))
            }
        }
    }

    private fun reviewInstruction(event: ReviewFindingsRaisedEvent): String = buildString {
        val verdict = event.verdict.name
        append("The semantic reviewer returned $verdict for stage ${event.stageId.value}")
        if (event.blocked) append(" (blocking — the stage will retry with this feedback)")
        append(". ")
        if (event.findings.isEmpty()) {
            append("It raised no specific findings. Tell the operator the review result plainly.")
        } else {
            append("Summarise its findings for the operator in plain language:\n")
            event.findings.take(MAX_REVIEW_FINDINGS).forEach { f ->
                val loc = if (f.target.isNotBlank()) " [${f.target}]" else ""
                appendLine("- ${f.severity} ${f.category}$loc: ${f.message.take(MAX_FINDING_CHARS)}")
            }
            if (event.findings.size > MAX_REVIEW_FINDINGS) {
                appendLine("- …and ${event.findings.size - MAX_REVIEW_FINDINGS} more.")
            }
        }
    }

    private fun enqueue(sessionId: SessionId, trigger: NarrationTrigger, pauseKey: String? = null) {
        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
        if (pauseKey != null) {
            lane.pendingPauses.add(pauseKey)
        }
        lane.channel.trySend(QueuedNarration(trigger, pauseKey))
    }

    /** Marks the pause for [pauseKey] resolved so the lane worker skips it if still queued. */
    private fun dropPendingPause(sessionId: SessionId, pauseKey: String) {
        lanes[sessionId.value]?.pendingPauses?.remove(pauseKey)
    }

    /** Marks every queued pause for the session resolved (used on resume). */
    private fun dropAllPendingPauses(sessionId: SessionId) {
        lanes[sessionId.value]?.pendingPauses?.clear()
    }

    /** Closes a terminated session's lane so its worker drains any buffered narration (the terminal
     *  one enqueued just before this) and then exits, self-removing the lane from [lanes]. Without
     *  this, every session leaks its channel + worker coroutine + map entry forever. */
    private fun closeLane(sessionId: SessionId) {
        lanes[sessionId.value]?.channel?.close()
    }

    private fun startLane(sessionId: SessionId): SessionLane {
        val channel = Channel<QueuedNarration>(capacity = Channel.UNLIMITED)
        val lane = SessionLane(channel, used = 0)
        scope.launch {
            for (queued in channel) {
                // A pause whose key was dropped (approval resolved / resumed) is stale: skip it
                // rather than narrate a paused state that no longer holds. removeIf-style check:
                // claim the key so a re-enqueued pause with the same id is not also dropped.
                if (queued.pauseKey != null && !lane.pendingPauses.remove(queued.pauseKey)) {
                    log.debug(
                        "skipping stale pause narration for session {} (key {})",
                        sessionId.value,
                        queued.pauseKey,
                    )
                    continue
                }
                val trigger = queued.trigger
                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,
                        )
                    }
            }
            // Channel closed on workflow termination: drop the lane so it stops leaking. A later event
            // for the same session (none expected post-terminal) would lazily start a fresh lane.
            lanes.remove(sessionId.value)
        }
        return lane
    }

    companion object {
        private const val DEFAULT_MAX_PER_RUN = 100
        private const val MAX_PREVIEW_CHARS = 800
        private const val MAX_REVIEW_FINDINGS = 5
        private const val MAX_FINDING_CHARS = 200

        /** Supersession scope for pauses with no approval request (e.g. USER_REQUESTED). */
        private const val SESSION_PAUSE_KEY = "__session_pause__"
        private val log = LoggerFactory.getLogger(NarrationSubscriber::class.java)

        private fun pauseKeyForRequest(requestId: String): String = "req:$requestId"
    }
}
