use std::collections::{HashMap, HashSet};
use std::sync::{Mutex, OnceLock};

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Widget};

use crate::keyboard::display::{self, BACKSPACE, ENTER, SPACE, TAB};
use crate::keyboard::model::{KeyboardModel, PhysicalKey};
use crate::ui::theme::Theme;

pub struct KeyboardDiagram<'a> {
    pub selected_key: Option<char>,
    pub next_key: Option<char>,
    pub unlocked_keys: &'a [char],
    pub depressed_keys: &'a HashSet<char>,
    pub theme: &'a Theme,
    pub compact: bool,
    pub model: &'a KeyboardModel,
    pub shift_held: bool,
    pub caps_lock: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum KeyboardRenderMode {
    Compact,
    Full,
    FullFallback,
}

#[derive(Clone, Debug)]
struct KeyboardGeometry {
    key_width: u16,
    row_offsets: Vec<u16>,
    keyboard_width: u16,
    start_inset: u16,
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct GeometryCacheKey {
    layout_key: String,
    mode: KeyboardRenderMode,
    width: u16,
    height: u16,
}

const MAX_GEOMETRY_CACHE_ENTRIES: usize = 128;

impl<'a> KeyboardDiagram<'a> {
    pub fn new(
        next_key: Option<char>,
        unlocked_keys: &'a [char],
        depressed_keys: &'a HashSet<char>,
        theme: &'a Theme,
        model: &'a KeyboardModel,
    ) -> Self {
        Self {
            selected_key: None,
            next_key,
            unlocked_keys,
            depressed_keys,
            theme,
            compact: false,
            model,
            shift_held: false,
            caps_lock: false,
        }
    }

    pub fn caps_lock(mut self, caps_lock: bool) -> Self {
        self.caps_lock = caps_lock;
        self
    }

    pub fn selected_key(mut self, key: Option<char>) -> Self {
        self.selected_key = key;
        self
    }

    pub fn compact(mut self, compact: bool) -> Self {
        self.compact = compact;
        self
    }

    pub fn shift_held(mut self, shift_held: bool) -> Self {
        self.shift_held = shift_held;
        self
    }

    /// Check if a key (by display or base char) matches the selected key.
    fn is_key_selected(&self, display_char: char, base_char: char) -> bool {
        self.selected_key == Some(display_char) || self.selected_key == Some(base_char)
    }

    /// Check if a sentinel/modifier key matches the selected key.
    fn is_sentinel_selected(&self, sentinel: char) -> bool {
        self.selected_key == Some(sentinel)
    }
}

fn geometry_cache() -> &'static Mutex<HashMap<GeometryCacheKey, KeyboardGeometry>> {
    static CACHE: OnceLock<Mutex<HashMap<GeometryCacheKey, KeyboardGeometry>>> = OnceLock::new();
    CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}

fn rows_for_mode<'a>(model: &'a KeyboardModel, mode: KeyboardRenderMode) -> &'a [Vec<PhysicalKey>] {
    match mode {
        KeyboardRenderMode::Compact | KeyboardRenderMode::FullFallback => model.letter_rows(),
        KeyboardRenderMode::Full => &model.rows,
    }
}

fn render_mode_for(inner: Rect, compact: bool) -> KeyboardRenderMode {
    if compact {
        KeyboardRenderMode::Compact
    } else if inner.height >= 4 && inner.width >= 75 {
        KeyboardRenderMode::Full
    } else {
        KeyboardRenderMode::FullFallback
    }
}

fn build_geometry(
    inner: Rect,
    model: &KeyboardModel,
    mode: KeyboardRenderMode,
) -> Option<KeyboardGeometry> {
    let rows = rows_for_mode(model, mode);
    match mode {
        KeyboardRenderMode::Compact => {
            if inner.height < 3 || inner.width < 21 {
                return None;
            }
            let key_width = 3;
            let row_offsets = vec![3, 4, 6];
            let keyboard_width = rows
                .iter()
                .enumerate()
                .map(|(row_idx, row)| {
                    let offset = row_offsets.get(row_idx).copied().unwrap_or(0);
                    offset + row.len() as u16 * key_width + 3
                })
                .max()
                .unwrap_or(0);
            Some(KeyboardGeometry {
                key_width,
                row_offsets,
                keyboard_width,
                start_inset: inner.width.saturating_sub(keyboard_width) / 2,
            })
        }
        KeyboardRenderMode::Full => {
            if inner.height < 4 || inner.width < 75 {
                return None;
            }
            let key_width = 5;
            let row_offsets = vec![0, 5, 5, 6];
            let keyboard_width = rows
                .iter()
                .enumerate()
                .map(|(row_idx, row)| {
                    let offset = row_offsets.get(row_idx).copied().unwrap_or(0);
                    let row_end = offset + row.len() as u16 * key_width;
                    match row_idx {
                        0 => row_end + 6,
                        2 => row_end + 7,
                        3 => row_end + 6,
                        _ => row_end,
                    }
                })
                .max()
                .unwrap_or(0);
            Some(KeyboardGeometry {
                key_width,
                row_offsets,
                keyboard_width,
                start_inset: inner.width.saturating_sub(keyboard_width) / 2,
            })
        }
        KeyboardRenderMode::FullFallback => {
            if inner.height < 3 || inner.width < 30 {
                return None;
            }
            let key_width = 5;
            let row_offsets = vec![1, 3, 5];
            let keyboard_width = rows
                .iter()
                .enumerate()
                .map(|(row_idx, row)| {
                    let offset = row_offsets.get(row_idx).copied().unwrap_or(0);
                    offset + row.len() as u16 * key_width
                })
                .max()
                .unwrap_or(0);
            Some(KeyboardGeometry {
                key_width,
                row_offsets,
                keyboard_width,
                start_inset: inner.width.saturating_sub(keyboard_width) / 2,
            })
        }
    }
}

