//! envseal native desktop — pure-Rust UI with zero webview attack surface.
//!
//! # Why not Tauri/Electron/HTML?
//!
//! For a security tool handling bank-level credentials, the UI itself is an
//! attack surface. WebView-based UIs (Tauri, Electron) embed millions of
//! lines of C++ (WebKitGTK/Chromium) and expose XSS, IPC, and CSP vectors.
//!
//! This app uses egui (immediate-mode Rust UI) rendered via OpenGL. The
//! entire attack surface is Rust code + a thin OpenGL layer. No DOM, no
//! JavaScript, no serialization boundary between UI and security logic.
//!
//! # Threading model
//!
//! egui's main thread renders ~60fps; any blocking IO inside
//! [`eframe::App::update`] freezes the UI. Every vault-touching op
//! therefore runs on a single dedicated [`worker`] thread. The main
//! thread sends [`worker::WorkerCmd`] over a channel and polls
//! [`worker::WorkerResult`]s on each frame, dispatching them into the
//! per-view state. All passphrase entry happens via an in-window
//! [`PassphraseModal`] — no PowerShell / zenity / osascript subprocess
//! is ever spawned from the GUI.

mod confirm_dialog;
mod injection_monitor;
mod passphrase_modal;
mod theme;
mod views;
mod virtual_keyboard;
mod worker;

use confirm_dialog::{ConfirmAction, ConfirmDialog};
use eframe::egui;
use envseal::security_config::{InputMethod, SecurityConfig, SecurityTier};
use injection_monitor::InjectionMonitor;
use passphrase_modal::{ModalAction, PassphraseModal};
use std::collections::HashMap;
use worker::{ReqId, UnlockOutcome, WorkerCmd, WorkerHandle, WorkerResult};
use zeroize::{Zeroize, Zeroizing};

#[derive(Debug)]
struct StartupError(String);

impl std::fmt::Display for StartupError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::error::Error for StartupError {}

/// Decode the bundled PNG logo into an `egui::IconData` (RGBA8). Returns
/// `None` if the embedded asset can't be decoded — the window then falls
/// back to the OS default icon rather than failing to start.
fn load_window_icon() -> Option<egui::IconData> {
    const LOGO_PNG: &[u8] = include_bytes!("../assets/logo.png");
    let img = image::load_from_memory(LOGO_PNG).ok()?.into_rgba8();
    let (width, height) = img.dimensions();
    Some(egui::IconData {
        rgba: img.into_raw(),
        width,
        height,
    })
}

/// Parse `--tab=<name>` / `--tab <name>` from argv. Used by `envseal demo`
/// to deep-link into a specific dashboard tab so the demo can land
/// directly on Audit without the user clicking through the sidebar.
fn parse_initial_tab() -> Option<Tab> {
    let mut args = std::env::args().skip(1);
    while let Some(arg) = args.next() {
        let value = if let Some(rest) = arg.strip_prefix("--tab=") {
            rest.to_string()
        } else if arg == "--tab" {
            args.next()?
        } else {
            continue;
        };
        return match value.to_ascii_lowercase().as_str() {
            "dashboard" => Some(Tab::Dashboard),
            "secrets" => Some(Tab::Secrets),
            "store" => Some(Tab::Store),
            "policy" => Some(Tab::Policy),
            "settings" => Some(Tab::Settings),
            "audit" => Some(Tab::Audit),
            "health" => Some(Tab::Health),
            "scanner" | "scan" => Some(Tab::Scanner),
            "import" => Some(Tab::Import),
            "export" => Some(Tab::Export),
            "doctor" => Some(Tab::Doctor),
            _ => None,
        };
    }
    None
}

fn main() -> eframe::Result<()> {
    let initial_tab = parse_initial_tab();
    let mut viewport = egui::ViewportBuilder::default()
        .with_title("envseal")
        .with_inner_size([900.0, 680.0])
        .with_min_inner_size([640.0, 480.0]);
    if let Some(icon) = load_window_icon() {
        viewport = viewport.with_icon(icon);
    }
    let options = eframe::NativeOptions {
        viewport,
        ..Default::default()
    };

    eframe::run_native(
        "envseal",
        options,
        Box::new(move |cc| {
            theme::apply_theme(&cc.egui_ctx, false);
            let mut app =
                App::new().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
            if let Some(tab) = initial_tab {
                app.active_tab = tab;
            }
            Ok(Box::new(app) as Box<dyn eframe::App>)
        }),
    )
}

