//! Tracks when Codex-owned transcript scrollback must be repaired after terminal resize.
//!
//! Terminal scrollback is not a retained widget tree: once Codex writes wrapped lines into the
//! terminal, the terminal owns those rows. Width resize reflow treats the in-memory transcript cells
//! as the source of truth, clears Codex-owned history, and re-emits the cells at the current width.
//! Height-only growth also schedules a rebuild so rows exposed above the inline viewport are
//! restored from the same source of truth.
//!
//! This module owns only scheduling and stream-time repair state. It does not know how to render
//! cells or clear terminal output; `app::resize_reflow` consumes this state and performs the
//! rebuild. The key invariant is that a reflow request which happens while streaming output is
//! active, or while transient stream cells are still waiting for consolidation, must trigger one
//! final source-backed reflow after the stream becomes source-backed history.

use std::time::Duration;
use std::time::Instant;

pub(crate) const TRANSCRIPT_REFLOW_DEBOUNCE: Duration = Duration::from_millis(75);

/// Tracks pending terminal-scrollback repair after a terminal resize.
///
/// The state intentionally separates observed terminal width from rebuilt terminal width. Terminal
/// emulators can report an intermediate size during drag-resize, then settle on the final size after
/// Codex has already rebuilt scrollback. Keeping those widths distinct lets the next draw request a
/// final rebuild instead of assuming the latest observed size has already been repaired.
#[derive(Debug, Default)]
pub(crate) struct TranscriptReflowState {
    last_observed_width: Option<u16>,
    last_reflow_width: Option<u16>,
    pending_reflow_width: Option<u16>,
    pending_until: Option<Instant>,
    ran_during_stream: bool,
    resize_requested_during_stream: bool,
}

impl TranscriptReflowState {
    /// Reset all width, pending deadline, and stream repair state.
    ///
    /// Call this when resize reflow is disabled or when the app discards the transcript state that
    /// pending reflow work would have rebuilt. Leaving stale deadlines behind would make a later
    /// draw attempt to rebuild history from unrelated cells.
    pub(crate) fn clear(&mut self) {
        *self = Self::default();
    }

    /// Record the width observed during a draw and report whether it is new or changed.
    ///
    /// The first observed width initializes the state without scheduling a rebuild because no
    /// old-width transcript has been emitted yet. Treating initialization as a real resize would
    /// make the first draw do redundant scrollback work.
    pub(crate) fn note_width(&mut self, width: u16) -> TranscriptWidthChange {
        let previous_width = self.last_observed_width.replace(width);
        if previous_width.is_none() {
            self.last_reflow_width = Some(width);
        }
        TranscriptWidthChange {
            changed: previous_width.is_some_and(|previous| previous != width),
            initialized: previous_width.is_none(),
        }
    }

    /// Return whether scrollback still needs to be rebuilt at `width`.
    ///
    /// This compares against the width that actually rebuilt scrollback, not just the most recently
    /// observed terminal width. A terminal can report the final size after the reflow that handled
    /// the resize event, so the follow-up draw must be able to request one more reflow even if
    /// the observed-width tracker already saw that value.
    pub(crate) fn reflow_needed_for_width(&self, width: u16) -> bool {
        self.last_reflow_width != Some(width) && self.pending_reflow_width != Some(width)
    }

    /// Schedule a trailing-debounced reflow and return whether it should run immediately.
    ///
    /// Repeated resize events push the deadline out so dragging a terminal edge rebuilds scrollback
    /// at the final observed width rather than at intermediate widths. `target_width` is present
    /// only for width-changing rebuilds; height-only exposure still needs a rebuild, but it must not
    /// suppress a later width repair for the same draw cycle.
    pub(crate) fn schedule_debounced(&mut self, target_width: Option<u16>) -> bool {
        let now = Instant::now();
        if let Some(target_width) = target_width {
            self.pending_reflow_width = Some(target_width);
        }
        self.pending_until = Some(now + TRANSCRIPT_REFLOW_DEBOUNCE);
        false
    }