fn geometry_for_mode(
    inner: Rect,
    model: &KeyboardModel,
    mode: KeyboardRenderMode,
) -> Option<KeyboardGeometry> {
    let key = GeometryCacheKey {
        layout_key: model.layout_key.to_string(),
        mode,
        width: inner.width,
        height: inner.height,
    };
    if let Some(geom) = geometry_cache()
        .lock()
        .expect("keyboard geometry cache poisoned")
        .get(&key)
        .cloned()
    {
        return Some(geom);
    }

    let built = build_geometry(inner, model, mode)?;
    let mut cache = geometry_cache()
        .lock()
        .expect("keyboard geometry cache poisoned");
    if cache.len() >= MAX_GEOMETRY_CACHE_ENTRIES {
        // Bounded cache: simple full-clear avoids unbounded growth across long resize sessions.
        cache.clear();
    }
    cache.insert(key, built.clone());
    Some(built)
}

fn geometry_for(inner: Rect, model: &KeyboardModel, compact: bool) -> Option<KeyboardGeometry> {
    geometry_for_mode(inner, model, render_mode_for(inner, compact))
}

fn show_shifted_for_key(key: &PhysicalKey, shift_held: bool, caps_lock: bool) -> bool {
    if key.base.is_alphabetic() {
        shift_held ^ caps_lock
    } else {
        shift_held
    }
}

fn brighten_color(color: Color) -> Color {
    match color {
        Color::Rgb(r, g, b) => Color::Rgb(
            r.saturating_add(60),
            g.saturating_add(60),
            b.saturating_add(60),
        ),
        other => other,
    }
}

fn color_to_rgb(color: Color) -> (u8, u8, u8) {
    match color {
        Color::Reset => (0, 0, 0),
        Color::Black => (0, 0, 0),
        Color::Red => (205, 49, 49),
        Color::Green => (13, 188, 121),
        Color::Yellow => (229, 229, 16),
        Color::Blue => (36, 114, 200),
        Color::Magenta => (188, 63, 188),
        Color::Cyan => (17, 168, 205),
        Color::Gray => (229, 229, 229),
        Color::DarkGray => (102, 102, 102),
        Color::LightRed => (241, 76, 76),
        Color::LightGreen => (35, 209, 139),
        Color::LightYellow => (245, 245, 67),
        Color::LightBlue => (59, 142, 234),
        Color::LightMagenta => (214, 112, 214),
        Color::LightCyan => (41, 184, 219),
        Color::White => (255, 255, 255),
        Color::Rgb(r, g, b) => (r, g, b),
        Color::Indexed(i) => {
            if i < 16 {
                const ANSI16: [(u8, u8, u8); 16] = [
                    (0, 0, 0),
                    (128, 0, 0),
                    (0, 128, 0),
                    (128, 128, 0),
                    (0, 0, 128),
                    (128, 0, 128),
                    (0, 128, 128),
                    (192, 192, 192),
                    (128, 128, 128),
                    (255, 0, 0),
                    (0, 255, 0),
                    (255, 255, 0),
                    (0, 0, 255),
                    (255, 0, 255),
                    (0, 255, 255),
                    (255, 255, 255),
                ];
                ANSI16[i as usize]
            } else if i <= 231 {
                let idx = i - 16;
                let r = idx / 36;
                let g = (idx % 36) / 6;
                let b = idx % 6;
                let cv = |n: u8| if n == 0 { 0 } else { 55 + n * 40 };
                (cv(r), cv(g), cv(b))
            } else {
                let v = 8 + (i - 232) * 10;
                (v, v, v)
            }
        }
    }
}