// ═══════════════════════════════════════════════════════════════
//  Application state
// ═══════════════════════════════════════════════════════════════

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Tab {
    Dashboard,
    Secrets,
    Store,
    Policy,
    Settings,
    Audit,
    Health,
    Scanner,
    Import,
    Export,
    Doctor,
}

impl Tab {
    const ALL: &[Tab] = &[
        Tab::Dashboard,
        Tab::Secrets,
        Tab::Store,
        Tab::Import,
        Tab::Export,
        Tab::Policy,
        Tab::Settings,
        Tab::Audit,
        Tab::Health,
        Tab::Scanner,
        Tab::Doctor,
    ];

    fn label(self) -> &'static str {
        match self {
            Tab::Dashboard => "Dashboard",
            Tab::Secrets => "Secrets",
            Tab::Store => "Store",
            Tab::Policy => "Policy",
            Tab::Settings => "Settings",
            Tab::Audit => "Audit",
            Tab::Health => "Health",
            Tab::Scanner => "Scanner",
            Tab::Import => "Import",
            Tab::Export => "Export",
            Tab::Doctor => "Doctor",
        }
    }
}

/// Toast notification shown briefly in the top-right corner.
struct Toast {
    message: String,
    is_error: bool,
    expires_at: std::time::Instant,
}

/// Whether the worker currently holds an unlocked vault.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VaultStatus {
    /// We haven't yet asked the worker; assume locked until told otherwise.
    Locked,
    /// Worker holds an unlocked [`envseal::vault::Vault`].
    Unlocked,
}

/// What the GUI was trying to do when the worker told it the vault
/// needs unlocking. Re-issued as a fresh [`WorkerCmd`] after a
/// successful [`WorkerCmd::UnlockVault`].
///
/// The payload owns its data so we don't have to keep the original
/// view state around — `Zeroizing<String>` for the secret value
/// scrubs its memory on drop after we re-send it.
enum PendingAction {
    StoreSecret {
        name: String,
        value: Zeroizing<String>,
        force: bool,
    },
    PeekSecret {
        name: String,
    },
    RevokeSecret {
        name: String,
    },
    LoadPolicy,
    RevokePolicyBinary {
        binary: String,
    },
    LoadSecurityConfig,
    SaveSecurityConfig {
        config: Box<SecurityConfig>,
    },
    /// Reserved for the (currently inert) Health tab — holds the
    /// max-age threshold the user picked. Wired through the worker
    /// already; the view just hasn't landed yet.
    #[allow(dead_code)]
    LoadHealth {
        max_days: Option<u64>,
    },
}

impl PendingAction {
    fn into_cmd(self, id: ReqId) -> WorkerCmd {
        match self {
            Self::StoreSecret { name, value, force } => WorkerCmd::StoreSecret {
                id,
                name,
                value,
                force,
            },
            Self::PeekSecret { name } => WorkerCmd::PeekSecret { id, name },
            Self::RevokeSecret { name } => WorkerCmd::RevokeSecret { id, name },
            Self::LoadPolicy => WorkerCmd::LoadPolicy { id },
            Self::RevokePolicyBinary { binary } => WorkerCmd::RevokePolicyBinary { id, binary },
            Self::LoadSecurityConfig => WorkerCmd::LoadSecurityConfig { id },
            Self::SaveSecurityConfig { config } => WorkerCmd::SaveSecurityConfig { id, config },
            Self::LoadHealth { max_days } => WorkerCmd::LoadHealth { id, max_days },
        }
    }
}

/// Top-level application state.
struct App {
    active_tab: Tab,
    dark_mode: bool,

    worker: WorkerHandle,
    /// Whether we've handed the worker our `egui::Context` for
    /// repaint requests. Set once on the first frame.
    worker_attached: bool,
    /// System-wide injected-input observer. Polled while the
    /// passphrase modal is open so a `SendInput`-driven attack
    /// can't supply the passphrase.
    injection_monitor: InjectionMonitor,

    vault_status: VaultStatus,
    /// Pending actions keyed by their request ID — used to look up
    /// what to retry when the worker replies `NeedsPassphrase`.
    in_flight: HashMap<ReqId, PendingAction>,

