use crate::protocol::{ClientMsg, DaemonState, StyledSpan, TermColor, MouseMode, MouseEncoding, socket_path};
use crossterm::{
    event::{
        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, KeyModifiers,
    },
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::Rect,
    style::{Color, Modifier, Style},
    text::{Line, Span, Text},
    widgets::{List, ListItem, Paragraph},
    Frame, Terminal,
};
use std::{
    io::{self, BufRead, BufReader, Write},
    os::unix::net::UnixStream,
    process::{Command, Stdio},
    sync::{Arc, Mutex},
    thread,
    time::{Duration, Instant},
};
use unicode_width::UnicodeWidthStr;
use base64::Engine;

pub fn run() -> io::Result<()> {
    let stream = connect_or_start()?;
    let stream_read = stream.try_clone()?;

    let state: Arc<Mutex<Option<DaemonState>>> = Arc::new(Mutex::new(None));
    let state_r = Arc::clone(&state);

    // Reader thread: receive state snapshots from the daemon.
    thread::spawn(move || {
        let reader = BufReader::new(stream_read);
        for line in reader.lines() {
            match line {
                Ok(l) => {
                    if let Ok(s) = serde_json::from_str::<DaemonState>(&l) {
                        *state_r.lock().unwrap() = Some(s);
                    }
                }
                Err(_) => break,
            }
        }
    });

    let mut writer = io::BufWriter::new(stream);

    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut term = Terminal::new(backend)?;

    // Tell the daemon the current terminal size (exclude bottom bar).
    let sz = term.size()?;
    // The daemon's PTY should be the content area only (terminal minus bottom bar).
    let rows = sz.height.saturating_sub(1);
    send_msg(&mut writer, &ClientMsg::Resize { cols: sz.width, rows })?;

    let mut show_menu = false;
    let mut selection: Option<Selection> = None;
    let mut context_menu: Option<ContextMenu> = None;
    let mut rename_state: Option<RenameState> = None;
    let result = run_loop(&mut term, &mut writer, &state, &mut show_menu, &mut selection, &mut context_menu, &mut rename_state);

    let _ = disable_raw_mode();
    let _ = execute!(term.backend_mut(), LeaveAlternateScreen, DisableMouseCapture);
    let _ = term.show_cursor();
    result
}

fn connect_or_start() -> io::Result<UnixStream> {
    let path = socket_path();

    if let Ok(s) = UnixStream::connect(&path) {
        return Ok(s);
    }

    // No daemon running — start one.
    let exe = std::env::current_exe()?;
    Command::new(&exe)
        .arg("--daemon")
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()?;

    // Poll for up to 3 seconds until the daemon is listening.
    let deadline = Instant::now() + Duration::from_secs(3);
    loop {
        thread::sleep(Duration::from_millis(100));
        if let Ok(s) = UnixStream::connect(&path) {
            return Ok(s);
        }
        if Instant::now() >= deadline {
            return Err(io::Error::new(io::ErrorKind::TimedOut, "daemon did not start in time"));
        }
    }
}

fn send_msg(writer: &mut impl Write, msg: &ClientMsg) -> io::Result<()> {
    let mut line = serde_json::to_string(msg).unwrap();
    line.push('\n');
    writer.write_all(line.as_bytes())?;
    writer.flush()
}

#[derive(Clone, Copy, Debug)]
struct Selection {
    start_row: u16,
    start_col: u16,
    end_row: u16,
    end_col: u16,
}

#[derive(Clone, Copy, Debug)]
enum ContextTarget {
    Tab(usize),
    Session(usize),
}

struct ContextMenu {
    target: ContextTarget,
    x: u16,
    y: u16,
    width: u16,
    height: u16,
    items: Vec<String>,
    selected: usize,
}

enum RenameKind {
    Tab(usize),
    Session(usize),
}

struct RenameState {
    kind: RenameKind,
    buffer: String,
}

enum Action {
    Msg(ClientMsg),
    Detach,
    OpenContextMenu(ContextTarget, u16), // (target, anchor_column)
    RenameSession,
}

