//! UTF-8-safe bounded output tail (B8 sub-task 1).
//!
//! A background command can print megabytes; the completion contract only keeps a
//! bounded *tail* (the most recent bytes). [`BoundedTail`] is a byte ring buffer
//! capped at construction; [`bounded_tail`] renders the retained bytes as a
//! `String`, trimming any partial UTF-8 sequence at the front so a multi-byte
//! character never gets sliced by the cap.

use std::collections::VecDeque;

use domain::background_task::BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES;

/// Default tail cap when `IDEA_BG_TAIL_BYTES` is unset (8 KiB).
const DEFAULT_TAIL_BYTES: usize = 8 * 1024;
/// Floor so a hostile/typo'd env value can't disable the tail entirely.
const MIN_TAIL_BYTES: usize = 1024;

/// Resolves the tail cap in bytes from `IDEA_BG_TAIL_BYTES`.
///
/// Defaults to 8 KiB, floored at 1 KiB, and clamped to the domain maximum
/// ([`BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES`], 16 KiB) so the persisted result
/// always validates.
#[must_use]
pub fn tail_cap_bytes() -> usize {
    std::env::var("IDEA_BG_TAIL_BYTES")
        .ok()
        .and_then(|v| v.trim().parse::<usize>().ok())
        .unwrap_or(DEFAULT_TAIL_BYTES)
        .clamp(MIN_TAIL_BYTES, BACKGROUND_TASK_OUTPUT_TAIL_MAX_BYTES)
}

/// A byte ring buffer that retains only the last `cap` bytes pushed into it.
#[derive(Debug)]
pub struct BoundedTail {
    buf: VecDeque<u8>,
    cap: usize,
}

impl BoundedTail {
    /// Builds a tail retaining at most `cap` bytes (a `cap` of `0` retains
    /// nothing but never panics).
    #[must_use]
    pub fn with_cap(cap: usize) -> Self {
        Self {
            buf: VecDeque::new(),
            cap,
        }
    }

    /// Appends a chunk, dropping the oldest bytes past the cap.
    pub fn push(&mut self, chunk: &[u8]) {
        if self.cap == 0 {
            return;
        }
        // If the incoming chunk alone exceeds the cap, only its tail can survive.
        let start = chunk.len().saturating_sub(self.cap);
        self.buf.extend(&chunk[start..]);
        let overflow = self.buf.len().saturating_sub(self.cap);
        if overflow > 0 {
            self.buf.drain(0..overflow);
        }
    }

    /// Returns whether any byte is retained.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.buf.is_empty()
    }

    /// Renders the retained bytes as a UTF-8 string, dropping a leading partial
    /// multi-byte sequence left dangling by the cap. Returns `None` when empty.
    #[must_use]
    pub fn to_tail_string(&self) -> Option<String> {
        if self.buf.is_empty() {
            return None;
        }
        let bytes: Vec<u8> = self.buf.iter().copied().collect();
        Some(bounded_tail(&bytes, self.cap))
    }
}

/// Renders the last `cap` bytes of `bytes` as a lossless-at-the-boundary
/// `String`.
///
/// The slice is taken from the end, then any partial UTF-8 sequence at the *front*
/// (a continuation byte with no lead byte, because the cap cut mid-character) is
/// skipped so the result is valid UTF-8 without replacement characters at the
/// seam. Invalid bytes in the interior are still replaced lossily.
#[must_use]
pub fn bounded_tail(bytes: &[u8], cap: usize) -> String {
    let start = bytes.len().saturating_sub(cap);
    let mut slice = &bytes[start..];
    // Only realign when we actually truncated the front (start > 0): otherwise a
    // legitimately leading continuation byte is just invalid input, handled below.
    if start > 0 {
        let mut skip = 0;
        while skip < slice.len() && is_utf8_continuation(slice[skip]) {
            skip += 1;
        }
        slice = &slice[skip..];
    }
    String::from_utf8_lossy(slice).into_owned()
}

/// Whether `b` is a UTF-8 continuation byte (`10xxxxxx`).
const fn is_utf8_continuation(b: u8) -> bool {
    b & 0b1100_0000 == 0b1000_0000
}

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

    #[test]
    fn keeps_only_the_last_cap_bytes() {
        let mut tail = BoundedTail::with_cap(4);
        tail.push(b"abcdef");
        assert_eq!(tail.to_tail_string().as_deref(), Some("cdef"));
    }

    #[test]
    fn push_bigger_than_cap_keeps_tail() {
        let mut tail = BoundedTail::with_cap(3);
        tail.push(b"0123456789");
        assert_eq!(tail.to_tail_string().as_deref(), Some("789"));
    }

    #[test]
    fn empty_tail_is_none() {
        let tail = BoundedTail::with_cap(8);
        assert!(tail.is_empty());
        assert_eq!(tail.to_tail_string(), None);
    }

    #[test]
    fn cut_multibyte_char_is_trimmed_at_front() {
        // "é" is 0xC3 0xA9. Cap of 1 would keep only the trailing 0xA9 (a lone
        // continuation byte); it must be dropped, yielding an empty string.
        let bytes = "é".as_bytes();
        assert_eq!(bounded_tail(bytes, 1), "");
        // Cap of 2 keeps the whole character.
        assert_eq!(bounded_tail(bytes, 2), "é");
    }

    #[test]
    fn interior_is_preserved() {
        assert_eq!(bounded_tail("héllo".as_bytes(), 100), "héllo");
    }
}
