// crates/atomcode-tuix/src/render/worker.rs
//
// Render worker — moves terminal I/O off the main event loop.
//
// ## Why
//
// Mac Terminal.app takes 30-60ms to process a full footer ANSI payload.
// When the event loop calls `renderer.render()` directly, that 30-60ms
// blocks the select! loop, which means:
//   - the spinner tick task can't deliver (drops),
//   - the next keystroke can't be read,
//   - agent events queue up behind the render.
//
// `InputThrottle` (see throttle.rs) mitigates the storm by coalescing
// InputPrompt/StreamingBox paints. This worker eliminates the blocking
// at the architectural level: the event loop sends `UiLine`s and
// lifecycle commands into a channel, a dedicated OS thread owns the
// inner renderer and drains the channel. Slow terminal ≠ stalled event
// loop.
//
// ## Sync vs. async lifecycle
//
// Most render calls are fire-and-forget: `render(UiLine)` just enqueues.
// Lifecycle methods that must complete before the caller proceeds —
// `reset`, `clear_screen`, `suspend_for_external`, `resume_from_external`,
// `shutdown` — send a command with an ACK oneshot channel and block
// until the worker reports done. The `/login` OAuth flow for example
// can't tolerate "renderer hasn't flipped raw mode yet" when the child
// process opens the browser.
//
// `flush` and `flush_deferred` are fire-and-forget (no ACK) — order is
// preserved because all commands travel the same channel.
//
// ## Shutdown
//
// `Drop` sends `Shutdown` and joins the thread, guaranteeing the final
// terminal-reset bytes land before `run()` returns. Dropping the sender
// alone would also let the worker exit on the next recv error, but an
// explicit Shutdown gives clean "process the last queued line + flush"
// semantics rather than "drop whatever is still in flight".

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

use super::{Renderer, UiLine};

/// Commands sent to the render worker thread.
enum RenderCmd {
    Line(UiLine),
    Flush,
    FlushDeferred,
    /// Terminal resize — fire-and-forget, the worker updates its
    /// internal DECSTBM region and repaints the footer.
    Resize(u16, u16),
    /// Remove the tail ApprovalPrompt body row (fire-and-forget).
    PopApprovalPrompt,
    /// Scroll the body viewport by `delta` rows. Negative = up,
    /// positive = down. RetainedRenderer's append-only path leaves
    /// scrollback to the host terminal, so this is effectively a
    /// trait no-op everywhere today.
    ScrollBody(i32),
    /// Jump body viewport to absolute top / bottom of scrollback.
    ScrollBodyToTop,
    ScrollBodyToBottom,
    /// Jump body viewport to the prev/next message boundary.
    /// Fire-and-forget — no ACK needed.
    ScrollToPrevMessage,
    ScrollToNextMessage,
    ScrollToPrevUserMessage,
    ScrollToNextUserMessage,
    /// Lifecycle operation requiring an ACK — the worker performs the
    /// op then sends `()` back so the caller can proceed.
    Ack {
        op: AckOp,
        ack: mpsc::Sender<()>,
    },
}

#[derive(Debug, Clone, Copy)]
enum AckOp {
    Reset,
    ClearScreen,
    SuspendForExternal,
    ResumeFromExternal,
    Shutdown,
}

/// Renderer facade that forwards every call to a background OS thread.
/// Implements the `Renderer` trait so the event loop can use it as a
/// drop-in replacement for `AnsiRenderer` / `PlainRenderer` — the wire
/// protocol is the same `UiLine` enum.
pub struct TaskRenderer {
    cmd_tx: mpsc::Sender<RenderCmd>,
    /// Join handle for the worker thread; `Some` until `Drop` takes it
    /// to `join()`.
    worker: Option<thread::JoinHandle<()>>,
}

impl TaskRenderer {
    /// Spawn the worker thread, handing it ownership of the inner
    /// renderer. After this returns the caller interacts with the inner
    /// renderer only via the returned facade.
    pub fn new(inner: Box<dyn Renderer>) -> Self {
        let (cmd_tx, cmd_rx) = mpsc::channel::<RenderCmd>();
        let worker = thread::Builder::new()
            .name("tuix-render".to_string())
            .spawn(move || run_worker(inner, cmd_rx))
            .expect("spawn render worker thread");
        Self {
            cmd_tx,
            worker: Some(worker),
        }
    }