fn run_loop(
    term: &mut Terminal<CrosstermBackend<io::Stdout>>,
    writer: &mut impl Write,
    state: &Arc<Mutex<Option<DaemonState>>>,
    show_menu: &mut bool,
    selection: &mut Option<Selection>,
    context_menu: &mut Option<ContextMenu>,
    rename_state: &mut Option<RenameState>,
) -> io::Result<()> {
    loop {
        let snapshot = state.lock().unwrap().clone();
        term.draw(|f| {
            match snapshot {
                Some(ref s) => { render(f, s, *show_menu, &*selection, context_menu.as_ref(), rename_state.as_ref()); }
                None => {
                    let area = f.area();
                    f.render_widget(Paragraph::new("Connecting..."), area);
                }
            }
        })?;

        if event::poll(Duration::from_millis(16))? {
            match event::read()? {
                Event::Mouse(me) => {
                    let size = term.size()?;
                    let h = size.height;
                    let w = size.width;

                    // If rename modal open, ignore mouse events.
                    if rename_state.is_some() {
                        continue;
                    }

                    // If a context menu is open, let it handle clicks.
                    if let Some(cm) = context_menu.as_mut() {
                        use crossterm::event::MouseEventKind;
                        if let MouseEventKind::Down(btn) = me.kind {
                            if me.row >= cm.y && me.row < cm.y + cm.height && me.column >= cm.x && me.column < cm.x + cm.width {
                                let idx = (me.row - cm.y) as usize;
                                cm.selected = std::cmp::min(idx, cm.items.len().saturating_sub(1));
                                if btn == crossterm::event::MouseButton::Left {
                                    match cm.target {
                                        ContextTarget::Tab(tab_idx) => {
                                            let sel_item = cm.items[cm.selected].as_str();
                                            if sel_item == "Rename Tab" {
                                                let cur_name = state.lock().unwrap().as_ref().map(|s| s.sessions[s.active_session].tabs[tab_idx].name.clone()).unwrap_or_default();
                                                *rename_state = Some(RenameState { kind: RenameKind::Tab(tab_idx), buffer: cur_name });
                                            } else if sel_item == "Close Tab" {
                                                let msg = ClientMsg::CloseTab(tab_idx);
                                                match send_msg(writer, &msg) {
                                                    Ok(()) => {}
                                                    Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => return Ok(()),
                                                    Err(e) => return Err(e),
                                                }
                                            }
                                        }
                                        ContextTarget::Session(sess_idx) => {
                                            let sel_item = cm.items[cm.selected].as_str();
                                            if sel_item == "Rename Session" {
                                                let cur_name = state.lock().unwrap().as_ref().map(|s| s.sessions[s.active_session].name.clone()).unwrap_or_default();
                                                *rename_state = Some(RenameState { kind: RenameKind::Session(sess_idx), buffer: cur_name });
                                            }
                                        }
                                    }
                                    *context_menu = None;
                                }
                            } else {
                                *context_menu = None;
                            }
                        }
                        continue;
                    }

                    // Normal mouse handling
                    let snap = state.lock().unwrap().clone();
                    if let Some(s) = snap
                        && let Some(action) = handle_mouse(&s, me, h, show_menu, selection) {
                            match action {
                                Action::Msg(msg) => {
                                    let is_exit = matches!(&msg, ClientMsg::Exit);
                                    let should_resize = matches!(&msg, ClientMsg::SwitchTab(_) | ClientMsg::SwitchSession(_) | ClientMsg::NewTab | ClientMsg::NewSession(_));
                                    match send_msg(writer, &msg) {
                                        Ok(()) => {
                                            if is_exit { return Ok(()); }
                                            if should_resize {
                                                // After creating or switching the active tab/session, inform the daemon
                                                // of the current terminal size so the newly-active PTY is resized.
                                                match term.size() {
                                                    Ok(sz) => {
                                                        let rows = sz.height.saturating_sub(1);
                                                        let cols = sz.width;
                                                        match send_msg(writer, &ClientMsg::Resize { cols, rows }) {
                                                            Ok(()) => {}
                                                            Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => { return Ok(()); }
                                                            Err(e) => return Err(e),
                                                        }
                                                    }
                                                    Err(e) => return Err(e),
                                                }
                                            }
                                        }
                                        Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => { return Ok(()); }
                                        Err(e) => return Err(e),
                                    }
                                }
                                Action::Detach => return Ok(()),
                                Action::OpenContextMenu(target, col) => {
                                    let items = match target { ContextTarget::Tab(_) => vec!["Rename Tab".to_string(), "Close Tab".to_string()], ContextTarget::Session(_) => vec!["Rename Session".to_string()] };
                                    let menu_h = items.len() as u16;
                                    let max_w = items.iter().map(|it| it.len()).max().unwrap_or(1) as u16;
                                    let menu_w = std::cmp::min(40, max_w + 4);
                                    let mut menu_x = col;
                                    if menu_x + menu_w > w { menu_x = w.saturating_sub(menu_w); }
                                    let menu_y = h.saturating_sub(1 + menu_h);
                                    *context_menu = Some(ContextMenu { target, x: menu_x, y: menu_y, width: menu_w, height: menu_h, items, selected: 0 });
                                }
                                Action::RenameSession => {
                                    let cur_name = s.sessions[s.active_session].name.clone();
                                    *rename_state = Some(RenameState { kind: RenameKind::Session(s.active_session), buffer: cur_name });
                                }
                            }
                        }
                }
                Event::Key(ke) => {
                    // Rename modal input handling
                    if let Some(rs) = rename_state.as_mut() {
                        if ke.kind == KeyEventKind::Press {
                            match ke.code {
                                KeyCode::Char(c) => { rs.buffer.push(c); }
                                KeyCode::Backspace => { rs.buffer.pop(); }
                                KeyCode::Enter => {
                                    match &rs.kind {
                                        RenameKind::Tab(idx) => {
                                            let msg = ClientMsg::RenameTab { index: *idx, name: rs.buffer.clone() };
                                            match send_msg(writer, &msg) {
                                                Ok(()) => {}
                                                Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => return Ok(()),
                                                Err(e) => return Err(e),
                                            }
                                        }
                                        RenameKind::Session(idx) => {
                                            let msg = ClientMsg::RenameSession { index: *idx, name: rs.buffer.clone() };
                                            match send_msg(writer, &msg) {
                                                Ok(()) => {}
                                                Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => return Ok(()),
                                                Err(e) => return Err(e),
                                            }
                                        }
                                    }
                                    *rename_state = None;
                                }
                                KeyCode::Esc => { *rename_state = None; }
                                _ => {}
                            }
                        }
                        continue;
                    }

                    // Context menu keyboard navigation
                    if let Some(cm) = context_menu.as_mut() {
                        if ke.kind == KeyEventKind::Press {
                            match ke.code {
                                KeyCode::Up => { if cm.selected > 0 { cm.selected -= 1; } }
                                KeyCode::Down => { if cm.selected + 1 < cm.items.len() { cm.selected += 1; } }
                                KeyCode::Enter => {
                                    match cm.target {
                                        ContextTarget::Tab(tab_idx) => {
                                            let sel_item = cm.items[cm.selected].as_str();
                                            if sel_item == "Rename Tab" {
                                                let cur_name = state.lock().unwrap().as_ref().map(|s| s.sessions[s.active_session].tabs[tab_idx].name.clone()).unwrap_or_default();
                                                *rename_state = Some(RenameState { kind: RenameKind::Tab(tab_idx), buffer: cur_name });
                                            } else if sel_item == "Close Tab" {
                                                let msg = ClientMsg::CloseTab(tab_idx);
                                                match send_msg(writer, &msg) {
                                                    Ok(()) => {}
                                                    Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => return Ok(()),
                                                    Err(e) => return Err(e),
                                                }
                                            }
                                        }
                                        ContextTarget::Session(sess_idx) => {
                                            let sel_item = cm.items[cm.selected].as_str();
                                            if sel_item == "Rename Session" {
                                                let cur_name = state.lock().unwrap().as_ref().map(|s| s.sessions[s.active_session].name.clone()).unwrap_or_default();
                                                *rename_state = Some(RenameState { kind: RenameKind::Session(sess_idx), buffer: cur_name });
                                            }
                                        }
                                    }
                                    *context_menu = None;
                                }
                                KeyCode::Esc => { *context_menu = None; }
                                _ => {}
                            }
                        }
                        continue;
                    }

                    // Normal key handling
                    if ke.kind == KeyEventKind::Press && !*show_menu
                        && let Some(bytes) = key_to_bytes(&ke) {
                            match send_msg(writer, &ClientMsg::Input(bytes)) {
                                Ok(()) => {}
                                Err(ref e) if e.kind() == io::ErrorKind::BrokenPipe || e.kind() == io::ErrorKind::ConnectionReset => return Ok(()),
                                Err(e) => return Err(e),
                            }
                        }
                }
                Event::Resize(cols, rows) => {
                    // Convert terminal height to content rows (exclude bottom bar).
                    let rows = rows.saturating_sub(1);
                    match send_msg(writer, &ClientMsg::Resize { cols, rows }) {
                        Ok(()) => {}
                        Err(ref e)
                            if e.kind() == io::ErrorKind::BrokenPipe
                                || e.kind() == io::ErrorKind::ConnectionReset =>
                        {
                            return Ok(());
                        }
                        Err(e) => return Err(e),
                    }
                }
                _ => {}
            }
        }
    }
}