    /// Schedule an immediate reflow for the next draw opportunity.
    ///
    /// This is used after stream consolidation when waiting for the debounce interval would leave
    /// visible terminal-wrapped stream rows in the finalized transcript.
    pub(crate) fn schedule_immediate(&mut self) {
        self.pending_reflow_width = None;
        self.pending_until = Some(Instant::now());
    }

    #[cfg(test)]
    pub(crate) fn set_due_for_test(&mut self) {
        self.pending_until = Some(Instant::now() - Duration::from_millis(1));
    }

    pub(crate) fn pending_is_due(&self, now: Instant) -> bool {
        self.pending_until.is_some_and(|deadline| now >= deadline)
    }

    pub(crate) fn pending_until(&self) -> Option<Instant> {
        self.pending_until
    }

    pub(crate) fn has_pending_reflow(&self) -> bool {
        self.pending_until.is_some()
    }

    pub(crate) fn clear_pending_reflow(&mut self) {
        self.pending_until = None;
        self.pending_reflow_width = None;
    }

    /// Remember the terminal width that actually rebuilt transcript scrollback.
    ///
    /// Resize scheduling is driven by observed widths, but debounced redraws may run before a
    /// terminal emulator has settled on its final size. Keeping the rendered width separate avoids
    /// confusing "seen during a draw" with "scrollback has been repaired at this width".
    pub(crate) fn mark_reflowed_width(&mut self, width: u16) -> bool {
        self.last_reflow_width.replace(width) != Some(width)
    }

    /// Remember that a reflow actually rebuilt history before stream consolidation completed.
    ///
    /// A mid-stream rebuild can only render the transient stream cells that exist at that moment.
    /// The consolidation handler must later rebuild again from the finalized source-backed cell or
    /// the transcript can keep old stream wrapping.
    pub(crate) fn mark_ran_during_stream(&mut self) {
        self.ran_during_stream = true;
    }

    /// Remember that the terminal width changed while streaming or pre-consolidation cells existed.
    ///
    /// This captures the case where the debounce did not fire before the stream finished. Without
    /// this flag, consolidation could complete without the final source-backed resize repair.
    /// Marking the request rather than forcing immediate rendering keeps resize drag behavior
    /// debounced while still guaranteeing that finalized stream cells replace transient rows.
    pub(crate) fn mark_resize_requested_during_stream(&mut self) {
        self.resize_requested_during_stream = true;
    }

    /// Return whether stream finalization needs a source-backed reflow and clear the request.
    ///
    /// This is a draining read because each resize-during-stream episode should force at most one
    /// post-consolidation repair. Calling it before consolidation would drop the repair request and
    /// leave finalized scrollback shaped by transient stream rows.
    pub(crate) fn take_stream_finish_reflow_needed(&mut self) -> bool {
        let needed = self.ran_during_stream || self.resize_requested_during_stream;
        self.ran_during_stream = false;
        self.resize_requested_during_stream = false;
        needed
    }

    /// Clear only the stream repair flags while preserving width and pending-deadline state.
    ///
    /// Use this after a required final stream reflow has completed. Calling `clear()` here would
    /// also forget the last observed width and make the next draw look like first initialization.
    pub(crate) fn clear_stream_flags(&mut self) {
        self.ran_during_stream = false;
        self.resize_requested_during_stream = false;
    }
}

/// Describes how the latest draw width relates to the previous observed draw width.
///
/// `initialized` means this was the first width observed by the state machine. `changed` means a
/// previously observed transcript width exists and differs from the new width.
pub(crate) struct TranscriptWidthChange {
    pub(crate) changed: bool,
    pub(crate) initialized: bool,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn schedule_debounced_postpones_existing_reflow() {
        let mut state = TranscriptReflowState::default();

        assert!(!state.schedule_debounced(/*target_width*/ None));
        let first_deadline = state.pending_until().expect("pending reflow");

        std::thread::sleep(Duration::from_millis(1));
        assert!(!state.schedule_debounced(/*target_width*/ None));

        assert!(
            state.pending_until().expect("pending reflow") > first_deadline,
            "a later resize should push the debounce deadline out"
        );
    }

