use ratatui::{
    layout::{Constraint, Layout, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap},
    Frame,
};
use tsafe_core::profile::get_default_profile;

use crate::app::{App, EditFocus, LoginFocus, Screen};

// ── Colour palette ────────────────────────────────────────────────────────────
// C_ACCENT  — structural borders, brand chrome
// C_FOCUS   — active input field border (keeps C_WARN free for real warnings)
// C_SELECT  — list-row highlight background (visible on dark terminals)
// C_WARN    — expiry warnings, caution notices only
// C_ERR     — errors
// C_DIM     — hints, inactive text
// C_OK      — success, profile active, revealed secret values
const C_ACCENT: Color = Color::Cyan;
const C_FOCUS: Color = Color::LightCyan;
#[allow(dead_code)] // reserved for expiry / caution notices
const C_WARN: Color = Color::Yellow;
const C_ERR: Color = Color::Red;
const C_DIM: Color = Color::DarkGray;
const C_OK: Color = Color::Green;
const C_SELECT: Color = Color::Rgb(30, 60, 90);

/// Resolved colour palette for a given [`crate::app::Theme`].
pub struct ThemeColors {
    pub accent: Color,
    pub focus: Color,
    pub warn: Color,
    pub err: Color,
    pub dim: Color,
    pub ok: Color,
    pub select: Color,
}

pub fn theme_colors(theme: crate::app::Theme) -> ThemeColors {
    use crate::app::Theme;
    match theme {
        Theme::Dark => ThemeColors {
            accent: Color::Cyan,
            focus: Color::LightCyan,
            warn: Color::Yellow,
            err: Color::Red,
            dim: Color::DarkGray,
            ok: Color::Green,
            select: Color::Rgb(30, 60, 90),
        },
        Theme::Light => ThemeColors {
            accent: Color::Blue,
            focus: Color::Rgb(0, 80, 160),
            warn: Color::Rgb(180, 130, 0),
            err: Color::Red,
            dim: Color::Gray,
            ok: Color::Rgb(0, 150, 0),
            select: Color::Rgb(180, 210, 240),
        },
        Theme::HighContrast => ThemeColors {
            accent: Color::White,
            focus: Color::Yellow,
            warn: Color::Yellow,
            err: Color::LightRed,
            dim: Color::Gray,
            ok: Color::LightGreen,
            select: Color::Rgb(0, 0, 160),
        },
    }
}

/// Top-level render dispatcher.
pub fn render(f: &mut Frame, app: &mut App) {
    match &app.screen.clone() {
        Screen::Login => render_login(f, app),
        Screen::Dashboard => render_dashboard(f, app),
        Screen::EditSecret { is_new } => {
            render_dashboard(f, app);
            render_edit_modal(f, app, *is_new);
        }
        Screen::ConfirmDelete => {
            render_dashboard(f, app);
            render_confirm_delete(f, app);
        }
        Screen::RotatePassword => {
            render_dashboard(f, app);
            render_rotate_modal(f, app);
        }
        Screen::SnapshotRestore => {
            render_dashboard(f, app);
            render_snapshot_restore(f, app);
        }
        Screen::AuditLog => render_audit_log(f, app),
        Screen::NewProfile { step } => render_new_profile(f, app, *step),
        Screen::History { .. } => render_history_screen(f, app),
        Screen::MoveSecret { source } => {
            render_dashboard(f, app);
            render_move_modal(f, app, source);
        }
        Screen::NsBulk { copy, from } => {
            render_dashboard(f, app);
            render_ns_bulk_modal(f, app, *copy, from);
        }
        Screen::Quitting => {}
    }

    // Help overlay sits on top of everything — rendered last so it always wins.
    if app.help_visible {
        render_help_overlay(f, app);
    }
}