    // View states.
    store: views::store::StoreState,
    secrets: views::secrets::SecretsState,
    policy: views::policy::PolicyState,
    settings: views::settings::SettingsState,
    audit: views::audit::AuditState,
    dashboard: views::dashboard::DashboardState,
    health: views::health::HealthState,
    scanner: views::scanner::ScannerState,
    import_view: views::import::ImportState,
    export_view: views::export::ExportState,
    doctor: views::doctor::DoctorState,

    toasts: Vec<Toast>,
    confirm: Option<ConfirmDialog>,
    passphrase_modal: Option<PassphraseModal<PendingAction>>,
}

impl App {
    fn new() -> Result<Self, StartupError> {
        let mut app = Self {
            active_tab: Tab::Dashboard,
            dark_mode: false,
            worker: worker::spawn().map_err(StartupError)?,
            worker_attached: false,
            injection_monitor: InjectionMonitor::spawn(),
            vault_status: VaultStatus::Locked,
            in_flight: HashMap::new(),
            store: views::store::StoreState::default(),
            secrets: views::secrets::SecretsState::default(),
            policy: views::policy::PolicyState::default(),
            settings: views::settings::SettingsState::default(),
            audit: views::audit::AuditState::default(),
            dashboard: views::dashboard::DashboardState::default(),
            health: views::health::HealthState::default(),
            scanner: views::scanner::ScannerState::default(),
            import_view: views::import::ImportState::default(),
            export_view: views::export::ExportState::default(),
            doctor: views::doctor::DoctorState::default(),
            toasts: Vec::new(),
            confirm: None,
            passphrase_modal: None,
        };
        // Filesystem-only refreshes — these don't need an unlocked vault.
        app.refresh_secrets();
        app.refresh_audit();
        Ok(app)
    }

    // ─── command issuance helpers ────────────────────────────────

    fn next_id(&mut self) -> ReqId {
        self.worker.new_req_id()
    }

    fn send(&self, cmd: WorkerCmd) {
        self.worker.send(cmd);
    }

    fn refresh_secrets(&mut self) {
        let id = self.next_id();
        self.secrets.start_refresh(id);
        self.send(WorkerCmd::ListSecrets { id });
    }

    fn refresh_audit(&mut self) {
        let id = self.next_id();
        let filter = self.audit.as_filter();
        self.audit.start_refresh(id);
        self.send(WorkerCmd::LoadAuditLog { id, n: 100, filter });
    }

    fn refresh_health(&mut self) {
        let id = self.next_id();
        let max_days = Some(self.health.max_age_days as u64);
        self.health.start_refresh(id);
        self.in_flight
            .insert(id, PendingAction::LoadHealth { max_days });
        self.send(WorkerCmd::LoadHealth { id, max_days });
    }

    fn refresh_policy(&mut self) {
        let id = self.next_id();
        self.policy.start_refresh(id);
        self.in_flight.insert(id, PendingAction::LoadPolicy);
        self.send(WorkerCmd::LoadPolicy { id });
    }

    fn refresh_settings(&mut self) {
        let id = self.next_id();
        self.settings.start_refresh(id);
        self.in_flight.insert(id, PendingAction::LoadSecurityConfig);
        self.send(WorkerCmd::LoadSecurityConfig { id });
    }

    fn submit_store(&mut self, name: String, value: Zeroizing<String>, force: bool) {
        let id = self.next_id();
        self.store.start_submit(id);
        self.in_flight.insert(
            id,
            PendingAction::StoreSecret {
                name: name.clone(),
                value: Zeroizing::new(value.as_str().to_owned()),
                force,
            },
        );
        self.send(WorkerCmd::StoreSecret {
            id,
            name,
            value,
            force,
        });
    }

    fn submit_peek(&mut self, name: String) {
        let id = self.next_id();
        self.in_flight
            .insert(id, PendingAction::PeekSecret { name: name.clone() });
        self.send(WorkerCmd::PeekSecret { id, name });
    }

    fn submit_revoke(&mut self, name: String) {
        let id = self.next_id();
        self.in_flight
            .insert(id, PendingAction::RevokeSecret { name: name.clone() });
        self.send(WorkerCmd::RevokeSecret { id, name });
    }

    fn submit_revoke_policy_binary(&mut self, binary: String) {
        let id = self.next_id();
        self.in_flight.insert(
            id,
            PendingAction::RevokePolicyBinary {
                binary: binary.clone(),
            },
        );
        self.send(WorkerCmd::RevokePolicyBinary { id, binary });
    }