fn relative_luminance(color: Color) -> f64 {
    let (r, g, b) = color_to_rgb(color);
    let to_linear = |c: u8| {
        let x = c as f64 / 255.0;
        if x <= 0.03928 {
            x / 12.92
        } else {
            ((x + 0.055) / 1.055).powf(2.4)
        }
    };
    0.2126 * to_linear(r) + 0.7152 * to_linear(g) + 0.0722 * to_linear(b)
}

fn contrast_ratio(a: Color, b: Color) -> f64 {
    let la = relative_luminance(a);
    let lb = relative_luminance(b);
    let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
    (hi + 0.05) / (lo + 0.05)
}

fn readable_fg(bg: Color, preferred: Color) -> Color {
    let mut best = preferred;
    let mut best_ratio = contrast_ratio(preferred, bg);
    for candidate in [Color::White, Color::Black] {
        let ratio = contrast_ratio(candidate, bg);
        if ratio > best_ratio {
            best = candidate;
            best_ratio = ratio;
        }
    }
    best
}

/// Blend a color toward the background at the given ratio (0.0 = full bg, 1.0 = full color).
fn blend_toward_bg(color: Color, bg: Color, ratio: f32) -> Color {
    match (color, bg) {
        (Color::Rgb(r, g, b), Color::Rgb(br, bg_g, bb)) => {
            let mix = |c: u8, base: u8| -> u8 {
                (base as f32 + (c as f32 - base as f32) * ratio).round() as u8
            };
            Color::Rgb(mix(r, br), mix(g, bg_g), mix(b, bb))
        }
        _ => color,
    }
}

/// Compute style for a modifier key box (Tab, Enter, Shift, Space, Backspace).
fn modifier_key_style(
    is_depressed: bool,
    is_next: bool,
    is_selected: bool,
    colors: &crate::ui::theme::ThemeColors,
) -> Style {
    if is_depressed {
        let bg = brighten_color(colors.accent_dim());
        Style::default()
            .fg(readable_fg(bg, colors.fg()))
            .bg(bg)
            .add_modifier(Modifier::BOLD)
    } else if is_next {
        let bg = blend_toward_bg(colors.accent(), colors.bg(), 0.35);
        Style::default().fg(readable_fg(bg, colors.accent())).bg(bg)
    } else if is_selected {
        let bg = colors.accent_dim();
        Style::default().fg(readable_fg(bg, colors.fg())).bg(bg)
    } else {
        Style::default().fg(colors.fg()).bg(colors.bg())
    }
}

fn key_style(
    is_depressed: bool,
    is_next: bool,
    is_selected: bool,
    is_unlocked: bool,
    colors: &crate::ui::theme::ThemeColors,
) -> Style {
    if is_depressed {
        let bg = brighten_color(colors.accent_dim());
        Style::default()
            .fg(readable_fg(bg, colors.fg()))
            .bg(bg)
            .add_modifier(Modifier::BOLD)
    } else if is_next {
        let bg = blend_toward_bg(colors.accent(), colors.bg(), 0.35);
        Style::default().fg(readable_fg(bg, colors.accent())).bg(bg)
    } else if is_selected {
        let bg = colors.accent_dim();
        Style::default().fg(readable_fg(bg, colors.fg())).bg(bg)
    } else if is_unlocked {
        Style::default().fg(colors.fg()).bg(colors.bg())
    } else {
        Style::default().fg(colors.text_pending()).bg(colors.bg())
    }
}

impl Widget for KeyboardDiagram<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let colors = &self.theme.colors;

        let block = Block::bordered()
            .title(" Keyboard ")
            .border_style(Style::default().fg(colors.border()))
            .style(Style::default().bg(colors.bg()));
        let inner = block.inner(area);
        block.render(area, buf);

        if self.compact {
            self.render_compact(inner, buf);
        } else {
            self.render_full(inner, buf);
        }
    }
}

