//! Signal and event handlers for the query stream pipeline.
//!
//! These functions extract the logic from the `jp_macro::select!` closures to
//! improve readability and testability.
//! Each handler:
//!
//! 1. Shows appropriate UI (interrupt menus) when needed
//! 2. Delegates state transitions to the `TurnCoordinator` state machine
//! 3. Returns a `LoopAction` for the caller to handle control flow

use jp_conversation::ConversationStream;
use jp_inquire::prompt::PromptBackend;
use jp_llm::event::{Event, EventMatcher, EventPatch, FinishReason, PatchAction};
use jp_printer::Printer;
use tokio_util::sync::CancellationToken;
use tracing::{info, trace};

use super::handler::{InterruptAction, InterruptHandler};
use crate::{
    cmd::query::turn::{Action, CommittedEvent, TurnCoordinator, TurnPhase},
    signals::SignalTo,
};

/// Action to take in a select loop.
///
/// Used by handlers that operate within a loop context (streaming, LLM events).
/// The type parameter `T` is the return type when exiting the function early.
#[derive(Debug)]
pub enum LoopAction<T> {
    /// Continue the loop (wait for next event).
    Continue,

    /// Break the inner loop.
    Break,

    /// Return from the function with the given value.
    Return(T),
}

/// Handle a signal received during LLM streaming.
///
/// Shows the interrupt menu when Ctrl+C is received, then delegates to the turn
/// coordinator's state machine for state transitions and content injection.
pub fn handle_streaming_signal(
    signal: SignalTo,
    turn_coordinator: &mut TurnCoordinator,
    conversation_stream: &mut ConversationStream,
    printer: &Printer,
    backend: &dyn PromptBackend,
    llm_stream_finished: bool,
) -> LoopAction<()> {
    info!(?signal, "Received signal during streaming.");

    match signal {
        SignalTo::Quit => {
            // Treat Quit like Stop: save partial content and exit gracefully.
            // This ensures we don't lose progress on hard quit signals.
            turn_coordinator.handle_quit(conversation_stream);

            LoopAction::Break
        }

        SignalTo::Shutdown => {
            // Flush the renderer's markdown buffer to the printer queue,
            // then drain the printer queue instantly (skip typewriter
            // delays) so all generated content is visible before the
            // interrupt menu appears.
            turn_coordinator.flush_renderer();
            printer.flush_instant();

            let action = InterruptHandler::with_backend(backend)
                .handle_streaming_interrupt(&mut printer.prompt_writer(), !llm_stream_finished);

            // `Resume` means "keep waiting for the current stream." The state
            // machine is a no-op for it, and we must NOT break the inner loop:
            // breaking drops the live `SelectAll` and forces a redundant new
            // HTTP request, which can land us in inconsistent state. Continue
            // polling instead.
            let is_resume = matches!(action, InterruptAction::Resume);

            // Delegate state transition to the turn coordinator
            match turn_coordinator.handle_streaming_interrupt(action, conversation_stream) {
                // Return without persisting this cycle (previous turn cycles
                // are already persisted).
                TurnPhase::Aborted => LoopAction::Return(()),

                // Resume keeps the existing stream alive.
                _ if is_resume => LoopAction::Continue,

                // All other phases break from loop, persist, then outer loop
                // decides.
                _ => LoopAction::Break,
            }
        }
    }
}

/// Handle a successful event from the LLM stream.
///
/// Stream errors are handled separately by [`handle_stream_error`], which is
/// the single source of truth for all retry logic.
///
/// Returns the loop-control signal alongside any committed event the shell
/// should react to immediately.
/// The committed event is surfaced directly from [`EventBuilder::handle_flush`]
/// (via the coordinator) so the shell never has to infer it from the
/// conversation stream's tail — a duplicate flush from a misbehaving provider
/// commits nothing and so cannot cause a double dispatch.
///
/// [`EventBuilder::handle_flush`]: jp_llm::event_builder::EventBuilder::handle_flush
/// [`handle_stream_error`]: crate::cmd::query::stream::handle_stream_error
pub fn handle_llm_event(
    event: Event,
    turn_coordinator: &mut TurnCoordinator,
    conversation_stream: &mut ConversationStream,
) -> (LoopAction<()>, CommittedEvent) {
    // `Patch` is a side-channel instruction from the provider to fix bad events
    // in the conversation stream. This can be handled directly instead of
    // passing through the turn coordinator.
    if let Event::Patch(patches) = event {
        apply_history_patches(conversation_stream, &patches);
        return (LoopAction::Continue, CommittedEvent::None);
    }

    // `Retry` means the provider wants us to rebuild the request and try again.
    // Break the inner streaming loop while keeping the phase as `Streaming` so
    // the outer turn loop re-enters with a fresh request.
    if matches!(event, Event::Finished(FinishReason::Retry)) {
        return (LoopAction::Break, CommittedEvent::None);
    }

    let outcome = turn_coordinator.handle_event(conversation_stream, event);
    let loop_action = match outcome.action {
        Action::Done | Action::ExecuteTools => LoopAction::Break,
        Action::Continue | Action::SendFollowUp => LoopAction::Continue,
    };

    (loop_action, outcome.committed)
}