// ── Login screen ─────────────────────────────────────────────────────────────
fn render_login(f: &mut Frame, app: &App) {
    let area = f.area();
    let block = Block::default()
        .title(" tsafe — Login ")
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);
    f.render_widget(block, area);

    let inner = inner_area(area, 2);
    let chunks = Layout::vertical([
        Constraint::Length(3), // profile selector
        Constraint::Length(1), // spacer
        Constraint::Length(3), // password input
        Constraint::Length(1), // error/hint
        Constraint::Min(0),    // padding
    ])
    .split(inner);

    // Profile selector
    let default_profile = get_default_profile();
    let profile_str = if app.profiles.is_empty() {
        "(no profiles — press n to create one)".to_string()
    } else {
        let selected_profile = app
            .profiles
            .get(app.profile_cursor)
            .map(|s| s.as_str())
            .unwrap_or("");
        let default_marker = if selected_profile.eq_ignore_ascii_case(&default_profile) {
            " *"
        } else {
            ""
        };
        format!(
            "  {}{}  [{}/{}]",
            selected_profile,
            default_marker,
            app.profile_cursor + 1,
            app.profiles.len(),
        )
    };
    let profile_border = if app.login_focus == LoginFocus::Profile {
        Style::default().fg(C_FOCUS)
    } else {
        Style::default().fg(C_ACCENT)
    };
    let profile_widget = Paragraph::new(profile_str).block(
        Block::default()
            .title(" Profile (Tab to focus, then ↑↓) ")
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(profile_border),
    );
    f.render_widget(profile_widget, chunks[0]);

    // Password input
    let pw_display = "●".repeat(app.password_buf.len());
    let pw_border = if app.login_focus == LoginFocus::Password {
        Style::default().fg(C_FOCUS)
    } else {
        Style::default().fg(C_ACCENT)
    };
    let pw_widget = Paragraph::new(pw_display).block(
        Block::default()
            .title(" Master Password ")
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(pw_border),
    );
    f.render_widget(pw_widget, chunks[2]);

    // Error / hint line
    let hint = if let Some(err) = &app.login_error {
        Line::from(Span::styled(err.as_str(), Style::default().fg(C_ERR)))
    } else if app.profiles.is_empty() {
        Line::from(Span::styled(
            "n  create profile  ·  Ctrl-C  quit",
            Style::default().fg(C_DIM),
        ))
    } else if app.login_focus == LoginFocus::Profile {
        Line::from(Span::styled(
            "↑↓ change profile  ·  n new  ·  ? help  ·  q quit (empty password)  ·  Tab → master password",
            Style::default().fg(C_DIM),
        ))
    } else {
        Line::from(Span::styled(
            "Empty + Enter → Touch ID / keyring (if configured)  ·  Tab → profile row (↑↓ n ? q)  ·  5m idle exits  ·  Ctrl-C quit",
            Style::default().fg(C_DIM),
        ))
    };
    f.render_widget(Paragraph::new(hint), chunks[3]);
}

// ── Main dashboard ────────────────────────────────────────────────────────────
fn render_dashboard(f: &mut Frame, app: &mut App) {
    let area = f.area();

    // Top bar + bottom status
    let outer = Layout::vertical([
        Constraint::Length(1), // title bar
        Constraint::Min(0),    // body
        Constraint::Length(1), // status bar
    ])
    .split(area);

    render_title_bar(f, app, outer[0]);
    render_status_bar(f, app, outer[2]);

    // Body: profile panel (left) | secret panel (right)
    let body = outer[1];
    let cols =
        Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(75)]).split(body);

    render_profile_panel(f, app, cols[0]);
    render_secret_panel(f, app, cols[1]);
}

fn render_title_bar(f: &mut Frame, app: &App, area: Rect) {
    let c = theme_colors(app.theme);
    let cols = Layout::horizontal([Constraint::Min(0), Constraint::Length(26)]).split(area);

    let profile = app.active_profile.as_deref().unwrap_or("(none)");
    let prefix = format!(" tsafe · {profile}  ");
    // Each hint pair: "key action  " — measure chars so we stop before overflow.
    let available = cols[0].width as usize;
    let mut used = prefix.chars().count();

    let hints = app.screen.footer_hints();
    let mut included = 0usize;
    for (key_label, action) in hints {
        let pair_len = key_label.chars().count() + 1 + action.chars().count() + 2; // "k act  "
        if used + pair_len > available.saturating_sub(6) {
            // reserve 6 for " · ?" overflow indicator
            break;
        }
        used += pair_len;
        included += 1;
    }
    let truncated = included < hints.len();

    let mut spans: Vec<Span> = vec![
        Span::styled(
            " tsafe ",
            Style::default().fg(c.accent).add_modifier(Modifier::BOLD),
        ),
        Span::styled(" · ", Style::default().fg(c.dim)),
        Span::styled(
            profile.to_string(),
            Style::default().fg(c.ok).add_modifier(Modifier::BOLD),
        ),
        Span::styled("  ", Style::default()),
    ];
    for (key_label, action) in &hints[..included] {
        spans.push(Span::styled(
            key_label.to_string(),
            Style::default().fg(c.focus),
        ));
        spans.push(Span::styled(
            format!(" {action}  "),
            Style::default().fg(c.dim),
        ));
    }
    if truncated {
        spans.push(Span::styled("· ", Style::default().fg(c.dim)));
        spans.push(Span::styled("?", Style::default().fg(c.focus)));
        spans.push(Span::styled(" more", Style::default().fg(c.dim)));
    }
    f.render_widget(Paragraph::new(Line::from(spans)), cols[0]);

    // Version badge — theme warn colour when an update is waiting, dim otherwise.
    let current = tsafe_core::update::display_version();
    let badge = if let Some(ref latest) = app.update_available {
        Line::from(vec![Span::styled(
            format!(" v{current} → v{latest} ↑ "),
            Style::default().fg(c.warn).add_modifier(Modifier::BOLD),
        )])
    } else {
        Line::from(vec![Span::styled(
            format!(" v{current} "),
            Style::default().fg(c.dim),
        )])
    };
    f.render_widget(
        Paragraph::new(badge).alignment(ratatui::layout::Alignment::Right),
        cols[1],
    );
}