impl KeyboardDiagram<'_> {
    pub fn key_at_position(
        area: Rect,
        model: &KeyboardModel,
        compact: bool,
        x: u16,
        y: u16,
    ) -> Option<char> {
        let inner = Block::bordered().inner(area);
        if compact {
            return key_at_compact_position(inner, model, x, y);
        }

        if inner.height >= 4 && inner.width >= 75 {
            key_at_full_position(inner, model, x, y)
        } else {
            key_at_full_fallback_position(inner, model, x, y)
        }
    }

    pub fn shift_at_position(
        area: Rect,
        model: &KeyboardModel,
        compact: bool,
        x: u16,
        y: u16,
    ) -> bool {
        let inner = Block::bordered().inner(area);
        if compact {
            return shift_at_compact_position(inner, model, x, y);
        }

        if inner.height >= 4 && inner.width >= 75 {
            shift_at_full_position(inner, model, x, y)
        } else {
            shift_at_full_fallback_position(inner, model, x, y)
        }
    }

    fn render_compact(&self, inner: Rect, buf: &mut Buffer) {
        let colors = &self.theme.colors;
        let letter_rows = self.model.letter_rows();
        let Some(geometry) = geometry_for_mode(inner, self.model, KeyboardRenderMode::Compact)
        else {
            return;
        };
        let key_width = geometry.key_width;
        let offsets = &geometry.row_offsets;
        let start_x = inner.x + geometry.start_inset;

        for (row_idx, row) in letter_rows.iter().enumerate() {
            let y = inner.y + row_idx as u16;
            if y >= inner.y + inner.height {
                break;
            }

            let offset = offsets.get(row_idx).copied().unwrap_or(0);

            // Render leading modifier key
            match row_idx {
                0 => {
                    let is_dep = self.depressed_keys.contains(&TAB);
                    let is_next = self.next_key == Some(TAB);
                    let is_sel = self.is_sentinel_selected(TAB);
                    let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                    buf.set_string(start_x, y, "[T]", style);
                }
                2 => {
                    let is_dep = self.shift_held;
                    let style = modifier_key_style(is_dep, false, false, colors);
                    buf.set_string(start_x, y, "[S]", style);
                }
                _ => {}
            }

            for (col_idx, physical_key) in row.iter().enumerate() {
                let x = start_x + offset + col_idx as u16 * key_width;
                if x + key_width > inner.x + inner.width {
                    break;
                }

                let show_shifted =
                    show_shifted_for_key(physical_key, self.shift_held, self.caps_lock);
                let display_char = if show_shifted {
                    physical_key.shifted
                } else {
                    physical_key.base
                };
                let base_char = physical_key.base;

                let is_depressed = self.depressed_keys.contains(&base_char);
                let is_unlocked = self.unlocked_keys.contains(&display_char)
                    || self.unlocked_keys.contains(&base_char);
                let is_next =
                    self.next_key == Some(display_char) || self.next_key == Some(base_char);
                let is_sel = self.is_key_selected(display_char, base_char);

                let style = key_style(is_depressed, is_next, is_sel, is_unlocked, colors);

                let display = format!("[{display_char}]");
                buf.set_string(x, y, &display, style);
            }

            // Render trailing modifier key
            let row_end_x = start_x + offset + row.len() as u16 * key_width;
            match row_idx {
                1 => {
                    if row_end_x + 3 <= inner.x + inner.width {
                        let is_dep = self.depressed_keys.contains(&ENTER);
                        let is_next = self.next_key == Some(ENTER);
                        let is_sel = self.is_sentinel_selected(ENTER);
                        let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                        buf.set_string(row_end_x, y, "[E]", style);
                    }
                }
                2 => {
                    if row_end_x + 3 <= inner.x + inner.width {
                        let is_dep = self.shift_held;
                        let style = modifier_key_style(is_dep, false, false, colors);
                        buf.set_string(row_end_x, y, "[S]", style);
                    }
                }
                _ => {}
            }
        }

        // Backspace at end of first row
        if inner.height >= 3 && !letter_rows.is_empty() {
            let y = inner.y;
            let row_end_x = start_x + offsets[0] + letter_rows[0].len() as u16 * key_width;
            if row_end_x + 3 <= inner.x + inner.width {
                let is_dep = self.depressed_keys.contains(&BACKSPACE);
                let is_next = self.next_key == Some(BACKSPACE);
                let is_sel = self.is_sentinel_selected(BACKSPACE);
                let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                buf.set_string(row_end_x, y, "[B]", style);
            }
        }
    }

    fn render_full(&self, inner: Rect, buf: &mut Buffer) {
        let colors = &self.theme.colors;
        let Some(geometry) = geometry_for(inner, self.model, false) else {
            return;
        };
        if render_mode_for(inner, false) != KeyboardRenderMode::Full {
            self.render_full_fallback(inner, buf);
            return;
        }
        let key_width = geometry.key_width;
        let offsets = &geometry.row_offsets;
        let keyboard_width = geometry.keyboard_width;
        let start_x = inner.x + geometry.start_inset;

        for (row_idx, row) in self.model.rows.iter().enumerate() {
            let y = inner.y + row_idx as u16;
            if y >= inner.y + inner.height {
                break;
            }

            let offset = offsets.get(row_idx).copied().unwrap_or(0);

            // Render leading modifier keys
            match row_idx {
                1 => {
                    if offset >= 5 {
                        let is_dep = self.depressed_keys.contains(&TAB);
                        let is_next = self.next_key == Some(TAB);
                        let is_sel = self.is_sentinel_selected(TAB);
                        let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                        let label = format!("[{}]", display::key_short_label(TAB));
                        buf.set_string(start_x, y, &label, style);
                    }
                }
                2 => {
                    if offset >= 5 {
                        if self.caps_lock {
                            let bg = colors.accent_dim();
                            let style = Style::default()
                                .fg(readable_fg(bg, colors.warning()))
                                .bg(bg);
                            buf.set_string(start_x, y, "[Cap]", style);
                        } else {
                            let style = Style::default().fg(colors.text_pending()).bg(colors.bg());
                            buf.set_string(start_x, y, "[   ]", style);
                        }
                    }
                }
                3 => {
                    if offset >= 6 {
                        let is_dep = self.shift_held;
                        let style = modifier_key_style(is_dep, false, false, colors);
                        buf.set_string(start_x, y, "[Shft]", style);
                    }
                }
                _ => {}
            }

            for (col_idx, physical_key) in row.iter().enumerate() {
                let x = start_x + offset + col_idx as u16 * key_width;
                if x + key_width > inner.x + inner.width {
                    break;
                }

                let show_shifted =
                    show_shifted_for_key(physical_key, self.shift_held, self.caps_lock);
                let display_char = if show_shifted {
                    physical_key.shifted
                } else {
                    physical_key.base
                };
                let base_char = physical_key.base;

                let is_depressed = self.depressed_keys.contains(&base_char);
                let is_unlocked = self.unlocked_keys.contains(&display_char)
                    || self.unlocked_keys.contains(&base_char);
                let is_next =
                    self.next_key == Some(display_char) || self.next_key == Some(base_char);
                let is_sel = self.is_key_selected(display_char, base_char);

                let style = key_style(is_depressed, is_next, is_sel, is_unlocked, colors);

                let display = format!("[ {display_char} ]");
                buf.set_string(x, y, &display, style);
            }

            // Render trailing modifier keys
            let after_x = start_x + offset + row.len() as u16 * key_width;
            match row_idx {
                0 => {
                    if after_x + 6 <= inner.x + inner.width {
                        let is_dep = self.depressed_keys.contains(&BACKSPACE);
                        let is_next = self.next_key == Some(BACKSPACE);
                        let is_sel = self.is_sentinel_selected(BACKSPACE);
                        let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                        let label = format!("[{}]", display::key_short_label(BACKSPACE));
                        buf.set_string(after_x, y, &label, style);
                    }
                }
                2 => {
                    if after_x + 7 <= inner.x + inner.width {
                        let is_dep = self.depressed_keys.contains(&ENTER);
                        let is_next = self.next_key == Some(ENTER);
                        let is_sel = self.is_sentinel_selected(ENTER);
                        let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                        let label = format!("[{}]", display::key_display_name(ENTER));
                        buf.set_string(after_x, y, &label, style);
                    }
                }
                3 => {
                    if after_x + 6 <= inner.x + inner.width {
                        let is_dep = self.shift_held;
                        let style = modifier_key_style(is_dep, false, false, colors);
                        buf.set_string(after_x, y, "[Shft]", style);
                    }
                }
                _ => {}
            }
        }

        // Space bar row (row 4)
        let space_y = inner.y + 4;
        if space_y < inner.y + inner.height {
            let space_name = display::key_display_name(SPACE);
            let space_label = format!("[       {space_name}       ]");
            let space_width = space_label.len() as u16;
            let space_x = start_x + (keyboard_width.saturating_sub(space_width)) / 2;
            if space_x + space_width <= inner.x + inner.width {
                let is_dep = self.depressed_keys.contains(&SPACE);
                let is_next = self.next_key == Some(SPACE);
                let is_sel = self.is_sentinel_selected(SPACE);
                let style = modifier_key_style(is_dep, is_next, is_sel, colors);
                buf.set_string(space_x, space_y, space_label, style);
            }
        }
    }

    fn render_full_fallback(&self, inner: Rect, buf: &mut Buffer) {
        let colors = &self.theme.colors;
        let letter_rows = self.model.letter_rows();
        let Some(geometry) = geometry_for_mode(inner, self.model, KeyboardRenderMode::FullFallback)
        else {
            return;
        };
        let key_width = geometry.key_width;
        let offsets = &geometry.row_offsets;
        let start_x = inner.x + geometry.start_inset;

        for (row_idx, row) in letter_rows.iter().enumerate() {
            let y = inner.y + row_idx as u16;
            if y >= inner.y + inner.height {
                break;
            }

            let offset = offsets.get(row_idx).copied().unwrap_or(0);

            for (col_idx, physical_key) in row.iter().enumerate() {
                let x = start_x + offset + col_idx as u16 * key_width;
                if x + key_width > inner.x + inner.width {
                    break;
                }

                let show_shifted =
                    show_shifted_for_key(physical_key, self.shift_held, self.caps_lock);
                let display_char = if show_shifted {
                    physical_key.shifted
                } else {
                    physical_key.base
                };
                let base_char = physical_key.base;

                let is_depressed = self.depressed_keys.contains(&base_char);
                let is_unlocked = self.unlocked_keys.contains(&display_char)
                    || self.unlocked_keys.contains(&base_char);
                let is_next =
                    self.next_key == Some(display_char) || self.next_key == Some(base_char);
                let is_sel = self.is_key_selected(display_char, base_char);

                let style = key_style(is_depressed, is_next, is_sel, is_unlocked, colors);

                let display = format!("[ {display_char} ]");
                buf.set_string(x, y, &display, style);
            }
        }
    }
}