/// Apply provider-issued metadata patches to historical conversation events.
///
/// NOTE: This mutates the stream in-place, which deviates from the append-only
/// principle established in RFD 064 (non-destructive compaction).
/// This is acceptable for now because the targets are opaque provider metadata
/// (cryptographic signatures), not user-visible content, and the overlay/
/// projection infrastructure from RFD 064 does not exist yet.
/// Once RFD 064 lands, this should migrate to an append-only patch event that
/// the projection layer applies at request-build time.
fn apply_history_patches(stream: &mut ConversationStream, patches: &[EventPatch]) {
    let mut count = 0;

    for event in stream.iter_mut() {
        for patch in patches {
            let matched = match &patch.matcher {
                EventMatcher::MetadataValue { key, value } => event
                    .event
                    .metadata
                    .get(key)
                    .and_then(|v| v.as_str())
                    .is_some_and(|v| v == value),
                _ => false,
            };

            if !matched {
                continue;
            }

            match &patch.action {
                PatchAction::RemoveMetadata(key) => event.event.metadata.remove(key),
                _ => continue,
            };

            count += 1;
        }
    }

    if count > 0 {
        tracing::debug!(count, "Applied history patches to conversation stream.");
    }
}

/// Result of handling a signal during tool execution.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolSignalResult {
    /// Continue waiting for tool execution to complete.
    Continue,

    /// Cancel current execution and restart with the same tools.
    /// The caller should wait for cancellation to complete, then re-execute.
    Restart,

    /// Cancel current execution and override cancelled tool responses with the
    /// user-supplied message.
    Cancelled { response: String },
}

/// Handle a signal received during tool execution.
///
/// Shows the tool interrupt menu when Ctrl+C is received, then delegates to the
/// turn coordinator for state machine updates.
///
/// If any tool is currently showing an interactive prompt (permission,
/// question, result edit), the interrupt menu is suppressed and we let the
/// active prompt handle Ctrl+C instead.
/// This prevents UI conflicts between the interrupt menu and the active prompt.
///
/// # Arguments
///
/// - `is_prompting` - Whether any tool is currently showing an interactive
///   prompt.
/// - `backend` - Allows injecting a mock prompt backend for testing.
pub fn handle_tool_signal(
    signal: SignalTo,
    cancellation_token: &CancellationToken,
    turn_coordinator: &mut TurnCoordinator,
    is_prompting: bool,
    printer: &Printer,
    backend: &dyn PromptBackend,
) -> ToolSignalResult {
    match signal {
        SignalTo::Quit => {
            // For hard quit during tool execution, we cancel and let the normal
            // flow handle persistence (responses will be cancelled).
            cancellation_token.cancel();
            turn_coordinator.force_complete();
            ToolSignalResult::Continue
        }

        SignalTo::Shutdown => {
            // If any tool is showing an interactive prompt, don't show the
            // interrupt menu. Let the active prompt handle Ctrl+C (it will
            // typically return OperationCanceled which the executor handles).
            if is_prompting {
                trace!("Suppressing interrupt menu: tool prompt is active");
                return ToolSignalResult::Continue;
            }

            let action = InterruptHandler::with_backend(backend)
                .handle_tool_interrupt(&mut printer.prompt_writer());

            // Notify the state machine (reserved for future state transitions).
            turn_coordinator.handle_tool_interrupt(&action);

            match action {
                InterruptAction::RestartTool => {
                    info!("Restarting tool execution");
                    cancellation_token.cancel();
                    ToolSignalResult::Restart
                }
                InterruptAction::ToolCancelled { response } => {
                    cancellation_token.cancel();
                    ToolSignalResult::Cancelled { response }
                }
                _ => ToolSignalResult::Continue,
            }
        }
    }
}

#[cfg(test)]
#[path = "signals_tests.rs"]
mod tests;