fn render_status_bar(f: &mut Frame, app: &App, area: Rect) {
    let text = app.status_message.as_deref().unwrap_or("");
    let style = if text.starts_with("Error") || text.starts_with("✗") {
        Style::default().fg(C_ERR)
    } else {
        Style::default().fg(C_DIM)
    };
    f.render_widget(Paragraph::new(Span::styled(text, style)), area);
}

fn render_profile_panel(f: &mut Frame, app: &App, area: Rect) {
    let default_profile = get_default_profile();
    let items: Vec<ListItem> = app
        .profiles
        .iter()
        .map(|p| {
            let style = if Some(p.as_str()) == app.active_profile.as_deref() {
                Style::default().fg(C_OK).add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(Color::White)
            };
            let default_marker = if p.eq_ignore_ascii_case(&default_profile) {
                " *"
            } else {
                ""
            };
            ListItem::new(format!(" {p}{default_marker}")).style(style)
        })
        .collect();

    let mut state = ListState::default();
    state.select(Some(app.profile_cursor));

    let list = List::new(items)
        .block(
            Block::default()
                .title(" Profiles ")
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded)
                .border_style(Style::default().fg(C_DIM)),
        )
        .highlight_style(Style::default().bg(C_SELECT).add_modifier(Modifier::BOLD))
        .highlight_symbol("▶ ");

    f.render_stateful_widget(list, area, &mut state);
}

fn render_secret_panel(f: &mut Frame, app: &mut App, area: Rect) {
    // Split: optional search bar on top
    let chunks = if app.search_mode {
        Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(area)
    } else {
        Layout::vertical([Constraint::Length(0), Constraint::Min(0)]).split(area)
    };

    if app.search_mode {
        let search_bar = Paragraph::new(format!(" /{}", app.search_query)).block(
            Block::default()
                .title(" Search ")
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded)
                .border_style(Style::default().fg(C_FOCUS)),
        );
        f.render_widget(search_bar, chunks[0]);
    }

    let entries = app.visible_entries();
    let items: Vec<ListItem> = entries
        .iter()
        .map(|entry| match entry {
            crate::app::ListEntry::Namespace {
                name,
                count,
                collapsed,
            } => {
                let icon = if *collapsed { "▶" } else { "▼" };
                ListItem::new(Line::from(vec![Span::styled(
                    format!(" {icon} {name}  ({count})"),
                    Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD),
                )]))
            }
            crate::app::ListEntry::Key(k) => {
                // Strip the `ns/` prefix for display when the key is namespaced
                let display = if let Some(pos) = k.find('/') {
                    &k[pos + 1..]
                } else {
                    k.as_str()
                };
                let indent = if k.contains('/') { "   " } else { "" };
                let value_hint = if let Some(r) = app.revealed.get(k) {
                    Span::styled(format!("  {}", &*r.value), Style::default().fg(C_OK))
                } else {
                    Span::styled("  ••••••", Style::default().fg(C_DIM))
                };
                ListItem::new(Line::from(vec![
                    Span::styled(
                        format!("{indent}{display}"),
                        Style::default().fg(Color::White),
                    ),
                    value_hint,
                ]))
            }
        })
        .collect();

    let count = app.filtered_keys().len();
    let mut state = ListState::default();
    if !entries.is_empty() {
        state.select(Some(app.secret_cursor.min(entries.len() - 1)));
    }

    let list = List::new(items)
        .block(
            Block::default()
                .title(format!(" Secrets ({count}) "))
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded)
                .border_style(Style::default().fg(C_ACCENT)),
        )
        .highlight_style(Style::default().bg(C_SELECT).add_modifier(Modifier::BOLD))
        .highlight_symbol("▶ ");

    f.render_stateful_widget(list, chunks[1], &mut state);
}

