//! Pure status detectors over terminal output. No I/O.
use spacesh_proto::status::SurfaceState;

/// Scans a byte stream for OSC 133 semantic-prompt markers and yields the
/// status each marker implies. Robust to escape sequences split across feeds:
/// an incomplete trailing marker is buffered until the next feed.
///
/// Markers: ESC ] 133 ; A ST (prompt) → Idle; ; C ST (command output) → Work;
///          ; D [;exit] ST (command end) → Done (exit 0) / Error (exit != 0).
/// ST is BEL (0x07) or ESC \ (0x1b 0x5c). The `B` marker (input start) is ignored.
#[derive(Default)]
pub struct Osc133Scanner {
    buf: Vec<u8>,
}

impl Osc133Scanner {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn feed(&mut self, bytes: &[u8]) -> Vec<SurfaceState> {
        self.buf.extend_from_slice(bytes);
        let mut out = Vec::new();
        let prefix: &[u8] = b"\x1b]133;";
        loop {
            // Find the next marker start.
            let Some(start) = find(&self.buf, prefix) else {
                // No marker start. Keep only a possible partial prefix at the tail.
                self.buf = keep_partial_tail(&self.buf, prefix);
                break;
            };
            // Drop anything before the marker start.
            if start > 0 {
                self.buf.drain(0..start);
            }
            // After the prefix, find the terminator (BEL or ESC \).
            let body_start = prefix.len();
            let Some((body_end, term_len)) = find_terminator(&self.buf, body_start) else {
                break; // incomplete marker; wait for more bytes
            };
            let body = &self.buf[body_start..body_end];
            if let Some(state) = classify(body) {
                out.push(state);
            }
            // Consume through the terminator.
            self.buf.drain(0..body_end + term_len);
        }
        out
    }
}

/// Classify the `133;` body (e.g. `A`, `C`, `D`, `D;0`, `D;1`).
fn classify(body: &[u8]) -> Option<SurfaceState> {
    let s = std::str::from_utf8(body).ok()?;
    let mut parts = s.split(';');
    match parts.next()? {
        "C" => Some(SurfaceState::Work),
        "A" => Some(SurfaceState::Idle),
        "D" => {
            // exit code is the next part, if present.
            match parts.next() {
                Some(code) if code != "0" && !code.is_empty() => Some(SurfaceState::Error),
                _ => Some(SurfaceState::Done),
            }
        }
        _ => None, // B and others: no status
    }
}

fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
    if needle.is_empty() || hay.len() < needle.len() {
        return None;
    }
    hay.windows(needle.len()).position(|w| w == needle)
}

/// Terminator search from `from`: returns (index_of_terminator, terminator_len).
fn find_terminator(hay: &[u8], from: usize) -> Option<(usize, usize)> {
    let mut i = from;
    while i < hay.len() {
        if hay[i] == 0x07 {
            return Some((i, 1));
        }
        if hay[i] == 0x1b && i + 1 < hay.len() && hay[i + 1] == 0x5c {
            return Some((i, 2));
        }
        i += 1;
    }
    None
}

/// Keep only the longest suffix of `buf` that is a strict prefix of `needle`
/// (a possibly-incomplete marker start), so it can complete on the next feed.
fn keep_partial_tail(buf: &[u8], needle: &[u8]) -> Vec<u8> {
    let max = needle.len().saturating_sub(1).min(buf.len());
    for n in (1..=max).rev() {
        let tail = &buf[buf.len() - n..];
        if needle.starts_with(tail) {
            return tail.to_vec();
        }
    }
    Vec::new()
}

/// Stateless best-effort heuristics over a window of recent terminal text.
pub struct FallbackScanner;

impl FallbackScanner {
    /// Returns a status implied by the tail text, or None for "no change".
    pub fn scan(text: &str) -> Option<SurfaceState> {
        let tail = text.trim_end();
        let last_line = tail.lines().last().unwrap_or("");
        // Confirmation / input prompts → waiting for the user.
        let wait_markers = ["(y/n)", "(Y/n)", "(y/N)", "[y/N]", "[Y/n]", "Press enter", "press enter", "❯ 1.", "? "];
        if wait_markers.iter().any(|m| last_line.contains(m)) {
            return Some(SurfaceState::Wait);
        }
        // Spinner glyphs at the tail → working.
        let spinners = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', '|', '/', '-', '\\'];
        if let Some(c) = last_line.chars().rev().find(|c| !c.is_whitespace()) {
            if spinners.contains(&c) {
                return Some(SurfaceState::Work);
            }
        }
        None
    }
}

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

    #[test]
    fn osc133_c_then_d0_gives_work_then_done() {
        let mut s = Osc133Scanner::new();
        let evs = s.feed(b"\x1b]133;C\x07hello\x1b]133;D;0\x07");
        assert_eq!(evs, vec![SurfaceState::Work, SurfaceState::Done]);
    }

    #[test]
    fn osc133_d_nonzero_is_error() {
        let mut s = Osc133Scanner::new();
        assert_eq!(s.feed(b"\x1b]133;D;1\x07"), vec![SurfaceState::Error]);
    }

    #[test]
    fn osc133_a_is_idle_and_st_can_be_esc_backslash() {
        let mut s = Osc133Scanner::new();
        assert_eq!(s.feed(b"\x1b]133;A\x1b\\"), vec![SurfaceState::Idle]);
    }

    #[test]
    fn osc133_split_across_feeds_is_buffered() {
        let mut s = Osc133Scanner::new();
        assert_eq!(s.feed(b"\x1b]133;C"), vec![]); // no terminator yet
        assert_eq!(s.feed(b"\x07"), vec![SurfaceState::Work]);
    }

    #[test]
    fn osc133_split_in_prefix_is_buffered() {
        let mut s = Osc133Scanner::new();
        assert_eq!(s.feed(b"text\x1b]13"), vec![]); // partial prefix retained
        assert_eq!(s.feed(b"3;C\x07"), vec![SurfaceState::Work]);
    }

    #[test]
    fn osc133_ignores_plain_text() {
        let mut s = Osc133Scanner::new();
        assert_eq!(s.feed(b"just some output\n"), vec![]);
        assert!(s.feed(b"more\n").is_empty());
    }

    #[test]
    fn fallback_detects_confirmation_and_spinner() {
        assert_eq!(FallbackScanner::scan("Apply changes? (y/n)"), Some(SurfaceState::Wait));
        assert_eq!(FallbackScanner::scan("building ⠹"), Some(SurfaceState::Work));
        assert_eq!(FallbackScanner::scan("normal output"), None);
    }
}