    fn submit_save_settings(&mut self, config: SecurityConfig) {
        let id = self.next_id();
        let boxed = Box::new(config);
        self.settings.start_save(id);
        self.in_flight.insert(
            id,
            PendingAction::SaveSecurityConfig {
                config: boxed.clone(),
            },
        );
        self.send(WorkerCmd::SaveSecurityConfig { id, config: boxed });
    }

    fn submit_unlock(&mut self, passphrase: Zeroizing<String>) {
        let id = self.next_id();
        if let Some(modal) = &mut self.passphrase_modal {
            modal.submitting = true;
        }
        self.send(WorkerCmd::UnlockVault { id, passphrase });
    }

    fn lock_vault(&mut self) {
        self.send(WorkerCmd::LockVault);
    }

    fn toast(&mut self, message: impl Into<String>, is_error: bool) {
        self.toasts.push(Toast {
            message: message.into(),
            is_error,
            expires_at: std::time::Instant::now() + std::time::Duration::from_secs(4),
        });
    }

    // ─── result dispatch ─────────────────────────────────────────

    fn dispatch(&mut self, result: WorkerResult) {
        match result {
            WorkerResult::SecretsListed { id, result } => match result {
                Ok(views) => self.secrets.apply_listed(id, views),
                Err(e) => {
                    self.secrets.apply_error(id, e.clone());
                    self.toast(format!("List failed: {e}"), true);
                }
            },
            WorkerResult::SecretPeeked { id, result } => {
                self.in_flight.remove(&id);
                match result {
                    Ok(p) => self.toast(format!("{} : {}", p.name, p.redacted), false),
                    Err(e) => self.toast(format!("Peek failed: {e}"), true),
                }
            }
            WorkerResult::SecretStored { id, result } => {
                self.in_flight.remove(&id);
                self.store.finish_submit();
                match result {
                    Ok((name, signals)) => {
                        self.toast(format!("Stored '{name}'"), false);
                        // Entropy / quality signals, if any. Surfaced
                        // separately so a low-entropy secret still
                        // shows the success toast but the user also
                        // sees the concern.
                        for sig in signals {
                            self.toast(
                                format!(
                                    "warning: {}: {} — {}",
                                    sig.label, sig.detail, sig.mitigation
                                ),
                                true,
                            );
                        }
                        self.store.clear();
                        self.refresh_secrets();
                    }
                    Err(e) => self.toast(format!("Store failed: {e}"), true),
                }
            }
            WorkerResult::SecretRevoked { id, result } => {
                self.in_flight.remove(&id);
                match result {
                    Ok(name) => {
                        self.toast(format!("Revoked '{name}'"), false);
                        self.refresh_secrets();
                    }
                    Err(e) => self.toast(format!("Revoke failed: {e}"), true),
                }
            }
            WorkerResult::PolicyLoaded { id, result } => {
                self.in_flight.remove(&id);
                match result {
                    Ok(rules) => self.policy.apply_loaded(id, rules),
                    Err(e) => {
                        self.policy.apply_error(id, e.clone());
                        self.toast(format!("Policy load failed: {e}"), true);
                    }
                }
            }
            WorkerResult::PolicyRuleRevoked { id, result } => {
                self.in_flight.remove(&id);
                match result {
                    Ok(_) => {
                        self.toast("Binary access revoked", false);
                        self.refresh_policy();
                    }
                    Err(e) => self.toast(format!("Revoke failed: {e}"), true),
                }
            }
            WorkerResult::SecurityConfigLoaded { id, result } => {
                self.in_flight.remove(&id);
                match result {
                    Ok(config) => self.settings.apply_loaded(id, *config),
                    Err(e) => {
                        self.settings.apply_error(id, e.clone());
                        self.toast(format!("Settings load failed: {e}"), true);
                    }
                }
            }
            WorkerResult::SecurityConfigSaved { id, result } => {
                self.in_flight.remove(&id);
                self.settings.finish_save();
                match result {
                    Ok(()) => self.toast("Settings saved", false),
                    Err(e) => self.toast(format!("Save failed: {e}"), true),
                }
            }
            WorkerResult::AuditLogLoaded {
                id,
                entries,
                dropped_lines,
            } => {
                self.audit.apply_loaded(id, entries, dropped_lines);
            }
            WorkerResult::HealthLoaded { id, result } => {
                self.in_flight.remove(&id);
                match result {
                    Ok(report) => self.health.apply_loaded(id, report),
                    Err(e) => {
                        self.health.apply_error(id);
                        self.toast(format!("Health check failed: {e}"), true);
                    }
                }
            }
            WorkerResult::VaultUnlocked { result, .. } => match result {
                Ok(outcome) => {
                    self.vault_status = VaultStatus::Unlocked;
                    let pending = self
                        .passphrase_modal
                        .take()
                        .map(|mut m| std::mem::take(&mut m.pending))
                        .unwrap_or_default();
                    match outcome {
                        UnlockOutcome::Initialized => {
                            self.toast("Vault initialized", false);
                        }
                        UnlockOutcome::Unlocked => {
                            self.toast("Vault unlocked", false);
                        }
                    }
                    // Re-issue every queued action in arrival order.
                    // Each one re-enters the in_flight map under a new
                    // request id so subsequent error paths can still
                    // correlate them.
                    for action in pending {
                        let id = self.next_id();
                        let cmd = action.into_cmd(id);
                        self.send(cmd);
                    }
                    // Auto-load the data the user is currently looking
                    // at — without this, sitting on Settings/Audit/etc.
                    // and unlocking would leave the panel showing
                    // "Settings will load after the vault is unlocked"
                    // forever, until the user clicked away and back.
                    match self.active_tab {
                        Tab::Secrets => self.refresh_secrets(),
                        Tab::Policy => self.refresh_policy(),
                        Tab::Settings => self.refresh_settings(),
                        Tab::Audit => self.refresh_audit(),
                        Tab::Health => self.refresh_health(),
                        Tab::Dashboard
                        | Tab::Store
                        | Tab::Scanner
                        | Tab::Import
                        | Tab::Export
                        | Tab::Doctor => {}
                    }
                }
                Err(e) => {
                    if let Some(modal) = &mut self.passphrase_modal {
                        modal.submitting = false;
                        modal.passphrase.zeroize();
                        modal.confirm.zeroize();
                        modal.error = Some(e);
                    }
                }
            },
            WorkerResult::VaultLocked => {
                self.vault_status = VaultStatus::Locked;
                self.toast("Vault locked", false);
            }
            WorkerResult::NeedsPassphrase {
                is_new_vault,
                original_id,
            } => {
                let pending = self.in_flight.remove(&original_id);
                match (&mut self.passphrase_modal, pending) {
                    // Modal already up — append to its queue so the
                    // user types their passphrase once and ALL pending
                    // actions retry after unlock. Avoids the race
                    // where a second click silently drops the first.
                    (Some(modal), Some(action)) => modal.pending.push(action),
                    // Modal already up but the cmd didn't have a
                    // PendingAction recorded (e.g. we forgot to
                    // register it) — modal stays unchanged.
                    (Some(_), None) => {}
                    // No modal yet — open one. Snapshot the injection
                    // counter NOW so any synthetic event arriving
                    // during entry exceeds this baseline and trips
                    // the modal's refusal-to-submit guard. is_new_vault
                    // is captured at the moment of the FIRST bounce,
                    // which is correct: it flips false the instant
                    // master.key first appears on disk.
                    //
                    // Read the system defaults to decide whether to
                    // render the on-screen keypad. We can't ask the
                    // (locked) vault yet — system defaults plus
                    // /etc/envseal/system.toml are the right source.
                    (None, initial) => {
                        let baseline = self.injection_monitor.count();
                        let defaults = envseal::security_config::load_system_defaults();
                        let use_kb =
                            defaults.effective_input_method() == InputMethod::VirtualKeyboard;
                        self.passphrase_modal = Some(PassphraseModal::new(
                            is_new_vault,
                            initial,
                            baseline,
                            use_kb,
                        ));
                    }
                }
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════
//  egui main loop
// ═══════════════════════════════════════════════════════════════

impl eframe::App for App {
    fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        if !self.worker_attached {
            self.worker.attach_context(ctx.clone());
            self.worker_attached = true;
        }

        // Drain worker results before rendering so this frame's UI
        // reflects the freshest state.
        for result in self.worker.drain_results() {
            self.dispatch(result);
        }

        // Expire toasts.
        let now = std::time::Instant::now();
        self.toasts.retain(|t| t.expires_at > now);

        // Repaint while a toast is animating out, otherwise let
        // egui idle. Worker results call ctx.request_repaint() from
        // their own thread on arrival.
        if !self.toasts.is_empty() {
            ctx.request_repaint_after(std::time::Duration::from_millis(100));
        }
    }

    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        let ctx = ui.ctx().clone();

        egui::Panel::left("sidebar")
            .exact_size(176.0)
            .show_inside(ui, |ui| {
                self.render_sidebar(ui);
            });

        egui::CentralPanel::default().show_inside(ui, |ui| {
            self.render_main(ui);
        });

        self.render_toasts(&ctx);
        self.render_passphrase_modal(&ctx);
        self.render_confirm_dialog(&ctx);
    }
}

impl App {
    fn render_sidebar(&mut self, ui: &mut egui::Ui) {
        ui.add_space(14.0);
        ui.horizontal(|ui| {
            ui.add_space(12.0);
            ui.heading("envseal");
        });
        ui.add_space(6.0);
        ui.horizontal(|ui| {
            ui.add_space(16.0);
            let (badge_color, label) = match self.vault_status {
                VaultStatus::Unlocked => (egui::Color32::from_rgb(0x22, 0xc5, 0x5e), "unlocked"),
                VaultStatus::Locked => (egui::Color32::from_rgb(0xf5, 0x9e, 0x0b), "locked"),
            };
            ui.colored_label(badge_color, format!("vault {label}"));
        });

        ui.add_space(14.0);
        ui.separator();
        ui.add_space(8.0);

        for &tab in Tab::ALL {
            let selected = self.active_tab == tab;
            let bg = if selected {
                egui::Color32::from_black_alpha(40)
            } else {
                egui::Color32::TRANSPARENT
            };
            let btn = ui.add(
                egui::Button::new(egui::RichText::new(tab.label()).size(13.5))
                    .fill(bg)
                    .frame(true)
                    .min_size(egui::vec2(152.0, 30.0))
                    .corner_radius(6.0),
            );
            if btn.clicked() && self.active_tab != tab {
                self.active_tab = tab;
                match tab {
                    Tab::Secrets => self.refresh_secrets(),
                    Tab::Policy => self.refresh_policy(),
                    Tab::Settings => self.refresh_settings(),
                    Tab::Audit => self.refresh_audit(),
                    Tab::Dashboard
                    | Tab::Store
                    | Tab::Health
                    | Tab::Scanner
                    | Tab::Import
                    | Tab::Export
                    | Tab::Doctor => {}
                }
            }
        }

        ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
            ui.add_space(10.0);
            ui.label(
                egui::RichText::new(concat!("v", env!("CARGO_PKG_VERSION")))
                    .size(11.0)
                    .weak(),
            );
            ui.add_space(4.0);
            if ui
                .small_button(if self.dark_mode {
                    "Light mode"
                } else {
                    "Dark mode"
                })
                .clicked()
            {
                self.dark_mode = !self.dark_mode;
                theme::apply_theme(ui.ctx(), self.dark_mode);
            }
            ui.add_space(4.0);
            if self.vault_status == VaultStatus::Unlocked && ui.small_button("Lock vault").clicked()
            {
                self.lock_vault();
            }
            ui.add_space(8.0);
        });
    }