// ── Edit modal ────────────────────────────────────────────────────────────────
fn render_edit_modal(f: &mut Frame, app: &App, is_new: bool) {
    let title = if is_new {
        " Add Secret "
    } else {
        " Edit Secret "
    };
    let area = centered_rect(60, 40, f.area());
    f.render_widget(Clear, area);

    let block = Block::default()
        .title(title)
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);
    f.render_widget(block, area);

    let inner = inner_area(area, 2);
    let chunks = Layout::vertical([
        Constraint::Length(3),
        Constraint::Length(1),
        Constraint::Length(3),
        Constraint::Length(1),
        Constraint::Min(0),
    ])
    .split(inner);

    let key_style = if app.edit_focus == EditFocus::Key {
        Style::default().fg(C_FOCUS)
    } else {
        Style::default().fg(C_DIM)
    };
    let val_style = if app.edit_focus == EditFocus::Value {
        Style::default().fg(C_FOCUS)
    } else {
        Style::default().fg(C_DIM)
    };

    let key_widget = Paragraph::new(app.edit_key.as_str()).block(
        Block::default()
            .title(" KEY ")
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(key_style),
    );
    f.render_widget(key_widget, chunks[0]);

    // VALUE: mask unless focused
    let val_display = if app.edit_focus == EditFocus::Value {
        app.edit_value.clone()
    } else {
        "●".repeat(app.edit_value.len())
    };
    let val_widget = Paragraph::new(val_display).block(
        Block::default()
            .title(" VALUE (masked when not focused) ")
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(val_style),
    );
    f.render_widget(val_widget, chunks[2]);

    let hint = if let Some(err) = &app.edit_error {
        Line::from(Span::styled(err.as_str(), Style::default().fg(C_ERR)))
    } else {
        Line::from(Span::styled(
            "Tab  switch field  ·  Enter  save  ·  Esc  cancel",
            Style::default().fg(C_DIM),
        ))
    };
    f.render_widget(Paragraph::new(hint), chunks[3]);
}

// ── Confirm-delete modal ──────────────────────────────────────────────────────
fn render_confirm_delete(f: &mut Frame, app: &App) {
    let key = app.selected_key().unwrap_or_default();
    // Wider modal so long namespaced keys aren't truncated; 5-line height.
    let area = centered_rect(70, 28, f.area());
    f.render_widget(Clear, area);

    // Render the key with namespace dim + leaf bold-red so a fat-fingered
    // delete on the wrong row is obvious. ISC-1, ISC-2.
    let key_line = if let Some(slash) = key.rfind('/') {
        let (ns, leaf) = key.split_at(slash);
        Line::from(vec![
            Span::raw("  Delete  "),
            Span::styled(format!("{ns}/"), Style::default().fg(C_DIM)),
            Span::styled(
                leaf.trim_start_matches('/'),
                Style::default()
                    .fg(C_ERR)
                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
            ),
            Span::raw(" ?"),
        ])
    } else {
        Line::from(vec![
            Span::raw("  Delete  "),
            Span::styled(
                key.as_str(),
                Style::default()
                    .fg(C_ERR)
                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
            ),
            Span::raw(" ?"),
        ])
    };

    let text = vec![
        Line::from(""),
        key_line,
        Line::from(""),
        Line::from(Span::styled(
            "  y/Enter confirm · n/Esc cancel",
            Style::default().fg(C_DIM),
        )),
    ];
    let block = Block::default()
        .title(" Confirm Delete ")
        .title_style(Style::default().fg(C_ERR).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(C_ERR));
    f.render_widget(Paragraph::new(text).block(block), area);
}
// ── Move / rename secret modal ───────────────────────────────────────────────
fn render_move_modal(f: &mut Frame, app: &App, source: &str) {
    let area = centered_rect(60, 30, f.area());
    f.render_widget(Clear, area);

    let dest = &app.mv_dest_buf;
    let cursor_col = dest.chars().count() as u16;

    let input_line = Line::from(vec![
        Span::styled("  To:  ", Style::default().fg(C_DIM)),
        Span::styled(dest.as_str(), Style::default().fg(Color::White)),
    ]);

    let mut lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled("  From: ", Style::default().fg(C_DIM)),
            Span::styled(
                source,
                Style::default().fg(C_OK).add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(""),
        input_line,
        Line::from(""),
        Line::from(Span::styled(
            "  Enter confirm  ·  Esc cancel",
            Style::default().fg(C_DIM),
        )),
    ];

    if let Some(err) = &app.mv_error {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("  {err}"),
            Style::default().fg(C_ERR),
        )));
    }

    let block = Block::default()
        .title(" Move / Rename Secret ")
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(C_ACCENT));

    f.render_widget(Paragraph::new(lines).block(block), area);

    // Place the cursor after the typed text inside the input field.
    // Row 4 of the content (0-indexed) is the "To:" line; +1 for the block border.
    let input_row = area.y + 1 + 4; // border + blank + from-line + blank + to-line
    let input_col = area.x + 1 + 7 + cursor_col; // border + "  To:  "
    if input_row < area.y + area.height && input_col < area.x + area.width {
        f.set_cursor_position((input_col, input_row));
    }
}