fn rect_contains(area: Rect, x: u16, y: u16) -> bool {
    x >= area.x && x < area.x + area.width && y >= area.y && y < area.y + area.height
}

fn key_at_compact_position(inner: Rect, model: &KeyboardModel, x: u16, y: u16) -> Option<char> {
    let geometry = geometry_for_mode(inner, model, KeyboardRenderMode::Compact)?;
    let letter_rows = model.letter_rows();
    let key_width = geometry.key_width;
    let offsets = &geometry.row_offsets;
    let start_x = inner.x + geometry.start_inset;

    for (row_idx, row) in letter_rows.iter().enumerate() {
        let row_y = inner.y + row_idx as u16;
        if y != row_y {
            continue;
        }

        match row_idx {
            0 => {
                let tab_rect = Rect::new(start_x, row_y, 3, 1);
                if rect_contains(tab_rect, x, y) {
                    return Some(TAB);
                }
            }
            2 => {
                let shft_rect = Rect::new(start_x, row_y, 3, 1);
                if rect_contains(shft_rect, x, y) {
                    return None;
                }
            }
            _ => {}
        }

        let offset = offsets.get(row_idx).copied().unwrap_or(0);
        for (col_idx, key) in row.iter().enumerate() {
            let key_x = start_x + offset + col_idx as u16 * key_width;
            let key_rect = Rect::new(key_x, row_y, 3, 1);
            if rect_contains(key_rect, x, y) {
                return Some(key.base);
            }
        }

        let row_end_x = start_x + offset + row.len() as u16 * key_width;
        match row_idx {
            1 => {
                let enter_rect = Rect::new(row_end_x, row_y, 3, 1);
                if rect_contains(enter_rect, x, y) {
                    return Some(ENTER);
                }
            }
            2 => {
                let shft_rect = Rect::new(row_end_x, row_y, 3, 1);
                if rect_contains(shft_rect, x, y) {
                    return None;
                }
            }
            _ => {}
        }
    }

    if inner.height >= 3 {
        let y0 = inner.y;
        let row_end_x = start_x + offsets[0] + letter_rows[0].len() as u16 * key_width;
        let back_rect = Rect::new(row_end_x, y0, 3, 1);
        if rect_contains(back_rect, x, y) {
            return Some(BACKSPACE);
        }
    }
    None
}