fn render(f: &mut Frame, state: &DaemonState, show_menu: bool, selection: &Option<Selection>, context_menu: Option<&ContextMenu>, rename_state: Option<&RenameState>) {
    let area = f.area();
    let content_height = area.height.saturating_sub(1);
    let content_rect = Rect { x: 0, y: 0, width: area.width, height: content_height };

    let mut lines: Vec<Line> = Vec::new();
    let rows_to_render = std::cmp::min(content_height as usize, state.screen.len());
    for row_idx in 0..rows_to_render {
        let spans = spans_for_row_with_selection(&state.screen[row_idx], row_idx as u16, selection);
        lines.push(Line::from(spans));
    }
    f.render_widget(Paragraph::new(Text::from(lines)), content_rect);

    draw_bottom_bar(f, state, area.width, area.height);
    if show_menu {
        draw_menu(f, state, area.width, area.height);
    }

    if let Some(cm) = context_menu {
        draw_context_menu(f, state, cm);
    }
    if let Some(rs) = rename_state {
        draw_rename_modal(f, rs);
    }

    // Show the real terminal cursor so the host terminal provides blinking.
    if !state.cursor_hidden && state.cursor_row < content_height && state.cursor_col < area.width {
        f.set_cursor_position((state.cursor_col, state.cursor_row));
    }
}

