use std::sync::{Arc, Mutex};

use alacritty_terminal::event::{Event, EventListener};
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::{Column, Line, Point};
use alacritty_terminal::term::{Config, Term};
use alacritty_terminal::vte::ansi::{NamedColor, Processor, Rgb};

/// Fixed-size terminal dimensions for the daemon-side grid.
#[derive(Clone, Copy)]
pub struct GridSize {
    pub cols: usize,
    pub lines: usize,
}

impl Dimensions for GridSize {
    fn total_lines(&self) -> usize {
        self.lines
    }
    fn screen_lines(&self) -> usize {
        self.lines
    }
    fn columns(&self) -> usize {
        self.cols
    }
}

/// One escape sequence the terminal model wants written back to the PTY in
/// response to a query: either a ready-made byte reply (DA/DSR/etc.) or a color
/// report whose value must be resolved from the term's palette at drain time.
enum Reply {
    Bytes(Vec<u8>),
    Color(usize, Arc<dyn Fn(Rgb) -> String + Send + Sync>),
}

/// Collects the escape sequences the terminal model wants written back to the PTY
/// (Primary/Secondary Device Attributes, DSR cursor/status reports, OSC color
/// queries, etc.). Programs like fish, yazi and vim block on these replies at
/// startup; with a void listener they hang ~2s and then warn ("could not read
/// response to Primary Device Attribute query") or render with the wrong theme.
///
/// The daemon is the authoritative responder for the PTY — the GUI's xterm.js is
/// display-only and must NOT echo its own replies back (its duplicate arrives an
/// IPC roundtrip late and gets typed into the shell as literal gibberish).
#[derive(Clone, Default)]
pub struct ReplyCollector {
    buf: Arc<Mutex<Vec<Reply>>>,
}

impl EventListener for ReplyCollector {
    fn send_event(&self, event: Event) {
        let reply = match event {
            Event::PtyWrite(text) => Reply::Bytes(text.into_bytes()),
            // OSC 10/11/12 color query — alacritty defers the value to the embedder
            // (us) via a formatter; resolve it against the palette in take_replies.
            Event::ColorRequest(index, fmt) => Reply::Color(index, fmt),
            _ => return,
        };
        if let Ok(mut b) = self.buf.lock() {
            b.push(reply);
        }
    }
}

/// Fallback palette colors when a program queries one the term has not had set
/// explicitly. Matches the GUI's default theme so OSC 11 (background) reports a
/// dark color and light/dark detection in TUIs stays correct.
fn default_color(index: usize) -> Rgb {
    if index == NamedColor::Background as usize { Rgb { r: 0x0a, g: 0x0d, b: 0x12 } }
    else if index == NamedColor::Foreground as usize { Rgb { r: 0xe6, g: 0xed, b: 0xf3 } }
    else if index == NamedColor::Cursor as usize { Rgb { r: 0xe6, g: 0xed, b: 0xf3 } }
    else { Rgb { r: 0x80, g: 0x80, b: 0x80 } }
}

/// Owns an alacritty terminal model and feeds raw PTY bytes into it.
pub struct GridSurface {
    term: Term<ReplyCollector>,
    parser: Processor,
    size: GridSize,
    replies: ReplyCollector,
}

impl GridSurface {
    pub fn new(cols: u16, rows: u16) -> Self {
        let size = GridSize { cols: cols as usize, lines: rows as usize };
        let replies = ReplyCollector::default();
        let term = Term::new(Config::default(), &size, replies.clone());
        Self { term, parser: Processor::new(), size, replies }
    }

    pub fn feed(&mut self, bytes: &[u8]) {
        self.parser.advance(&mut self.term, bytes);
    }

    /// Drain any escape sequences the model produced in response to queries fed so
    /// far. The caller must write these back to the PTY for query-driven programs
    /// (fish, vim, etc.) to proceed without timing out.
    pub fn take_replies(&mut self) -> Vec<u8> {
        let replies = {
            let Ok(mut b) = self.replies.buf.lock() else { return Vec::new(); };
            std::mem::take(&mut *b)
        };
        let mut out = Vec::new();
        for reply in replies {
            match reply {
                Reply::Bytes(bytes) => out.extend_from_slice(&bytes),
                Reply::Color(index, fmt) => {
                    let rgb = self.term.colors()[index].unwrap_or_else(|| default_color(index));
                    out.extend_from_slice(fmt(rgb).as_bytes());
                }
            }
        }
        out
    }

    pub fn resize(&mut self, cols: u16, rows: u16) {
        self.size = GridSize { cols: cols as usize, lines: rows as usize };
        self.term.resize(self.size);
    }

    pub fn size(&self) -> GridSize {
        self.size
    }

    /// Read the visible character at (line, col) — used by tests and the snapshot writer.
    pub fn char_at(&self, line: usize, col: usize) -> char {
        let point = Point::new(Line(line as i32), Column(col));
        self.term.grid()[point].c
    }

    pub fn term(&self) -> &Term<ReplyCollector> {
        &self.term
    }

    /// The visible grid as text — the last `lines` rows, trailing blanks trimmed.
    /// Used by the fallback detector.
    pub fn tail_text(&self, lines: usize) -> String {
        let size = self.size();
        let start = size.lines.saturating_sub(lines);
        let mut out = String::new();
        for line in start..size.lines {
            let mut row = String::new();
            for col in 0..size.cols {
                row.push(self.char_at(line, col));
            }
            out.push_str(row.trim_end());
            out.push('\n');
        }
        out
    }
}

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

    #[test]
    fn feeding_plain_text_lands_in_the_grid() {
        let mut g = GridSurface::new(20, 5);
        g.feed(b"hello");
        assert_eq!(g.char_at(0, 0), 'h');
        assert_eq!(g.char_at(0, 4), 'o');
    }

    #[test]
    fn carriage_return_and_newline_move_the_cursor() {
        let mut g = GridSurface::new(20, 5);
        g.feed(b"ab\r\ncd");
        assert_eq!(g.char_at(0, 0), 'a');
        assert_eq!(g.char_at(1, 0), 'c');
    }

    #[test]
    fn primary_device_attribute_query_gets_a_reply() {
        // fish (and friends) send ESC[c at startup and block on the response.
        let mut g = GridSurface::new(20, 5);
        g.feed(b"\x1b[c");
        let reply = g.take_replies();
        assert!(reply.starts_with(b"\x1b[?"), "expected a DA1 reply, got {reply:?}");
        // Replies are drained, not duplicated.
        assert!(g.take_replies().is_empty());
    }

    #[test]
    fn osc_background_color_query_gets_a_reply() {
        // yazi/vim send OSC 11 ("\x1b]11;?\x07") to detect the background color and
        // block on the reply; the daemon must answer it authoritatively.
        let mut g = GridSurface::new(20, 5);
        g.feed(b"\x1b]11;?\x07");
        let reply = String::from_utf8(g.take_replies()).unwrap();
        assert!(reply.starts_with("\x1b]11;rgb:"), "expected an OSC 11 reply, got {reply:?}");
        assert!(g.take_replies().is_empty());
    }
}
