//! Background OAuth polling for the QR-fast-path onboarding modal.
//!
//! On first-launch the wizard renders a QR for the AtomGit OAuth
//! short link and synchronously holds onto the `LoginSession`. This
//! module spawns a background thread that calls `LoginSession::
//! poll_once` every 2s — the moment AtomGit reports the user has
//! finished the in-browser consent, the task calls `finish()` to
//! exchange state → token (writing `auth.toml` as a side effect) and
//! pushes an [`OauthEvent::Authorized`] onto the event-loop channel.
//!
//! Why `std::thread::spawn` and not `tokio::spawn`:
//! [`LoginSession::poll_once`] / `finish` use a `reqwest::blocking::Client`.
//! Running them on a tokio worker would either block other tasks on
//! the same worker (no `spawn_blocking` indirection) or require a
//! reshape of the blocking client into async. A dedicated OS thread
//! sidesteps both — it sleeps between polls without touching the
//! runtime, and `tokio::sync::mpsc::Sender::blocking_send` lets it
//! push events back into the tokio world when it has something to
//! report.
//!
//! Cancellation: there isn't any. If the user hits Esc on the modal,
//! the modal closes but this thread continues polling until it
//! reaches a terminal state (Authorized / Err). `OauthEvent` arriving
//! on a closed-modal event loop is a silent no-op — the handler
//! checks `app.active_modal.is_some()` before acting. Worst case the
//! thread quietly writes a fresh `auth.toml` on its own — which is
//! the same effect as the user later running `/codingplan` after Esc,
//! so it's harmless.

use std::sync::Arc;
use std::time::Duration;

use tokio::sync::mpsc;

use atomcode_core::auth::oauth::{LoginSession, PollOutcome};
use atomcode_telemetry::Telemetry;

/// Outcome the background poll thread emits at exactly one of: a
/// successful auth-token exchange, a fatal poll/finish error, or
/// (never) cancellation — the thread doesn't observe cancel signals.
#[derive(Debug)]
pub enum OauthEvent {
    /// The user finished AtomGit consent and `finish()` successfully
    /// wrote `auth.toml`. Event loop closes the modal + flips
    /// `pending_run_codingplan` so the existing `/codingplan` driver
    /// picks up the freshly-saved auth and claims the plan.
    Authorized,
    /// Either `poll_once` or `finish` returned an error. Carries the
    /// `Display`-formatted reason for the user — event loop closes
    /// the modal and surfaces this in scrollback so the user knows
    /// whether to retry (`/codingplan`), check the network, or check
    /// their system clock (for sign-stale errors).
    Failed(String),
}

/// Spawn the background poll thread. Returns immediately; the thread
/// owns the [`LoginSession`] and lives until it emits exactly one
/// [`OauthEvent`].
///
/// `wake_tx` is pulsed AFTER the event is queued so the event-loop
/// `tokio::select!` arm that reads `oauth_event_rx` actually fires —
/// `oauth_event_rx.recv()` alone would only fire on the next
/// scheduling tick, which on an idle TUI can be many seconds.
pub fn spawn_oauth_poll(
    session: LoginSession,
    tel: Option<Arc<Telemetry>>,
    event_tx: mpsc::UnboundedSender<OauthEvent>,
    wake_tx: mpsc::Sender<()>,
) {
    std::thread::spawn(move || {
        // Loop polls `&session` without moving it; on Authorized we
        // break out and `finish(session)` consumes it. Doing the
        // poll vs. consume split this way means `session` stays
        // intact across iterations.
        let poll_outcome: Result<(), String> = loop {
            match session.poll_once() {
                Ok(PollOutcome::Authorized) => break Ok(()),
                Ok(PollOutcome::Pending) => {
                    std::thread::sleep(Duration::from_secs(2));
                }
                Err(e) => break Err(format!("{e:#}")),
            }
        };

        let event = match poll_outcome {
            Ok(()) => {
                // finish() consumes session → exchanges state for
                // token → returns AuthInfo. NOTE: finish does NOT
                // write auth.toml — the caller is responsible. The
                // existing CLI `login()` driver pairs finish + save;
                // we have to mirror it here or downstream
                // `is_logged_in()` returns false and the subsequent
                // /codingplan flow re-runs login, popping a second
                // QR + asking the user to scan AGAIN.
                match session.finish(tel.as_ref()) {
                    Ok(auth_info) => {
                        match atomcode_core::auth::save_auth(&auth_info) {
                            Ok(()) => OauthEvent::Authorized,
                            Err(e) => OauthEvent::Failed(format!(
                                "auth.toml write failed: {e:#}"
                            )),
                        }
                    }
                    Err(e) => OauthEvent::Failed(format!("{e:#}")),
                }
            }
            Err(reason) => OauthEvent::Failed(reason),
        };

        // Queue the event BEFORE the wake pulse. Wake without an
        // event in the channel would make `oauth_event_rx.recv()`
        // hang and the wake handler look broken — order matters.
        let _ = event_tx.send(event);
        // `blocking_send` on a `Sender<()>` from a std thread is the
        // documented bridge between std-thread producers and tokio
        // consumers. wake_rx is bounded at 1 so multiple wakes
        // coalesce, but we only emit one here anyway.
        let _ = wake_tx.blocking_send(());
    });
}