    fn render_main(&mut self, ui: &mut egui::Ui) {
        match self.active_tab {
            Tab::Dashboard => {
                let action = views::dashboard::render(
                    ui,
                    &mut self.dashboard,
                    self.vault_status == VaultStatus::Unlocked,
                );
                if let Some(views::dashboard::DashboardAction::GoToTab(tab)) = action {
                    self.active_tab = tab;
                    match tab {
                        Tab::Secrets => self.refresh_secrets(),
                        Tab::Policy => self.refresh_policy(),
                        Tab::Settings => self.refresh_settings(),
                        Tab::Audit => self.refresh_audit(),
                        _ => {}
                    }
                }
            }
            Tab::Secrets => {
                let action = views::secrets::render(ui, &mut self.secrets);
                if let Some(name) = action.peek {
                    self.submit_peek(name);
                }
                if let Some(name) = action.revoke {
                    self.confirm = Some(ConfirmDialog::new(ConfirmAction::RevokeSecret { name }));
                }
                if action.refresh {
                    self.refresh_secrets();
                }
            }
            Tab::Store => {
                if let Some(submit) = views::store::render(ui, &mut self.store) {
                    self.submit_store(submit.name, submit.value, submit.force);
                }
            }
            Tab::Policy => {
                let action = views::policy::render(ui, &mut self.policy);
                if let Some(binary) = action.revoke_binary {
                    self.confirm = Some(ConfirmDialog::new(ConfirmAction::RevokePolicyBinary {
                        binary,
                    }));
                }
                if action.refresh {
                    self.refresh_policy();
                }
            }
            Tab::Settings => {
                let action = views::settings::render(ui, &mut self.settings);
                if let Some(config) = action.save {
                    if let Some(ref old) = self.settings.config {
                        let required = old.validate_tier_change(config.tier);
                        if required != SecurityTier::Standard {
                            let tier_name = match required {
                                SecurityTier::Standard => "standard",
                                SecurityTier::Hardened => "hardened",
                                SecurityTier::Lockdown => "lockdown",
                            };
                            self.toast(
                                format!("Tier downgrade requires {tier_name} authentication"),
                                true,
                            );
                        } else {
                            self.submit_save_settings(config);
                        }
                    } else {
                        self.submit_save_settings(config);
                    }
                }
                if action.refresh {
                    self.refresh_settings();
                }
            }
            Tab::Audit => {
                if views::audit::render(ui, &mut self.audit) {
                    self.refresh_audit();
                }
            }
            Tab::Health => {
                if views::health::render(ui, &mut self.health) {
                    self.refresh_health();
                }
            }
            Tab::Scanner => {
                views::scanner::render(ui, &mut self.scanner);
            }
            Tab::Import => {
                views::import::render(ui, &mut self.import_view);
            }
            Tab::Export => {
                views::export::render(ui, &mut self.export_view);
            }
            Tab::Doctor => {
                views::doctor::render(ui, &mut self.doctor);
            }
        }
    }