// ── Copy / move whole namespace modal ─────────────────────────────────────────
fn render_ns_bulk_modal(f: &mut Frame, app: &App, copy: bool, from: &str) {
    let area = centered_rect(62, 34, f.area());
    f.render_widget(Clear, area);

    let title = if copy {
        " Copy Namespace "
    } else {
        " Move Namespace "
    };
    let verb = if copy {
        "Copy all keys under"
    } else {
        "Move all keys under"
    };

    let dest = &app.mv_dest_buf;
    let cursor_col = dest.chars().count() as u16;

    let input_line = Line::from(vec![
        Span::styled("  To namespace:  ", Style::default().fg(C_DIM)),
        Span::styled(dest.as_str(), Style::default().fg(Color::White)),
    ]);

    let mut lines = vec![
        Line::from(""),
        Line::from(vec![
            Span::styled(format!("  {verb} "), Style::default().fg(C_DIM)),
            Span::styled(
                format!("{from}/"),
                Style::default().fg(C_OK).add_modifier(Modifier::BOLD),
            ),
        ]),
        Line::from(Span::styled(
            "  (single segment, no slash — e.g. staging)",
            Style::default().fg(C_DIM),
        )),
        Line::from(""),
        input_line,
        Line::from(""),
        Line::from(Span::styled(
            "  Enter confirm  ·  Esc cancel",
            Style::default().fg(C_DIM),
        )),
        Line::from(Span::styled(
            "  If a destination key exists, use CLI: tsafe ns copy|move … --force",
            Style::default().fg(C_DIM),
        )),
    ];

    if let Some(err) = &app.mv_error {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("  {err}"),
            Style::default().fg(C_ERR),
        )));
    }

    let block = Block::default()
        .title(title)
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(C_ACCENT));

    f.render_widget(Paragraph::new(lines).block(block), area);

    let input_row = area.y + 1 + 5; // inner row index 5: blank, verb, hint, blank, input
    let to_ns_prefix = "  To namespace:  ";
    let input_col = area.x + 1 + to_ns_prefix.chars().count() as u16 + cursor_col;
    if input_row < area.y + area.height && input_col < area.x + area.width {
        f.set_cursor_position((input_col, input_row));
    }
}
// ── Rotate-password modal ─────────────────────────────────────────────────────
fn render_rotate_modal(f: &mut Frame, app: &App) {
    let area = centered_rect(60, 45, f.area());
    f.render_widget(Clear, area);

    let block = Block::default()
        .title(" Rotate Master Password ")
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);
    f.render_widget(block, area);

    let inner = inner_area(area, 2);
    let chunks = Layout::vertical([
        Constraint::Length(3),
        Constraint::Length(1),
        Constraint::Length(3),
        Constraint::Length(1),
        Constraint::Min(0),
    ])
    .split(inner);

    let new1_style = if app.rotate_step == 0 {
        Style::default().fg(C_FOCUS)
    } else {
        Style::default().fg(C_DIM)
    };
    let new2_style = if app.rotate_step == 1 {
        Style::default().fg(C_FOCUS)
    } else {
        Style::default().fg(C_DIM)
    };

    f.render_widget(
        Paragraph::new("●".repeat(app.rotate_new1.len())).block(
            Block::default()
                .title(" New Password ")
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded)
                .border_style(new1_style),
        ),
        chunks[0],
    );
    f.render_widget(
        Paragraph::new("●".repeat(app.rotate_new2.len())).block(
            Block::default()
                .title(" Confirm New Password ")
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded)
                .border_style(new2_style),
        ),
        chunks[2],
    );

    let hint = if let Some(err) = &app.rotate_error {
        Line::from(Span::styled(err.as_str(), Style::default().fg(C_ERR)))
    } else {
        let msg = if app.rotate_step == 0 {
            "Enter  set password  ·  Esc  cancel"
        } else {
            "Enter  confirm & rotate  ·  Esc  cancel"
        };
        Line::from(Span::styled(msg, Style::default().fg(C_DIM)))
    };
    f.render_widget(Paragraph::new(hint), chunks[3]);
}

// ── Snapshot-restore picker modal ─────────────────────────────────────────────
fn render_snapshot_restore(f: &mut Frame, app: &App) {
    let area = centered_rect(70, 60, f.area());
    f.render_widget(Clear, area);

    let items: Vec<ListItem> = app
        .snapshot_paths
        .iter()
        .enumerate()
        .map(|(i, p)| {
            let name = p
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("(unknown)");
            let style = if i == app.snapshot_cursor {
                Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(Color::White)
            };
            ListItem::new(format!(" {name}")).style(style)
        })
        .collect();

    let mut list_state = ratatui::widgets::ListState::default();
    list_state.select(Some(app.snapshot_cursor));

    let list = List::new(items)
        .block(
            Block::default()
                .title(" Restore Snapshot (↑↓  Enter=restore  Esc=cancel) ")
                .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded),
        )
        .highlight_style(Style::default().bg(C_SELECT).add_modifier(Modifier::BOLD))
        .highlight_symbol("▶ ");

    f.render_stateful_widget(list, area, &mut list_state);
}