#[allow(dead_code)]
fn span_to_ratatui(s: &StyledSpan) -> Span<'static> {
    let style = Style::default()
        .fg(term_color(s.fg.clone()))
        .bg(term_color(s.bg.clone()));
    let style = if s.bold { style.add_modifier(Modifier::BOLD) } else { style };
    Span::styled(s.text.clone(), style)
}

fn term_color(c: TermColor) -> Color {
    match c {
        TermColor::Default => Color::Reset,
        TermColor::Indexed(n) => Color::Indexed(n),
        TermColor::Rgb(r, g, b) => Color::Rgb(r, g, b),
    }
}

// Selection and clipboard helpers
fn spans_for_row_with_selection(row_spans: &[StyledSpan], row_idx: u16, selection: &Option<Selection>) -> Vec<Span<'static>> {
    let mut result: Vec<Span<'static>> = Vec::new();
    let mut cur_col: usize = 0;
    // compute row length in cells
    let row_len: usize = row_spans.iter().map(|sp| sp.text.chars().count()).sum();
    let mut selected = false;
    let mut sel_start_col: usize = 0;
    let mut sel_end_col: usize = 0;
    if let Some(sel) = selection {
        let min_row = sel.start_row.min(sel.end_row);
        let max_row = sel.start_row.max(sel.end_row);
        if row_idx >= min_row && row_idx <= max_row {
            selected = true;
            if row_idx == min_row { sel_start_col = sel.start_col.min(sel.end_col) as usize; } else { sel_start_col = 0; }
            if row_idx == max_row { sel_end_col = sel.start_col.max(sel.end_col) as usize; } else { sel_end_col = row_len.saturating_sub(1); }
        }
    }
    for sp in row_spans.iter() {
        let chars: Vec<char> = sp.text.chars().collect();
        let span_len = chars.len();
        if span_len == 0 {
            continue;
        }
        let span_start = cur_col;
        let span_end = cur_col + span_len - 1;
        if !selected {
            // No selection on this row – render whole span normally.
            let style = Style::default().fg(term_color(sp.fg.clone())).bg(term_color(sp.bg.clone()));
            let style = if sp.bold { style.add_modifier(Modifier::BOLD) } else { style };
            result.push(Span::styled(sp.text.clone(), style));
        } else {
            let overlap_start = std::cmp::max(span_start, sel_start_col);
            let overlap_end = std::cmp::min(span_end, sel_end_col);
            if overlap_start > overlap_end {
                // no overlap
                let style = Style::default().fg(term_color(sp.fg.clone())).bg(term_color(sp.bg.clone()));
                let style = if sp.bold { style.add_modifier(Modifier::BOLD) } else { style };
                result.push(Span::styled(sp.text.clone(), style));
            } else {
                // left part
                if overlap_start > span_start {
                    let left_len = overlap_start - span_start;
                    let left_text: String = chars[0..left_len].iter().collect();
                    let style = Style::default().fg(term_color(sp.fg.clone())).bg(term_color(sp.bg.clone()));
                    let style = if sp.bold { style.add_modifier(Modifier::BOLD) } else { style };
                    result.push(Span::styled(left_text, style));
                }
                // middle (selected)
                let mid_start = overlap_start - span_start;
                let mid_len = overlap_end - overlap_start + 1;
                let mid_text: String = chars[mid_start..mid_start + mid_len].iter().collect();
                let mut sel_style = Style::default().bg(Color::LightBlue).fg(Color::Black);
                if sp.bold { sel_style = sel_style.add_modifier(Modifier::BOLD); }
                result.push(Span::styled(mid_text, sel_style));
                // right part
                if overlap_end < span_end {
                    let right_start = overlap_end - span_start + 1;
                    let right_text: String = chars[right_start..].iter().collect();
                    let style = Style::default().fg(term_color(sp.fg.clone())).bg(term_color(sp.bg.clone()));
                    let style = if sp.bold { style.add_modifier(Modifier::BOLD) } else { style };
                    result.push(Span::styled(right_text, style));
                }
            }
        }
        cur_col += span_len;
    }
    result
}

fn get_selected_text(state: &DaemonState, sel: &Selection) -> String {
    let min_row = sel.start_row.min(sel.end_row) as usize;
    let max_row = sel.start_row.max(sel.end_row) as usize;
    let mut out_lines: Vec<String> = Vec::new();
    for row in min_row..=max_row {
        if row >= state.screen.len() { break; }
        let row_spans = &state.screen[row];
        let row_text: String = row_spans.iter().map(|sp| sp.text.clone()).collect::<Vec<_>>().join("");
        let row_chars: Vec<char> = row_text.chars().collect();
        let row_len = row_chars.len();
        let start_col = if row == min_row { sel.start_col.min(sel.end_col) as usize } else { 0 };
        let end_col = if row == max_row { sel.start_col.max(sel.end_col) as usize } else { row_len.saturating_sub(1) };
        if row_len == 0 || start_col >= row_len || start_col > end_col {
            out_lines.push(String::new());
            continue;
        }
        let end_col = std::cmp::min(end_col, row_len.saturating_sub(1));
        let s: String = row_chars[start_col..=end_col].iter().collect();
        out_lines.push(s);
    }
    out_lines.join("\n")
}