    fn render_toasts(&self, ctx: &egui::Context) {
        if self.toasts.is_empty() {
            return;
        }
        egui::Area::new(egui::Id::new("toasts"))
            .anchor(egui::Align2::RIGHT_TOP, egui::vec2(-16.0, 48.0))
            .show(ctx, |ui| {
                for toast in &self.toasts {
                    let secs_left = toast
                        .expires_at
                        .saturating_duration_since(std::time::Instant::now())
                        .as_secs_f32();
                    let fade = (secs_left / 4.0).clamp(0.2, 1.0);
                    let color = if toast.is_error {
                        egui::Color32::from_rgb(239, 68, 68)
                    } else {
                        egui::Color32::from_rgb(34, 197, 94)
                    };
                    egui::Frame::new()
                        .fill(color.linear_multiply(0.10 * fade))
                        .stroke(egui::Stroke::new(1.0, color))
                        .corner_radius(8.0)
                        .inner_margin(12.0)
                        .show(ui, |ui| {
                            ui.label(egui::RichText::new(&toast.message).color(color));
                        });
                    ui.add_space(4.0);
                }
            });
    }

    /// Thin dispatch wrapping [`passphrase_modal::render`] — supplies
    /// the per-frame injection-monitor readings and turns the
    /// returned [`ModalAction`] into App state changes (close +
    /// toast, or send `UnlockVault`).
    fn render_passphrase_modal(&mut self, ctx: &egui::Context) {
        let Some(modal) = self.passphrase_modal.as_mut() else {
            return;
        };
        let live_count = self.injection_monitor.count();
        let supported = self.injection_monitor.supported();
        let action = passphrase_modal::render(ctx, modal, live_count, supported);
        match action {
            ModalAction::Cancel => {
                self.passphrase_modal = None;
                self.toast("Vault unlock cancelled", true);
            }
            ModalAction::Submit(p) => self.submit_unlock(p),
            ModalAction::None => {}
        }
    }

    /// Thin dispatch wrapping [`confirm_dialog::render`] — translates
    /// the returned [`confirm_dialog::Outcome`] into the appropriate
    /// `submit_*` worker command.
    fn render_confirm_dialog(&mut self, ctx: &egui::Context) {
        match confirm_dialog::render(ctx, &mut self.confirm) {
            confirm_dialog::Outcome::Confirmed(action) => match action {
                ConfirmAction::RevokeSecret { name } => self.submit_revoke(name),
                ConfirmAction::RevokePolicyBinary { binary } => {
                    self.submit_revoke_policy_binary(binary);
                }
            },
            confirm_dialog::Outcome::Cancelled | confirm_dialog::Outcome::None => {}
        }
    }
}
