//! Per-turn background task: drives one `/v1/responses` stream and forwards
//! each event to the UI as an [`AppEvent`]. Cancellable so Esc/Ctrl-C aborts an
//! in-flight turn by dropping the HTTP stream.

use futures_util::StreamExt;
use hagent_core::mcp::{self, AddTarget, McpServerSettings, TokenStore};
use hagent_core::protocol::request::CreateRequest;
use hagent_core::Client;
use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;

use crate::app::{AppEvent, TokensReady};

/// Spawn the one-time file-editing-skill bootstrap: upload the bundled skill to
/// the server and report the new skill id (or an error string) back to the UI.
/// Fire-and-forget — there's nothing to cancel for a single POST.
pub fn spawn_bootstrap(client: Client, tx: UnboundedSender<AppEvent>) {
    tokio::spawn(async move {
        let result = client
            .ensure_skill()
            .await
            .map(|skill| skill.id)
            .map_err(|e| e.to_string());
        let _ = tx.send(AppEvent::BootstrapResult(result));
    });
}

/// Spawn a forced re-bootstrap: optionally delete the existing file-editing
/// skill(s) first, then upload a fresh copy, reporting the resulting id (or an
/// error string) back to the UI. Reuses the same [`AppEvent::BootstrapResult`]
/// completion path as [`spawn_bootstrap`].
pub fn spawn_rebootstrap(client: Client, delete_existing: bool, tx: UnboundedSender<AppEvent>) {
    tokio::spawn(async move {
        let result = client
            .recreate_skill(delete_existing)
            .await
            .map(|skill| skill.id)
            .map_err(|e| e.to_string());
        let _ = tx.send(AppEvent::BootstrapResult(result));
    });
}

/// Spawn a one-shot fetch of the server's model list (for the `/model` picker),
/// reporting the models — id + pricing — (or an error string) back to the UI.
pub fn spawn_list_models(client: Client, tx: UnboundedSender<AppEvent>) {
    tokio::spawn(async move {
        let result = client.list_models().await.map_err(|e| e.to_string());
        let _ = tx.send(AppEvent::ModelsLoaded(result));
    });
}

/// Resolve/refresh OAuth tokens for the given MCP servers off the UI thread,
/// reporting the resolved `Authorization` per label plus any servers still
/// needing an interactive login back to the app. Fire-and-forget.
pub fn spawn_ensure_mcp_tokens(
    mut servers: Vec<McpServerSettings>,
    store: TokenStore,
    tx: UnboundedSender<AppEvent>,
) {
    tokio::spawn(async move {
        let outcome = mcp::ensure_tokens(&mut servers, &store).await;
        let resolved = servers
            .into_iter()
            .map(|s| (s.server_label, s.resolved_authorization))
            .collect();
        let _ = tx.send(AppEvent::McpTokensReady(TokensReady {
            resolved,
            needs_login: outcome.needs_login,
        }));
    });
}

/// Run the interactive OAuth login for one MCP server (opening the system
/// browser), reporting success/failure back to the UI. Fire-and-forget.
pub fn spawn_mcp_login(
    server: McpServerSettings,
    store: TokenStore,
    tx: UnboundedSender<AppEvent>,
) {
    tokio::spawn(async move {
        let result = mcp::login(&server, &store, |url| {
            // In the TUI we can't print the URL without corrupting the screen,
            // so just launch the browser; `hagent mcp login` is the headless
            // fallback that also prints it.
            let _ = open::that(url);
        })
        .await
        .map(|_| ())
        .map_err(|e| e.to_string());
        let _ = tx.send(AppEvent::McpLoginResult(result));
    });
}

/// Write a newly-added MCP server to the chosen destination (probing it for auth
/// and a default name first) and report the resolved settings back to the UI.
/// `label` is the user-supplied `server_label`, or `None` to derive one from the
/// discovered name / hostname. Fire-and-forget.
pub fn spawn_mcp_add(
    target: AddTarget,
    label: Option<String>,
    url: String,
    tx: UnboundedSender<AppEvent>,
) {
    tokio::spawn(async move {
        let result = mcp::add_server(target, label.as_deref(), &url)
            .await
            .map_err(|e| e.to_string());
        let _ = tx.send(AppEvent::McpAdded(result));
    });
}

/// Spawn a turn. Returns a token whose `.cancel()` aborts the stream.
pub fn spawn_turn(
    client: Client,
    request: CreateRequest,
    tx: UnboundedSender<AppEvent>,
) -> CancellationToken {
    let token = CancellationToken::new();
    let child = token.clone();
    tokio::spawn(async move {
        tokio::select! {
            biased;
            _ = child.cancelled() => {
                let _ = tx.send(AppEvent::StreamCancelled);
            }
            _ = drive(client, request, &tx) => {
                let _ = tx.send(AppEvent::StreamDone);
            }
        }
    });
    token
}

async fn drive(client: Client, request: CreateRequest, tx: &UnboundedSender<AppEvent>) {
    let mut stream = match client.stream(request).await {
        Ok(s) => s,
        Err(e) => {
            let _ = tx.send(AppEvent::StreamError(e.to_string()));
            return;
        }
    };
    while let Some(event) = stream.next().await {
        match event {
            Ok(e) => {
                if tx.send(AppEvent::Stream(e)).is_err() {
                    break; // UI gone
                }
            }
            Err(e) => {
                let _ = tx.send(AppEvent::StreamError(e.to_string()));
            }
        }
    }
}