// ── Audit log screen ──────────────────────────────────────────────────────────
fn render_audit_log(f: &mut Frame, app: &App) {
    let area = f.area();
    let lines: Vec<Line> = app
        .audit_lines
        .iter()
        .skip(app.audit_scroll)
        .take(area.height.saturating_sub(4) as usize)
        .map(|l| Line::from(Span::raw(l.as_str())))
        .collect();

    let block = Block::default()
        .title(format!(
            " Audit Log — {} (↑↓ scroll · q back) ",
            app.active_profile.as_deref().unwrap_or("")
        ))
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);

    f.render_widget(
        Paragraph::new(lines)
            .block(block)
            .wrap(Wrap { trim: false }),
        area,
    );
}

// ── History viewer ──────────────────────────────────────────────────────────
fn render_history_screen(f: &mut Frame, app: &App) {
    let area = f.area();
    let c = theme_colors(app.theme);

    let key_name = match &app.screen {
        Screen::History { key } => key.as_str(),
        _ => "",
    };

    let visible_rows = area.height.saturating_sub(4) as usize;
    let mut lines: Vec<Line> = Vec::with_capacity(visible_rows + 1);

    // Column header
    lines.push(Line::from(Span::styled(
        "  Version         Timestamp (UTC)          ",
        Style::default().fg(c.dim).add_modifier(Modifier::BOLD),
    )));

    for (version, ts) in app
        .history_entries
        .iter()
        .skip(app.history_scroll)
        .take(visible_rows)
    {
        let ver_label = if *version == 0 {
            "  v0 (current)  ".to_string()
        } else {
            format!("  v{version:<14}  ")
        };
        let ts_str = ts.format("%Y-%m-%d %H:%M:%S").to_string();
        lines.push(Line::from(vec![
            Span::styled(ver_label, Style::default().fg(c.ok)),
            Span::styled(ts_str, Style::default().fg(Color::White)),
            Span::styled(
                "  \u{2022}\u{2022}\u{2022}\u{2022}\u{2022}\u{2022}",
                Style::default().fg(c.dim),
            ),
        ]));
    }

    let total = app.history_entries.len();
    let block = Block::default()
        .title(format!(
            " History — {key_name}  ({total} version{}  \u{2191}\u{2193}/j/k scroll  q back) ",
            if total == 1 { "" } else { "s" }
        ))
        .title_style(Style::default().fg(c.accent).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);
    f.render_widget(
        Paragraph::new(lines)
            .block(block)
            .wrap(Wrap { trim: false }),
        area,
    );
}