fn copy_to_clipboard(text: &str) -> io::Result<()> {
    // First try OSC 52 to delegate the copy to the host terminal emulator.
    // OSC 52 format: ESC ] 52 ; c ; <base64-data> BEL
    let encoded = base64::engine::general_purpose::STANDARD.encode(text.as_bytes());
    let osc = format!("\x1b]52;c;{}\x07", encoded);
    // Attempt to write to stdout so the terminal emulator receives the OSC.
    {
        use std::io::Write as _;
        let mut out = std::io::stdout();
        if out.write_all(osc.as_bytes()).is_ok() {
            let _ = out.flush();
            return Ok(());
        }
    }

    // If writing failed, fall back to local clipboard helpers.
    if let Ok(mut cmd) = Command::new("wl-copy")
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        if let Some(mut stdin) = cmd.stdin.take() {
            use std::io::Write as _;
            stdin.write_all(text.as_bytes())?;
        }
        let s = cmd.wait()?;
        if s.success() { return Ok(()); }
    }
    if let Ok(mut cmd) = Command::new("xclip")
        .args(["-selection", "clipboard"])
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        if let Some(mut stdin) = cmd.stdin.take() {
            use std::io::Write as _;
            stdin.write_all(text.as_bytes())?;
        }
        let s = cmd.wait()?;
        if s.success() { return Ok(()); }
    }
    if let Ok(mut cmd) = Command::new("xsel")
        .args(["--clipboard", "--input"])
        .stdin(Stdio::piped())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
    {
        if let Some(mut stdin) = cmd.stdin.take() {
            use std::io::Write as _;
            stdin.write_all(text.as_bytes())?;
        }
        let s = cmd.wait()?;
        if s.success() { return Ok(()); }
    }
    Err(io::Error::other("no clipboard command available"))
}

fn draw_bottom_bar(f: &mut Frame, state: &DaemonState, w: u16, h: u16) {
    if state.sessions.is_empty() {
        return;
    }
    let s = &state.sessions[state.active_session];
    let mut bar = format!("🐉  {}:  ", s.name);
    for (i, tab) in s.tabs.iter().enumerate() {
        if i == s.active_tab {
            bar.push_str(&format!("[{}]  ", tab.name));
        } else {
            bar.push_str(&format!("{}  ", tab.name));
        }
    }
    let dw = bar.as_str().width();
    if dw < w as usize {
        bar.push_str(&" ".repeat(w as usize - dw));
    }
    f.render_widget(
        Paragraph::new(bar).style(Style::default().bg(Color::Cyan).fg(Color::Black)),
        Rect { x: 0, y: h - 1, width: w, height: 1 },
    );
}

/// Menu layout (row index → action):
///   0  New Tab  |  1  New Session  |  2  Rename Session  |  3  Detach  |  4  Exit
///   5  ──── Sessions ────
///   6+ one entry per session
fn menu_items(state: &DaemonState) -> Vec<ListItem<'static>> {
    let mut items: Vec<ListItem> = vec![
        ListItem::new("  New Tab"),
        ListItem::new("  New Session"),
        ListItem::new("  Rename Session"),
        ListItem::new("  Detach"),
        ListItem::new("  Exit"),
        ListItem::new("──── Sessions ────"),
    ];
    for (i, s) in state.sessions.iter().enumerate() {
        let prefix = if i == state.active_session { "▶ " } else { "  " };
        items.push(ListItem::new(format!("{}{}", prefix, s.name)));
    }
    items
}

fn draw_menu(f: &mut Frame, state: &DaemonState, w: u16, h: u16) {
    let items = menu_items(state);
    let menu_h = items.len() as u16;
    let menu_w = w.min(40);
    let menu_rect = Rect { x: 0, y: h.saturating_sub(1 + menu_h), width: menu_w, height: menu_h };

    // Fill the menu background so underlying terminal text doesn't show through.
    let mut bg = String::new();
    for _ in 0..menu_h {
        bg.push_str(&" ".repeat(menu_w as usize));
        bg.push('\n');
    }
    f.render_widget(Paragraph::new(bg).style(Style::default().bg(Color::Cyan)), menu_rect);

    // Render the menu items on top.
    f.render_widget(
        List::new(items).style(Style::default().bg(Color::Cyan).fg(Color::Black)),
        menu_rect,
    );
}