fn shift_at_compact_position(inner: Rect, model: &KeyboardModel, x: u16, y: u16) -> bool {
    let Some(geometry) = geometry_for_mode(inner, model, KeyboardRenderMode::Compact) else {
        return false;
    };
    let letter_rows = model.letter_rows();
    let key_width = geometry.key_width;
    let offsets = &geometry.row_offsets;
    let start_x = inner.x + geometry.start_inset;
    let shift_row_y = inner.y + 2;
    if y != shift_row_y {
        return false;
    }
    let left_shift = Rect::new(start_x, shift_row_y, 3, 1);
    if rect_contains(left_shift, x, y) {
        return true;
    }
    if letter_rows.len() <= 2 {
        return false;
    }
    let offset = offsets[2];
    let row_end_x = start_x + offset + letter_rows[2].len() as u16 * key_width;
    let right_shift = Rect::new(row_end_x, shift_row_y, 3, 1);
    rect_contains(right_shift, x, y)
}

fn key_at_full_position(inner: Rect, model: &KeyboardModel, x: u16, y: u16) -> Option<char> {
    let geometry = geometry_for_mode(inner, model, KeyboardRenderMode::Full)?;
    let key_width = geometry.key_width;
    let offsets = &geometry.row_offsets;
    let keyboard_width = geometry.keyboard_width;
    let start_x = inner.x + geometry.start_inset;

    for (row_idx, row) in model.rows.iter().enumerate() {
        let row_y = inner.y + row_idx as u16;
        if y != row_y {
            continue;
        }
        let offset = offsets.get(row_idx).copied().unwrap_or(0);

        match row_idx {
            1 => {
                let tab_rect = Rect::new(start_x, row_y, 5, 1);
                if rect_contains(tab_rect, x, y) {
                    return Some(TAB);
                }
            }
            2 => {
                let cap_rect = Rect::new(start_x, row_y, 5, 1);
                if rect_contains(cap_rect, x, y) {
                    return None;
                }
            }
            3 => {
                let shft_rect = Rect::new(start_x, row_y, 6, 1);
                if rect_contains(shft_rect, x, y) {
                    return None;
                }
            }
            _ => {}
        }

        for (col_idx, key) in row.iter().enumerate() {
            let key_x = start_x + offset + col_idx as u16 * key_width;
            let key_rect = Rect::new(key_x, row_y, key_width, 1);
            if rect_contains(key_rect, x, y) {
                return Some(key.base);
            }
        }

        let after_x = start_x + offset + row.len() as u16 * key_width;
        match row_idx {
            0 => {
                let rect = Rect::new(after_x, row_y, 6, 1);
                if rect_contains(rect, x, y) {
                    return Some(BACKSPACE);
                }
            }
            2 => {
                let rect = Rect::new(after_x, row_y, 7, 1);
                if rect_contains(rect, x, y) {
                    return Some(ENTER);
                }
            }
            3 => {
                let rect = Rect::new(after_x, row_y, 6, 1);
                if rect_contains(rect, x, y) {
                    return None;
                }
            }
            _ => {}
        }
    }

    let space_y = inner.y + 4;
    if y == space_y {
        let space_name = display::key_display_name(SPACE);
        let space_label = format!("[       {space_name}       ]");
        let space_width = space_label.len() as u16;
        let space_x = start_x + (keyboard_width.saturating_sub(space_width)) / 2;
        let space_rect = Rect::new(space_x, space_y, space_width, 1);
        if rect_contains(space_rect, x, y) {
            return Some(SPACE);
        }
    }
    None
}