    #[test]
    fn schedule_debounced_postpones_due_existing_reflow() {
        let mut state = TranscriptReflowState::default();
        state.set_due_for_test();
        let before_reschedule = Instant::now();

        assert!(!state.schedule_debounced(/*target_width*/ None));
        assert!(
            state.pending_until().expect("pending reflow") > before_reschedule,
            "a resize after the old deadline should start a fresh quiet period"
        );
    }

    #[test]
    fn first_observed_width_marks_reflow_baseline() {
        let mut state = TranscriptReflowState::default();

        let width = state.note_width(/*width*/ 80);

        assert!(width.initialized);
        assert_eq!(state.last_observed_width, Some(80));
        assert_eq!(state.last_reflow_width, Some(80));
        assert!(!state.reflow_needed_for_width(/*width*/ 80));
    }

    #[test]
    fn mark_reflowed_width_records_actual_rebuild_width() {
        let mut state = TranscriptReflowState::default();
        state.note_width(/*width*/ 80);

        assert!(state.mark_reflowed_width(/*width*/ 100));

        assert_eq!(state.last_observed_width, Some(80));
        assert_eq!(state.last_reflow_width, Some(100));
    }

    #[test]
    fn reflow_needed_compares_against_actual_rebuild_width() {
        let mut state = TranscriptReflowState::default();
        state.note_width(/*width*/ 80);
        state.mark_reflowed_width(/*width*/ 90);
        state.note_width(/*width*/ 100);

        assert!(state.reflow_needed_for_width(/*width*/ 100));
    }

    #[test]
    fn pending_reflow_target_prevents_repeated_reschedule() {
        let mut state = TranscriptReflowState::default();
        state.note_width(/*width*/ 80);

        assert!(state.reflow_needed_for_width(/*width*/ 100));
        state.schedule_debounced(/*target_width*/ Some(100));

        assert!(!state.reflow_needed_for_width(/*width*/ 100));
    }

    #[test]
    fn clear_pending_reflow_allows_same_width_to_be_rescheduled() {
        let mut state = TranscriptReflowState::default();
        state.note_width(/*width*/ 80);
        state.schedule_debounced(/*target_width*/ Some(100));

        state.clear_pending_reflow();

        assert!(state.reflow_needed_for_width(/*width*/ 100));
    }

    #[test]
    fn mark_reflowed_width_reports_unchanged_width() {
        let mut state = TranscriptReflowState::default();
        assert!(state.mark_reflowed_width(/*width*/ 100));

        assert!(!state.mark_reflowed_width(/*width*/ 100));
        assert_eq!(state.last_reflow_width, Some(100));
    }

    #[test]
    fn take_stream_finish_reflow_needed_drains_resize_request() {
        let mut state = TranscriptReflowState::default();
        state.mark_resize_requested_during_stream();

        assert!(state.take_stream_finish_reflow_needed());
        assert!(!state.take_stream_finish_reflow_needed());
    }

    #[test]
    fn take_stream_finish_reflow_needed_drains_ran_during_stream() {
        let mut state = TranscriptReflowState::default();
        state.mark_ran_during_stream();

        assert!(state.take_stream_finish_reflow_needed());
        assert!(!state.take_stream_finish_reflow_needed());
    }

    #[test]
    fn clear_resets_stream_reflow_flags() {
        let mut state = TranscriptReflowState::default();
        state.mark_ran_during_stream();
        state.mark_resize_requested_during_stream();

        state.clear();

        assert!(!state.take_stream_finish_reflow_needed());
    }
}
