//! Randomized on-screen keyboard for the passphrase modal.
//!
//! # Why this exists
//!
//! Even with [`crate::injection_monitor`] catching synthetic
//! `SendInput` keystrokes, an attacker that combines screen scraping
//! with synthetic mouse events can still drive a normal egui
//! `TextEdit`. A randomized on-screen keypad raises the cost: the
//! attacker has to (a) screenshot the modal each frame, (b) OCR the
//! key positions, (c) plan a click sequence, and (d) send each
//! click via `mouse_event` — at which point [`crate::injection_monitor`]
//! catches the injected mouse events and trips the refusal-to-submit
//! guard. The two defenses compose.
//!
//! # When this is shown
//!
//! When the loaded `SecurityConfig::effective_input_method()` is
//! `VirtualKeyboard`. That happens automatically under Hardened /
//! Lockdown tiers, and on X11 sessions when `x11_auto_upgrade` is on
//! (which it is by default).
//!
//! # Layout
//!
//! 26 lowercase letters + 10 digits, shuffled per-instance into a
//! 6×6 grid. A Shift toggle flips the letter row to uppercase
//! (positions stay shuffled — Shift doesn't reveal the layout, just
//! changes the case rendered on each key).
//!
//! Backspace and "show typed length" controls live next to the grid;
//! the modal's existing Cancel / Submit buttons remain.

use eframe::egui;
use rand::seq::SliceRandom;

const COLS: usize = 10;
const ROWS: usize = 7;
const TOTAL: usize = COLS * ROWS;
const KEY_SIZE: f32 = 32.0; // Slightly smaller to fit 10 cols
const KEY_SPACING: f32 = 4.0;

/// Randomized layout owned by one [`crate::PassphraseModal`].
/// Re-shuffled on construction; the same instance keeps the same
/// layout until the modal is dismissed.
pub struct VirtualKeyboard {
    /// 70-character layout — `chars[r * COLS + c]` is the key shown
    /// at row `r`, column `c`.
    chars: [char; TOTAL],
    /// Whether the Shift toggle is engaged. When true, letter keys
    /// render their uppercase form and emit uppercase characters.
    shift: bool,
}

/// Result of one render frame — what the user clicked, if anything.
pub enum KeyPress {
    /// A character key was pressed. The caller appends to the
    /// passphrase string.
    Char(char),
    /// The Backspace key was pressed. Caller pops one character.
    Backspace,
}

impl VirtualKeyboard {
    /// Build a new keypad with a freshly randomized layout.
    #[must_use]
    pub fn new() -> Self {
        // 26 letters + 10 digits + 33 symbols/space + 1 pad = 70
        let symbols = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ £";
        let mut alphabet: Vec<char> = ('a'..='z')
            .chain('0'..='9')
            .chain(symbols.chars())
            .collect();
        debug_assert_eq!(alphabet.len(), TOTAL);
        let mut rng = rand::thread_rng();
        alphabet.shuffle(&mut rng);
        let mut chars = ['?'; TOTAL];
        for (i, c) in alphabet.into_iter().enumerate() {
            chars[i] = c;
        }
        Self {
            chars,
            shift: false,
        }
    }

    /// Render the keypad. Returns the key the user pressed this
    /// frame, if any. The caller is responsible for mutating its
    /// passphrase string in response.
    pub fn render(&mut self, ui: &mut egui::Ui) -> Option<KeyPress> {
        let mut press: Option<KeyPress> = None;

        ui.vertical(|ui| {
            // Grid of letter / digit keys.
            for row in 0..ROWS {
                ui.horizontal(|ui| {
                    for col in 0..COLS {
                        let idx = row * COLS + col;
                        let raw = self.chars[idx];
                        let display = if self.shift && raw.is_ascii_alphabetic() {
                            raw.to_ascii_uppercase()
                        } else {
                            raw
                        };
                        let label = format!("{display}");
                        let btn = ui.add(
                            egui::Button::new(egui::RichText::new(&label).size(15.0).monospace())
                                .min_size(egui::vec2(KEY_SIZE, KEY_SIZE))
                                .corner_radius(6.0),
                        );
                        if btn.clicked() && press.is_none() {
                            press = Some(KeyPress::Char(display));
                        }
                        ui.add_space(KEY_SPACING / 2.0);
                    }
                });
                ui.add_space(KEY_SPACING / 2.0);
            }

            ui.add_space(8.0);
            ui.horizontal(|ui| {
                let shift_label = if self.shift { "Shift ON" } else { "Shift" };
                let shift_btn = ui.add(
                    egui::Button::new(egui::RichText::new(shift_label).size(13.0))
                        .min_size(egui::vec2(KEY_SIZE * 2.0, KEY_SIZE * 0.8))
                        .corner_radius(6.0)
                        .fill(if self.shift {
                            egui::Color32::from_rgb(0x3b, 0x82, 0xf6)
                        } else {
                            ui.visuals().widgets.inactive.bg_fill
                        }),
                );
                if shift_btn.clicked() {
                    self.shift = !self.shift;
                }
                ui.add_space(KEY_SPACING);
                let bksp = ui.add(
                    egui::Button::new(egui::RichText::new("⌫ Backspace").size(13.0))
                        .min_size(egui::vec2(KEY_SIZE * 3.0, KEY_SIZE * 0.8))
                        .corner_radius(6.0),
                );
                if bksp.clicked() && press.is_none() {
                    press = Some(KeyPress::Backspace);
                }
            });
        });

        press
    }
}

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

    #[test]
    fn layout_contains_every_letter_and_digit_exactly_once() {
        let kb = VirtualKeyboard::new();
        let set: HashSet<char> = kb.chars.iter().copied().collect();
        let symbols = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ £";
        let expected: HashSet<char> = ('a'..='z')
            .chain('0'..='9')
            .chain(symbols.chars())
            .collect();
        assert_eq!(set, expected, "layout must be exactly a-z + 0-9 + symbols");
        assert_eq!(kb.chars.len(), 70);
    }

    #[test]
    fn two_instances_almost_always_differ() {
        // Probabilistic check: 70! permutations — collisions vanish
        // beyond rounding error. Run a few instances; not all should
        // be identical.
        let layouts: Vec<[char; TOTAL]> = (0..10).map(|_| VirtualKeyboard::new().chars).collect();
        let unique: HashSet<_> = layouts
            .iter()
            .map(|l| l.iter().collect::<String>())
            .collect();
        assert!(
            unique.len() > 1,
            "10 random layouts must not all be the same — RNG is broken"
        );
    }
}