fn shift_at_full_position(inner: Rect, model: &KeyboardModel, x: u16, y: u16) -> bool {
    let Some(geometry) = geometry_for_mode(inner, model, KeyboardRenderMode::Full) else {
        return false;
    };
    let key_width = geometry.key_width;
    let offsets = &geometry.row_offsets;
    let start_x = inner.x + geometry.start_inset;
    let shift_row_y = inner.y + 3;
    if y != shift_row_y {
        return false;
    }

    let left_shift = Rect::new(start_x, shift_row_y, 6, 1);
    if rect_contains(left_shift, x, y) {
        return true;
    }
    if model.rows.len() <= 3 {
        return false;
    }
    let offset = offsets[3];
    let row_end_x = start_x + offset + model.rows[3].len() as u16 * key_width;
    let right_shift = Rect::new(row_end_x, shift_row_y, 6, 1);
    rect_contains(right_shift, x, y)
}

fn key_at_full_fallback_position(
    inner: Rect,
    model: &KeyboardModel,
    x: u16,
    y: u16,
) -> Option<char> {
    let geometry = geometry_for_mode(inner, model, KeyboardRenderMode::FullFallback)?;
    let letter_rows = model.letter_rows();
    let key_width = geometry.key_width;
    let offsets = &geometry.row_offsets;
    let start_x = inner.x + geometry.start_inset;

    for (row_idx, row) in letter_rows.iter().enumerate() {
        let row_y = inner.y + row_idx as u16;
        if y != row_y {
            continue;
        }
        let offset = offsets.get(row_idx).copied().unwrap_or(0);
        for (col_idx, key) in row.iter().enumerate() {
            let key_x = start_x + offset + col_idx as u16 * key_width;
            let key_rect = Rect::new(key_x, row_y, key_width, 1);
            if rect_contains(key_rect, x, y) {
                return Some(key.base);
            }
        }
    }
    None
}

fn shift_at_full_fallback_position(_inner: Rect, _model: &KeyboardModel, _x: u16, _y: u16) -> bool {
    false
}

#[cfg(test)]
fn geometry_cache_len() -> usize {
    geometry_cache()
        .lock()
        .expect("keyboard geometry cache poisoned")
        .len()
}

#[cfg(test)]
fn geometry_cache_clear() {
    geometry_cache()
        .lock()
        .expect("keyboard geometry cache poisoned")
        .clear();
}