fn draw_context_menu(f: &mut Frame, _state: &DaemonState, cm: &ContextMenu) {
    let menu_rect = Rect { x: cm.x, y: cm.y, width: cm.width, height: cm.height };

    // Fill background
    let mut bg = String::new();
    for _ in 0..cm.height {
        bg.push_str(&" ".repeat(cm.width as usize));
        bg.push('\n');
    }
    f.render_widget(Paragraph::new(bg).style(Style::default().bg(Color::Cyan)), menu_rect);

    // Render items (prefix selected item with '>')
    let items: Vec<ListItem> = cm
        .items
        .iter()
        .enumerate()
        .map(|(i, label)| {
            let text = if i == cm.selected { format!("> {}", label) } else { format!("  {}", label) };
            ListItem::new(text)
        })
        .collect();

    f.render_widget(List::new(items).style(Style::default().bg(Color::Cyan).fg(Color::Black)), menu_rect);
}

fn draw_rename_modal(f: &mut Frame, rs: &RenameState) {
    let area = f.area();
    let w = std::cmp::min(60, area.width.saturating_sub(4));
    let h = 3u16;
    let x = (area.width.saturating_sub(w)) / 2;
    let y = (area.height.saturating_sub(1).saturating_sub(h)) / 2;
    let rect = Rect { x, y, width: w, height: h };

    let title = match rs.kind {
        RenameKind::Tab(i) => format!("Rename Tab {}: {}", i + 1, rs.buffer),
        RenameKind::Session(i) => format!("Rename Session {}: {}", i + 1, rs.buffer),
    };
    f.render_widget(Paragraph::new(title).style(Style::default().bg(Color::Black).fg(Color::White)), rect);
}

fn handle_mouse(
    state: &DaemonState,
    me: crossterm::event::MouseEvent,
    h: u16,
    show_menu: &mut bool,
    selection: &mut Option<Selection>,
) -> Option<Action> {
    use crossterm::event::{MouseEventKind, MouseButton};

    // Ignore simple mouse moves (no button) to avoid spamming the PTY with
    // escape sequences that printable apps will show as control characters.
    if matches!(me.kind, MouseEventKind::Moved) {
        return None;
    }

    // If the menu is open, only handle click events for the menu.
    if *show_menu {
        if matches!(me.kind, MouseEventKind::Down(_)) {
            return handle_menu_click(state, me.row, h, show_menu);
        }
        return None;
    }

    // Clicks on the status bar are handled locally and not forwarded. Only
    // treat button presses (Down) as interaction — ignore hover/move.
    if me.row == h - 1 {
        match me.kind {
            MouseEventKind::Down(btn) => {
                // Right-click on the bar opens a context menu (session or tab).
                if btn == MouseButton::Right {
                    // If clicked on the left icon area toggle main menu as before.
                    if me.column < 4 {
                        *show_menu = !*show_menu;
                        return None;
                    }

                    if state.sessions.is_empty() { return None; }
                    let s = &state.sessions[state.active_session];
                    // session name area
                    let session_start = 4u16;
                    let session_end = session_start + s.name.len() as u16;
                    if me.column >= session_start && me.column < session_end {
                        return Some(Action::OpenContextMenu(ContextTarget::Session(state.active_session), me.column));
                    }

                    // Tabs begin after "🐉  SessionName:  " = 4 + name.len() + 3
                    let prefix = 4u16 + s.name.len() as u16 + 3;
                    let mut cursor = prefix;
                    for i in 0..s.tabs.len() {
                        let name_len = s.tabs[i].name.len() as u16;
                        let len = if i == s.active_tab { name_len + 4 } else { name_len + 2 };
                        if me.column >= cursor && me.column < cursor + len {
                            return Some(Action::OpenContextMenu(ContextTarget::Tab(i), me.column));
                        }
                        cursor += len;
                    }
                    return None;
                }

                // Left-click on the bar performs normal bar actions.
                if btn == MouseButton::Left {
                    return handle_bar_click(state, me.column, show_menu);
                }
                return None;
            }
            _ => return None,
        }
    }

    // Ignore simple mouse moves (no button) — already handled earlier.
    // If the menu is open or the click was on the status bar we've already
    // returned. Here we decide between forwarding to the PTY or performing
    // local scrollback handling, and implement client-side selection when the
    // remote app has not enabled mouse reporting.
    match me.kind {
        MouseEventKind::Down(btn) => {
            // Start selection with left button when remote app isn't handling mouse.
            if btn == MouseButton::Left && state.mouse_mode == MouseMode::None {
                *selection = Some(Selection { start_row: me.row, start_col: me.column, end_row: me.row, end_col: me.column });
                return None;
            }
            if state.mouse_mode == MouseMode::None {
                return None;
            }
            let b = mouse_btn_to_code(btn, me.modifiers, false);
            Some(Action::Msg(ClientMsg::Input(mouse_event_bytes(
                state, b, me.column + 1, me.row + 1, false,
            ))))
        }
        MouseEventKind::Up(btn) => {
            // Finalize selection on left button release and copy to clipboard.
            if btn == MouseButton::Left && state.mouse_mode == MouseMode::None {
                if let Some(sel) = selection.as_ref() {
                    let text = get_selected_text(state, sel);
                    let _ = copy_to_clipboard(&text);
                }
                return None;
            }
            if state.mouse_mode == MouseMode::None {
                return None;
            }
            let b = mouse_btn_to_code(btn, me.modifiers, false);
            Some(Action::Msg(ClientMsg::Input(mouse_event_bytes(
                state, b, me.column + 1, me.row + 1, true,
            ))))
        }
        MouseEventKind::Drag(btn) => {
            // Update selection while dragging with left button.
            if btn == MouseButton::Left && state.mouse_mode == MouseMode::None {
                if let Some(sel) = selection.as_mut() {
                    sel.end_row = me.row;
                    sel.end_col = me.column;
                }
                return None;
            }
            if state.mouse_mode == MouseMode::None {
                return None;
            }
            let b = mouse_btn_to_code(btn, me.modifiers, true);
            Some(Action::Msg(ClientMsg::Input(mouse_event_bytes(
                state, b, me.column + 1, me.row + 1, false,
            ))))
        }
        MouseEventKind::ScrollUp => {
            if state.mouse_mode == MouseMode::None {
                // Local scrollback: scroll up by a few lines.
                Some(Action::Msg(ClientMsg::Scroll(3)))
            } else {
                let b = 64u32;
                Some(Action::Msg(ClientMsg::Input(mouse_event_bytes(
                    state, b, me.column + 1, me.row + 1, false,
                ))))
            }
        }
        MouseEventKind::ScrollDown => {
            if state.mouse_mode == MouseMode::None {
                // Local scrollback: scroll down by a few lines.
                Some(Action::Msg(ClientMsg::Scroll(-3)))
            } else {
                let b = 65u32;
                Some(Action::Msg(ClientMsg::Input(mouse_event_bytes(
                    state, b, me.column + 1, me.row + 1, false,
                ))))
            }
        }
        _ => None,
    }
}