// ── Help overlay ──────────────────────────────────────────────────────────────
fn render_help_overlay(f: &mut Frame, app: &App) {
    let area = centered_rect(72, 88, f.area());
    f.render_widget(Clear, area);

    // Build context-aware header based on the current screen.
    let context = match &app.screen {
        Screen::Login => "Login",
        Screen::Dashboard => "Dashboard",
        Screen::EditSecret { is_new } if *is_new => "Add Secret",
        Screen::EditSecret { .. } => "Edit Secret",
        Screen::ConfirmDelete => "Confirm Delete",
        Screen::RotatePassword => "Rotate Password",
        Screen::SnapshotRestore => "Snapshot Restore",
        Screen::AuditLog => "Audit Log",
        Screen::NewProfile { .. } => "New Profile",
        Screen::History { .. } => "Secret History",
        Screen::MoveSecret { .. } => "Move / Rename Secret",
        Screen::NsBulk { copy: true, .. } => "Copy Namespace",
        Screen::NsBulk { copy: false, .. } => "Move Namespace",
        Screen::Quitting => "Quitting",
    };

    let block = Block::default()
        .title(format!(" tsafe Help — {context} "))
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(Style::default().fg(C_ACCENT));
    f.render_widget(block, area);

    let inner = inner_area(area, 2);

    fn key(k: &'static str) -> Span<'static> {
        Span::styled(
            format!("{k:>10}"),
            Style::default()
                .fg(Color::LightCyan)
                .add_modifier(Modifier::BOLD),
        )
    }
    fn desc(d: &'static str) -> Span<'static> {
        Span::styled(format!("  {d}"), Style::default().fg(Color::White))
    }
    fn sep() -> Line<'static> {
        Line::from(Span::styled(
            "  ──────────────────────────────────────",
            Style::default().fg(Color::DarkGray),
        ))
    }
    fn hdr(h: &'static str) -> Line<'static> {
        Line::from(Span::styled(
            format!("  {h}"),
            Style::default()
                .fg(Color::Cyan)
                .add_modifier(Modifier::BOLD),
        ))
    }

    let lines: Vec<Line> = vec![
        hdr("Global"),
        Line::from(vec![key("?"), desc("Show / hide this help overlay")]),
        Line::from(vec![key("Ctrl-C"), desc("Quit immediately")]),
        Line::from(Span::styled(
            "  Also: exits after 5 minutes with no input (idle timeout).",
            Style::default().fg(Color::DarkGray),
        )),
        Line::from(vec![
            key("Esc"),
            desc("Dismiss this overlay / cancel current action"),
        ]),
        Line::from(Span::raw("")),
        sep(),
        hdr("Login screen"),
        Line::from(vec![
            key("Tab"),
            desc("Switch focus: profile row ⇄ master password"),
        ]),
        Line::from(vec![
            key("↑ ↓"),
            desc("Change profile (only when the profile row is focused)"),
        ]),
        Line::from(vec![key("Enter"), desc("Unlock selected profile")]),
        Line::from(vec![
            key("n / ? / q"),
            desc("Commands on profile row; in password field they type literally"),
        ]),
        Line::from(vec![
            key("Ctrl-C"),
            desc("Quit anytime; 5m idle also exits"),
        ]),
        Line::from(Span::raw("")),
        sep(),
        hdr("Dashboard"),
        Line::from(vec![key("↑ ↓ j k"), desc("Navigate secrets")]),
        Line::from(vec![
            key("Space"),
            desc("On namespace header: expand / collapse group"),
        ]),
        Line::from(vec![key("n"), desc("Add new secret")]),
        Line::from(vec![key("e / Enter"), desc("Edit selected secret")]),
        Line::from(vec![
            key("d"),
            desc("Delete selected secret (with confirmation)"),
        ]),
        Line::from(vec![
            key("y"),
            desc("Copy selected secret value to the system clipboard"),
        ]),
        Line::from(vec![key("r"), desc("Reveal / hide selected secret value")]),
        Line::from(vec![key("R"), desc("Rotate master password")]),
        Line::from(vec![key("S"), desc("Open snapshot restore picker")]),
        Line::from(vec![key("a"), desc("View audit log")]),
        Line::from(vec![
            key("p"),
            desc("Switch to a different profile (re-auth)"),
        ]),
        Line::from(vec![key("/"), desc("Enter search mode")]),
        Line::from(vec![
            key("h"),
            desc("View version history for selected secret"),
        ]),
        Line::from(vec![
            key("c"),
            desc("With namespace header selected: copy all keys to another namespace"),
        ]),
        Line::from(vec![
            key("m"),
            desc("With key: move one secret — with namespace header: move all keys under it"),
        ]),
        Line::from(vec![
            key("T"),
            desc("Cycle colour theme (Dark → Light → High Contrast)"),
        ]),
        Line::from(vec![key("q"), desc("Quit")]),
        Line::from(Span::raw("")),
        sep(),
        hdr("Add / Edit Secret modal"),
        Line::from(vec![
            key("Tab"),
            desc("Switch focus between KEY and VALUE fields"),
        ]),
        Line::from(vec![key("Enter"), desc("Save secret")]),
        Line::from(vec![key("Esc"), desc("Cancel — return to Dashboard")]),
        Line::from(Span::raw("")),
        sep(),
        hdr("Copy / Move namespace (modal)"),
        Line::from(vec![
            key("Enter"),
            desc("Run copy or move for all keys under the source prefix"),
        ]),
        Line::from(vec![key("Esc"), desc("Cancel")]),
        Line::from(Span::raw("")),
        sep(),
        hdr("Rotate Password modal"),
        Line::from(vec![
            key("Enter"),
            desc("Step 1: set new password  /  Step 2: confirm & rotate"),
        ]),
        Line::from(vec![key("Esc"), desc("Cancel")]),
        Line::from(Span::raw("")),
        sep(),
        hdr("Snapshot / Audit Log"),
        Line::from(vec![key("↑ ↓ j k"), desc("Scroll / navigate")]),
        Line::from(vec![key("Enter"), desc("Restore selected snapshot")]),
        Line::from(vec![key("q / Esc"), desc("Return to Dashboard")]),
        Line::from(Span::raw("")),
        Line::from(Span::styled(
            "  Press ? or Esc to close this overlay",
            Style::default().fg(Color::DarkGray),
        )),
    ];

    f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}

// ── New-profile wizard modal ──────────────────────────────────────────────────
fn render_new_profile(f: &mut Frame, app: &App, step: u8) {
    let area = centered_rect(60, 50, f.area());
    f.render_widget(Clear, area);

    let block = Block::default()
        .title(" Create New Profile ")
        .title_style(Style::default().fg(C_ACCENT).add_modifier(Modifier::BOLD))
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded);
    f.render_widget(block, area);

    let inner = inner_area(area, 2);
    let chunks = Layout::vertical([
        Constraint::Length(1), // step indicator
        Constraint::Length(1), // spacer
        Constraint::Length(3), // input field
        Constraint::Length(1), // error/hint
        Constraint::Min(0),
    ])
    .split(inner);

    let step_text = match step {
        0 => "Step 1/3 — profile name",
        1 => "Step 2/3 — master password",
        _ => "Step 3/3 — confirm password",
    };
    f.render_widget(
        Paragraph::new(Span::styled(step_text, Style::default().fg(C_DIM))),
        chunks[0],
    );

    let (field_title, field_content) = match step {
        0 => (" Profile Name ", app.new_profile_name.clone()),
        1 => (" Master Password ", "●".repeat(app.new_profile_pw1.len())),
        _ => (" Confirm Password ", "●".repeat(app.new_profile_pw2.len())),
    };
    let input_widget = Paragraph::new(field_content).block(
        Block::default()
            .title(field_title)
            .borders(Borders::ALL)
            .border_type(BorderType::Rounded)
            .border_style(Style::default().fg(C_FOCUS)),
    );
    f.render_widget(input_widget, chunks[2]);

    let hint = if let Some(err) = &app.new_profile_error {
        Line::from(Span::styled(err.as_str(), Style::default().fg(C_ERR)))
    } else {
        let msg = if step == 0 {
            "Enter  next  ·  Esc  back to login"
        } else {
            "Enter  next  ·  Esc  back"
        };
        Line::from(Span::styled(msg, Style::default().fg(C_DIM)))
    };
    f.render_widget(Paragraph::new(hint), chunks[3]);
}

// ── Layout helpers ────────────────────────────────────────────────────────────

fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
    let layout = Layout::vertical([
        Constraint::Percentage((100 - percent_y) / 2),
        Constraint::Percentage(percent_y),
        Constraint::Percentage((100 - percent_y) / 2),
    ])
    .split(r);
    Layout::horizontal([
        Constraint::Percentage((100 - percent_x) / 2),
        Constraint::Percentage(percent_x),
        Constraint::Percentage((100 - percent_x) / 2),
    ])
    .split(layout[1])[1]
}