#[cfg(test)]
fn geometry_cache_matching_entries(
    layout_key: &str,
    mode: KeyboardRenderMode,
    width: u16,
    height: u16,
) -> usize {
    geometry_cache()
        .lock()
        .expect("keyboard geometry cache poisoned")
        .keys()
        .filter(|k| {
            k.layout_key == layout_key && k.mode == mode && k.width == width && k.height == height
        })
        .count()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Mutex, OnceLock};

    fn cache_test_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    fn assert_roundtrip_for_render_mode(
        model: &KeyboardModel,
        area: Rect,
        compact: bool,
        mode: KeyboardRenderMode,
    ) {
        let inner = Block::bordered().inner(area);
        let geometry =
            geometry_for_mode(inner, model, mode).expect("expected geometry for test render mode");
        let rows = rows_for_mode(model, mode);

        for (row_idx, row) in rows.iter().enumerate() {
            let row_y = inner.y + row_idx as u16;
            let offset = geometry.row_offsets.get(row_idx).copied().unwrap_or(0);
            for (col_idx, key) in row.iter().enumerate() {
                let key_x =
                    inner.x + geometry.start_inset + offset + col_idx as u16 * geometry.key_width;
                let hit_x = key_x;
                let hit_y = row_y;
                let hit = KeyboardDiagram::key_at_position(area, model, compact, hit_x, hit_y);
                assert_eq!(
                    hit,
                    Some(key.base),
                    "round-trip hit-test mismatch for layout={}, mode={mode:?}, row={row_idx}, col={col_idx}, key={}",
                    model.layout_key,
                    key.base
                );
            }
        }
    }

    #[test]
    fn geometry_cache_reuses_entries_for_same_layout_mode_and_viewport() {
        let _guard = cache_test_lock()
            .lock()
            .expect("cache test lock should not be poisoned");
        geometry_cache_clear();

        let model = KeyboardModel::from_key("qwerty").expect("qwerty model must exist");
        let area = Rect::new(0, 0, 100, 10);
        let inner = Block::bordered().inner(area);
        let mode = render_mode_for(inner, false);

        let _ = KeyboardDiagram::key_at_position(area, &model, false, 10, 2);
        assert_eq!(
            geometry_cache_matching_entries(model.layout_key, mode, inner.width, inner.height),
            1
        );

        for _ in 0..50 {
            let _ = KeyboardDiagram::key_at_position(area, &model, false, 12, 2);
            let _ = KeyboardDiagram::shift_at_position(area, &model, false, 5, 3);
        }

        assert_eq!(
            geometry_cache_matching_entries(model.layout_key, mode, inner.width, inner.height),
            1,
            "expected exactly one cached geometry entry for repeated same key"
        );
    }

    #[test]
    fn geometry_cache_distinguishes_layout_and_viewport_keys() {
        let _guard = cache_test_lock()
            .lock()
            .expect("cache test lock should not be poisoned");
        geometry_cache_clear();

        let qwerty = KeyboardModel::from_key("qwerty").expect("qwerty model must exist");
        let azerty = KeyboardModel::from_key("fr_azerty").expect("fr_azerty model must exist");

        let _ = KeyboardDiagram::key_at_position(Rect::new(0, 0, 100, 10), &qwerty, false, 8, 2);
        let after_first = geometry_cache_len();
        assert!(after_first >= 1);

        let _ = KeyboardDiagram::key_at_position(Rect::new(0, 0, 120, 10), &qwerty, false, 8, 2);
        let _ = KeyboardDiagram::key_at_position(Rect::new(0, 0, 100, 10), &azerty, false, 8, 2);

        assert!(
            geometry_cache_len() >= after_first + 2,
            "expected separate cached geometry entries for viewport/layout changes"
        );
    }

    #[test]
    fn geometry_cache_is_bounded() {
        let _guard = cache_test_lock()
            .lock()
            .expect("cache test lock should not be poisoned");
        geometry_cache_clear();

        let model = KeyboardModel::from_key("qwerty").expect("qwerty model must exist");
        for i in 0..(MAX_GEOMETRY_CACHE_ENTRIES as u16 + 10) {
            let width = 90 + i;
            let area = Rect::new(0, 0, width, 10);
            let _ = KeyboardDiagram::key_at_position(area, &model, false, 10, 2);
        }

        assert!(
            geometry_cache_len() <= MAX_GEOMETRY_CACHE_ENTRIES,
            "geometry cache exceeded bounded capacity"
        );
    }

    #[test]
    fn hit_test_roundtrip_invariants_hold_for_all_layouts() {
        let _guard = cache_test_lock()
            .lock()
            .expect("cache test lock should not be poisoned");
        for &layout_key in KeyboardModel::supported_layout_keys() {
            let model = KeyboardModel::from_key(layout_key).expect("profile should exist");

            // Full render mode.
            assert_roundtrip_for_render_mode(
                &model,
                Rect::new(0, 0, 100, 10),
                false,
                KeyboardRenderMode::Full,
            );
            // Full fallback mode (non-compact, but too small for full keyboard).
            assert_roundtrip_for_render_mode(
                &model,
                Rect::new(0, 0, 60, 8),
                false,
                KeyboardRenderMode::FullFallback,
            );
            // Compact mode.
            assert_roundtrip_for_render_mode(
                &model,
                Rect::new(0, 0, 60, 8),
                true,
                KeyboardRenderMode::Compact,
            );
        }
    }
}