fn mouse_btn_to_code(btn: crossterm::event::MouseButton, mods: KeyModifiers, motion: bool) -> u32 {
    let base = match btn {
        crossterm::event::MouseButton::Left => 0u32,
        crossterm::event::MouseButton::Middle => 1u32,
        crossterm::event::MouseButton::Right => 2u32,
    };
    let mut code = base;
    if mods.contains(KeyModifiers::SHIFT) { code += 4; }
    if mods.contains(KeyModifiers::ALT) { code += 8; }
    if mods.contains(KeyModifiers::CONTROL) { code += 16; }
    if motion { code += 32; }
    code
}

fn mouse_event_bytes(state: &DaemonState, btn_code: u32, col: u16, row: u16, is_release: bool) -> Vec<u8> {
    match state.mouse_encoding {
        MouseEncoding::Sgr => {
            // SGR encoding: CSI '<' Cb ; Cx ; Cy (M or m)
            let m = if is_release { 'm' } else { 'M' };
            let s = format!("\x1b[<{};{};{}{}", btn_code, col, row, m);
            s.into_bytes()
        }
        // Default and Utf8: fall back to legacy X10-like encoding: ESC [ M Cb Cx Cy
        MouseEncoding::Default | MouseEncoding::Utf8 => {
            let cb = (32u8).wrapping_add(btn_code as u8);
            let cx = (32u8).wrapping_add(col as u8);
            let cy = (32u8).wrapping_add(row as u8);
            let v = vec![0x1b, b'[', b'M', cb, cx, cy];
            v
        }
    }
}

fn handle_bar_click(state: &DaemonState, col: u16, show_menu: &mut bool) -> Option<Action> {
    // "🐉  " = 4 display columns (emoji=2, two spaces=2).
    if col < 4 {
        *show_menu = !*show_menu;
        return None;
    }
    *show_menu = false;

    if state.sessions.is_empty() {
        return None;
    }
    let s = &state.sessions[state.active_session];
    // Tabs begin after "🐉  SessionName:  " = 4 + name.len + 3 display cols.
    let prefix = 4u16 + s.name.len() as u16 + 3;
    let mut cursor = prefix;

    for i in 0..s.tabs.len() {
        let name_len = s.tabs[i].name.len() as u16;
        // Active tab: "[name]  " (+4), inactive: "name  " (+2).
        let len = if i == s.active_tab { name_len + 4 } else { name_len + 2 };
        if col >= cursor && col < cursor + len {
            return Some(Action::Msg(ClientMsg::SwitchTab(i)));
        }
        cursor += len;
    }
    None
}