    /// Send an ACK op and block until the worker reports done. 10s
    /// bound keeps us from hanging forever if the worker ever wedges,
    /// while giving slow CI machines / thermal-throttled laptops /
    /// debug builds enough headroom that routine lifecycle ops don't
    /// spuriously timeout.
    ///
    /// 2s was the original budget — a worker processing `Shutdown`
    /// normally takes < 1ms, so 2s felt like plenty. But on a loaded
    /// CI runner mid-cargo-test, a few tests would sporadically fail
    /// on the timeout line because the OS hadn't scheduled the worker
    /// thread fast enough. CC-style TUI harnesses use ~10s for the
    /// same reason.
    fn ack(&self, op: AckOp) {
        let (ack_tx, ack_rx) = mpsc::channel();
        if self
            .cmd_tx
            .send(RenderCmd::Ack { op, ack: ack_tx })
            .is_err()
        {
            // Worker is gone (already shut down) — nothing to do.
            return;
        }
        let _ = ack_rx.recv_timeout(Duration::from_secs(10));
    }
}

impl Renderer for TaskRenderer {
    fn render(&mut self, line: UiLine) {
        let _ = self.cmd_tx.send(RenderCmd::Line(line));
    }

    fn flush(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::Flush);
    }

    fn shutdown(&mut self) {
        self.ack(AckOp::Shutdown);
    }

    fn reset(&mut self) {
        self.ack(AckOp::Reset);
    }

    fn clear_screen(&mut self) {
        self.ack(AckOp::ClearScreen);
    }

    fn suspend_for_external(&mut self) {
        self.ack(AckOp::SuspendForExternal);
    }

    fn resume_from_external(&mut self) {
        self.ack(AckOp::ResumeFromExternal);
    }

    fn flush_deferred(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::FlushDeferred);
    }

    fn on_resize(&mut self, cols: u16, rows: u16) {
        let _ = self.cmd_tx.send(RenderCmd::Resize(cols, rows));
    }

    fn pop_approval_prompt(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::PopApprovalPrompt);
    }

    fn scroll_body(&mut self, delta: i32) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollBody(delta));
    }

    fn scroll_body_to_top(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollBodyToTop);
    }

    fn scroll_body_to_bottom(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollBodyToBottom);
    }

    fn scroll_to_prev_message(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollToPrevMessage);
    }

    fn scroll_to_next_message(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollToNextMessage);
    }

    fn scroll_to_prev_user_message(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollToPrevUserMessage);
    }

    fn scroll_to_next_user_message(&mut self) {
        let _ = self.cmd_tx.send(RenderCmd::ScrollToNextUserMessage);
    }
}

impl Drop for TaskRenderer {
    fn drop(&mut self) {
        // Idempotent shutdown — `Renderer::shutdown` may have already
        // run, in which case the worker is already gone and this call
        // is a no-op (ack() swallows the send error).
        self.ack(AckOp::Shutdown);
        if let Some(handle) = self.worker.take() {
            let _ = handle.join();
        }
    }
}

