//! WebSocket I/O wrappers — the ONLY place in `service/ws/` that may perform
//! side effects such as spawning async tasks, subscribing to broadcast channels,
//! reading state from disk, or interacting with the system clock.

use crate::agents::ReconciliationEvent;
use crate::http::context::AppContext;
use crate::http::workflow::load_pipeline_state;
use crate::io::onboarding;
use crate::io::watcher::WatcherEvent;
use crate::io::wizard;
use crate::log_buffer;
use crate::service::status::Subscription;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};

use super::message::{self, WizardStepInfo, WsResponse};

/// Load the initial pipeline state from the CRDT and convert to a WsResponse.
pub fn load_initial_pipeline_state(ctx: &AppContext) -> Option<WsResponse> {
    load_pipeline_state(ctx).ok().map(|s| s.into())
}

/// Check whether the project needs onboarding and return the response.
pub fn check_onboarding(ctx: &AppContext) -> WsResponse {
    let needs = ctx
        .state
        .get_project_root()
        .map(|root| onboarding::check_onboarding_status(&root).needs_onboarding())
        .unwrap_or(false);
    WsResponse::OnboardingStatus {
        needs_onboarding: needs,
    }
}

/// Load the active wizard state (if any) and convert to a WsResponse.
pub fn load_wizard_state(ctx: &AppContext) -> Option<WsResponse> {
    let root = ctx.state.get_project_root().ok()?;
    let ws = wizard::WizardState::load(&root)?;
    let steps: Vec<WizardStepInfo> = message::wizard_steps_to_info(&ws.steps);
    Some(WsResponse::WizardState {
        steps,
        current_step_index: ws.current_step_index(),
        completed: ws.completed,
    })
}

/// Load recent log entries and convert them to WsResponse messages.
pub fn load_recent_logs(count: usize) -> Vec<WsResponse> {
    log_buffer::global()
        .get_recent_entries(count, None, None)
        .into_iter()
        .map(|entry| WsResponse::LogEntry {
            timestamp: entry.timestamp,
            level: entry.level.as_str().to_string(),
            message: entry.message,
        })
        .collect()
}

/// Spawn a background task that forwards live log entries to the client.
///
/// Returns when the client's send channel closes or the log broadcast ends.
pub fn subscribe_logs(tx: mpsc::UnboundedSender<WsResponse>) {
    let mut log_rx = log_buffer::global().subscribe();
    tokio::spawn(async move {
        loop {
            match log_rx.recv().await {
                Ok(entry) => {
                    if tx
                        .send(WsResponse::LogEntry {
                            timestamp: entry.timestamp,
                            level: entry.level.as_str().to_string(),
                            message: entry.message,
                        })
                        .is_err()
                    {
                        break;
                    }
                }
                Err(broadcast::error::RecvError::Lagged(_)) => continue,
                Err(broadcast::error::RecvError::Closed) => break,
            }
        }
    });
}

/// Spawn a background task that forwards filesystem watcher events to the client.
///
/// After work-item and agent-state events, also pushes a refreshed pipeline state.
pub fn subscribe_watcher(
    tx: mpsc::UnboundedSender<WsResponse>,
    ctx: Arc<AppContext>,
    mut watcher_rx: broadcast::Receiver<WatcherEvent>,
) {
    tokio::spawn(async move {
        loop {
            match watcher_rx.recv().await {
                Ok(evt) => {
                    let refresh = message::needs_pipeline_refresh(&evt);
                    let ws_msg: Option<WsResponse> = evt.into();
                    if let Some(msg) = ws_msg
                        && tx.send(msg).is_err()
                    {
                        break;
                    }
                    if refresh
                        && let Ok(state) = load_pipeline_state(ctx.as_ref())
                        && tx.send(state.into()).is_err()
                    {
                        break;
                    }
                }
                Err(broadcast::error::RecvError::Lagged(_)) => continue,
                Err(broadcast::error::RecvError::Closed) => break,
            }
        }
    });
}

/// Spawn a background task that forwards status broadcaster events to the client.
///
/// Each [`StatusEvent`](crate::service::status::StatusEvent) is delivered as a
/// [`WsResponse::StatusUpdate`] with the structured event fields intact, so the
/// frontend can do per-type presentation without parsing strings.
pub fn subscribe_status(tx: mpsc::UnboundedSender<WsResponse>, mut subscription: Subscription) {
    tokio::spawn(async move {
        while let Some(event) = subscription.recv().await {
            if tx.send(WsResponse::StatusUpdate { event }).is_err() {
                break;
            }
        }
    });
}

/// Spawn a background task that forwards real-time pipeline-transition events to
/// the client, filtered to those visible to `persona` based on its scope filter.
///
/// Each matching event is delivered as a [`WsResponse::PipelineEvent`] frame.
/// Events that occur while no subscriber is connected are NOT delivered here;
/// [`crate::llm_session::assemble_prompt_context`] catches up on those at turn time.
pub fn subscribe_persona_pipeline_events(tx: mpsc::UnboundedSender<WsResponse>, persona: String) {
    let Some(mut rx) = crate::pipeline_event_bus::subscribe() else {
        return;
    };
    tokio::spawn(async move {
        loop {
            match rx.recv().await {
                Ok(event) => {
                    if crate::pipeline_event_bus::event_matches_persona(&event, &persona) {
                        let line = crate::pipeline_event_bus::render_event(&event);
                        if tx.send(WsResponse::PipelineEvent { line }).is_err() {
                            break;
                        }
                    }
                }
                Err(broadcast::error::RecvError::Lagged(_)) => continue,
                Err(broadcast::error::RecvError::Closed) => break,
            }
        }
    });
}

/// Spawn a background task that forwards reconciliation events to the client.
pub fn subscribe_reconciliation(
    tx: mpsc::UnboundedSender<WsResponse>,
    mut reconcile_rx: broadcast::Receiver<ReconciliationEvent>,
) {
    tokio::spawn(async move {
        loop {
            match reconcile_rx.recv().await {
                Ok(evt) => {
                    if tx
                        .send(WsResponse::ReconciliationProgress {
                            story_id: evt.story_id,
                            status: evt.status,
                            message: evt.message,
                        })
                        .is_err()
                    {
                        break;
                    }
                }
                Err(broadcast::error::RecvError::Lagged(_)) => continue,
                Err(broadcast::error::RecvError::Closed) => break,
            }
        }
    });
}