fn handle_menu_click(
    state: &DaemonState,
    row: u16,
    h: u16,
    show_menu: &mut bool,
) -> Option<Action> {
    let menu_h = (6 + state.sessions.len()) as u16;
    let menu_top = h.saturating_sub(1 + menu_h);
    *show_menu = false;

    if row < menu_top {
        return None; // click above menu — just close it
    }

    match (row - menu_top) as usize {
        0 => Some(Action::Msg(ClientMsg::NewTab)),
        1 => {
            let n = state.sessions.len() + 1;
            Some(Action::Msg(ClientMsg::NewSession(format!("Session{}", n))))
        }
        2 => Some(Action::RenameSession),
        3 => Some(Action::Detach),
        4 => Some(Action::Msg(ClientMsg::Exit)),
        5 => None, // separator
        i => {
            let si = i - 6;
            if si < state.sessions.len() {
                Some(Action::Msg(ClientMsg::SwitchSession(si)))
            } else {
                None
            }
        }
    }
}

fn key_to_bytes(ke: &crossterm::event::KeyEvent) -> Option<Vec<u8>> {
    let ctrl = ke.modifiers.contains(KeyModifiers::CONTROL);
    let shift = ke.modifiers.contains(KeyModifiers::SHIFT);
    let alt = ke.modifiers.contains(KeyModifiers::ALT);

    // VT modifier parameter: shift=1, alt=2, ctrl=4 (combined, then add 1).
    let vt_mod = (if shift { 1 } else { 0 })
        + (if alt { 2 } else { 0 })
        + (if ctrl { 4 } else { 0 });

    match ke.code {
        // ── Ctrl+Char ────────────────────────────────────────────────────────
        KeyCode::Char(c) if ctrl => {
            let lo = c.to_ascii_lowercase();
            let byte: Option<u8> = match lo {
                'a'..='z' => Some(lo as u8 - b'a' + 1),
                '@' | ' ' => Some(0x00),
                '[' => Some(0x1b),
                '\\' => Some(0x1c),
                ']' => Some(0x1d),
                '^' => Some(0x1e),
                '_' => Some(0x1f),
                _ => None,
            };
            // Wrap in Alt escape if needed, then emit.
            match byte {
                Some(b) if alt => Some(vec![0x1b, b]),
                Some(b) => Some(vec![b]),
                None => None,
            }
        }

        // ── Plain / Shift+Char ───────────────────────────────────────────────
        // Crossterm already delivers the shifted character (e.g. 'A' for Shift+a),
        // so we just UTF-8 encode it. Wrap in ESC if Alt is also held.
        KeyCode::Char(c) => {
            let mut buf = [0u8; 4];
            let bytes = c.encode_utf8(&mut buf).as_bytes().to_vec();
            if alt {
                let mut out = vec![0x1b];
                out.extend_from_slice(&bytes);
                Some(out)
            } else {
                Some(bytes)
            }
        }

        // ── Arrow keys ───────────────────────────────────────────────────────
        KeyCode::Up | KeyCode::Down | KeyCode::Right | KeyCode::Left => {
            let suffix = match ke.code {
                KeyCode::Up => b'A',
                KeyCode::Down => b'B',
                KeyCode::Right => b'C',
                KeyCode::Left => b'D',
                _ => unreachable!(),
            };
            Some(if vt_mod == 0 {
                vec![0x1b, b'[', suffix]
            } else {
                format!("\x1b[1;{}{}", vt_mod + 1, suffix as char).into_bytes()
            })
        }

        // ── Home / End ───────────────────────────────────────────────────────
        KeyCode::Home => Some(if vt_mod == 0 {
            b"\x1b[H".to_vec()
        } else {
            format!("\x1b[1;{}H", vt_mod + 1).into_bytes()
        }),
        KeyCode::End => Some(if vt_mod == 0 {
            b"\x1b[F".to_vec()
        } else {
            format!("\x1b[1;{}F", vt_mod + 1).into_bytes()
        }),

        // ── Tilde-encoded keys ────────────────────────────────────────────────
        KeyCode::Delete => Some(if vt_mod == 0 {
            b"\x1b[3~".to_vec()
        } else {
            format!("\x1b[3;{}~", vt_mod + 1).into_bytes()
        }),
        KeyCode::PageUp => Some(if vt_mod == 0 {
            b"\x1b[5~".to_vec()
        } else {
            format!("\x1b[5;{}~", vt_mod + 1).into_bytes()
        }),
        KeyCode::PageDown => Some(if vt_mod == 0 {
            b"\x1b[6~".to_vec()
        } else {
            format!("\x1b[6;{}~", vt_mod + 1).into_bytes()
        }),

        // ── Simple single-byte keys ──────────────────────────────────────────
        KeyCode::Enter => Some(b"\r".to_vec()),
        KeyCode::Backspace => Some(b"\x7f".to_vec()),
        KeyCode::Tab if shift => Some(b"\x1b[Z".to_vec()),
        KeyCode::Tab => Some(b"\t".to_vec()),
        KeyCode::Esc => Some(b"\x1b".to_vec()),

        _ => None,
    }
}