fn run_worker(mut inner: Box<dyn Renderer>, cmd_rx: mpsc::Receiver<RenderCmd>) {
    use std::time::Instant;
    while let Ok(cmd) = cmd_rx.recv() {
        // Measure the wall-clock time each terminal I/O takes so the log
        // shows where Mac Terminal.app / iTerm2 / etc. actually spend time.
        // Big `flush` durations = kernel pipe backpressure from a slow
        // terminal emulator; big `render` durations = our own bytes taking
        // forever to serialize or intermediate `write_all` blocking.
        match cmd {
            RenderCmd::Line(line) => {
                let tag = ui_line_tag(&line);
                let t0 = Instant::now();
                inner.render(line);
                crate::tuix_trace!("REN", "Line {} render={}µs", tag, t0.elapsed().as_micros());
            }
            RenderCmd::Flush => {
                let t0 = Instant::now();
                inner.flush();
                crate::tuix_trace!("REN", "Flush flush={}µs", t0.elapsed().as_micros());
            }
            RenderCmd::FlushDeferred => {
                // Skip logging when it's a true no-op (no pending payload
                // and window not elapsed). throttle.rs already logs when
                // this path actually paints.
                let t0 = Instant::now();
                inner.flush_deferred();
                let d = t0.elapsed();
                if d.as_micros() > 100 {
                    crate::tuix_trace!("REN", "FlushDeferred deferred={}µs", d.as_micros());
                }
            }
            RenderCmd::Resize(cols, rows) => {
                let t0 = Instant::now();
                inner.on_resize(cols, rows);
                crate::tuix_trace!(
                    "REN",
                    "Resize {}x{} dur={}µs",
                    cols,
                    rows,
                    t0.elapsed().as_micros()
                );
            }
            RenderCmd::PopApprovalPrompt => {
                inner.pop_approval_prompt();
            }
            RenderCmd::ScrollBody(delta) => {
                inner.scroll_body(delta);
            }
            RenderCmd::ScrollBodyToTop => {
                inner.scroll_body_to_top();
            }
            RenderCmd::ScrollBodyToBottom => {
                inner.scroll_body_to_bottom();
            }
            RenderCmd::ScrollToPrevMessage => {
                inner.scroll_to_prev_message();
            }
            RenderCmd::ScrollToNextMessage => {
                inner.scroll_to_next_message();
            }
            RenderCmd::ScrollToPrevUserMessage => {
                inner.scroll_to_prev_user_message();
            }
            RenderCmd::ScrollToNextUserMessage => {
                inner.scroll_to_next_user_message();
            }
            RenderCmd::Ack { op, ack } => {
                let t0 = Instant::now();
                match op {
                    AckOp::Reset => inner.reset(),
                    AckOp::ClearScreen => inner.clear_screen(),
                    AckOp::SuspendForExternal => inner.suspend_for_external(),
                    AckOp::ResumeFromExternal => inner.resume_from_external(),
                    AckOp::Shutdown => {
                        inner.shutdown();
                        crate::tuix_trace!(
                            "REN",
                            "Ack Shutdown dur={}µs",
                            t0.elapsed().as_micros()
                        );
                        let _ = ack.send(());
                        // Exit the loop — drop `inner` + `cmd_rx`.
                        // Any queued commands after this point are
                        // discarded (the sender's next send errors,
                        // which callers treat as "worker gone").
                        return;
                    }
                }
                crate::tuix_trace!("REN", "Ack {:?} dur={}µs", op, t0.elapsed().as_micros());
                let _ = ack.send(());
            }
        }
    }
    // Sender dropped without explicit Shutdown — still run shutdown so
    // the terminal isn't left in raw mode on abrupt exit paths.
    inner.shutdown();
}