fn inner_area(r: Rect, margin: u16) -> Rect {
    Rect {
        x: r.x + margin,
        y: r.y + margin,
        width: r.width.saturating_sub(margin * 2),
        height: r.height.saturating_sub(margin * 2),
    }
}

#[cfg(test)]
mod theme_tests {
    use super::theme_colors;
    use crate::app::{App, Screen, Theme};
    use ratatui::style::Color;
    use ratatui::{backend::TestBackend, buffer::Buffer, Terminal};

    fn buffer_to_plain_string(buf: &Buffer) -> String {
        let area = buf.area;
        let mut out = String::new();
        for y in area.y..area.y + area.height {
            for x in area.x..area.x + area.width {
                out.push_str(buf[(x, y)].symbol());
            }
            out.push('\n');
        }
        out
    }

    #[test]
    fn theme_colors_dark_matches_palette() {
        let t = theme_colors(Theme::Dark);
        assert_eq!(t.accent, Color::Cyan);
        assert_eq!(t.focus, Color::LightCyan);
        assert_eq!(t.err, Color::Red);
        assert_eq!(t.select, Color::Rgb(30, 60, 90));
    }

    #[test]
    fn theme_colors_light_distinct_from_dark() {
        let t = theme_colors(Theme::Light);
        assert_eq!(t.accent, Color::Blue);
        assert_eq!(t.focus, Color::Rgb(0, 80, 160));
        assert_eq!(t.select, Color::Rgb(180, 210, 240));
    }

    #[test]
    fn theme_colors_high_contrast_uses_light_red_for_errors() {
        let t = theme_colors(Theme::HighContrast);
        assert_eq!(t.accent, Color::White);
        assert_eq!(t.err, Color::LightRed);
        assert_eq!(t.select, Color::Rgb(0, 0, 160));
    }

    #[test]
    fn help_overlay_mentions_y_clipboard_shortcut() {
        let mut app = App::new_for_test(vec!["demo".into()], Screen::Dashboard);
        app.active_profile = Some("demo".into());
        app.help_visible = true;
        app.theme = Theme::Dark;

        let backend = TestBackend::new(100, 40);
        let mut terminal = Terminal::new(backend).expect("terminal");
        terminal
            .draw(|f| super::render(f, &mut app))
            .expect("draw help overlay");

        let plain = buffer_to_plain_string(terminal.backend().buffer());
        assert!(
            plain.contains("Copy selected secret value to the system clipboard"),
            "help overlay missing clipboard text:\n{plain}"
        );
        assert!(
            plain.contains("         y  Copy selected secret value to the system clipboard"),
            "help overlay missing y hotkey line:\n{plain}"
        );
    }
}