/// Short tag for logging which UiLine variant the worker is processing.
/// Keeps trace lines column-aligned so `grep Line` output is readable.
fn ui_line_tag(l: &UiLine) -> &'static str {
    match l {
        UiLine::Welcome { .. } => "Welcome",
        UiLine::User(_) => "User",
            UiLine::AssistantText(_) => "AssistantText",
            UiLine::ReasoningText(_) => "ReasoningText",
            UiLine::AssistantLineBreak => "AssistantLineBreak",
        UiLine::ToolCall { .. } => "ToolCall",
            UiLine::ToolCallInFlight { .. } => "ToolCallInFlight",
            UiLine::ToolCallCommit { .. } => "ToolCallCommit",
            UiLine::ToolGroupRender { .. } => "ToolGroupRender",
            UiLine::ToolGroupChildUpdate { .. } => "ToolGroupChildUpdate",
            UiLine::ToolGroupSummary { .. } => "ToolGroupSummary",
        UiLine::ToolResult { .. } => "ToolResult",
        UiLine::DiffLine { .. } => "DiffLine",
        UiLine::DiffBlock(_) => "DiffBlock",
        UiLine::ApprovalPrompt { .. } => "ApprovalPrompt",
        UiLine::Error(_) => "Error",
        UiLine::Warning(_) => "Warning",
        UiLine::TurnCancelled => "TurnCancelled",
        UiLine::TurnComplete => "TurnComplete",
        UiLine::Spinner { .. } => "Spinner",
        UiLine::StreamingBox { .. } => "StreamingBox",
        UiLine::ClearTransient => "ClearTransient",
        UiLine::InputPrompt { .. } => "InputPrompt",
        UiLine::InputCommit => "InputCommit",
        UiLine::CommandOutput(_) => "CommandOutput",
        UiLine::ImageAttachment(_) => "ImageAttachment",
        UiLine::VisionPreprocessSuccess { .. } => "VisionPreprocessSuccess",
        UiLine::TurnSeparator { .. } => "TurnSeparator",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::render::Renderer;
    use std::sync::{Arc, Mutex};

    /// Counting test renderer — records every call so tests can assert
    /// the worker forwards correctly.
    #[derive(Default)]
    struct Counts {
        renders: usize,
        flushes: usize,
        shutdowns: usize,
        resets: usize,
        clear_screens: usize,
        suspends: usize,
        resumes: usize,
        deferred: usize,
    }

    struct TestRenderer {
        counts: Arc<Mutex<Counts>>,
    }

    impl Renderer for TestRenderer {
        fn render(&mut self, _line: UiLine) {
            self.counts.lock().unwrap().renders += 1;
        }
        fn flush(&mut self) {
            self.counts.lock().unwrap().flushes += 1;
        }
        fn shutdown(&mut self) {
            self.counts.lock().unwrap().shutdowns += 1;
        }
        fn reset(&mut self) {
            self.counts.lock().unwrap().resets += 1;
        }
        fn clear_screen(&mut self) {
            self.counts.lock().unwrap().clear_screens += 1;
        }
        fn suspend_for_external(&mut self) {
            self.counts.lock().unwrap().suspends += 1;
        }
        fn resume_from_external(&mut self) {
            self.counts.lock().unwrap().resumes += 1;
        }
        fn flush_deferred(&mut self) {
            self.counts.lock().unwrap().deferred += 1;
        }
    }

    fn setup() -> (TaskRenderer, Arc<Mutex<Counts>>) {
        let counts = Arc::new(Mutex::new(Counts::default()));
        let inner = Box::new(TestRenderer {
            counts: counts.clone(),
        });
        (TaskRenderer::new(inner), counts)
    }

    #[test]
    fn render_and_flush_forward_to_inner() {
        let (mut r, counts) = setup();
        r.render(UiLine::User("hi".into()));
        r.render(UiLine::User("there".into()));
        r.flush();
        // Force ordering: reset is an ACK op that blocks until the
        // worker has drained earlier commands, so after reset() returns
        // the renders + flush must already be counted.
        r.reset();
        let c = counts.lock().unwrap();
        assert_eq!(c.renders, 2);
        assert_eq!(c.flushes, 1);
        assert_eq!(c.resets, 1);
    }

    #[test]
    fn lifecycle_ack_blocks_until_worker_done() {
        let (mut r, counts) = setup();
        // Chain several lifecycle ACKs — each must complete in order
        // before the next returns.
        r.clear_screen();
        assert_eq!(counts.lock().unwrap().clear_screens, 1);
        r.suspend_for_external();
        assert_eq!(counts.lock().unwrap().suspends, 1);
        r.resume_from_external();
        assert_eq!(counts.lock().unwrap().resumes, 1);
    }

    #[test]
    fn shutdown_drops_worker_and_later_sends_are_noops() {
        let (mut r, counts) = setup();
        r.render(UiLine::User("before".into()));
        r.shutdown();
        assert_eq!(counts.lock().unwrap().shutdowns, 1);
        // Worker is gone — these must not panic, even though no one is
        // listening on the channel anymore.
        r.render(UiLine::User("after".into()));
        r.flush();
        // Second shutdown is idempotent.
        r.shutdown();
    }

    #[test]
    fn drop_triggers_shutdown_when_not_called_explicitly() {
        let counts = {
            let counts = Arc::new(Mutex::new(Counts::default()));
            let inner = Box::new(TestRenderer {
                counts: counts.clone(),
            });
            let mut r = TaskRenderer::new(inner);
            r.render(UiLine::User("one".into()));
            counts
            // r dropped here — Drop must shut the worker down + join.
        };
        // By the time Drop returns, the worker has finished, so the
        // render AND one shutdown are accounted for.
        let c = counts.lock().unwrap();
        assert_eq!(c.renders, 1);
        assert_eq!(c.shutdowns, 1);
    }

    #[test]
    fn flush_deferred_fire_and_forget() {
        let (mut r, counts) = setup();
        r.flush_deferred();
        // No ACK on flush_deferred — have to fence with a separate ACK
        // to observe it deterministically.
        r.reset();
        assert_eq!(counts.lock().unwrap().deferred, 1);
    }
}