#![forbid(unsafe_code)]
#![deny(clippy::disallowed_types)]

mod action;
mod editor;
mod folder_picker;
mod launcher;
#[cfg(target_os = "macos")]
mod native_menu;
mod navigation;
mod palette;

use std::{
    num::NonZeroUsize,
    path::PathBuf,
    sync::{
        Arc, Mutex,
        atomic::{AtomicBool, Ordering},
    },
    thread,
    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};

use editor::{EntryEditor, FieldNavigation};
use iced::{
    Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Size,
    Subscription, Task, Theme, event, keyboard, mouse, time, touch,
    widget::{
        button, canvas, column, container, mouse_area, pane_grid, progress_bar, row, scrollable,
        text, text_editor, text_input, tooltip,
    },
    window,
};
use ironstorage::{
    authentication::{
        AuthenticationClock, AuthenticationError, AuthenticationHandle, AuthenticationSession,
        NativeAuthenticationHandle, NativeAuthenticationSession,
    },
    command::{CopyRequest, FindRequest, GrepRequest, InitRequest, MoveRequest, RemoveRequest},
    crypto::KeyInfo,
    desktop::{
        DesktopError, DesktopErrorKind, DesktopGitOutcome, DesktopGitRequest, DesktopGitResult,
        DesktopMutationRequest, DesktopOtpCode, DesktopOtpMetadata, DesktopOtpMutation,
        DesktopOtpUri, DesktopStorage,
    },
    document::{
        DocumentError, EntryDocument, EntryField, EntryFieldDiagnostic, EntryFieldId,
        EntryFieldKind, EntrySensitivity,
    },
    generate::GeneratorConfig,
    git::{
        GitConflict, GitConflictChoice, GitConflictResolution, GitError, GitOperationControl,
        GitProgressPhase, GitSnapshot,
    },
    kdbx::{KdbxImportMode, KdbxImportRequest},
    mutation::{MutationAction, MutationOutcome, MutationSelection},
    otp::{OtpCodeValidity, OtpKind},
    presentation::{ClipboardWait, NativeClipboardManager, QrMatrix},
    read::{FindResults, GrepResults, TreeModel, TreeNodeId},
    repository::SecretBytes,
    secret_store::SecretStoreBackend,
    write::{OverwriteDecision, WriteOutcome},
};
use navigation::{NavigationIntent, NavigationKey, NavigationTree};
use palette::CommandPalette;
use zeroize::Zeroizing;

use action::{ActionContext, MenuGroup, UiAction};
#[cfg(target_os = "macos")]
use native_menu::NativeMenu;

type OpenCompletion = Arc<Mutex<Option<Result<EntryDocument, String>>>>;
type SaveCompletion = Arc<Mutex<Option<(EntryEditor, Result<WriteOutcome, DesktopError>)>>>;
type TreeCompletion = Arc<Mutex<Option<Result<TreeModel, String>>>>;
type CreateCompletion = Arc<Mutex<Option<(SecretBytes, Result<EntryDocument, String>)>>>;
type SearchCompletion = Arc<Mutex<Option<Result<SearchResults, String>>>>;
type GitCompletion = Arc<Mutex<Option<Result<DesktopGitResult, DesktopError>>>>;
type GitProgress = Arc<Mutex<Option<GitProgressPhase>>>;
type OtpCompletion = Arc<Mutex<Option<Result<OtpTaskResult, DesktopError>>>>;
type SecretInputCompletion = Arc<Mutex<Option<Result<Option<SecretBytes>, String>>>>;

#[derive(Clone, Debug, Eq, PartialEq)]
struct RecipientSummary {
    directory: String,
    recipients: usize,
    reencrypted: usize,
}

#[derive(Clone, Debug)]
struct RecipientSuccess {
    storage: DesktopStorage,
    key: KeyInfo,
    summary: RecipientSummary,
}

#[derive(Clone)]
enum Message {
    Action(UiAction),
    FieldAction(EntryFieldId, UiAction),
    ToggleMenu(MenuGroup),
    PaletteQueryChanged(String),
    PaletteCancel,
    PaletteInvoke(UiAction),
    DismissUtility,
    WindowResolved(UiAction, Option<window::Id>),
    FolderPicked(Result<Option<PathBuf>, String>),
    SettingsVaultChanged(String),
    SettingsDefaultKeyChanged(String),
    SettingsTimeoutChanged(String),
    PickSettingsVault,
    SettingsVaultPicked(Result<Option<PathBuf>, String>),
    InstallCommandLinks,
    SaveSettings,
    SettingsFinished {
        generation: u64,
        result: Box<Result<(DesktopStorage, NativeAuthenticationSession, KeyInfo), String>>,
    },
    VaultSwitched {
        generation: u64,
        result: Box<Result<DesktopStorage, String>>,
    },
    RecipientPathChanged(String),
    ToggleRecipient(usize),
    SelectDefaultRecipient(usize),
    ToggleRecipientConfirmation,
    SubmitRecipient,
    RecipientFinished {
        generation: u64,
        result: Box<Result<RecipientSuccess, String>>,
    },
    NewEntryPathChanged(String),
    ToggleNewEntryGeneration,
    NewEntryLengthChanged(String),
    ToggleNewEntrySymbols,
    SubmitNewEntry,
    CreateFinished {
        generation: u64,
        completion: CreateCompletion,
    },
    SearchQueryChanged(String),
    AddSearchTerm,
    RemoveSearchTerm(usize),
    ToggleSearchCase,
    ToggleSearchInvert,
    ToggleSearchLineNumbers,
    ToggleSearchFixedStrings,
    SubmitSearch,
    SearchFinished {
        generation: u64,
        completion: SearchCompletion,
    },
    ActivateSearchResult(TreeNodeId),
    MutationDestinationChanged(String),
    SelectMutationDestination(String),
    ToggleMutationOverwrite,
    ToggleMutationConfirmation,
    SubmitMutation,
    MutationFinished {
        generation: u64,
        result: Box<Result<MutationOutcome, String>>,
    },
    RunGit(DesktopGitRequest),
    ChooseGitConflict(usize, GitConflictChoice),
    ResolveGitConflicts,
    CancelGit,
    GitFinished {
        generation: u64,
        completion: GitCompletion,
    },
    OtpEntryChanged(String),
    OtpUriChanged(Zeroizing<String>),
    ToggleOtpReplace,
    ToggleOtpRemovalConfirmation,
    PickOtpQr,
    OtpQrPicked(SecretInputCompletion),
    SubmitOtpImport,
    SubmitOtpRemoval,
    RunOtpCode(bool),
    RunOtpUri {
        qr: bool,
        copy: bool,
    },
    ConfirmHotp,
    OtpFinished {
        generation: u64,
        completion: OtpCompletion,
    },
    KdbxSourceChanged(String),
    KdbxKeyFileChanged(String),
    KdbxPasswordChanged(Zeroizing<String>),
    PickKdbxSource,
    KdbxSourcePicked(Result<Option<PathBuf>, String>),
    PickKdbxKeyFile,
    KdbxKeyFilePicked(Result<Option<PathBuf>, String>),
    ToggleKdbxQuickAdd,
    ToggleKdbxConfirmation,
    SubmitKdbxImport,
    KdbxFinished {
        generation: u64,
        result: Box<Result<(ironstorage::kdbx::KdbxImportOutcome, TreeModel), String>>,
    },
    #[cfg(target_os = "macos")]
    PollNativeMenu,
    StartupLoaded(Box<Result<(DesktopStorage, NativeAuthenticationSession, KeyInfo), String>>),
    TreeLoaded {
        generation: u64,
        completion: TreeCompletion,
    },
    SidebarActivate(TreeNodeId),
    SidebarContext(TreeNodeId),
    SidebarContextAction(TreeNodeId, UiAction),
    SidebarNavigate(NavigationKey),
    TogglePaneFocus,
    PaneResized(pane_grid::ResizeEvent),
    OpenEntry,
    OpenFinished {
        generation: u64,
        entry: String,
        completion: OpenCompletion,
    },
    AuthenticationFinished {
        generation: u64,
        result: Result<NativeAuthenticationHandle, String>,
    },
    FieldChanged(EntryFieldId, Zeroizing<String>),
    FieldEdited(EntryFieldId, text_editor::Action),
    AddFieldLine(EntryFieldId, usize),
    AddAfter(Option<EntryFieldId>),
    Remove(EntryFieldId),
    MoveUp(EntryFieldId),
    MoveDown(EntryFieldId),
    BeginEdit,
    SelectField(EntryFieldId),
    RequestGenerate(EntryFieldId),
    GenerateLengthChanged(String),
    ToggleGenerateSymbols,
    ToggleGenerateConfirmation,
    SubmitGenerate,
    CancelGenerate,
    Copy(EntryFieldId),
    CopyFinished {
        generation: u64,
        result: Result<String, String>,
    },
    SaveFinished {
        generation: u64,
        completion: SaveCompletion,
    },
    RequestReload,
    RequestClose(window::Id),
    ConfirmSave,
    ConfirmDiscard,
    CancelDiscard,
    ReloadConflict,
    KeepConflictDraft,
    UserActivity,
    Tick,
    Lock,
}

#[derive(Debug)]
enum AuthenticationView {
    Loading,
    Locked,
    Authenticating,
    Unlocked(Duration),
    Unavailable(String),
}

#[derive(Default)]
struct SensitiveUiState {
    clipboard_cancel: Option<Arc<AtomicBool>>,
    clipboard_generation: u64,
    clipboard_deadline: Option<Instant>,
    otp: Option<OtpDisplay>,
    otp_uri: Option<SecretBytes>,
    otp_qr: Option<QrMatrix>,
}

impl SensitiveUiState {
    fn clear(&mut self) {
        self.clipboard_generation = self.clipboard_generation.wrapping_add(1);
        self.cancel_clipboard();
        self.otp = None;
        self.otp_uri = None;
        self.otp_qr = None;
    }

    fn cancel_clipboard(&mut self) {
        if let Some(cancel) = self.clipboard_cancel.take() {
            cancel.store(true, Ordering::Release);
        }
        self.clipboard_deadline = None;
    }

    fn begin_copy(&mut self, timeout: Duration) -> (u64, Arc<AtomicBool>) {
        self.clipboard_generation = self.clipboard_generation.wrapping_add(1);
        self.cancel_clipboard();
        let cancel = Arc::new(AtomicBool::new(false));
        self.clipboard_cancel = Some(Arc::clone(&cancel));
        self.clipboard_deadline = Some(Instant::now() + timeout);
        (self.clipboard_generation, cancel)
    }

    fn finish_copy(&mut self, generation: u64) -> bool {
        if generation != self.clipboard_generation {
            return false;
        }
        self.clipboard_cancel = None;
        self.clipboard_deadline = None;
        true
    }

    fn clipboard_remaining(&self, now: Instant) -> Option<u64> {
        self.clipboard_deadline.map(|deadline| {
            let remaining = deadline.saturating_duration_since(now);
            remaining
                .as_secs()
                .saturating_add(u64::from(remaining.subsec_nanos() != 0))
        })
    }
}

struct App {
    authentication: AuthenticationView,
    storage: Option<DesktopStorage>,
    session: Option<NativeAuthenticationSession>,
    key: Option<KeyInfo>,
    handle: Option<NativeAuthenticationHandle>,
    sensitive: SensitiveUiState,
    git_control: Option<GitOperationControl>,
    git_progress: Option<GitProgress>,
    authentication_generation: u64,
    operation_generation: u64,
    tree_generation: u64,
    vault_generation: u64,
    settings_generation: u64,
    workflow_generation: u64,
    otp_generation: u64,
    otp_pending: bool,
    selection_after_refresh: Option<TreeNodeId>,
    panes: pane_grid::State<PaneKind>,
    pane_focus: PaneFocus,
    navigation: NavigationTree,
    tree_state: TreeState,
    after_authentication: Option<PendingAction>,
    entry_path: String,
    editor: Option<EntryEditor>,
    content_mode: ContentMode,
    saving: bool,
    switching_vault: bool,
    confirmation: Option<PendingAction>,
    after_save: Option<PendingAction>,
    generation_form: Option<GenerateForm>,
    conflict: bool,
    status: String,
    open_menu: Option<MenuGroup>,
    utility: Option<UtilityView>,
    context_target: Option<TreeNodeId>,
    palette: CommandPalette,
    #[cfg(target_os = "macos")]
    native_menu: Option<NativeMenu>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PaneKind {
    Sidebar,
    Content,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PaneFocus {
    Sidebar,
    Content,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ContentMode {
    Viewer,
    Editor,
}

enum UtilityView {
    About,
    Settings(SettingsForm),
    Recipients(RecipientForm),
    NewEntry(NewEntryForm),
    Search(SearchForm),
    Mutation(MutationForm),
    Git(GitForm),
    Otp(OtpForm),
    Kdbx(KdbxForm),
    Help,
}

struct OtpDisplay {
    entry: String,
    code: SecretBytes,
    validity: OtpCodeValidity,
    metadata: DesktopOtpMetadata,
    observed_at: u64,
}

impl OtpDisplay {
    fn remaining_at(&self, unix_seconds: u64) -> Option<u64> {
        self.validity.remaining_at(unix_seconds)
    }
}

struct OtpForm {
    entry: String,
    uri: Zeroizing<String>,
    replace: bool,
    remove_confirmed: bool,
    copy_after_code: bool,
    hotp_confirmation: bool,
    running: bool,
    error: Option<String>,
}

#[derive(Clone, Eq, PartialEq)]
struct KdbxForm {
    source: String,
    key_file: String,
    password: Zeroizing<String>,
    quick_add: bool,
    confirmed: bool,
    running: bool,
    error: Option<String>,
    summary: Option<String>,
}

impl std::fmt::Debug for KdbxForm {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("KdbxForm")
            .field("source", &self.source)
            .field("key_file", &self.key_file)
            .field("password", &"[REDACTED]")
            .field("quick_add", &self.quick_add)
            .field("confirmed", &self.confirmed)
            .field("running", &self.running)
            .field("error", &self.error)
            .field("summary", &self.summary)
            .finish()
    }
}

impl Default for KdbxForm {
    fn default() -> Self {
        Self {
            source: String::new(),
            key_file: String::new(),
            password: Zeroizing::new(String::new()),
            quick_add: false,
            confirmed: false,
            running: false,
            error: None,
            summary: None,
        }
    }
}

impl KdbxForm {
    fn request(&self) -> Result<(KdbxImportRequest, SecretBytes), String> {
        if self.source.trim().is_empty() {
            return Err("Choose a KDBX database file.".to_owned());
        }
        if !self.confirmed {
            return Err("Confirm the additive import before continuing.".to_owned());
        }
        Ok((
            KdbxImportRequest::new(
                self.source.trim(),
                (!self.key_file.trim().is_empty()).then(|| self.key_file.trim().into()),
                if self.quick_add {
                    KdbxImportMode::QuickAdd
                } else {
                    KdbxImportMode::AddAndUpdate
                },
            ),
            SecretBytes::new(self.password.as_bytes().to_vec()),
        ))
    }
}

impl OtpForm {
    fn new(entry: String) -> Self {
        Self {
            entry,
            uri: Zeroizing::new(String::new()),
            replace: false,
            remove_confirmed: false,
            copy_after_code: false,
            hotp_confirmation: false,
            running: false,
            error: None,
        }
    }
}

enum OtpTaskResult {
    Code {
        entry: String,
        copy: bool,
        observed_at: u64,
        outcome: DesktopOtpCode,
    },
    Uri {
        entry: String,
        copy: bool,
        outcome: DesktopOtpUri,
    },
    Imported(DesktopOtpMutation),
    Removed(DesktopOtpMutation),
}

#[derive(Clone, Debug)]
struct GitConflictSelection {
    conflict: GitConflict,
    choice: Option<GitConflictChoice>,
}

#[derive(Clone, Debug, Default)]
struct GitForm {
    snapshot: Option<GitSnapshot>,
    progress: Option<GitProgressPhase>,
    running: bool,
    error: Option<String>,
    conflicts: Vec<GitConflictSelection>,
}

impl GitForm {
    fn resolutions(&self) -> Result<Vec<GitConflictResolution>, String> {
        self.conflicts
            .iter()
            .map(|selection| {
                selection
                    .choice
                    .map(|choice| {
                        GitConflictResolution::new(selection.conflict.path().to_owned(), choice)
                    })
                    .ok_or_else(|| {
                        format!(
                            "Choose the local or remote version for {}.",
                            selection.conflict.path().display()
                        )
                    })
            })
            .collect()
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SearchMode {
    Names,
    Contents,
}

enum SearchResults {
    Names(FindResults),
    Contents(GrepResults),
}

struct SearchForm {
    mode: SearchMode,
    query: String,
    terms: Vec<String>,
    ignore_case: bool,
    invert_match: bool,
    line_numbers: bool,
    fixed_strings: bool,
    running: bool,
    error: Option<String>,
    results: Option<SearchResults>,
}

impl SearchForm {
    fn new(mode: SearchMode) -> Self {
        Self {
            mode,
            query: String::new(),
            terms: Vec::new(),
            ignore_case: false,
            invert_match: false,
            line_numbers: true,
            fixed_strings: false,
            running: false,
            error: None,
            results: None,
        }
    }

    fn request(&self) -> Result<SearchRequest, String> {
        Ok(match self.mode {
            SearchMode::Names => {
                let mut terms = self.terms.clone();
                if !self.query.is_empty() {
                    terms.push(self.query.clone());
                }
                if terms.is_empty() {
                    return Err("Enter at least one name search term.".to_owned());
                }
                SearchRequest::Names(FindRequest { terms })
            }
            SearchMode::Contents => SearchRequest::Contents(GrepRequest {
                pattern: self.query.clone(),
                ignore_case: self.ignore_case,
                invert_match: self.invert_match,
                line_number: self.line_numbers,
                fixed_strings: self.fixed_strings,
            }),
        })
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum SearchRequest {
    Names(FindRequest),
    Contents(GrepRequest),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MutationKind {
    Move,
    Copy,
    Delete,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct MutationForm {
    kind: MutationKind,
    source: TreeNodeId,
    destination: String,
    overwrite: bool,
    confirmed: bool,
    running: bool,
    error: Option<String>,
}

impl MutationForm {
    fn new(kind: MutationKind, source: TreeNodeId) -> Self {
        Self {
            destination: source.path().display().to_string(),
            kind,
            source,
            overwrite: false,
            confirmed: false,
            running: false,
            error: None,
        }
    }

    fn request(&self) -> Result<DesktopMutationRequest, String> {
        let source = self.source.path().display().to_string();
        match self.kind {
            MutationKind::Move | MutationKind::Copy if self.destination == source => {
                Err("Choose a different destination.".to_owned())
            }
            MutationKind::Move => Ok(DesktopMutationRequest::Move(MoveRequest {
                source,
                destination: self.destination.clone(),
                force: false,
            })),
            MutationKind::Copy => Ok(DesktopMutationRequest::Copy(CopyRequest {
                source,
                destination: self.destination.clone(),
                force: false,
            })),
            MutationKind::Delete if !self.confirmed => {
                Err("Confirm permanent removal first.".to_owned())
            }
            MutationKind::Delete => Ok(DesktopMutationRequest::Remove(RemoveRequest {
                entry: source,
                recursive: self.source.is_directory(),
                force: false,
            })),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum RecipientWorkflowKind {
    InitializeStore,
    NewFolder,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct RecipientChoice {
    fingerprint: String,
    label: String,
    selected: bool,
    default: bool,
    can_default: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct RecipientForm {
    kind: RecipientWorkflowKind,
    path: String,
    recipients: Vec<RecipientChoice>,
    default_key: String,
    confirmed: bool,
    running: bool,
    error: Option<String>,
}

impl RecipientForm {
    fn new(
        kind: RecipientWorkflowKind,
        storage: &DesktopStorage,
        path: String,
    ) -> Result<Self, String> {
        let default_key = storage.default_key();
        let mut recipients = storage
            .key_infos()
            .map_err(|error| error.to_string())?
            .into_iter()
            .map(|key| {
                let default = key.fingerprint().as_str() == default_key;
                RecipientChoice {
                    label: format!(
                        "{} — {}",
                        key.key_id(),
                        key.user_ids()
                            .first()
                            .map_or("unknown identity", String::as_str)
                    ),
                    fingerprint: key.fingerprint().to_string(),
                    selected: default,
                    default,
                    can_default: key.has_secret(),
                }
            })
            .collect::<Vec<_>>();
        if !recipients.iter().any(|choice| choice.selected)
            && let Some(first) = recipients.first_mut()
        {
            first.selected = true;
        }
        let default_key = recipients
            .iter()
            .find(|choice| choice.default && choice.can_default)
            .or_else(|| recipients.iter().find(|choice| choice.can_default))
            .map(|choice| choice.fingerprint.clone())
            .ok_or_else(|| "No encryption key with secret key material is available.".to_owned())?;
        Ok(Self {
            kind,
            path,
            recipients,
            default_key,
            confirmed: false,
            running: false,
            error: None,
        })
    }

    fn request(&self) -> Result<InitRequest, String> {
        let path = self.path.trim();
        if self.kind == RecipientWorkflowKind::NewFolder && path.is_empty() {
            return Err("Enter a folder path.".to_owned());
        }
        let key_identities = self
            .recipients
            .iter()
            .filter(|choice| choice.selected)
            .map(|choice| choice.fingerprint.clone())
            .collect::<Vec<_>>();
        if key_identities.is_empty() {
            return Err("Select at least one encryption recipient.".to_owned());
        }
        if self.kind == RecipientWorkflowKind::InitializeStore
            && !self.recipients.iter().any(|choice| {
                choice.fingerprint == self.default_key && choice.selected && choice.can_default
            })
        {
            return Err("Select a recipient with secret material as the default key.".to_owned());
        }
        if !self.confirmed {
            return Err(
                "Confirm the recipient-policy replacement and selective re-encryption.".to_owned(),
            );
        }
        Ok(InitRequest {
            path: (!path.is_empty()).then(|| path.to_owned()),
            key_identities,
        })
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct NewEntryForm {
    path: String,
    generate: bool,
    length: String,
    no_symbols: bool,
    running: bool,
    error: Option<String>,
}

impl NewEntryForm {
    fn new(path: String) -> Self {
        Self {
            path,
            generate: false,
            length: GeneratorConfig::pass_defaults()
                .default_length()
                .to_string(),
            no_symbols: false,
            running: false,
            error: None,
        }
    }

    fn password(&self) -> Result<SecretBytes, String> {
        if !self.generate {
            return Ok(SecretBytes::new(Vec::new()));
        }
        let length = parse_generation_length(&self.length)?;
        GeneratorConfig::pass_defaults()
            .generate_secret(length, self.no_symbols)
            .map_err(|error| error.to_string())
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct GenerateForm {
    id: EntryFieldId,
    length: String,
    no_symbols: bool,
    replacement: bool,
    confirmed: bool,
    error: Option<String>,
}

impl GenerateForm {
    fn new(id: EntryFieldId, replacement: bool) -> Self {
        Self {
            id,
            length: GeneratorConfig::pass_defaults()
                .default_length()
                .to_string(),
            no_symbols: false,
            replacement,
            confirmed: false,
            error: None,
        }
    }

    fn generate(&self) -> Result<SecretBytes, String> {
        if self.replacement && !self.confirmed {
            return Err("Confirm replacement of the current field value.".to_owned());
        }
        GeneratorConfig::pass_defaults()
            .generate_secret(parse_generation_length(&self.length)?, self.no_symbols)
            .map_err(|error| error.to_string())
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct SettingsForm {
    vault: String,
    default_key: String,
    authentication_timeout: String,
    saving: bool,
    error: Option<String>,
    command_links_status: Option<String>,
}

impl SettingsForm {
    fn new(storage: &DesktopStorage) -> Self {
        Self {
            vault: storage.vault().display().to_string(),
            default_key: storage.default_key().to_owned(),
            authentication_timeout: storage
                .authentication_timeout()
                .duration()
                .as_secs()
                .to_string(),
            saving: false,
            error: None,
            command_links_status: None,
        }
    }

    fn settings(
        &self,
        storage: &DesktopStorage,
    ) -> Result<ironstorage::config::ConfigSettings, String> {
        let timeout = self
            .authentication_timeout
            .trim()
            .parse::<u64>()
            .map_err(|_| "Authentication timeout must be a whole number of seconds.".to_owned())?;
        let mut settings = storage.settings();
        settings.set_vault(PathBuf::from(self.vault.trim()));
        settings.set_default_key(self.default_key.trim().to_owned());
        settings.set_authentication_timeout(Duration::from_secs(timeout));
        Ok(settings)
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum TreeState {
    Loading,
    Empty,
    Ready,
    Error(String),
}

#[derive(Clone, Debug, Eq, PartialEq)]
enum PendingAction {
    OpenVault(PathBuf),
    OpenEntry(String),
    Reload(String),
    CloseWindow(window::Id),
    ApplyRecipients(RecipientForm),
    CreateEntry(NewEntryForm),
    SearchContents(GrepRequest),
    Mutate(MutationForm),
    Git(DesktopGitRequest),
    ImportKdbx(KdbxForm),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DirtyDecision {
    Confirm,
    Execute,
}

#[derive(Debug, Eq, PartialEq)]
enum LeasePoll {
    Idle,
    Active(Duration),
    Expired,
}

fn main() -> iced::Result {
    iced::application(App::new, App::update, App::view)
        .title(ironstorage::PRODUCT_NAME)
        .subscription(App::subscription)
        .exit_on_close_request(false)
        .window(window_settings())
        .run()
}

fn window_settings() -> window::Settings {
    window::Settings {
        size: Size::new(960.0, 680.0),
        min_size: Some(Size::new(480.0, 360.0)),
        ..window::Settings::default()
    }
}

impl App {
    fn new() -> (Self, Task<Message>) {
        let app = Self {
            authentication: AuthenticationView::Loading,
            storage: None,
            session: None,
            key: None,
            handle: None,
            sensitive: SensitiveUiState::default(),
            git_control: None,
            git_progress: None,
            authentication_generation: 0,
            operation_generation: 0,
            tree_generation: 0,
            vault_generation: 0,
            settings_generation: 0,
            workflow_generation: 0,
            otp_generation: 0,
            otp_pending: false,
            selection_after_refresh: None,
            panes: pane_grid::State::with_configuration(pane_grid::Configuration::Split {
                axis: pane_grid::Axis::Vertical,
                ratio: 0.28,
                a: Box::new(pane_grid::Configuration::Pane(PaneKind::Sidebar)),
                b: Box::new(pane_grid::Configuration::Pane(PaneKind::Content)),
            }),
            pane_focus: PaneFocus::Sidebar,
            navigation: NavigationTree::default(),
            tree_state: TreeState::Loading,
            after_authentication: None,
            entry_path: String::new(),
            editor: None,
            content_mode: ContentMode::Viewer,
            saving: false,
            switching_vault: false,
            confirmation: None,
            after_save: None,
            generation_form: None,
            conflict: false,
            status: "Loading shared configuration…".to_owned(),
            open_menu: None,
            utility: None,
            context_target: None,
            palette: CommandPalette::default(),
            #[cfg(target_os = "macos")]
            native_menu: None,
        };
        (
            app,
            Task::perform(load_authentication(), |result| {
                Message::StartupLoaded(Box::new(result))
            }),
        )
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Action(UiAction::CommandPalette) => return self.toggle_palette(),
            Message::Action(action) => {
                if self.palette.is_open() {
                    self.palette.close();
                    let restore = self.restore_focus();
                    let action = self.invoke_action(action);
                    return Task::batch([restore, action]);
                }
                return self.invoke_action(action);
            }
            Message::FieldAction(id, action) => {
                if let Some(editor) = self.editor.as_mut() {
                    editor.select(id);
                }
                return self.invoke_action(action);
            }
            Message::ToggleMenu(group) => {
                self.open_menu = (self.open_menu != Some(group)).then_some(group);
            }
            Message::PaletteQueryChanged(query) => {
                self.palette.update_query(query);
                return iced::widget::operation::snap_to(
                    command_palette_scroll_id(),
                    scrollable::RelativeOffset::START,
                );
            }
            Message::PaletteCancel => {
                self.touch_user_activity();
                if self.utility.is_some() {
                    return self.update(Message::DismissUtility);
                }
                if self.palette.is_open() {
                    self.palette.close();
                    return self.restore_focus();
                }
            }
            Message::PaletteInvoke(action) => return self.invoke_palette_action(action),
            Message::DismissUtility => {
                if self.utility.as_ref().is_some_and(|utility| {
                    matches!(utility, UtilityView::Settings(form) if form.saving)
                        || matches!(utility, UtilityView::Recipients(form) if form.running)
                        || matches!(utility, UtilityView::NewEntry(form) if form.running)
                        || matches!(utility, UtilityView::Search(form) if form.running)
                        || matches!(utility, UtilityView::Mutation(form) if form.running)
                        || matches!(utility, UtilityView::Git(form) if form.running)
                        || matches!(utility, UtilityView::Otp(form) if form.running)
                        || matches!(utility, UtilityView::Kdbx(form) if form.running)
                }) {
                    self.status = "Wait for the active workflow to finish…".to_owned();
                } else {
                    if matches!(&self.utility, Some(UtilityView::Otp(_))) {
                        self.sensitive.otp_uri = None;
                        self.sensitive.otp_qr = None;
                        self.otp_generation = self.otp_generation.wrapping_add(1);
                        self.otp_pending = false;
                    }
                    self.utility = None;
                }
            }
            Message::WindowResolved(action, id) => {
                let Some(id) = id else {
                    return Task::none();
                };
                return match action {
                    UiAction::CloseWindow | UiAction::Quit => {
                        self.request_action(PendingAction::CloseWindow(id))
                    }
                    UiAction::Minimize => window::minimize(id, true),
                    _ => Task::none(),
                };
            }
            Message::FolderPicked(result) => match result {
                Ok(Some(path)) => return self.request_action(PendingAction::OpenVault(path)),
                Ok(None) => {
                    self.status = "Open Folder cancelled; current vault unchanged.".to_owned();
                }
                Err(error) => {
                    self.status =
                        format!("Folder picker failed: {error}. Current vault unchanged.");
                }
            },
            Message::SettingsVaultChanged(vault) => {
                if let Some(UtilityView::Settings(form)) = &mut self.utility
                    && !form.saving
                {
                    form.vault = vault;
                    form.error = None;
                }
            }
            Message::SettingsDefaultKeyChanged(default_key) => {
                if let Some(UtilityView::Settings(form)) = &mut self.utility
                    && !form.saving
                {
                    form.default_key = default_key;
                    form.error = None;
                }
            }
            Message::SettingsTimeoutChanged(timeout) => {
                if let Some(UtilityView::Settings(form)) = &mut self.utility
                    && !form.saving
                {
                    form.authentication_timeout = timeout;
                    form.error = None;
                }
            }
            Message::PickSettingsVault => {
                let initial = match &self.utility {
                    Some(UtilityView::Settings(form)) if !form.saving => {
                        Some(PathBuf::from(&form.vault))
                    }
                    _ => None,
                };
                if initial.is_some() {
                    return Task::perform(
                        folder_picker::pick_folder(initial),
                        Message::SettingsVaultPicked,
                    );
                }
            }
            Message::SettingsVaultPicked(result) => {
                if let Some(UtilityView::Settings(form)) = &mut self.utility
                    && !form.saving
                {
                    match result {
                        Ok(Some(path)) => {
                            form.vault = path.display().to_string();
                            form.error = None;
                        }
                        Ok(None) => {}
                        Err(error) => form.error = Some(format!("Folder picker failed: {error}")),
                    }
                }
            }
            Message::InstallCommandLinks => {
                if let Some(UtilityView::Settings(form)) = &mut self.utility
                    && !form.saving
                {
                    form.command_links_status =
                        Some(match launcher::install_packaged_launchers() {
                            Ok(targets) => format!(
                                "Installed {} and {}.",
                                targets[0].display(),
                                targets[1].display()
                            ),
                            Err(error) => format!("Command links were not installed: {error}"),
                        });
                    self.status = form.command_links_status.clone().unwrap_or_default();
                }
            }
            Message::SaveSettings => return self.begin_settings_save(),
            Message::SettingsFinished { generation, result } => {
                if generation != self.settings_generation {
                    if let Ok((_storage, session, _key)) = *result {
                        let _ignored = session.manual_lock();
                    }
                    return Task::none();
                }
                match *result {
                    Ok((storage, session, key)) => {
                        if let Some(current) = &self.session {
                            let _ignored = current.manual_lock();
                        }
                        self.authentication_generation =
                            self.authentication_generation.wrapping_add(1);
                        self.operation_generation = self.operation_generation.wrapping_add(1);
                        self.sensitive.clear();
                        self.storage = Some(storage);
                        self.session = Some(session);
                        self.key = Some(key);
                        self.handle = None;
                        self.authentication = AuthenticationView::Locked;
                        self.editor = None;
                        self.content_mode = ContentMode::Viewer;
                        self.navigation = NavigationTree::default();
                        self.tree_state = TreeState::Loading;
                        self.entry_path.clear();
                        self.confirmation = None;
                        self.after_save = None;
                        self.after_authentication = None;
                        self.generation_form = None;
                        self.conflict = false;
                        self.utility = None;
                        self.status = "Settings saved; protected content was locked.".to_owned();
                        return self.begin_tree_refresh();
                    }
                    Err(error) => {
                        if let Some(UtilityView::Settings(form)) = &mut self.utility {
                            form.saving = false;
                            form.error = Some(error.clone());
                        }
                        self.status = format!("Settings were not saved: {error}");
                    }
                }
            }
            Message::VaultSwitched { generation, result } => {
                if generation != self.vault_generation {
                    return Task::none();
                }
                self.switching_vault = false;
                match *result {
                    Ok(storage) => {
                        let vault = storage.vault().display().to_string();
                        self.operation_generation = self.operation_generation.wrapping_add(1);
                        self.authentication_generation =
                            self.authentication_generation.wrapping_add(1);
                        self.sensitive.clear();
                        if let Some(session) = &self.session {
                            let _ignored = session.manual_lock();
                        }
                        self.storage = Some(storage);
                        self.handle = None;
                        self.authentication = AuthenticationView::Locked;
                        self.editor = None;
                        self.content_mode = ContentMode::Viewer;
                        self.navigation = NavigationTree::default();
                        self.tree_state = TreeState::Loading;
                        self.entry_path.clear();
                        self.confirmation = None;
                        self.after_save = None;
                        self.after_authentication = None;
                        self.generation_form = None;
                        self.conflict = false;
                        self.status = format!("Opened password store at {vault}.");
                        return self.begin_tree_refresh();
                    }
                    Err(error) => {
                        self.status =
                            format!("Open Folder failed: {error}. Current vault unchanged.");
                    }
                }
            }
            Message::RecipientPathChanged(path) => {
                if let Some(UtilityView::Recipients(form)) = &mut self.utility
                    && !form.running
                    && form.kind == RecipientWorkflowKind::NewFolder
                {
                    form.path = path;
                    form.error = None;
                    form.confirmed = false;
                }
            }
            Message::ToggleRecipient(index) => {
                if let Some(UtilityView::Recipients(form)) = &mut self.utility
                    && !form.running
                    && let Some(recipient) = form.recipients.get_mut(index)
                {
                    recipient.selected = !recipient.selected;
                    if form.kind == RecipientWorkflowKind::InitializeStore
                        && !recipient.selected
                        && recipient.fingerprint == form.default_key
                    {
                        form.default_key.clear();
                    }
                    form.error = None;
                    form.confirmed = false;
                }
            }
            Message::SelectDefaultRecipient(index) => {
                if let Some(UtilityView::Recipients(form)) = &mut self.utility
                    && !form.running
                    && form.kind == RecipientWorkflowKind::InitializeStore
                    && let Some(recipient) = form.recipients.get_mut(index)
                    && recipient.can_default
                {
                    recipient.selected = true;
                    form.default_key.clone_from(&recipient.fingerprint);
                    form.error = None;
                    form.confirmed = false;
                }
            }
            Message::ToggleRecipientConfirmation => {
                if let Some(UtilityView::Recipients(form)) = &mut self.utility
                    && !form.running
                {
                    form.confirmed = !form.confirmed;
                    form.error = None;
                }
            }
            Message::SubmitRecipient => {
                let Some(UtilityView::Recipients(form)) = &self.utility else {
                    return Task::none();
                };
                if form.running {
                    return Task::none();
                }
                if self.editor.as_ref().is_some_and(EntryEditor::is_dirty) {
                    if let Some(UtilityView::Recipients(form)) = &mut self.utility {
                        form.error = Some("Save or discard the current draft first.".to_owned());
                    }
                    return Task::none();
                }
                if let Err(error) = form.request() {
                    if let Some(UtilityView::Recipients(form)) = &mut self.utility {
                        form.error = Some(error);
                    }
                    return Task::none();
                }
                let form = form.clone();
                if self.handle.is_none() {
                    self.after_authentication = Some(PendingAction::ApplyRecipients(form));
                    return self.begin_authentication();
                }
                return self.begin_recipient_workflow(form);
            }
            Message::RecipientFinished { generation, result } => {
                if generation != self.workflow_generation {
                    return Task::none();
                }
                match *result {
                    Ok(summary) => {
                        if let Some(session) = &self.session {
                            let _ignored = session.manual_lock();
                        }
                        self.authentication_generation =
                            self.authentication_generation.wrapping_add(1);
                        self.sensitive.clear();
                        self.storage = Some(summary.storage);
                        self.key = Some(summary.key);
                        self.handle = None;
                        self.authentication = AuthenticationView::Locked;
                        self.utility = None;
                        self.editor = None;
                        self.content_mode = ContentMode::Viewer;
                        self.conflict = false;
                        self.status = format!(
                            "Recipient policy applied at {} for {} recipient(s); {} entry/entries re-encrypted.",
                            summary.summary.directory,
                            summary.summary.recipients,
                            summary.summary.reencrypted
                        );
                        return self.begin_tree_refresh();
                    }
                    Err(error) => {
                        if let Some(UtilityView::Recipients(form)) = &mut self.utility {
                            form.running = false;
                            form.error = Some(error.clone());
                        }
                        self.status = format!("Recipient workflow failed: {error}");
                    }
                }
            }
            Message::NewEntryPathChanged(path) => {
                if let Some(UtilityView::NewEntry(form)) = &mut self.utility
                    && !form.running
                {
                    form.path = path;
                    form.error = None;
                }
            }
            Message::ToggleNewEntryGeneration => {
                if let Some(UtilityView::NewEntry(form)) = &mut self.utility
                    && !form.running
                {
                    form.generate = !form.generate;
                    form.error = None;
                }
            }
            Message::NewEntryLengthChanged(length) => {
                if let Some(UtilityView::NewEntry(form)) = &mut self.utility
                    && !form.running
                {
                    form.length = length;
                    form.error = None;
                }
            }
            Message::ToggleNewEntrySymbols => {
                if let Some(UtilityView::NewEntry(form)) = &mut self.utility
                    && !form.running
                {
                    form.no_symbols = !form.no_symbols;
                    form.error = None;
                }
            }
            Message::SubmitNewEntry => {
                let Some(UtilityView::NewEntry(form)) = &self.utility else {
                    return Task::none();
                };
                if form.running {
                    return Task::none();
                }
                if form.path.trim().is_empty() {
                    if let Some(UtilityView::NewEntry(form)) = &mut self.utility {
                        form.error = Some("Enter an entry path.".to_owned());
                    }
                    return Task::none();
                }
                if let Err(error) = form.password() {
                    if let Some(UtilityView::NewEntry(form)) = &mut self.utility {
                        form.error = Some(error);
                    }
                    return Task::none();
                }
                let form = form.clone();
                if self.handle.is_none() {
                    self.after_authentication = Some(PendingAction::CreateEntry(form));
                    return self.begin_authentication();
                }
                return self.begin_create_entry(form);
            }
            Message::CreateFinished {
                generation,
                completion,
            } => {
                let Some((password, result)) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.workflow_generation {
                    return Task::none();
                }
                match result.and_then(|document| {
                    EntryEditor::new_entry(document, password).map_err(|error| error.to_string())
                }) {
                    Ok(editor) => {
                        self.entry_path = editor.entry();
                        self.editor = Some(editor);
                        self.content_mode = ContentMode::Editor;
                        self.pane_focus = PaneFocus::Content;
                        self.utility = None;
                        self.conflict = false;
                        self.status =
                            "New entry draft ready; nothing is stored until Save.".to_owned();
                    }
                    Err(error) => {
                        if let Some(UtilityView::NewEntry(form)) = &mut self.utility {
                            form.running = false;
                            form.error = Some(error.clone());
                        }
                        self.status = format!("New entry failed: {error}. Store unchanged.");
                    }
                }
            }
            Message::SearchQueryChanged(query) => {
                if let Some(UtilityView::Search(form)) = &mut self.utility
                    && !form.running
                {
                    form.query = query;
                    form.error = None;
                    form.results = None;
                }
            }
            Message::AddSearchTerm => {
                if let Some(UtilityView::Search(form)) = &mut self.utility
                    && !form.running
                    && form.mode == SearchMode::Names
                    && !form.query.is_empty()
                {
                    form.terms.push(std::mem::take(&mut form.query));
                    form.error = None;
                    form.results = None;
                }
            }
            Message::RemoveSearchTerm(index) => {
                if let Some(UtilityView::Search(form)) = &mut self.utility
                    && !form.running
                    && form.mode == SearchMode::Names
                    && index < form.terms.len()
                {
                    form.terms.remove(index);
                    form.error = None;
                    form.results = None;
                }
            }
            Message::ToggleSearchCase => self.update_search_form(|form| {
                form.ignore_case = !form.ignore_case;
            }),
            Message::ToggleSearchInvert => self.update_search_form(|form| {
                form.invert_match = !form.invert_match;
            }),
            Message::ToggleSearchLineNumbers => self.update_search_form(|form| {
                form.line_numbers = !form.line_numbers;
            }),
            Message::ToggleSearchFixedStrings => self.update_search_form(|form| {
                form.fixed_strings = !form.fixed_strings;
            }),
            Message::SubmitSearch => {
                let request = match &self.utility {
                    Some(UtilityView::Search(form)) if !form.running => form.request(),
                    _ => return Task::none(),
                };
                let request = match request {
                    Ok(request) => request,
                    Err(error) => {
                        if let Some(UtilityView::Search(form)) = &mut self.utility {
                            form.error = Some(error);
                        }
                        return Task::none();
                    }
                };
                if let SearchRequest::Contents(request) = &request
                    && self.handle.is_none()
                {
                    self.after_authentication =
                        Some(PendingAction::SearchContents(request.clone()));
                    return self.begin_authentication();
                }
                return self.begin_search(request);
            }
            Message::SearchFinished {
                generation,
                completion,
            } => {
                let Some(result) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.workflow_generation {
                    return Task::none();
                }
                match result {
                    Ok(results) => {
                        let count = match &results {
                            SearchResults::Names(results) => results.matches().len(),
                            SearchResults::Contents(results) => results.entries().len(),
                        };
                        if let Some(UtilityView::Search(form)) = &mut self.utility {
                            form.running = false;
                            form.error = None;
                            form.results = Some(results);
                        }
                        self.status = format!("Search completed with {count} matching object(s).");
                    }
                    Err(error) => {
                        if let Some(UtilityView::Search(form)) = &mut self.utility {
                            form.running = false;
                            form.error = Some(error.clone());
                        }
                        self.status = format!("Search failed: {error}");
                    }
                }
            }
            Message::ActivateSearchResult(id) => {
                self.utility = None;
                self.pane_focus = if id.is_directory() {
                    PaneFocus::Sidebar
                } else {
                    PaneFocus::Content
                };
                let _selected = self.navigation.select_id(&id);
                if let TreeNodeId::Entry(path) = id {
                    return self.request_action(PendingAction::OpenEntry(path.to_string()));
                }
            }
            Message::MutationDestinationChanged(destination) => {
                if let Some(UtilityView::Mutation(form)) = &mut self.utility
                    && !form.running
                {
                    form.destination = destination;
                    form.overwrite = false;
                    form.error = None;
                }
            }
            Message::SelectMutationDestination(destination) => {
                if let Some(UtilityView::Mutation(form)) = &mut self.utility
                    && !form.running
                {
                    form.destination = destination;
                    form.overwrite = false;
                    form.error = None;
                }
            }
            Message::ToggleMutationOverwrite => {
                if let Some(UtilityView::Mutation(form)) = &mut self.utility
                    && !form.running
                {
                    form.overwrite = !form.overwrite;
                    form.error = None;
                }
            }
            Message::ToggleMutationConfirmation => {
                if let Some(UtilityView::Mutation(form)) = &mut self.utility
                    && !form.running
                {
                    form.confirmed = !form.confirmed;
                    form.error = None;
                }
            }
            Message::SubmitMutation => {
                let form = match &self.utility {
                    Some(UtilityView::Mutation(form)) if !form.running => form.clone(),
                    _ => return Task::none(),
                };
                if let Err(error) = form.request() {
                    if let Some(UtilityView::Mutation(form)) = &mut self.utility {
                        form.error = Some(error);
                    }
                    return Task::none();
                }
                return self.request_action(PendingAction::Mutate(form));
            }
            Message::MutationFinished { generation, result } => {
                if generation != self.workflow_generation {
                    return Task::none();
                }
                match *result {
                    Ok(outcome) => {
                        self.selection_after_refresh =
                            outcome.selection().map(|selection| match selection {
                                MutationSelection::Entry(path) => TreeNodeId::Entry(path.clone()),
                                MutationSelection::Directory(path) => {
                                    TreeNodeId::Directory(path.clone())
                                }
                            });
                        let verb = match outcome.action() {
                            MutationAction::Remove => "Removed",
                            MutationAction::Move => "Moved",
                            MutationAction::Copy => "Copied",
                        };
                        self.utility = None;
                        self.editor = None;
                        self.content_mode = ContentMode::Viewer;
                        self.entry_path = self
                            .selection_after_refresh
                            .as_ref()
                            .map_or_else(String::new, |id| id.path().display().to_string());
                        self.status = format!(
                            "{verb} {} encrypted entry/entries; refreshing the tree.",
                            outcome.entries().len()
                        );
                        return self.begin_tree_refresh();
                    }
                    Err(error) => {
                        if let Some(UtilityView::Mutation(form)) = &mut self.utility {
                            form.running = false;
                            form.error = Some(error.clone());
                        }
                        self.status = format!("Mutation failed: {error}. Store unchanged.");
                    }
                }
            }
            Message::RunGit(request) => {
                if !matches!(&self.utility, Some(UtilityView::Git(form)) if !form.running) {
                    return Task::none();
                }
                let pending = PendingAction::Git(request.clone());
                return if request.changes_worktree() {
                    self.request_action(pending)
                } else {
                    self.execute_action(pending)
                };
            }
            Message::ChooseGitConflict(index, choice) => {
                if let Some(UtilityView::Git(form)) = &mut self.utility
                    && !form.running
                    && let Some(selection) = form.conflicts.get_mut(index)
                {
                    selection.choice = Some(choice);
                    form.error = None;
                }
            }
            Message::ResolveGitConflicts => {
                let resolutions = match &self.utility {
                    Some(UtilityView::Git(form)) if !form.running => form.resolutions(),
                    _ => return Task::none(),
                };
                match resolutions {
                    Ok(resolutions) => {
                        return self.request_action(PendingAction::Git(
                            DesktopGitRequest::Resolve(resolutions),
                        ));
                    }
                    Err(error) => {
                        if let Some(UtilityView::Git(form)) = &mut self.utility {
                            form.error = Some(error.clone());
                        }
                        self.status = error;
                    }
                }
            }
            Message::CancelGit => {
                if let Some(control) = &self.git_control {
                    control.cancel();
                    self.status = "Cancelling Git operation…".to_owned();
                }
            }
            Message::GitFinished {
                generation,
                completion,
            } => {
                let Some(result) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.workflow_generation {
                    return Task::none();
                }
                self.git_control = None;
                self.git_progress = None;
                match result {
                    Ok(result) => {
                        let (outcome, snapshot, tree) = result.into_parts();
                        if git_outcome_changes_worktree(&outcome) {
                            self.editor = None;
                            self.content_mode = ContentMode::Viewer;
                            self.conflict = false;
                        }
                        if let Some(tree) = tree {
                            self.navigation.replace(&tree);
                            self.tree_state =
                                tree_state_from_result(Ok(self.navigation.is_empty()));
                        }
                        if let Some(UtilityView::Git(form)) = &mut self.utility {
                            form.snapshot = Some(snapshot);
                            form.progress = None;
                            form.running = false;
                            form.error = None;
                            form.conflicts.clear();
                        }
                        self.status = git_outcome_message(&outcome);
                    }
                    Err(error) => {
                        let conflicts = error
                            .conflicts()
                            .iter()
                            .cloned()
                            .map(|conflict| GitConflictSelection {
                                conflict,
                                choice: None,
                            })
                            .collect::<Vec<_>>();
                        let message = git_failure_message(&error);
                        if let Some(UtilityView::Git(form)) = &mut self.utility {
                            form.progress = None;
                            form.running = false;
                            form.error = Some(message.clone());
                            form.conflicts = conflicts;
                        }
                        self.status = if error.kind() == DesktopErrorKind::Conflict {
                            format!("Git requires explicit conflict resolution: {message}")
                        } else {
                            format!("Git operation failed: {message}")
                        };
                    }
                }
            }
            Message::OtpEntryChanged(entry) => {
                if let Some(UtilityView::Otp(form)) = &mut self.utility
                    && !form.running
                {
                    form.entry = entry;
                    form.error = None;
                }
            }
            Message::OtpUriChanged(uri) => {
                if let Some(UtilityView::Otp(form)) = &mut self.utility
                    && !form.running
                {
                    form.uri = uri;
                    form.error = None;
                }
            }
            Message::ToggleOtpReplace => {
                if let Some(UtilityView::Otp(form)) = &mut self.utility
                    && !form.running
                {
                    form.replace = !form.replace;
                    form.error = None;
                }
            }
            Message::ToggleOtpRemovalConfirmation => {
                if let Some(UtilityView::Otp(form)) = &mut self.utility
                    && !form.running
                {
                    form.remove_confirmed = !form.remove_confirmed;
                    form.error = None;
                }
            }
            Message::PickOtpQr => {
                if matches!(&self.utility, Some(UtilityView::Otp(form)) if !form.running) {
                    return Task::perform(
                        async { Arc::new(Mutex::new(Some(folder_picker::pick_qr_image().await))) },
                        Message::OtpQrPicked,
                    );
                }
            }
            Message::OtpQrPicked(completion) => match take_completion(&completion) {
                None => return Task::none(),
                Some(Ok(Some(image))) => return self.begin_otp_import(Some(image)),
                Some(Ok(None)) => {
                    self.status = "OTP QR import cancelled; store unchanged.".to_owned()
                }
                Some(Err(error)) => {
                    if let Some(UtilityView::Otp(form)) = &mut self.utility {
                        form.error = Some(error.clone());
                    }
                    self.status = format!("OTP QR import failed: {error}. Store unchanged.");
                }
            },
            Message::SubmitOtpImport => return self.begin_otp_import(None),
            Message::SubmitOtpRemoval => {
                let Some(UtilityView::Otp(form)) = &self.utility else {
                    return Task::none();
                };
                if !form.remove_confirmed {
                    if let Some(UtilityView::Otp(form)) = &mut self.utility {
                        form.error = Some("Confirm permanent OTP removal first.".to_owned());
                    }
                    return Task::none();
                }
                return self.begin_otp_removal();
            }
            Message::RunOtpCode(copy) => {
                let hotp = self
                    .editor
                    .as_ref()
                    .and_then(|editor| editor.focused().and_then(|id| editor.document().field(id)))
                    .and_then(|field| field.metadata().otp())
                    .is_some_and(|otp| otp.kind() == OtpKind::Hotp);
                if hotp {
                    if let Some(UtilityView::Otp(form)) = &mut self.utility
                        && !form.running
                    {
                        form.copy_after_code = copy;
                        form.hotp_confirmation = true;
                        form.error = None;
                    }
                    self.status =
                        "Confirm HOTP generation; storage will commit the advanced counter."
                            .to_owned();
                } else {
                    return self.begin_otp_code(copy, false);
                }
            }
            Message::RunOtpUri { qr, copy } => return self.begin_otp_uri(qr, copy),
            Message::ConfirmHotp => {
                let copy = match &mut self.utility {
                    Some(UtilityView::Otp(form)) if form.hotp_confirmation && !form.running => {
                        form.hotp_confirmation = false;
                        form.copy_after_code
                    }
                    _ => return Task::none(),
                };
                return self.begin_otp_code(copy, true);
            }
            Message::OtpFinished {
                generation,
                completion,
            } => {
                let Some(result) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.otp_generation {
                    return Task::none();
                }
                self.otp_pending = false;
                if let Some(UtilityView::Otp(form)) = &mut self.utility {
                    form.running = false;
                }
                match result {
                    Ok(OtpTaskResult::Code {
                        entry,
                        copy,
                        observed_at,
                        outcome,
                    }) => {
                        let (code, validity, metadata, tree, document) = outcome.into_parts();
                        if let Some(tree) = tree {
                            self.navigation.replace(&tree);
                            self.tree_state =
                                tree_state_from_result(Ok(self.navigation.is_empty()));
                        }
                        if let Some(document) = document {
                            let mut editor = EntryEditor::new(document);
                            if let Some(id) = editor
                                .fields()
                                .iter()
                                .find(|field| field.metadata().kind() == EntryFieldKind::OtpUri)
                                .map(|field| field.id())
                            {
                                editor.select(id);
                            }
                            self.editor = Some(editor);
                            self.content_mode = ContentMode::Viewer;
                        }
                        let copy_value = copy.then(|| SecretBytes::new(code.expose().to_vec()));
                        self.sensitive.otp_uri = None;
                        self.sensitive.otp_qr = None;
                        self.sensitive.otp = Some(OtpDisplay {
                            entry,
                            code,
                            validity,
                            metadata,
                            observed_at,
                        });
                        self.status = validity.counter().map_or_else(
                            || "TOTP code generated from storage metadata.".to_owned(),
                            |counter| format!("HOTP counter {counter} generated and committed."),
                        );
                        if let Some(value) = copy_value {
                            return self.begin_secret_copy(value);
                        }
                    }
                    Ok(OtpTaskResult::Uri {
                        entry,
                        copy,
                        outcome,
                    }) => {
                        let (payload, matrix) = outcome.into_parts();
                        if copy {
                            self.sensitive.otp_uri = None;
                            self.sensitive.otp_qr = None;
                            self.status = format!("Copying OTP URI for {entry}.");
                            return self.begin_secret_copy(payload);
                        }
                        self.sensitive.otp_uri = matrix
                            .is_none()
                            .then(|| SecretBytes::new(payload.expose().to_vec()));
                        self.sensitive.otp_qr = matrix;
                        self.status = format!("Presented OTP URI for {entry}.");
                    }
                    Ok(OtpTaskResult::Imported(outcome)) | Ok(OtpTaskResult::Removed(outcome)) => {
                        let (entry, document, tree) = outcome.into_parts();
                        self.navigation.replace(&tree);
                        self.tree_state = tree_state_from_result(Ok(self.navigation.is_empty()));
                        let mut editor = EntryEditor::new(document);
                        if let Some(id) = editor
                            .fields()
                            .iter()
                            .find(|field| field.metadata().kind() == EntryFieldKind::OtpUri)
                            .map(|field| field.id())
                        {
                            editor.select(id);
                        }
                        self.editor = Some(editor);
                        self.entry_path = entry;
                        self.content_mode = ContentMode::Viewer;
                        self.sensitive.otp = None;
                        self.sensitive.otp_uri = None;
                        self.sensitive.otp_qr = None;
                        if let Some(UtilityView::Otp(form)) = &mut self.utility {
                            form.uri.clear();
                            form.replace = false;
                            form.remove_confirmed = false;
                            form.error = None;
                        }
                        self.status = "OTP mutation committed and entry reloaded.".to_owned();
                    }
                    Err(error) => {
                        if let Some(UtilityView::Otp(form)) = &mut self.utility {
                            form.error = Some(error.to_string());
                        }
                        self.status = format!("OTP operation failed: {error}. Store unchanged.");
                    }
                }
            }
            Message::KdbxSourceChanged(source) => {
                if let Some(UtilityView::Kdbx(form)) = &mut self.utility
                    && !form.running
                {
                    form.source = source;
                    form.error = None;
                    form.summary = None;
                }
            }
            Message::KdbxKeyFileChanged(key_file) => {
                if let Some(UtilityView::Kdbx(form)) = &mut self.utility
                    && !form.running
                {
                    form.key_file = key_file;
                    form.error = None;
                    form.summary = None;
                }
            }
            Message::KdbxPasswordChanged(password) => {
                if let Some(UtilityView::Kdbx(form)) = &mut self.utility
                    && !form.running
                {
                    form.password = password;
                    form.error = None;
                    form.summary = None;
                }
            }
            Message::PickKdbxSource => {
                return Task::perform(folder_picker::pick_kdbx_file(), Message::KdbxSourcePicked);
            }
            Message::KdbxSourcePicked(result) => match result {
                Ok(Some(path)) => {
                    if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                        form.source = path.display().to_string();
                        form.error = None;
                        form.summary = None;
                    }
                }
                Ok(None) => {}
                Err(error) => {
                    if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                        form.error = Some(error);
                    }
                }
            },
            Message::PickKdbxKeyFile => {
                return Task::perform(folder_picker::pick_key_file(), Message::KdbxKeyFilePicked);
            }
            Message::KdbxKeyFilePicked(result) => match result {
                Ok(Some(path)) => {
                    if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                        form.key_file = path.display().to_string();
                        form.error = None;
                        form.summary = None;
                    }
                }
                Ok(None) => {}
                Err(error) => {
                    if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                        form.error = Some(error);
                    }
                }
            },
            Message::ToggleKdbxQuickAdd => {
                if let Some(UtilityView::Kdbx(form)) = &mut self.utility
                    && !form.running
                {
                    form.quick_add = !form.quick_add;
                    form.summary = None;
                }
            }
            Message::ToggleKdbxConfirmation => {
                if let Some(UtilityView::Kdbx(form)) = &mut self.utility
                    && !form.running
                {
                    form.confirmed = !form.confirmed;
                }
            }
            Message::SubmitKdbxImport => {
                let Some(UtilityView::Kdbx(form)) = &self.utility else {
                    return Task::none();
                };
                if form.running {
                    return Task::none();
                }
                if self.handle.is_none() {
                    self.after_authentication = Some(PendingAction::ImportKdbx(form.clone()));
                    return self.begin_authentication();
                }
                return self.begin_kdbx_import(form.clone());
            }
            Message::KdbxFinished { generation, result } => {
                if generation != self.workflow_generation {
                    return Task::none();
                }
                match *result {
                    Ok((outcome, tree)) => {
                        self.navigation.replace(&tree);
                        self.tree_state = tree_state_from_result(Ok(self.navigation.is_empty()));
                        let summary = format!(
                            "KDBX import: {} added, {} updated, {} unchanged, {} skipped.",
                            outcome.added(),
                            outcome.updated(),
                            outcome.unchanged(),
                            outcome.skipped()
                        );
                        if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                            form.running = false;
                            form.error = None;
                            form.summary = Some(summary.clone());
                            form.password = Zeroizing::new(String::new());
                            form.confirmed = false;
                        }
                        self.status = summary;
                    }
                    Err(error) => {
                        if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                            form.running = false;
                            form.error = Some(error.clone());
                        }
                        self.status = format!("KDBX import failed: {error}");
                    }
                }
            }
            #[cfg(target_os = "macos")]
            Message::PollNativeMenu => {
                if self.native_menu.is_none() {
                    match NativeMenu::install(self.action_context()) {
                        Ok(menu) => self.native_menu = Some(menu),
                        Err(error) => {
                            self.status = format!("Native menu unavailable: {error}");
                            return Task::none();
                        }
                    }
                }
                let context = self.action_context();
                if let Some(menu) = &self.native_menu {
                    menu.sync(context);
                }
                if let Some(action) = self.native_menu.as_ref().and_then(NativeMenu::poll) {
                    return self.invoke_action(action);
                }
            }
            Message::StartupLoaded(result) => match *result {
                Ok((storage, session, key)) => {
                    self.storage = Some(storage);
                    self.session = Some(session);
                    self.key = Some(key);
                    self.authentication = AuthenticationView::Locked;
                    self.status = "Entry names remain available while protected content is locked."
                        .to_owned();
                    return self.begin_tree_refresh();
                }
                Err(error) => {
                    self.tree_state = TreeState::Error(error.clone());
                    self.authentication = AuthenticationView::Unavailable(error.clone());
                    self.status = error;
                }
            },
            Message::TreeLoaded {
                generation,
                completion,
            } => {
                let Some(result) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.tree_generation {
                    return Task::none();
                }
                match result {
                    Ok(model) => {
                        self.navigation.replace(&model);
                        if let Some(id) = self.selection_after_refresh.take() {
                            let _selected = self.navigation.select_id(&id);
                        }
                        self.tree_state = tree_state_from_result(Ok(self.navigation.is_empty()));
                        self.status = if self.tree_state == TreeState::Empty {
                            "The password store is empty.".to_owned()
                        } else {
                            "Password-store tree refreshed.".to_owned()
                        };
                    }
                    Err(error) => {
                        self.tree_state = tree_state_from_result(Err(error.clone()));
                        self.status = format!("Tree refresh failed: {error}");
                    }
                }
            }
            Message::SidebarActivate(id) => {
                self.context_target = None;
                self.touch_user_activity();
                self.pane_focus = PaneFocus::Sidebar;
                let intent = self.navigation.activate(id);
                return self.handle_navigation(intent);
            }
            Message::SidebarContext(id) => {
                let _selected = self.navigation.select_id(&id);
                self.context_target = Some(id);
                self.pane_focus = PaneFocus::Sidebar;
            }
            Message::SidebarContextAction(id, action) => {
                let _selected = self.navigation.select_id(&id);
                self.context_target = None;
                return self.invoke_action(action);
            }
            Message::SidebarNavigate(key) => {
                self.touch_user_activity();
                if self.palette.is_open() {
                    return match key {
                        NavigationKey::Previous => {
                            self.palette
                                .move_selection(false, self.palette.results().len());
                            self.scroll_palette_selection()
                        }
                        NavigationKey::Next => {
                            self.palette
                                .move_selection(true, self.palette.results().len());
                            self.scroll_palette_selection()
                        }
                        NavigationKey::Activate => self
                            .palette
                            .selected_action()
                            .map_or_else(Task::none, |action| self.invoke_palette_action(action)),
                        NavigationKey::First => {
                            self.palette.select_first();
                            self.scroll_palette_selection()
                        }
                        NavigationKey::Last => {
                            self.palette.select_last(self.palette.results().len());
                            self.scroll_palette_selection()
                        }
                        NavigationKey::Collapse | NavigationKey::Expand => Task::none(),
                    };
                }
                match self.pane_focus {
                    PaneFocus::Sidebar => {
                        let intent = self.navigation.navigate(key);
                        return self.handle_navigation(intent);
                    }
                    PaneFocus::Content if self.content_mode == ContentMode::Viewer => {
                        return self.navigate_viewer(key);
                    }
                    PaneFocus::Content => {}
                }
            }
            Message::TogglePaneFocus => {
                self.touch_user_activity();
                self.pane_focus = match self.pane_focus {
                    PaneFocus::Sidebar => PaneFocus::Content,
                    PaneFocus::Content => PaneFocus::Sidebar,
                };
            }
            Message::PaneResized(event) => {
                self.panes
                    .resize(event.split, event.ratio.clamp(0.18, 0.55));
            }
            Message::OpenEntry => {
                self.pane_focus = PaneFocus::Content;
                let entry = self.entry_path.trim().to_owned();
                if entry.is_empty() {
                    self.status = "Enter an entry path first.".to_owned();
                } else {
                    return self.request_action(PendingAction::OpenEntry(entry));
                }
            }
            Message::OpenFinished {
                generation,
                entry,
                completion,
            } => {
                let Some(result) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.operation_generation {
                    return Task::none();
                }
                match result {
                    Ok(document) => {
                        let generate_totp = document_has_single_totp(&document);
                        self.sensitive.otp = None;
                        self.sensitive.otp_uri = None;
                        self.sensitive.otp_qr = None;
                        self.entry_path = entry.clone();
                        let _selected = self.navigation.select_entry_path(&entry);
                        self.pane_focus = PaneFocus::Content;
                        self.editor = Some(EntryEditor::new(document));
                        self.content_mode = ContentMode::Viewer;
                        self.conflict = false;
                        self.status = format!("Viewing {entry}");
                        if generate_totp {
                            return self.begin_otp_code_for(entry, false, false);
                        }
                    }
                    Err(error) => self.status = format!("Open failed: {error}"),
                }
            }
            Message::AuthenticationFinished { generation, result } => {
                if generation != self.authentication_generation {
                    if result.is_ok()
                        && let Some(session) = &self.session
                    {
                        let _ignored = session.manual_lock();
                    }
                    return Task::none();
                }
                match result {
                    Ok(handle) => match handle.remaining_time() {
                        Ok(remaining) => {
                            self.handle = Some(handle);
                            self.authentication = AuthenticationView::Unlocked(remaining);
                            self.status = "Protected content is unlocked.".to_owned();
                            if let Some(action) = self.after_authentication.take() {
                                return self.execute_action(action);
                            }
                        }
                        Err(error) => self.authentication_lost(error.to_string()),
                    },
                    Err(error) => {
                        self.authentication_lost(error.clone());
                        self.status = format!("Authentication failed: {error}");
                    }
                }
            }
            Message::FieldChanged(id, value) => {
                self.pane_focus = PaneFocus::Content;
                self.edit(|editor| {
                    editor.select(id);
                    editor.replace_value(id, SecretBytes::new(value.as_bytes().to_vec()))
                });
            }
            Message::FieldEdited(id, action) => {
                self.pane_focus = PaneFocus::Content;
                self.edit(|editor| editor.edit_multiline(id, action));
            }
            Message::AddFieldLine(id, line) => {
                self.pane_focus = PaneFocus::Content;
                self.edit(|editor| editor.add_value_line_after(id, line));
            }
            Message::AddAfter(id) => self.edit(|editor| editor.add_after(id)),
            Message::Remove(id) => self.edit(|editor| editor.remove(id)),
            Message::MoveUp(id) => self.edit(|editor| editor.move_up(id)),
            Message::MoveDown(id) => self.edit(|editor| editor.move_down(id)),
            Message::BeginEdit => {
                if authentication_allows_content(&self.authentication)
                    && let Some(editor) = &self.editor
                {
                    self.content_mode = ContentMode::Editor;
                    self.status = format!("Editing {}", editor.entry());
                }
            }
            Message::SelectField(id) => {
                self.pane_focus = PaneFocus::Content;
                if let Some(editor) = self.editor.as_mut() {
                    editor.select(id);
                }
            }
            Message::RequestGenerate(id) => {
                let has_value = self
                    .editor
                    .as_ref()
                    .and_then(|editor| editor.document().field(id))
                    .is_some_and(|field| !field.value().is_empty());
                self.generation_form = Some(GenerateForm::new(id, has_value));
            }
            Message::GenerateLengthChanged(length) => {
                if let Some(form) = &mut self.generation_form {
                    form.length = length;
                    form.error = None;
                }
            }
            Message::ToggleGenerateSymbols => {
                if let Some(form) = &mut self.generation_form {
                    form.no_symbols = !form.no_symbols;
                    form.error = None;
                }
            }
            Message::ToggleGenerateConfirmation => {
                if let Some(form) = &mut self.generation_form {
                    form.confirmed = !form.confirmed;
                    form.error = None;
                }
            }
            Message::SubmitGenerate => {
                let Some(form) = self.generation_form.clone() else {
                    return Task::none();
                };
                match form.generate() {
                    Ok(password) => {
                        self.generation_form = None;
                        self.edit(|editor| editor.replace_value(form.id, password));
                    }
                    Err(error) => {
                        if let Some(form) = &mut self.generation_form {
                            form.error = Some(error);
                        }
                    }
                }
            }
            Message::CancelGenerate => self.generation_form = None,
            Message::Copy(id) => {
                if !authentication_allows_content(&self.authentication) {
                    return Task::none();
                }
                let Some(editor) = &self.editor else {
                    return Task::none();
                };
                if self.storage.is_none() {
                    return Task::none();
                }
                let value = match editor.copy_value(id) {
                    Ok(value) => value,
                    Err(error) => {
                        self.status = error.to_string();
                        return Task::none();
                    }
                };
                return self.begin_secret_copy(value);
            }
            Message::CopyFinished { generation, result } => {
                if self.sensitive.finish_copy(generation) {
                    self.status = result.unwrap_or_else(|error| format!("Clipboard: {error}"));
                }
            }
            Message::SaveFinished {
                generation,
                completion,
            } => {
                let Some((editor, result)) = take_completion(&completion) else {
                    return Task::none();
                };
                if generation != self.operation_generation {
                    return Task::none();
                }
                self.saving = false;
                match result {
                    Ok(outcome) => {
                        self.editor = None;
                        self.conflict = false;
                        if let Some(action) = self.after_save.take() {
                            return self.execute_action(action);
                        }
                        return self.begin_open(outcome.path().to_string());
                    }
                    Err(error) => {
                        self.conflict = error.kind() == DesktopErrorKind::Conflict;
                        self.status = format!("Save failed: {error}. Draft retained.");
                        self.editor = Some(editor);
                        self.confirmation = self.after_save.take();
                    }
                }
            }
            Message::RequestReload => {
                if let Some(entry) = self.editor.as_ref().map(EntryEditor::entry) {
                    return self.request_action(PendingAction::Reload(entry));
                }
            }
            Message::RequestClose(id) => {
                return self.request_action(PendingAction::CloseWindow(id));
            }
            Message::ConfirmSave => {
                self.after_save = self.confirmation.take();
                return self.begin_save();
            }
            Message::ConfirmDiscard => {
                let action = self.confirmation.take();
                self.editor = None;
                self.content_mode = ContentMode::Viewer;
                self.conflict = false;
                if let Some(action) = action {
                    return self.execute_action(action);
                }
            }
            Message::CancelDiscard => self.confirmation = None,
            Message::ReloadConflict => {
                if let Some(entry) = self.editor.as_ref().map(EntryEditor::entry) {
                    self.editor = None;
                    self.content_mode = ContentMode::Viewer;
                    self.conflict = false;
                    return self.execute_action(PendingAction::Reload(entry));
                }
            }
            Message::KeepConflictDraft => self.conflict = false,
            Message::UserActivity => self.touch_user_activity(),
            Message::Tick => {
                self.poll_git_progress();
                if let Some(session) = &self.session {
                    match poll_lease(session, &mut self.handle, &mut self.sensitive) {
                        Ok(LeasePoll::Active(remaining)) => {
                            self.authentication = AuthenticationView::Unlocked(remaining);
                        }
                        Ok(LeasePoll::Expired) => {
                            self.authentication_lost("the authentication lease expired".to_owned());
                        }
                        Ok(LeasePoll::Idle) => {}
                        Err(error) => self.authentication_lost(error.to_string()),
                    }
                }
                if let Some(remaining) = self.sensitive.clipboard_remaining(Instant::now()) {
                    self.status = format!("Clipboard cleanup in {remaining}s.");
                }
                let unix_seconds = current_unix_seconds().ok();
                let refresh_totp = unix_seconds.and_then(|now| {
                    self.sensitive.otp.as_mut().and_then(|display| {
                        display.observed_at = now;
                        (display.validity.counter().is_none()
                            && display.remaining_at(now) == Some(0))
                        .then(|| display.entry.clone())
                    })
                });
                if let Some(entry) = refresh_totp
                    && self
                        .editor
                        .as_ref()
                        .is_some_and(|editor| editor.entry() == entry)
                    && !self.otp_pending
                    && self.handle.is_some()
                {
                    return self.begin_otp_code_for(entry, false, false);
                }
            }
            Message::Lock => {
                self.authentication_generation = self.authentication_generation.wrapping_add(1);
                let result = self
                    .session
                    .as_ref()
                    .map_or(Ok(()), NativeAuthenticationSession::manual_lock);
                self.authentication_lost("manually locked".to_owned());
                if let Err(error) = result {
                    self.authentication = AuthenticationView::Unavailable(error.to_string());
                    self.status = format!("Lock failed: {error}");
                }
            }
        }
        Task::none()
    }

    fn action_context(&self) -> ActionContext {
        let focused = self
            .editor
            .as_ref()
            .and_then(|editor| editor.focused().and_then(|id| editor.document().field(id)));
        ActionContext {
            storage_ready: self.storage.is_some(),
            tree_loading: self.tree_state == TreeState::Loading,
            unlocked: authentication_allows_content(&self.authentication),
            document_open: self.editor.is_some(),
            editing: self.content_mode == ContentMode::Editor,
            dirty: self.editor.as_ref().is_some_and(EntryEditor::is_dirty),
            saving: self.saving,
            switching_vault: self.switching_vault,
            modal_open: self.confirmation.is_some()
                || self.generation_form.is_some()
                || self.utility.is_some(),
            focused_field: focused.is_some(),
            focused_generatable: focused.is_some_and(|field| {
                field.metadata().sensitivity() == EntrySensitivity::Sensitive
                    && field.metadata().kind() != EntryFieldKind::OtpUri
            }),
            focused_otp: focused.is_some_and(|field| field.metadata().otp().is_some()),
            entry_path: !self.entry_path.trim().is_empty(),
            selected_object: self.navigation.selected().is_some(),
            git_running: self.git_control.is_some(),
        }
    }

    fn invoke_action(&mut self, action: UiAction) -> Task<Message> {
        self.open_menu = None;
        if !action::enabled(action, self.action_context()) {
            if self.content_mode == ContentMode::Editor
                && matches!(
                    action,
                    UiAction::Undo
                        | UiAction::Redo
                        | UiAction::Cut
                        | UiAction::CopyField
                        | UiAction::Paste
                )
            {
                return Task::none();
            }
            self.status = format!(
                "{} is unavailable in the current state.",
                action::spec_for(action).label
            );
            return Task::none();
        }
        match action {
            UiAction::About => self.utility = Some(UtilityView::About),
            UiAction::Settings => {
                if let Some(storage) = &self.storage {
                    self.utility = Some(UtilityView::Settings(SettingsForm::new(storage)));
                    return iced::widget::operation::focus(settings_vault_id());
                }
            }
            UiAction::InitializeStore | UiAction::NewFolder => {
                let Some(storage) = &self.storage else {
                    return Task::none();
                };
                let kind = if action == UiAction::InitializeStore {
                    RecipientWorkflowKind::InitializeStore
                } else {
                    RecipientWorkflowKind::NewFolder
                };
                let path = if kind == RecipientWorkflowKind::NewFolder {
                    self.entry_path
                        .trim()
                        .rsplit_once('/')
                        .map_or_else(String::new, |(parent, _)| format!("{parent}/"))
                } else {
                    String::new()
                };
                match RecipientForm::new(kind, storage, path) {
                    Ok(form) => self.utility = Some(UtilityView::Recipients(form)),
                    Err(error) => self.status = format!("Cannot load recipients: {error}"),
                }
            }
            UiAction::NewEntry => {
                self.utility = Some(UtilityView::NewEntry(NewEntryForm::new(
                    self.entry_path.trim().to_owned(),
                )));
            }
            UiAction::Find => {
                self.utility = Some(UtilityView::Search(SearchForm::new(SearchMode::Names)));
            }
            UiAction::SearchContents => {
                self.utility = Some(UtilityView::Search(SearchForm::new(SearchMode::Contents)));
            }
            UiAction::Help => self.utility = Some(UtilityView::Help),
            UiAction::CommandPalette => return self.toggle_palette(),
            UiAction::OpenFolder => {
                let initial = self
                    .storage
                    .as_ref()
                    .map(|storage| storage.vault().to_owned());
                self.status = "Choose a password-store folder…".to_owned();
                return Task::perform(folder_picker::pick_folder(initial), Message::FolderPicked);
            }
            UiAction::OpenEntry => return self.update(Message::OpenEntry),
            UiAction::Save => return self.begin_save(),
            UiAction::CloseWindow | UiAction::Quit | UiAction::Minimize => {
                return window::oldest().map(move |id| Message::WindowResolved(action, id));
            }
            UiAction::CopyField | UiAction::CopyEditedField => {
                if let Some(id) = self.editor.as_ref().and_then(EntryEditor::focused) {
                    return self.update(Message::Copy(id));
                }
            }
            UiAction::TogglePaneFocus => return self.update(Message::TogglePaneFocus),
            UiAction::Refresh => return self.begin_tree_refresh(),
            UiAction::ReloadEntry => return self.update(Message::RequestReload),
            UiAction::EditEntry => return self.update(Message::BeginEdit),
            UiAction::GeneratePassword => {
                if let Some(id) = self.editor.as_ref().and_then(EntryEditor::focused) {
                    return self.update(Message::RequestGenerate(id));
                }
            }
            UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry => {
                let Some(source) = self.navigation.selected().cloned() else {
                    return Task::none();
                };
                let kind = match action {
                    UiAction::MoveEntry => MutationKind::Move,
                    UiAction::CopyEntry => MutationKind::Copy,
                    UiAction::DeleteEntry => MutationKind::Delete,
                    _ => unreachable!(),
                };
                self.utility = Some(UtilityView::Mutation(MutationForm::new(kind, source)));
            }
            UiAction::ImportOtp => {
                self.sensitive.otp = None;
                self.sensitive.otp_uri = None;
                self.sensitive.otp_qr = None;
                self.utility = Some(UtilityView::Otp(OtpForm::new(
                    self.entry_path.trim().to_owned(),
                )));
            }
            UiAction::ImportKdbx => {
                self.utility = Some(UtilityView::Kdbx(KdbxForm::default()));
            }
            UiAction::GenerateOtp
            | UiAction::CopyOtp
            | UiAction::ShowOtpUri
            | UiAction::CopyOtpUri
            | UiAction::ShowOtpQr
            | UiAction::RemoveOtp => {
                let Some(editor) = &self.editor else {
                    return Task::none();
                };
                let Some(otp_kind) = editor
                    .focused()
                    .and_then(|id| editor.document().field(id))
                    .and_then(|field| field.metadata().otp())
                    .map(|otp| otp.kind())
                else {
                    return Task::none();
                };
                let entry = editor.entry();
                if matches!(action, UiAction::GenerateOtp | UiAction::CopyOtp)
                    && otp_kind == OtpKind::Totp
                {
                    return self.begin_otp_code_for(entry, action == UiAction::CopyOtp, false);
                }
                self.utility = Some(UtilityView::Otp(OtpForm::new(entry)));
                match action {
                    UiAction::GenerateOtp | UiAction::CopyOtp if otp_kind == OtpKind::Hotp => {
                        if let Some(UtilityView::Otp(form)) = &mut self.utility {
                            form.copy_after_code = action == UiAction::CopyOtp;
                            form.hotp_confirmation = true;
                        }
                        self.status =
                            "Confirm HOTP generation; storage will commit the advanced counter."
                                .to_owned();
                    }
                    UiAction::GenerateOtp | UiAction::CopyOtp => unreachable!(),
                    UiAction::ShowOtpUri => return self.begin_otp_uri(false, false),
                    UiAction::CopyOtpUri => return self.begin_otp_uri(false, true),
                    UiAction::ShowOtpQr => return self.begin_otp_uri(true, false),
                    UiAction::RemoveOtp => {
                        self.status =
                            "Confirm OTP removal; the entry change will be committed.".to_owned();
                    }
                    _ => unreachable!(),
                }
            }
            UiAction::GitStatus | UiAction::GitPull | UiAction::GitPush | UiAction::GitSync => {
                let request = match action {
                    UiAction::GitStatus => DesktopGitRequest::Refresh,
                    UiAction::GitPull => DesktopGitRequest::Pull,
                    UiAction::GitPush => DesktopGitRequest::Push,
                    UiAction::GitSync => DesktopGitRequest::Sync,
                    _ => unreachable!(),
                };
                self.utility = Some(UtilityView::Git(GitForm::default()));
                let pending = PendingAction::Git(request.clone());
                return if request.changes_worktree() {
                    self.request_action(pending)
                } else {
                    self.execute_action(pending)
                };
            }
            UiAction::Lock => return self.update(Message::Lock),
            UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste => {}
        }
        Task::none()
    }

    fn toggle_palette(&mut self) -> Task<Message> {
        if self.palette.is_open() {
            self.palette.close();
            return self.restore_focus();
        }
        let context = self.action_context();
        if let Some(reason) = action::disabled_reason(UiAction::CommandPalette, context) {
            self.status = reason.to_owned();
            return Task::none();
        }
        self.open_menu = None;
        self.utility = None;
        self.palette.open();
        iced::widget::operation::focus(command_palette_input_id())
    }

    fn invoke_palette_action(&mut self, selected: UiAction) -> Task<Message> {
        if !self.palette.is_open() {
            return Task::none();
        }
        let context = self.action_context();
        if let Some(reason) = action::disabled_reason(selected, context) {
            self.status = format!("{}: {reason}.", action::spec_for(selected).label);
            return Task::none();
        }
        self.palette.close();
        let restore = self.restore_focus();
        let action = self.invoke_action(selected);
        Task::batch([restore, action])
    }

    fn scroll_palette_selection(&self) -> Task<Message> {
        let result_count = self.palette.results().len();
        let y = if result_count <= 1 {
            0.0
        } else {
            self.palette.selected() as f32 / (result_count - 1) as f32
        };
        iced::widget::operation::snap_to(
            command_palette_scroll_id(),
            scrollable::RelativeOffset { x: 0.0, y },
        )
    }

    fn restore_focus(&self) -> Task<Message> {
        match self.pane_focus {
            PaneFocus::Sidebar => iced::widget::operation::focus_next(),
            PaneFocus::Content => iced::widget::operation::focus(
                self.editor
                    .as_ref()
                    .and_then(EntryEditor::focused)
                    .map_or_else(content_focus_id, editor_field_input_id),
            ),
        }
    }

    fn begin_tree_refresh(&mut self) -> Task<Message> {
        let Some(storage) = self.storage.clone() else {
            return Task::none();
        };
        self.tree_generation = self.tree_generation.wrapping_add(1);
        let generation = self.tree_generation;
        self.tree_state = TreeState::Loading;
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(
                    storage.tree().map_err(|error| error.to_string()),
                )))
            },
            move |completion| Message::TreeLoaded {
                generation,
                completion,
            },
        )
    }

    fn handle_navigation(&mut self, intent: NavigationIntent) -> Task<Message> {
        match intent {
            NavigationIntent::OpenEntry(entry) => {
                self.request_action(PendingAction::OpenEntry(entry))
            }
            NavigationIntent::None => iced::widget::operation::snap_to(
                sidebar_scroll_id(),
                scrollable::RelativeOffset {
                    x: 0.0,
                    y: self.navigation.selected_ratio(),
                },
            ),
        }
    }

    fn request_action(&mut self, action: PendingAction) -> Task<Message> {
        if self.git_control.is_some() {
            self.status = "Cancel or wait for the active Git operation first.".to_owned();
            return Task::none();
        }
        if self.switching_vault && matches!(action, PendingAction::CloseWindow(_)) {
            self.status =
                "Wait for vault validation to finish before closing IronStorage.".to_owned();
            return Task::none();
        }
        if self.saving {
            self.after_save = Some(action);
            self.status = "Waiting for the active save to finish…".to_owned();
            return Task::none();
        }
        if dirty_decision(self.editor.as_ref()) == DirtyDecision::Confirm {
            if let Some(entry) = self.editor.as_ref().map(EntryEditor::entry) {
                let _restored = self.navigation.select_entry_path(&entry);
            }
            self.confirmation = Some(action);
            Task::none()
        } else {
            self.execute_action(action)
        }
    }

    fn execute_action(&mut self, action: PendingAction) -> Task<Message> {
        match action {
            PendingAction::OpenVault(path) => self.begin_vault_switch(path),
            PendingAction::OpenEntry(entry) | PendingAction::Reload(entry) => {
                self.editor = None;
                self.content_mode = ContentMode::Viewer;
                self.begin_open(entry)
            }
            PendingAction::CloseWindow(id) => {
                self.sensitive.clear();
                window::close(id)
            }
            PendingAction::ApplyRecipients(form) => self.begin_recipient_workflow(form),
            PendingAction::CreateEntry(form) => self.begin_create_entry(form),
            PendingAction::SearchContents(request) => {
                self.begin_search(SearchRequest::Contents(request))
            }
            PendingAction::Mutate(form) if self.handle.is_none() => {
                self.after_authentication = Some(PendingAction::Mutate(form));
                self.begin_authentication()
            }
            PendingAction::Mutate(form) => self.begin_mutation(form),
            PendingAction::Git(request)
                if request.requires_authentication() && self.handle.is_none() =>
            {
                self.after_authentication = Some(PendingAction::Git(request));
                self.begin_authentication()
            }
            PendingAction::Git(request) => self.begin_git(request),
            PendingAction::ImportKdbx(form) if self.handle.is_none() => {
                self.after_authentication = Some(PendingAction::ImportKdbx(form));
                self.begin_authentication()
            }
            PendingAction::ImportKdbx(form) => self.begin_kdbx_import(form),
        }
    }

    fn begin_kdbx_import(&mut self, form: KdbxForm) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let (request, password) = match form.request() {
            Ok(request) => request,
            Err(error) => {
                if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
                    form.error = Some(error);
                }
                return Task::none();
            }
        };
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        let generation = self.workflow_generation;
        if let Some(UtilityView::Kdbx(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
            form.summary = None;
        }
        self.status = "Importing KeePass database through crates/storage…".to_owned();
        Task::perform(
            async move {
                Box::new(
                    storage
                        .import_kdbx_active(&handle, &request, password)
                        .map_err(|error| error.to_string()),
                )
            },
            move |result| Message::KdbxFinished { generation, result },
        )
    }

    fn update_search_form(&mut self, update: impl FnOnce(&mut SearchForm)) {
        if let Some(UtilityView::Search(form)) = &mut self.utility
            && !form.running
            && form.mode == SearchMode::Contents
        {
            update(form);
            form.error = None;
            form.results = None;
        }
    }

    fn begin_search(&mut self, request: SearchRequest) -> Task<Message> {
        let Some(storage) = self.storage.clone() else {
            return Task::none();
        };
        let handle = self.handle.clone();
        if matches!(request, SearchRequest::Contents(_)) && handle.is_none() {
            if let SearchRequest::Contents(request) = request {
                self.after_authentication = Some(PendingAction::SearchContents(request));
            }
            return self.begin_authentication();
        }
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        let generation = self.workflow_generation;
        if let Some(UtilityView::Search(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
            form.results = None;
        }
        self.status = match request {
            SearchRequest::Names(_) => "Searching entry names…".to_owned(),
            SearchRequest::Contents(_) => "Searching decrypted entry contents…".to_owned(),
        };
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(match request {
                    SearchRequest::Names(request) => storage
                        .find(&request)
                        .map(SearchResults::Names)
                        .map_err(|error| error.to_string()),
                    SearchRequest::Contents(request) => storage
                        .grep_active(
                            &handle.expect("content search requires authentication"),
                            &request,
                        )
                        .map(SearchResults::Contents)
                        .map_err(|error| error.to_string()),
                })))
            },
            move |completion| Message::SearchFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_mutation(&mut self, form: MutationForm) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let request = match form.request() {
            Ok(request) => request,
            Err(error) => {
                if let Some(UtilityView::Mutation(form)) = &mut self.utility {
                    form.error = Some(error);
                }
                return Task::none();
            }
        };
        let overwrite = if form.kind == MutationKind::Delete || form.overwrite {
            OverwriteDecision::Allow
        } else {
            OverwriteDecision::Decline
        };
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        let generation = self.workflow_generation;
        if let Some(UtilityView::Mutation(current)) = &mut self.utility {
            current.running = true;
            current.error = None;
        }
        self.status = "Applying storage-owned tree mutation…".to_owned();
        Task::perform(
            async move {
                Box::new(
                    storage
                        .mutate_active(&handle, &request, overwrite)
                        .map_err(|error| error.to_string()),
                )
            },
            move |result| Message::MutationFinished { generation, result },
        )
    }

    fn begin_git(&mut self, request: DesktopGitRequest) -> Task<Message> {
        let Some(storage) = self.storage.clone() else {
            return Task::none();
        };
        let handle = self.handle.clone();
        if request.requires_authentication() && handle.is_none() {
            self.after_authentication = Some(PendingAction::Git(request));
            return self.begin_authentication();
        }
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        let generation = self.workflow_generation;
        let progress = Arc::new(Mutex::new(None));
        let reported = Arc::clone(&progress);
        let control = GitOperationControl::new(move |phase| {
            if let Ok(mut current) = reported.lock() {
                *current = Some(phase);
            }
        });
        self.git_control = Some(control.clone());
        self.git_progress = Some(progress);
        if let Some(UtilityView::Git(form)) = &mut self.utility {
            form.progress = Some(GitProgressPhase::Validating);
            form.running = true;
            form.error = None;
        }
        self.status = format!("Git {}…", git_request_name(&request));
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(storage.git_operation(
                    handle.as_ref(),
                    &request,
                    &control,
                ))))
            },
            move |completion| Message::GitFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_otp_code(&mut self, copy: bool, confirm_hotp: bool) -> Task<Message> {
        let entry = match &self.utility {
            Some(UtilityView::Otp(form)) if !form.running => form.entry.trim().to_owned(),
            _ => return Task::none(),
        };
        self.begin_otp_code_for(entry, copy, confirm_hotp)
    }

    fn begin_otp_code_for(
        &mut self,
        entry: String,
        copy: bool,
        confirm_hotp: bool,
    ) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let unix_seconds = match current_unix_seconds() {
            Ok(unix_seconds) => unix_seconds,
            Err(error) => {
                if let Some(UtilityView::Otp(form)) = &mut self.utility {
                    form.error = Some(error.clone());
                }
                self.status = error;
                return Task::none();
            }
        };
        self.otp_generation = self.otp_generation.wrapping_add(1);
        let generation = self.otp_generation;
        self.otp_pending = true;
        if let Some(UtilityView::Otp(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
        }
        self.status = format!("Generating OTP code for {entry}…");
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(
                    storage
                        .otp_code_active(&handle, &entry, unix_seconds, confirm_hotp)
                        .map(|outcome| OtpTaskResult::Code {
                            entry,
                            copy,
                            observed_at: unix_seconds,
                            outcome,
                        }),
                )))
            },
            move |completion| Message::OtpFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_otp_uri(&mut self, qr: bool, copy: bool) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let entry = match &self.utility {
            Some(UtilityView::Otp(form)) if !form.running => form.entry.trim().to_owned(),
            _ => return Task::none(),
        };
        self.otp_generation = self.otp_generation.wrapping_add(1);
        let generation = self.otp_generation;
        self.otp_pending = true;
        if let Some(UtilityView::Otp(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
        }
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(
                    storage
                        .otp_uri_active(&handle, &entry, qr)
                        .map(|outcome| OtpTaskResult::Uri {
                            entry,
                            copy,
                            outcome,
                        }),
                )))
            },
            move |completion| Message::OtpFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_otp_import(&mut self, image: Option<SecretBytes>) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let (entry, uri, replace) = match &self.utility {
            Some(UtilityView::Otp(form)) if !form.running => (
                form.entry.trim().to_owned(),
                SecretBytes::new(form.uri.as_bytes().to_vec()),
                form.replace,
            ),
            _ => return Task::none(),
        };
        if entry.is_empty() {
            if let Some(UtilityView::Otp(form)) = &mut self.utility {
                form.error = Some("Enter an entry path.".to_owned());
            }
            return Task::none();
        }
        if image.is_none() && uri.expose().is_empty() {
            if let Some(UtilityView::Otp(form)) = &mut self.utility {
                form.error = Some("Enter an otpauth URI or choose a QR image.".to_owned());
            }
            return Task::none();
        }
        let replace = if replace {
            OverwriteDecision::Allow
        } else {
            OverwriteDecision::Decline
        };
        self.otp_generation = self.otp_generation.wrapping_add(1);
        let generation = self.otp_generation;
        self.otp_pending = true;
        if let Some(UtilityView::Otp(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
        }
        self.status = format!("Importing OTP into {entry}…");
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(
                    match image {
                        Some(image) => {
                            storage.import_otp_qr_active(&handle, &entry, image, replace)
                        }
                        None => storage.import_otp_active(&handle, &entry, uri, replace),
                    }
                    .map(OtpTaskResult::Imported),
                )))
            },
            move |completion| Message::OtpFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_otp_removal(&mut self) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let entry = match &self.utility {
            Some(UtilityView::Otp(form)) if !form.running => form.entry.trim().to_owned(),
            _ => return Task::none(),
        };
        self.otp_generation = self.otp_generation.wrapping_add(1);
        let generation = self.otp_generation;
        self.otp_pending = true;
        if let Some(UtilityView::Otp(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
        }
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some(
                    storage
                        .remove_otp_active(&handle, &entry)
                        .map(OtpTaskResult::Removed),
                )))
            },
            move |completion| Message::OtpFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_secret_copy(&mut self, value: SecretBytes) -> Task<Message> {
        let Some(storage) = &self.storage else {
            return Task::none();
        };
        let timeout = storage.clipboard_timeout();
        let (generation, cancel) = self.sensitive.begin_copy(timeout.duration());
        self.status = format!(
            "Copied; automatic clipboard cleanup in {}s.",
            timeout.duration().as_secs()
        );
        Task::perform(copy_to_clipboard(value, timeout, cancel), move |result| {
            Message::CopyFinished { generation, result }
        })
    }

    fn begin_recipient_workflow(&mut self, form: RecipientForm) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let request = match form.request() {
            Ok(request) => request,
            Err(error) => {
                if let Some(UtilityView::Recipients(form)) = &mut self.utility {
                    form.error = Some(error);
                }
                return Task::none();
            }
        };
        let default_key = form.default_key.clone();
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        let generation = self.workflow_generation;
        if let Some(UtilityView::Recipients(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
        }
        self.status = "Applying storage-owned recipient policy…".to_owned();
        Task::perform(
            async move {
                let result = (|| {
                    let key = storage
                        .key_infos()
                        .map_err(|error| error.to_string())?
                        .into_iter()
                        .find(|key| key.fingerprint().as_str() == default_key)
                        .ok_or_else(|| "the selected default GPG key is unavailable".to_owned())?;
                    let (storage, outcome) = storage
                        .apply_active_recipient_policy(&handle, &request, &default_key)
                        .map_err(|error| error.to_string())?;
                    Ok(RecipientSuccess {
                        storage,
                        key,
                        summary: RecipientSummary {
                            directory: if outcome.directory().as_path().as_os_str().is_empty() {
                                "store root".to_owned()
                            } else {
                                outcome.directory().as_path().display().to_string()
                            },
                            recipients: outcome.recipients().len(),
                            reencrypted: outcome.reencrypted_entries().len(),
                        },
                    })
                })();
                Box::new(result)
            },
            move |result| Message::RecipientFinished { generation, result },
        )
    }

    fn begin_create_entry(&mut self, form: NewEntryForm) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let entry = form.path.trim().to_owned();
        if entry.is_empty() {
            if let Some(UtilityView::NewEntry(form)) = &mut self.utility {
                form.error = Some("Enter an entry path.".to_owned());
            }
            return Task::none();
        }
        let password = match form.password() {
            Ok(password) => password,
            Err(error) => {
                if let Some(UtilityView::NewEntry(form)) = &mut self.utility {
                    form.error = Some(error);
                }
                return Task::none();
            }
        };
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        let generation = self.workflow_generation;
        if let Some(UtilityView::NewEntry(form)) = &mut self.utility {
            form.running = true;
            form.error = None;
        }
        self.status = format!("Preparing new entry draft for {entry}…");
        Task::perform(
            async move {
                Arc::new(Mutex::new(Some((
                    password,
                    storage
                        .create_active_document(&handle, &entry)
                        .map_err(|error| error.to_string()),
                ))))
            },
            move |completion| Message::CreateFinished {
                generation,
                completion,
            },
        )
    }

    fn begin_settings_save(&mut self) -> Task<Message> {
        let Some(storage) = self.storage.clone() else {
            return Task::none();
        };
        let settings = match &self.utility {
            Some(UtilityView::Settings(form)) if !form.saving => match form.settings(&storage) {
                Ok(settings) => settings,
                Err(error) => {
                    if let Some(UtilityView::Settings(form)) = &mut self.utility {
                        form.error = Some(error.clone());
                    }
                    self.status = error;
                    return Task::none();
                }
            },
            _ => return Task::none(),
        };
        self.settings_generation = self.settings_generation.wrapping_add(1);
        let generation = self.settings_generation;
        if let Some(UtilityView::Settings(form)) = &mut self.utility {
            form.saving = true;
            form.error = None;
        }
        self.status = "Validating and saving shared settings…".to_owned();
        Task::perform(
            async move {
                Box::new(
                    storage
                        .update_settings_and_bootstrap(settings)
                        .map(|bootstrap| bootstrap.into_parts())
                        .map_err(|error| error.to_string()),
                )
            },
            move |result| Message::SettingsFinished { generation, result },
        )
    }

    fn begin_vault_switch(&mut self, path: PathBuf) -> Task<Message> {
        let Some(storage) = self.storage.clone() else {
            self.status = "Load a valid shared configuration before opening a folder.".to_owned();
            return Task::none();
        };
        self.vault_generation = self.vault_generation.wrapping_add(1);
        let generation = self.vault_generation;
        self.switching_vault = true;
        self.status = format!("Validating password store at {}…", path.display());
        Task::perform(
            async move {
                Box::new(
                    storage
                        .switch_vault(&path)
                        .map_err(|error| error.to_string()),
                )
            },
            move |result| Message::VaultSwitched { generation, result },
        )
    }

    fn begin_open(&mut self, entry: String) -> Task<Message> {
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            self.after_authentication = Some(PendingAction::OpenEntry(entry));
            return self.begin_authentication();
        };
        self.operation_generation = self.operation_generation.wrapping_add(1);
        let generation = self.operation_generation;
        self.status = format!("Opening {entry}…");
        Task::perform(
            async move {
                let result = load_document(&storage, &entry, handle);
                let completion = Arc::new(Mutex::new(Some(result)));
                (entry, completion)
            },
            move |(entry, completion)| Message::OpenFinished {
                generation,
                entry,
                completion,
            },
        )
    }

    fn begin_authentication(&mut self) -> Task<Message> {
        let (Some(session), Some(key)) = (self.session.clone(), self.key.clone()) else {
            return Task::none();
        };
        if matches!(self.authentication, AuthenticationView::Authenticating) {
            return Task::none();
        }
        self.authentication_generation = self.authentication_generation.wrapping_add(1);
        let generation = self.authentication_generation;
        self.authentication = AuthenticationView::Authenticating;
        self.status = "Waiting for secure-storage authentication…".to_owned();
        Task::perform(
            async move {
                session
                    .authenticate(&key)
                    .map_err(|error| error.to_string())
            },
            move |result| Message::AuthenticationFinished { generation, result },
        )
    }

    fn begin_save(&mut self) -> Task<Message> {
        self.touch_user_activity();
        if self.content_mode != ContentMode::Editor
            || !authentication_allows_content(&self.authentication)
        {
            return Task::none();
        }
        let (Some(storage), Some(handle)) = (self.storage.clone(), self.handle.clone()) else {
            return Task::none();
        };
        let Some(editor) = self.editor.take() else {
            return Task::none();
        };
        if !editor.is_dirty() {
            self.editor = Some(editor);
            self.status = "The document has no changes to save.".to_owned();
            return Task::none();
        }
        self.operation_generation = self.operation_generation.wrapping_add(1);
        let generation = self.operation_generation;
        self.saving = true;
        self.status = format!("Saving {}…", editor.entry());
        Task::perform(
            async move {
                let result = storage.save_active_document(&handle, editor.document());
                Arc::new(Mutex::new(Some((editor, result))))
            },
            move |completion| Message::SaveFinished {
                generation,
                completion,
            },
        )
    }

    fn edit(&mut self, operation: impl FnOnce(&mut EntryEditor) -> Result<(), DocumentError>) {
        if self.content_mode != ContentMode::Editor
            || !authentication_allows_content(&self.authentication)
        {
            return;
        }
        let Some(editor) = self.editor.as_mut() else {
            return;
        };
        if let Err(error) = operation(editor) {
            self.status = error.to_string();
        } else {
            self.status = format!("Editing {} (unsaved changes)", editor.entry());
            self.conflict = false;
        }
    }

    fn navigate_viewer(&mut self, navigation: NavigationKey) -> Task<Message> {
        if !authentication_allows_content(&self.authentication) {
            return Task::none();
        }
        let Some(editor) = self.editor.as_mut() else {
            return Task::none();
        };
        match navigation {
            NavigationKey::Previous => editor.navigate(FieldNavigation::Previous),
            NavigationKey::Next => editor.navigate(FieldNavigation::Next),
            NavigationKey::First => editor.navigate(FieldNavigation::First),
            NavigationKey::Last => editor.navigate(FieldNavigation::Last),
            NavigationKey::Activate => {}
            NavigationKey::Collapse | NavigationKey::Expand => {}
        }
        iced::widget::operation::snap_to(
            viewer_scroll_id(),
            scrollable::RelativeOffset {
                x: 0.0,
                y: editor.focused_ratio(),
            },
        )
    }

    fn touch_user_activity(&mut self) {
        if let Some(handle) = &self.handle {
            match handle
                .touch_user_activity()
                .and_then(|()| handle.remaining_time())
            {
                Ok(remaining) => {
                    self.authentication = AuthenticationView::Unlocked(remaining);
                }
                Err(error) => self.authentication_lost(error.to_string()),
            }
        }
    }

    fn poll_git_progress(&mut self) {
        let phase = self
            .git_progress
            .as_ref()
            .and_then(|progress| progress.lock().ok().and_then(|phase| *phase));
        let Some(phase) = phase else {
            return;
        };
        if let Some(UtilityView::Git(form)) = &mut self.utility
            && form.progress != Some(phase)
        {
            form.progress = Some(phase);
            self.status = format!("Git {}… (Cancel remains available)", git_phase_name(phase));
        }
    }

    fn authentication_lost(&mut self, reason: String) {
        if let Some(control) = self.git_control.take() {
            control.cancel();
        }
        self.git_progress = None;
        self.operation_generation = self.operation_generation.wrapping_add(1);
        self.workflow_generation = self.workflow_generation.wrapping_add(1);
        self.handle = None;
        self.sensitive.clear();
        self.editor = None;
        self.content_mode = ContentMode::Viewer;
        self.saving = false;
        self.confirmation = None;
        self.after_save = None;
        self.after_authentication = None;
        self.generation_form = None;
        self.conflict = false;
        match &mut self.utility {
            Some(UtilityView::Recipients(form)) => {
                form.running = false;
                form.error = Some(reason.clone());
            }
            Some(UtilityView::NewEntry(form)) => {
                form.running = false;
                form.error = Some(reason.clone());
            }
            Some(UtilityView::Search(form)) if form.mode == SearchMode::Contents => {
                form.running = false;
                form.results = None;
                form.error = Some(reason.clone());
            }
            Some(UtilityView::Mutation(form)) => {
                form.running = false;
                form.error = Some(reason.clone());
            }
            Some(UtilityView::Git(form)) => {
                form.progress = None;
                form.running = false;
                form.error = Some(reason.clone());
            }
            Some(UtilityView::Otp(form)) => {
                form.running = false;
                form.hotp_confirmation = false;
                form.uri.clear();
                form.error = Some(reason.clone());
            }
            Some(UtilityView::Kdbx(form)) => {
                form.running = false;
                form.password = Zeroizing::new(String::new());
                form.confirmed = false;
                form.error = Some(reason.clone());
            }
            _ => {}
        }
        self.authentication = AuthenticationView::Locked;
        self.status = reason;
    }

    fn subscription(&self) -> Subscription<Message> {
        let mut subscriptions = vec![
            time::every(Duration::from_secs(1)).map(|_| Message::Tick),
            event::listen_with(|event, _status, _window| event_message(&event)),
            window::close_requests().map(Message::RequestClose),
        ];
        #[cfg(target_os = "macos")]
        subscriptions.push(time::every(Duration::from_millis(50)).map(|_| Message::PollNativeMenu));
        Subscription::batch(subscriptions)
    }

    fn view(&self) -> Element<'_, Message> {
        if let Some(action) = &self.confirmation {
            return confirmation_view(action);
        }
        if let Some(form) = &self.generation_form {
            return generation_view(form);
        }
        if let Some(utility) = &self.utility {
            return utility_view(self, utility);
        }

        let authentication = match &self.authentication {
            AuthenticationView::Loading => "Loading".to_owned(),
            AuthenticationView::Locked => "Locked".to_owned(),
            AuthenticationView::Authenticating => "Authenticating".to_owned(),
            AuthenticationView::Unlocked(remaining) => {
                format!("Unlocked · {}s", remaining.as_secs())
            }
            AuthenticationView::Unavailable(error) => format!("Unavailable · {error}"),
        };
        let panes = pane_grid(&self.panes, |_pane, kind, _maximized| {
            pane_grid::Content::new(match kind {
                PaneKind::Sidebar => sidebar_view(
                    &self.navigation,
                    &self.tree_state,
                    self.pane_focus == PaneFocus::Sidebar,
                    self.context_target.as_ref(),
                ),
                PaneKind::Content => content_view(self),
            })
        })
        .spacing(1)
        .min_size(140)
        .on_resize(8, Message::PaneResized);

        let vault = self.storage.as_ref().map_or_else(
            || "No vault".to_owned(),
            |storage| {
                storage
                    .vault()
                    .file_name()
                    .and_then(|name| name.to_str())
                    .map_or_else(
                        || storage.vault().display().to_string(),
                        |name| name.to_owned(),
                    )
            },
        );
        let top_bar = row![
            text(format!("IronStorage · {vault}"))
                .size(14)
                .width(Length::Fill),
            action_icon(Icon::Search, UiAction::Find),
            action_icon(Icon::Command, UiAction::CommandPalette),
            action_icon(Icon::Folder, UiAction::OpenFolder),
            action_icon(Icon::Settings, UiAction::Settings),
            action_icon(Icon::Power, UiAction::Lock),
        ]
        .align_y(iced::Alignment::Center)
        .spacing(2)
        .padding([5, 8]);
        let mut chrome = column![top_bar, platform_menu_bar(self)];
        if self.palette.is_open() {
            let shortcut = action::shortcut_label(UiAction::CommandPalette)
                .expect("the command palette has a registered shortcut");
            chrome = chrome
                .push(
                    text_input(
                        &format!("Search commands ({shortcut})"),
                        self.palette.query(),
                    )
                    .id(command_palette_input_id())
                    .on_input(Message::PaletteQueryChanged)
                    .padding(8),
                )
                .push(command_palette_results(self));
        }
        chrome = chrome.push(panes).push(
            row![
                text(authentication).size(12),
                text(&self.status).size(12).width(Length::Fill),
                text("Tab: next pane").size(12),
            ]
            .spacing(10)
            .padding([4, 8]),
        );

        container(chrome.height(Length::Fill))
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    }
}

fn sidebar_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("desktop-navigation-tree")
}

fn viewer_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("desktop-entry-viewer")
}

fn command_palette_input_id() -> iced::widget::Id {
    iced::widget::Id::new("desktop-command-palette")
}

fn command_palette_scroll_id() -> iced::widget::Id {
    iced::widget::Id::new("desktop-command-palette-results")
}

fn content_focus_id() -> iced::widget::Id {
    iced::widget::Id::new("desktop-entry-path")
}

fn settings_vault_id() -> iced::widget::Id {
    iced::widget::Id::new("desktop-settings-vault")
}

fn editor_field_input_id(id: EntryFieldId) -> iced::widget::Id {
    format!("desktop-entry-field-{}", id.value()).into()
}

fn action_hint(action: UiAction) -> String {
    let spec = action::spec_for(action);
    action::shortcut_label(action).map_or_else(
        || spec.label.to_owned(),
        |shortcut| format!("{} ({shortcut})", spec.label),
    )
}

#[derive(Clone, Copy)]
enum Icon {
    Add,
    Check,
    ChevronDown,
    ChevronRight,
    Close,
    Command,
    Copy,
    Delete,
    Down,
    Edit,
    Folder,
    FolderAdd,
    Generate,
    Link,
    Key,
    Move,
    Power,
    Qr,
    Refresh,
    Search,
    Settings,
    Up,
}

struct IconCanvas {
    icon: Icon,
    color: Option<Color>,
}

impl<Message> canvas::Program<Message> for IconCanvas {
    type State = ();

    fn draw(
        &self,
        _state: &Self::State,
        renderer: &Renderer,
        theme: &Theme,
        bounds: Rectangle,
        _cursor: mouse::Cursor,
    ) -> Vec<canvas::Geometry> {
        let mut frame = canvas::Frame::new(renderer, bounds.size());
        let path = canvas::Path::new(|path| match self.icon {
            Icon::Add => {
                path.move_to(Point::new(8.0, 2.0));
                path.line_to(Point::new(8.0, 14.0));
                path.move_to(Point::new(2.0, 8.0));
                path.line_to(Point::new(14.0, 8.0));
            }
            Icon::Check => {
                path.move_to(Point::new(2.0, 8.5));
                path.line_to(Point::new(6.0, 12.0));
                path.line_to(Point::new(14.0, 3.5));
            }
            Icon::ChevronDown => {
                path.move_to(Point::new(3.0, 5.5));
                path.line_to(Point::new(8.0, 10.5));
                path.line_to(Point::new(13.0, 5.5));
            }
            Icon::ChevronRight => {
                path.move_to(Point::new(5.5, 3.0));
                path.line_to(Point::new(10.5, 8.0));
                path.line_to(Point::new(5.5, 13.0));
            }
            Icon::Close => {
                path.move_to(Point::new(3.0, 3.0));
                path.line_to(Point::new(13.0, 13.0));
                path.move_to(Point::new(13.0, 3.0));
                path.line_to(Point::new(3.0, 13.0));
            }
            Icon::Command => {
                path.move_to(Point::new(2.0, 4.0));
                path.line_to(Point::new(6.0, 8.0));
                path.line_to(Point::new(2.0, 12.0));
                path.move_to(Point::new(8.0, 12.0));
                path.line_to(Point::new(14.0, 12.0));
            }
            Icon::Copy => {
                path.rectangle(Point::new(2.0, 2.0), Size::new(9.0, 9.0));
                path.rectangle(Point::new(5.0, 5.0), Size::new(9.0, 9.0));
            }
            Icon::Delete => {
                path.move_to(Point::new(3.0, 4.0));
                path.line_to(Point::new(13.0, 4.0));
                path.move_to(Point::new(6.0, 2.0));
                path.line_to(Point::new(10.0, 2.0));
                path.move_to(Point::new(4.5, 4.0));
                path.line_to(Point::new(5.5, 14.0));
                path.line_to(Point::new(10.5, 14.0));
                path.line_to(Point::new(11.5, 4.0));
                path.move_to(Point::new(7.0, 6.0));
                path.line_to(Point::new(7.0, 12.0));
                path.move_to(Point::new(9.0, 6.0));
                path.line_to(Point::new(9.0, 12.0));
            }
            Icon::Down => {
                path.move_to(Point::new(8.0, 2.0));
                path.line_to(Point::new(8.0, 12.0));
                path.move_to(Point::new(3.5, 8.0));
                path.line_to(Point::new(8.0, 12.5));
                path.line_to(Point::new(12.5, 8.0));
            }
            Icon::Edit => {
                path.move_to(Point::new(3.0, 12.5));
                path.line_to(Point::new(4.0, 8.5));
                path.line_to(Point::new(11.0, 1.5));
                path.line_to(Point::new(14.0, 4.5));
                path.line_to(Point::new(7.0, 11.5));
                path.line_to(Point::new(3.0, 12.5));
                path.move_to(Point::new(3.0, 14.0));
                path.line_to(Point::new(13.0, 14.0));
            }
            Icon::Folder | Icon::FolderAdd => {
                path.move_to(Point::new(1.5, 4.0));
                path.line_to(Point::new(6.0, 4.0));
                path.line_to(Point::new(7.5, 6.0));
                path.line_to(Point::new(14.5, 6.0));
                path.line_to(Point::new(13.0, 13.0));
                path.line_to(Point::new(2.5, 13.0));
                path.line_to(Point::new(1.5, 4.0));
                if matches!(self.icon, Icon::FolderAdd) {
                    path.move_to(Point::new(9.0, 9.5));
                    path.line_to(Point::new(13.0, 9.5));
                    path.move_to(Point::new(11.0, 7.5));
                    path.line_to(Point::new(11.0, 11.5));
                }
            }
            Icon::Generate => {
                path.rounded_rectangle(Point::new(2.0, 2.0), Size::new(12.0, 12.0), 2.0.into());
                for point in [
                    (5.0, 5.0),
                    (11.0, 5.0),
                    (8.0, 8.0),
                    (5.0, 11.0),
                    (11.0, 11.0),
                ] {
                    path.circle(Point::new(point.0, point.1), 0.5);
                }
            }
            Icon::Link => {
                path.rounded_rectangle(Point::new(1.5, 5.0), Size::new(8.5, 6.0), 3.0.into());
                path.rounded_rectangle(Point::new(6.0, 5.0), Size::new(8.5, 6.0), 3.0.into());
            }
            Icon::Key => {
                path.circle(Point::new(5.0, 5.0), 3.5);
                path.move_to(Point::new(7.5, 7.5));
                path.line_to(Point::new(14.0, 14.0));
                path.move_to(Point::new(10.5, 10.5));
                path.line_to(Point::new(12.5, 8.5));
                path.move_to(Point::new(12.5, 12.5));
                path.line_to(Point::new(14.0, 11.0));
            }
            Icon::Move => {
                path.move_to(Point::new(2.0, 13.5));
                path.line_to(Point::new(13.5, 2.0));
                path.move_to(Point::new(7.0, 2.0));
                path.line_to(Point::new(13.5, 2.0));
                path.line_to(Point::new(13.5, 8.5));
            }
            Icon::Power => {
                path.circle(Point::new(8.0, 8.5), 5.5);
                path.move_to(Point::new(8.0, 1.0));
                path.line_to(Point::new(8.0, 8.0));
            }
            Icon::Qr => {
                for point in [(2.0, 2.0), (9.5, 2.0), (2.0, 9.5)] {
                    path.rectangle(Point::new(point.0, point.1), Size::new(4.5, 4.5));
                }
                path.rectangle(Point::new(10.0, 10.0), Size::new(1.5, 1.5));
                path.rectangle(Point::new(13.0, 10.0), Size::new(1.5, 4.5));
                path.rectangle(Point::new(10.0, 13.0), Size::new(1.5, 1.5));
            }
            Icon::Refresh => {
                path.move_to(Point::new(13.5, 6.0));
                path.line_to(Point::new(13.5, 2.5));
                path.line_to(Point::new(10.0, 2.5));
                path.move_to(Point::new(13.0, 3.0));
                path.bezier_curve_to(
                    Point::new(9.0, 0.0),
                    Point::new(3.0, 2.0),
                    Point::new(2.5, 7.0),
                );
                path.bezier_curve_to(
                    Point::new(2.0, 12.0),
                    Point::new(8.0, 16.0),
                    Point::new(12.5, 12.0),
                );
            }
            Icon::Search => {
                path.circle(Point::new(6.5, 6.5), 4.5);
                path.move_to(Point::new(10.0, 10.0));
                path.line_to(Point::new(14.0, 14.0));
            }
            Icon::Settings => {
                path.circle(Point::new(8.0, 8.0), 3.0);
                path.circle(Point::new(8.0, 8.0), 5.5);
                for (from, to) in [
                    ((8.0, 0.5), (8.0, 2.5)),
                    ((8.0, 13.5), (8.0, 15.5)),
                    ((0.5, 8.0), (2.5, 8.0)),
                    ((13.5, 8.0), (15.5, 8.0)),
                ] {
                    path.move_to(Point::new(from.0, from.1));
                    path.line_to(Point::new(to.0, to.1));
                }
            }
            Icon::Up => {
                path.move_to(Point::new(8.0, 14.0));
                path.line_to(Point::new(8.0, 4.0));
                path.move_to(Point::new(3.5, 8.0));
                path.line_to(Point::new(8.0, 3.5));
                path.line_to(Point::new(12.5, 8.0));
            }
        });
        frame.stroke(
            &path,
            canvas::Stroke::default()
                .with_color(
                    self.color
                        .unwrap_or(theme.extended_palette().background.base.text),
                )
                .with_width(1.5)
                .with_line_cap(canvas::LineCap::Round)
                .with_line_join(canvas::LineJoin::Round),
        );
        vec![frame.into_geometry()]
    }
}

fn icon_view(icon: Icon) -> Element<'static, Message> {
    colored_icon_view(icon, None)
}

fn colored_icon_view(icon: Icon, color: Option<Color>) -> Element<'static, Message> {
    canvas(IconCanvas { icon, color })
        .width(Length::Fixed(16.0))
        .height(Length::Fixed(16.0))
        .into()
}

fn icon_control(icon: Icon, hint: String, message: Message) -> Element<'static, Message> {
    tooltip(
        button(icon_view(icon))
            .padding(6)
            .style(button::background)
            .on_press(message),
        container(text(hint).size(12))
            .padding([5, 8])
            .style(container::rounded_box),
        tooltip::Position::Bottom,
    )
    .gap(4)
    .delay(Duration::from_millis(350))
    .into()
}

const ENTRY_AREA_BACKGROUND: Color = Color::from_rgb8(18, 18, 20);
const ENTRY_FIELD_BACKGROUND: Color = Color::from_rgb8(24, 24, 27);
const ENTRY_FIELD_ACTIVE_BACKGROUND: Color = Color::from_rgb8(47, 47, 52);
const ENTRY_INPUT_BACKGROUND: Color = Color::from_rgb8(12, 12, 14);
const ENTRY_TEXT: Color = Color::from_rgb8(235, 235, 238);
const ENTRY_MUTED_TEXT: Color = Color::from_rgb8(165, 165, 172);
const ENTRY_BORDER: Color = Color::from_rgb8(72, 72, 78);
const ENTRY_FOCUSED_BORDER: Color = Color::from_rgb8(145, 145, 152);
const ENTRY_LABEL_WIDTH: f32 = 128.0;

fn entry_icon_control(icon: Icon, hint: String, message: Message) -> Element<'static, Message> {
    tooltip(
        button(colored_icon_view(icon, Some(ENTRY_TEXT)))
            .padding(6)
            .style(entry_icon_button)
            .on_press(message),
        container(text(hint).size(12))
            .padding([5, 8])
            .style(container::dark),
        tooltip::Position::Bottom,
    )
    .gap(4)
    .delay(Duration::from_millis(350))
    .into()
}

fn utility_icon_control(
    icon: Icon,
    hint: &'static str,
    message: Message,
    enabled: bool,
) -> Element<'static, Message> {
    let control = button(colored_icon_view(icon, Some(ENTRY_TEXT)))
        .padding(6)
        .style(entry_icon_button);
    tooltip(
        if enabled {
            control.on_press(message)
        } else {
            control
        },
        container(text(hint).size(12))
            .padding([5, 8])
            .style(container::dark),
        tooltip::Position::Bottom,
    )
    .gap(4)
    .delay(Duration::from_millis(350))
    .into()
}

fn utility_button<'a>(
    content: impl Into<Element<'a, Message>>,
) -> iced::widget::Button<'a, Message> {
    button(content).padding([6, 10]).style(entry_icon_button)
}

fn entry_icon_button(_theme: &Theme, status: button::Status) -> button::Style {
    button::Style {
        background: matches!(status, button::Status::Hovered | button::Status::Pressed)
            .then_some(Background::Color(ENTRY_FIELD_ACTIVE_BACKGROUND)),
        text_color: ENTRY_TEXT,
        ..button::Style::default()
    }
}

fn entry_area_style(_theme: &Theme) -> container::Style {
    container::Style::default()
        .background(ENTRY_AREA_BACKGROUND)
        .color(ENTRY_TEXT)
}

fn entry_field_style(_theme: &Theme, selected: bool) -> container::Style {
    container::Style::default()
        .background(if selected {
            ENTRY_FIELD_ACTIVE_BACKGROUND
        } else {
            ENTRY_FIELD_BACKGROUND
        })
        .color(ENTRY_TEXT)
}

fn entry_value_style(_theme: &Theme) -> container::Style {
    container::Style::default()
        .background(ENTRY_INPUT_BACKGROUND)
        .color(ENTRY_TEXT)
}

fn entry_input_style(_theme: &Theme, status: text_input::Status) -> text_input::Style {
    text_input::Style {
        background: Background::Color(ENTRY_INPUT_BACKGROUND),
        border: Border {
            radius: 3.0.into(),
            width: 1.0,
            color: if matches!(status, text_input::Status::Focused { .. }) {
                ENTRY_FOCUSED_BORDER
            } else {
                ENTRY_BORDER
            },
        },
        icon: ENTRY_MUTED_TEXT,
        placeholder: ENTRY_MUTED_TEXT,
        value: ENTRY_TEXT,
        selection: ENTRY_FIELD_ACTIVE_BACKGROUND,
    }
}

fn entry_editor_style(_theme: &Theme, status: text_editor::Status) -> text_editor::Style {
    text_editor::Style {
        background: Background::Color(ENTRY_INPUT_BACKGROUND),
        border: Border {
            radius: 3.0.into(),
            width: 1.0,
            color: if matches!(status, text_editor::Status::Focused { .. }) {
                ENTRY_FOCUSED_BORDER
            } else {
                ENTRY_BORDER
            },
        },
        placeholder: ENTRY_MUTED_TEXT,
        value: ENTRY_TEXT,
        selection: ENTRY_FIELD_ACTIVE_BACKGROUND,
    }
}

fn action_icon(icon: Icon, action: UiAction) -> Element<'static, Message> {
    icon_control(icon, action_hint(action), Message::Action(action))
}

fn selected_button(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.extended_palette();
    let mut style = button::text(theme, status);
    style.background = Some(Background::Color(palette.primary.weak.color));
    style.text_color = palette.primary.weak.text;
    style
}

fn command_palette_results(app: &App) -> Element<'_, Message> {
    let context = app.action_context();
    let results = app.palette.results();
    if results.is_empty() {
        return container(text("No matching commands"))
            .padding([8, 12])
            .width(Length::Fill)
            .into();
    }

    let mut rows = column![].spacing(2).padding([4, 8]);
    for (index, action) in results.into_iter().enumerate() {
        let spec = action::spec_for(action);
        let shortcut = action::shortcut_label(action).unwrap_or_default();
        let reason = action::disabled_reason(action, context);
        let content = row![
            text(spec.label).width(Length::Fill),
            text(shortcut).size(13),
        ]
        .spacing(12);
        let item = button(content)
            .width(Length::Fill)
            .style(if index == app.palette.selected() {
                selected_button
            } else {
                button::text
            });
        rows = rows.push(if reason.is_none() {
            item.on_press(Message::PaletteInvoke(action))
        } else {
            item
        });
    }
    container(
        scrollable(rows)
            .id(command_palette_scroll_id())
            .height(Length::Fixed(240.0)),
    )
    .width(Length::Fill)
    .into()
}

fn platform_menu_bar(app: &App) -> Element<'_, Message> {
    if cfg!(target_os = "macos") {
        return container(row![]).height(Length::Fixed(0.0)).into();
    }
    let mut headers = row![].spacing(2).padding([0, 8]);
    for group in MenuGroup::ALL {
        headers = headers.push(
            button(group.label())
                .on_press(Message::ToggleMenu(group))
                .style(if app.open_menu == Some(group) {
                    button::primary
                } else {
                    button::text
                }),
        );
    }
    let mut menu = column![headers].spacing(4);
    if let Some(group) = app.open_menu {
        let context = app.action_context();
        let mut actions = row![].spacing(4).padding([6, 8]);
        for spec in action::actions_in(group) {
            let label = action::shortcut_label(spec.action).map_or_else(
                || spec.label.to_owned(),
                |key| format!("{}  {key}", spec.label),
            );
            let item = button(text(label));
            actions = actions.push(if action::enabled(spec.action, context) {
                item.on_press(Message::Action(spec.action))
            } else {
                item
            });
        }
        menu = menu.push(
            scrollable(actions).direction(scrollable::Direction::Horizontal(
                scrollable::Scrollbar::default(),
            )),
        );
    }
    container(menu).width(Length::Fill).into()
}

fn utility_title(utility: &UtilityView) -> &'static str {
    match utility {
        UtilityView::About => "About IronStorage",
        UtilityView::Settings(_) => "Settings",
        UtilityView::Recipients(form) => match form.kind {
            RecipientWorkflowKind::InitializeStore => "Initialize Password Store",
            RecipientWorkflowKind::NewFolder => "Create Password Store Folder",
        },
        UtilityView::NewEntry(_) => "New Entry",
        UtilityView::Search(form) => match form.mode {
            SearchMode::Names => "Find Entries and Folders",
            SearchMode::Contents => "Search Decrypted Contents",
        },
        UtilityView::Mutation(form) => match form.kind {
            MutationKind::Move => "Move or Rename",
            MutationKind::Copy => "Copy Entry or Folder",
            MutationKind::Delete => "Delete Entry or Folder",
        },
        UtilityView::Git(_) => "Git Synchronization",
        UtilityView::Otp(_) => "One-Time Password",
        UtilityView::Kdbx(_) => "Import KeePass Database",
        UtilityView::Help => "IronStorage Help",
    }
}

fn utility_icon(utility: &UtilityView) -> Icon {
    match utility {
        UtilityView::About | UtilityView::Help => Icon::Key,
        UtilityView::Settings(_) => Icon::Settings,
        UtilityView::Recipients(_) => Icon::Key,
        UtilityView::NewEntry(_) => Icon::Add,
        UtilityView::Search(_) => Icon::Search,
        UtilityView::Mutation(form) => match form.kind {
            MutationKind::Move => Icon::Move,
            MutationKind::Copy => Icon::Copy,
            MutationKind::Delete => Icon::Delete,
        },
        UtilityView::Git(_) => Icon::Refresh,
        UtilityView::Otp(_) => Icon::Generate,
        UtilityView::Kdbx(_) => Icon::Down,
    }
}

fn utility_view<'a>(app: &'a App, utility: &'a UtilityView) -> Element<'a, Message> {
    let mut content = column![].spacing(10);
    match utility {
        UtilityView::About => {
            content = content
                .push(text(format!("Version {}", env!("CARGO_PKG_VERSION"))))
                .push(text("A native, pass-compatible password-store client."))
                .push(text("Compatible with pass and pass-otp."))
                .push(text("Copyright IronStorage contributors."))
                .push(text(
                    "Project license: pending selection. Third-party licenses are documented in DEPENDENCIES.md.",
                ));
        }
        UtilityView::Settings(form) => {
            let general = column![
                text("General").size(18),
                text("Password-store folder").size(13),
                row![
                    text_input("Vault path", &form.vault)
                        .id(settings_vault_id())
                        .on_input(Message::SettingsVaultChanged)
                        .on_submit(Message::SaveSettings)
                        .style(entry_input_style),
                    utility_icon_control(
                        Icon::Folder,
                        "Choose password-store folder",
                        Message::PickSettingsVault,
                        !form.saving,
                    ),
                ]
                .align_y(iced::Alignment::Center)
                .spacing(6),
                text("Default OpenPGP key fingerprint or identity").size(13),
                text_input("Default key", &form.default_key)
                    .on_input(Message::SettingsDefaultKeyChanged)
                    .on_submit(Message::SaveSettings)
                    .style(entry_input_style),
                text("Authentication inactivity timeout in seconds (1–86400)").size(13),
                text_input("Timeout seconds", &form.authentication_timeout)
                    .on_input(Message::SettingsTimeoutChanged)
                    .on_submit(Message::SaveSettings)
                    .style(entry_input_style),
            ]
            .spacing(8);
            content = content.push(
                container(general)
                    .padding(14)
                    .width(Length::Fill)
                    .style(|theme| entry_field_style(theme, false)),
            );

            let mut commands = column![
                row![
                    text("Command-line tools").size(18).width(Length::Fill),
                    utility_icon_control(
                        Icon::Link,
                        "Install CLI and TUI links",
                        Message::InstallCommandLinks,
                        !form.saving,
                    ),
                ]
                .align_y(iced::Alignment::Center),
                text(
                    "Install links to the CLI and TUI embedded in this application bundle in ~/.local/bin. Existing command files or links are replaced.",
                )
                .size(13),
            ]
            .spacing(8);
            if let Some(status) = &form.command_links_status {
                commands = commands.push(text(status).size(13));
            }
            content = content.push(
                container(commands)
                    .padding(14)
                    .width(Length::Fill)
                    .style(|theme| entry_field_style(theme, false)),
            );
            if let Some(storage) = &app.storage {
                content = content.push(
                    container(
                        column![
                            text("Configuration details").size(18),
                            text(format!(
                        "Shared configuration: {}",
                        storage.config_source().display()
                    ))
                            .size(13),
                            text(format!(
                                "Clipboard cleanup remains {} seconds.",
                                storage.clipboard_timeout().duration().as_secs()
                            ))
                            .size(13),
                            text(
                                "Save validates the repository, key material, entry tree, and authentication session before atomically replacing config.toml.",
                            )
                            .size(13),
                        ]
                        .spacing(6),
                    )
                    .padding(14)
                    .width(Length::Fill)
                    .style(entry_value_style),
                );
            }
            if let Some(error) = &form.error {
                content = content.push(text(format!("Settings error: {error}")));
            }
        }
        UtilityView::Recipients(form) => {
            match form.kind {
                RecipientWorkflowKind::InitializeStore => {
                    content = content.push(text(
                        "Apply the root .gpg-id policy to the currently open password-store folder.",
                    ));
                }
                RecipientWorkflowKind::NewFolder => {
                    content = content
                        .push(text("Folder path"))
                        .push(
                            text_input("team/services", &form.path)
                                .on_input(Message::RecipientPathChanged)
                                .on_submit(Message::SubmitRecipient)
                                .style(entry_input_style),
                        )
                        .push(text(
                            "An upstream-compatible nested .gpg-id persists the folder and controls its recipients.",
                        ));
                }
            }
            content = content.push(text("Encryption recipients").size(20));
            for (index, recipient) in form.recipients.iter().enumerate() {
                let configured = if recipient.default {
                    " · previously configured default"
                } else {
                    ""
                };
                let choice = utility_button(text(format!(
                    "[{}] {}{}",
                    if recipient.selected { "x" } else { " " },
                    recipient.label,
                    configured
                )));
                let choice = if form.running {
                    choice
                } else {
                    choice.on_press(Message::ToggleRecipient(index))
                };
                if form.kind == RecipientWorkflowKind::InitializeStore {
                    let selected_default = recipient.fingerprint == form.default_key;
                    let default = utility_button(text(if !recipient.can_default {
                        "Public recipient only"
                    } else if selected_default {
                        "Default key ✓"
                    } else {
                        "Use as default key"
                    }));
                    let default = if form.running || !recipient.can_default {
                        default
                    } else {
                        default.on_press(Message::SelectDefaultRecipient(index))
                    };
                    content = content.push(row![choice, default].spacing(8));
                } else {
                    content = content.push(choice);
                }
            }
            let confirmation = utility_button(text(format!(
                "[{}] Replace this recipient policy and selectively re-encrypt affected entries",
                if form.confirmed { "x" } else { " " }
            )));
            content = content.push(if form.running {
                confirmation
            } else {
                confirmation.on_press(Message::ToggleRecipientConfirmation)
            });
            if let Some(error) = &form.error {
                content = content.push(text(format!("Recipient error: {error}")));
            }
        }
        UtilityView::NewEntry(form) => {
            content = content.push(text("Entry path")).push(
                text_input("folder/account", &form.path)
                    .on_input(Message::NewEntryPathChanged)
                    .on_submit(Message::SubmitNewEntry)
                    .style(entry_input_style),
            );
            let generate = utility_button(text(format!(
                "[{}] Generate the initial password with storage policy",
                if form.generate { "x" } else { " " }
            )));
            content = content.push(if form.running {
                generate
            } else {
                generate.on_press(Message::ToggleNewEntryGeneration)
            });
            if form.generate {
                content = content.push(text("Password length")).push(
                    text_input("25", &form.length)
                        .on_input(Message::NewEntryLengthChanged)
                        .on_submit(Message::SubmitNewEntry)
                        .style(entry_input_style),
                );
                let symbols = utility_button(text(format!(
                    "[{}] Letters and digits only",
                    if form.no_symbols { "x" } else { " " }
                )));
                content = content.push(if form.running {
                    symbols
                } else {
                    symbols.on_press(Message::ToggleNewEntrySymbols)
                });
            }
            content = content.push(text(
                "Create opens a structured unsaved draft. Encryption and persistence happen only when Save succeeds.",
            ));
            if let Some(error) = &form.error {
                content = content.push(text(format!("New entry error: {error}")));
            }
        }
        UtilityView::Search(form) => {
            let input = text_input(
                if form.mode == SearchMode::Names {
                    "Name substring"
                } else {
                    "Regular expression"
                },
                &form.query,
            )
            .on_input(Message::SearchQueryChanged)
            .on_submit(Message::SubmitSearch)
            .style(entry_input_style);
            content = if form.mode == SearchMode::Names {
                let add = utility_button("Add another term");
                content.push(
                    row![
                        input,
                        if form.running || form.query.is_empty() {
                            add
                        } else {
                            add.on_press(Message::AddSearchTerm)
                        },
                    ]
                    .spacing(8),
                )
            } else {
                content.push(input)
            };
            if form.mode == SearchMode::Contents {
                content = content
                    .push(text(
                        "This search authenticates and decrypts through crates/storage; results are removed when the lease locks.",
                    ))
                    .push(
                        row![
                            search_option("Ignore case", form.ignore_case, Message::ToggleSearchCase, form.running),
                            search_option("Invert match", form.invert_match, Message::ToggleSearchInvert, form.running),
                            search_option("Line numbers", form.line_numbers, Message::ToggleSearchLineNumbers, form.running),
                            search_option("Fixed strings", form.fixed_strings, Message::ToggleSearchFixedStrings, form.running),
                        ]
                        .spacing(8),
                    );
            } else {
                content = content.push(text(
                    "Name search remains available while locked. Multiple terms use the storage crate's pass-compatible OR matching semantics.",
                ));
                for (index, term) in form.terms.iter().enumerate() {
                    let remove = utility_button(text(format!("Remove term · {term}")));
                    content = content.push(if form.running {
                        remove
                    } else {
                        remove.on_press(Message::RemoveSearchTerm(index))
                    });
                }
            }
            if let Some(error) = &form.error {
                content = content.push(text(format!("Search error: {error}")));
            }
            if let Some(results) = &form.results {
                content = content.push(text("Results").size(20));
                match results {
                    SearchResults::Names(results) => {
                        if results.matches().is_empty() {
                            content = content.push(text("No matching entries or folders."));
                        }
                        for matched in results.matches() {
                            let kind = if matched.id().is_directory() {
                                "Folder"
                            } else {
                                "Entry"
                            };
                            content = content.push(
                                utility_button(text(format!("{kind} · {}", matched.path())))
                                    .on_press(Message::ActivateSearchResult(matched.id().clone())),
                            );
                        }
                    }
                    SearchResults::Contents(results) => {
                        if results.is_empty() {
                            content = content.push(text("No decrypted content matches."));
                        }
                        for entry in results.entries() {
                            content = content.push(
                                utility_button(text(format!("Entry · {}", entry.path()))).on_press(
                                    Message::ActivateSearchResult(TreeNodeId::Entry(
                                        entry.path().clone(),
                                    )),
                                ),
                            );
                            for line in entry.lines() {
                                let prefix = if results.includes_line_numbers() {
                                    format!("{}: ", line.number())
                                } else {
                                    String::new()
                                };
                                content = content.push(text(format!(
                                    "  {prefix}{}",
                                    String::from_utf8_lossy(line.contents().expose())
                                )));
                            }
                        }
                    }
                }
            }
        }
        UtilityView::Mutation(form) => {
            content = content.push(text(format!("Source: {}", form.source.path().display())));
            if form.kind != MutationKind::Delete {
                content = content
                    .push(text("Destination entry path or existing folder"))
                    .push(
                        text_input("folder/name", &form.destination)
                            .on_input(Message::MutationDestinationChanged)
                            .on_submit(Message::SubmitMutation)
                            .style(entry_input_style),
                    )
                    .push(text("Choose an existing destination folder"));
                let mut destinations = row![].spacing(6);
                for directory in app.navigation.directories() {
                    let path = directory.path().display().to_string();
                    let label = if path.is_empty() { "Store root" } else { &path };
                    let choice = utility_button(text(label.to_owned()));
                    destinations = destinations.push(if form.running {
                        choice
                    } else {
                        choice.on_press(Message::SelectMutationDestination(path))
                    });
                }
                let overwrite = utility_button(text(format!(
                    "[{}] Replace an existing destination entry",
                    if form.overwrite { "x" } else { " " }
                )));
                content = content.push(destinations.wrap()).push(if form.running {
                    overwrite
                } else {
                    overwrite.on_press(Message::ToggleMutationOverwrite)
                });
            } else {
                let confirmation = utility_button(text(format!(
                    "[{}] Permanently remove this {} and commit the deletion",
                    if form.confirmed { "x" } else { " " },
                    if form.source.is_directory() {
                        "folder"
                    } else {
                        "entry"
                    }
                )));
                content = content.push(if form.running {
                    confirmation
                } else {
                    confirmation.on_press(Message::ToggleMutationConfirmation)
                });
            }
            content = content.push(text(
                "Validation, collisions, recipient-aware re-encryption, rollback, filesystem mutation, and Git commits are owned by crates/storage.",
            ));
            if let Some(error) = &form.error {
                content = content.push(text(format!("Mutation error: {error}")));
            }
        }
        UtilityView::Git(form) => {
            content = content.push(text(
                "All repository, HTTPS transport, credential, merge, and conflict decisions are owned by crates/storage. No git process or credential helper is launched.",
            ));
            if let Some(phase) = form.progress {
                content = content.push(text(format!("Progress: {}", git_phase_name(phase))));
            }
            if let Some(error) = &form.error {
                content = content.push(text(format!("Git error: {error}")));
            }
            if let Some(snapshot) = &form.snapshot {
                content = content
                    .push(text(format!("Repository: {}", snapshot.root().display())))
                    .push(text(format!("Branch: {}", snapshot.branch())));
                if let Some(remote) = snapshot.remote() {
                    content = content.push(text(format!(
                        "HTTPS remote: {} · {} · {} ahead / {} behind",
                        remote.name(),
                        remote.url(),
                        remote.ahead(),
                        remote.behind()
                    )));
                } else {
                    content = content.push(text("No HTTPS remote is configured."));
                }
                let status = snapshot.status();
                content = content.push(text(if status.is_clean() {
                    "Worktree: clean".to_owned()
                } else {
                    format!(
                        "Worktree: {} staged / {} unstaged change(s)",
                        status.staged().len(),
                        status.unstaged().len()
                    )
                }));
                for change in status.staged() {
                    content = content.push(text(format!(
                        "Staged · {:?} · {}",
                        change.kind(),
                        change.path().display()
                    )));
                }
                for change in status.unstaged() {
                    content = content.push(text(format!(
                        "Unstaged · {:?} · {}",
                        change.kind(),
                        change.path().display()
                    )));
                }
                content = content.push(text("Recent history").size(20));
                if snapshot.recent().is_empty() {
                    content = content.push(text("No commits yet."));
                }
                for commit in snapshot.recent() {
                    content = content.push(text(format!(
                        "{} · {} · {}",
                        &commit.id()[..commit.id().len().min(12)],
                        commit.author_name(),
                        commit.message()
                    )));
                }
            }
            if !form.conflicts.is_empty() {
                content = content.push(text("Merge conflicts").size(20)).push(text(
                    "Choose exactly one complete version for every path. Resolution is committed by crates/storage only after all choices are present.",
                ));
                for (index, selection) in form.conflicts.iter().enumerate() {
                    let local = utility_button(text(
                        if selection.choice == Some(GitConflictChoice::Local) {
                            "Local ✓"
                        } else {
                            "Use local"
                        },
                    ));
                    let remote = utility_button(text(
                        if selection.choice == Some(GitConflictChoice::Remote) {
                            "Remote ✓"
                        } else {
                            "Use remote"
                        },
                    ));
                    content = content.push(
                        column![
                            text(format!(
                                "{:?} · {}",
                                selection.conflict.kind(),
                                selection.conflict.path().display()
                            )),
                            row![
                                local.on_press(Message::ChooseGitConflict(
                                    index,
                                    GitConflictChoice::Local
                                )),
                                remote.on_press(Message::ChooseGitConflict(
                                    index,
                                    GitConflictChoice::Remote
                                )),
                            ]
                            .spacing(8),
                        ]
                        .spacing(4),
                    );
                }
            }
        }
        UtilityView::Otp(form) => {
            content = content
                .push(text(
                    "Code generation, counters, URI validation, QR payloads, entry mutation, Git commits, and clipboard policy are owned by crates/storage.",
                ))
                .push(text("Entry path"))
                .push(
                    text_input("folder/entry", &form.entry)
                        .on_input(Message::OtpEntryChanged)
                        .style(entry_input_style),
                );
            if let Some(display) = &app.sensitive.otp {
                content = content
                    .push(
                        text(format!(
                            "{} code for {}",
                            match display.metadata.kind() {
                                OtpKind::Totp => "TOTP",
                                OtpKind::Hotp => "HOTP",
                            },
                            display.entry,
                        ))
                        .size(20),
                    )
                    .push(
                        text(String::from_utf8_lossy(display.code.expose()).into_owned()).size(36),
                    )
                    .push(text(format!(
                        "{} · {} · {:?} · {} digits",
                        display.metadata.issuer().unwrap_or("unknown issuer"),
                        display.metadata.account(),
                        display.metadata.algorithm(),
                        display.metadata.digits(),
                    )));
                if let Some(remaining) = display.remaining_at(display.observed_at) {
                    content = content.push(text(format!("Valid for {remaining}s")));
                }
                if let Some(counter) = display.validity.counter() {
                    content = content.push(text(format!("Committed HOTP counter {counter}")));
                }
            }
            if let Some(uri) = &app.sensitive.otp_uri {
                content = content
                    .push(text("OTP provisioning URI").size(20))
                    .push(text(String::from_utf8_lossy(uri.expose()).into_owned()));
            }
            if let Some(matrix) = &app.sensitive.otp_qr {
                content = content
                    .push(text("OTP provisioning QR code").size(20))
                    .push(
                        canvas(QrCanvas(matrix))
                            .width(Length::Fixed(320.0))
                            .height(Length::Fixed(320.0)),
                    );
            }
            content = content
                .push(text("Import or replace OTP").size(20))
                .push(
                    text_input("otpauth://…", &form.uri)
                        .on_input(|value| Message::OtpUriChanged(Zeroizing::new(value)))
                        .style(entry_input_style),
                )
                .push(search_option(
                    "Replace an existing OTP URI",
                    form.replace,
                    Message::ToggleOtpReplace,
                    form.running,
                ));
            if form.hotp_confirmation {
                content = content.push(text(
                    "HOTP generation advances and commits the counter. This cannot be treated as a read-only refresh.",
                ));
            }
            content = content.push(search_option(
                "Permanently remove the OTP URI and commit the entry",
                form.remove_confirmed,
                Message::ToggleOtpRemovalConfirmation,
                form.running,
            ));
            if let Some(error) = &form.error {
                content = content.push(text(format!("OTP error: {error}")));
            }
        }
        UtilityView::Kdbx(form) => {
            content = content
                .push(text(
                    "The import is additive: full mode adds new entries and updates changed entries; quick-add mode only adds entries that do not exist. Nothing is deleted.",
                ))
                .push(text("KDBX database"))
                .push(
                    row![
                        text_input("Database.kdbx", &form.source)
                            .on_input(Message::KdbxSourceChanged)
                            .on_submit(Message::SubmitKdbxImport)
                            .style(entry_input_style),
                        if form.running {
                            utility_button("Choose…")
                        } else {
                            utility_button("Choose…").on_press(Message::PickKdbxSource)
                        },
                    ]
                    .spacing(8),
                )
                .push(text("Optional KeePass key file"))
                .push(
                    row![
                        text_input("No key file", &form.key_file)
                            .on_input(Message::KdbxKeyFileChanged)
                            .on_submit(Message::SubmitKdbxImport)
                            .style(entry_input_style),
                        if form.running {
                            utility_button("Choose…")
                        } else {
                            utility_button("Choose…").on_press(Message::PickKdbxKeyFile)
                        },
                    ]
                    .spacing(8),
                )
                .push(text("Database password"))
                .push(
                    text_input("Password", &form.password)
                        .secure(true)
                        .on_input(|value| Message::KdbxPasswordChanged(Zeroizing::new(value)))
                        .on_submit(Message::SubmitKdbxImport)
                        .style(entry_input_style),
                )
                .push(search_option(
                    "Quick add: only add entries not already present",
                    form.quick_add,
                    Message::ToggleKdbxQuickAdd,
                    form.running,
                ))
                .push(search_option(
                    "Confirm additive import and per-entry Git commits",
                    form.confirmed,
                    Message::ToggleKdbxConfirmation,
                    form.running,
                ));
            if let Some(summary) = &form.summary {
                content = content.push(text(summary));
            }
            if let Some(error) = &form.error {
                content = content.push(text(format!("KDBX import error: {error}")));
            }
        }
        UtilityView::Help => {
            content = content.push(text("Keyboard shortcuts").size(22));
            for spec in action::ACTIONS {
                if let Some(shortcut) = action::shortcut_label(spec.action) {
                    content = content.push(text(format!(
                        "{shortcut}  {} · {}",
                        spec.label,
                        spec.group.label()
                    )));
                }
            }
            content = content
                .push(text("Two-pane navigation").size(22))
                .push(text(
                    "The left pane navigates the storage-provided folder tree. The right pane opens the selected entry. Tab changes pane focus; arrows, Home, End, and Enter navigate the focused pane.",
                ))
                .push(text("Sensitive values, copying, and locking").size(22))
                .push(text(
                    "Opening protected content authenticates through the shared inactivity lease. Every field remains visible while unlocked, and Copy uses the configured cleanup timeout. Lock immediately drops decrypted entry, editor, OTP code, and clipboard state.",
                ))
                .push(text("Command palette").size(22))
                .push(text(
                    "Open the command palette with its platform shortcut, type a command or alias, use Up/Down to select, Enter to run, and Escape to cancel. Unavailable commands explain why they are disabled.",
                ))
                .push(text("Git actions").size(22))
                .push(text(
                    "Fetch, Pull, Push, status, and conflict actions use the embedded storage Git implementation. Remotes are HTTPS-only; conflicts require an explicit choice and are never silently discarded.",
                ))
                .push(text("OTP actions").size(22))
                .push(text(
                    "OTP actions operate on storage-recognized fields. TOTP codes are time-based; HOTP generation confirms and commits the counter advance. URI, QR, and clipboard outputs are explicit sensitive presentations.",
                ));
        }
    }
    let busy = matches!(utility, UtilityView::Settings(form) if form.saving)
        || matches!(utility, UtilityView::Recipients(form) if form.running)
        || matches!(utility, UtilityView::NewEntry(form) if form.running)
        || matches!(utility, UtilityView::Search(form) if form.running)
        || matches!(utility, UtilityView::Mutation(form) if form.running)
        || matches!(utility, UtilityView::Git(form) if form.running)
        || matches!(utility, UtilityView::Otp(form) if form.running)
        || matches!(utility, UtilityView::Kdbx(form) if form.running);
    let actions = match utility {
        UtilityView::Settings(form) => row![utility_icon_control(
            Icon::Check,
            if form.saving {
                "Validating settings…"
            } else {
                "Save settings"
            },
            Message::SaveSettings,
            !form.saving,
        )],
        UtilityView::Recipients(form) => row![utility_icon_control(
            Icon::Check,
            if form.running {
                "Applying recipient policy…"
            } else {
                "Apply recipient policy"
            },
            Message::SubmitRecipient,
            !form.running,
        )],
        UtilityView::NewEntry(form) => row![utility_icon_control(
            Icon::Check,
            if form.running {
                "Preparing draft…"
            } else {
                "Create draft"
            },
            Message::SubmitNewEntry,
            !form.running,
        )],
        UtilityView::Search(form) => row![utility_icon_control(
            Icon::Search,
            if form.running {
                "Searching…"
            } else {
                "Search"
            },
            Message::SubmitSearch,
            !form.running,
        )],
        UtilityView::Mutation(form) => {
            let icon = match form.kind {
                MutationKind::Move => Icon::Move,
                MutationKind::Copy => Icon::Copy,
                MutationKind::Delete => Icon::Delete,
            };
            row![utility_icon_control(
                icon,
                if form.running {
                    "Applying mutation…"
                } else {
                    "Apply mutation"
                },
                Message::SubmitMutation,
                !form.running,
            )]
        }
        UtilityView::Git(form) => {
            if form.running {
                row![utility_button("Cancel Git operation").on_press(Message::CancelGit)]
            } else {
                let mut actions = row![
                    utility_icon_control(
                        Icon::Refresh,
                        "Refresh Git status",
                        Message::RunGit(DesktopGitRequest::Refresh),
                        true,
                    ),
                    utility_icon_control(
                        Icon::Down,
                        "Pull from remote",
                        Message::RunGit(DesktopGitRequest::Pull),
                        true,
                    ),
                    utility_icon_control(
                        Icon::Up,
                        "Push to remote",
                        Message::RunGit(DesktopGitRequest::Push),
                        true,
                    ),
                    utility_icon_control(
                        Icon::Refresh,
                        "Synchronize with remote",
                        Message::RunGit(DesktopGitRequest::Sync),
                        true,
                    ),
                ]
                .spacing(2);
                if !form.conflicts.is_empty() {
                    actions = actions.push(utility_icon_control(
                        Icon::Check,
                        "Resolve selected versions",
                        Message::ResolveGitConflicts,
                        true,
                    ));
                }
                actions
            }
        }
        UtilityView::Otp(form) => {
            if form.running {
                row![utility_icon_control(
                    Icon::Refresh,
                    "Working…",
                    Message::RunOtpCode(false),
                    false,
                )]
            } else {
                let mut actions = row![
                    utility_icon_control(
                        Icon::Refresh,
                        "Generate OTP code",
                        Message::RunOtpCode(false),
                        true,
                    ),
                    utility_icon_control(
                        Icon::Copy,
                        "Copy OTP code",
                        Message::RunOtpCode(true),
                        true,
                    ),
                    utility_icon_control(
                        Icon::Link,
                        "Show provisioning URI",
                        Message::RunOtpUri {
                            qr: false,
                            copy: false,
                        },
                        true,
                    ),
                    utility_icon_control(
                        Icon::Copy,
                        "Copy provisioning URI",
                        Message::RunOtpUri {
                            qr: false,
                            copy: true,
                        },
                        true,
                    ),
                    utility_icon_control(
                        Icon::Qr,
                        "Show provisioning QR code",
                        Message::RunOtpUri {
                            qr: true,
                            copy: false,
                        },
                        true,
                    ),
                ]
                .spacing(2);
                if form.hotp_confirmation {
                    actions = actions.push(
                        utility_button("Confirm HOTP counter advance")
                            .on_press(Message::ConfirmHotp),
                    );
                }
                actions
                    .push(utility_icon_control(
                        Icon::Down,
                        "Import provisioning URI",
                        Message::SubmitOtpImport,
                        true,
                    ))
                    .push(utility_icon_control(
                        Icon::Qr,
                        "Import provisioning QR image",
                        Message::PickOtpQr,
                        true,
                    ))
                    .push(
                        utility_button("Remove OTP")
                            .style(button::danger)
                            .on_press(Message::SubmitOtpRemoval),
                    )
            }
        }
        UtilityView::Kdbx(form) => row![utility_icon_control(
            Icon::Down,
            if form.running {
                "Importing…"
            } else if form.quick_add {
                "Quick add KeePass entries"
            } else {
                "Import KeePass entries"
            },
            Message::SubmitKdbxImport,
            !form.running,
        )],
        UtilityView::About | UtilityView::Help => row![container(text("")).width(Length::Fill)],
    };

    let header = container(
        row![
            colored_icon_view(utility_icon(utility), Some(ENTRY_TEXT)),
            text(utility_title(utility)).size(20).width(Length::Fill),
            utility_icon_control(
                Icon::Close,
                "Close panel (Esc)",
                Message::DismissUtility,
                !busy,
            ),
        ]
        .align_y(iced::Alignment::Center)
        .spacing(8),
    )
    .padding([6, 10])
    .width(Length::Fill)
    .style(entry_value_style);
    let body = scrollable(
        container(
            container(content)
                .padding(16)
                .width(Length::Fill)
                .max_width(760)
                .style(|theme| entry_field_style(theme, false)),
        )
        .padding([12, 16])
        .width(Length::Fill)
        .center_x(Length::Fill),
    )
    .height(Length::Fill);
    let mut layout = column![header, body].spacing(1);
    if !matches!(utility, UtilityView::About | UtilityView::Help) {
        layout = layout.push(
            container(
                row![container(text("")).width(Length::Fill), actions]
                    .align_y(iced::Alignment::Center)
                    .spacing(2),
            )
            .padding([6, 10])
            .width(Length::Fill)
            .style(entry_value_style),
        );
    }
    container(layout)
        .width(Length::Fill)
        .height(Length::Fill)
        .style(entry_area_style)
        .into()
}

fn search_option<'a>(
    label: &'a str,
    selected: bool,
    message: Message,
    running: bool,
) -> iced::widget::Button<'a, Message> {
    let option = utility_button(text(format!(
        "[{}] {label}",
        if selected { "x" } else { " " }
    )));
    if running {
        option
    } else {
        option.on_press(message)
    }
}

struct QrCanvas<'a>(&'a QrMatrix);

impl<Message> canvas::Program<Message> for QrCanvas<'_> {
    type State = ();

    fn draw(
        &self,
        _state: &Self::State,
        renderer: &Renderer,
        _theme: &Theme,
        bounds: Rectangle,
        _cursor: iced::mouse::Cursor,
    ) -> Vec<canvas::Geometry> {
        let mut frame = canvas::Frame::new(renderer, bounds.size());
        frame.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
        let padded = self.0.width() + 8;
        let scale = (bounds.width.min(bounds.height) / padded as f32).floor();
        let offset = Point::new(
            (bounds.width - scale * padded as f32) / 2.0,
            (bounds.height - scale * padded as f32) / 2.0,
        );
        for y in 0..self.0.width() {
            for x in 0..self.0.width() {
                if self.0.is_dark(x, y) == Some(true) {
                    frame.fill_rectangle(
                        Point::new(
                            offset.x + (x + 4) as f32 * scale,
                            offset.y + (y + 4) as f32 * scale,
                        ),
                        iced::Size::new(scale, scale),
                        Color::BLACK,
                    );
                }
            }
        }
        vec![frame.into_geometry()]
    }
}

fn sidebar_view<'a>(
    navigation: &'a NavigationTree,
    state: &'a TreeState,
    focused: bool,
    context_target: Option<&'a TreeNodeId>,
) -> Element<'a, Message> {
    let mut rows = column![
        row![
            text(if focused { "EXPLORER •" } else { "EXPLORER" })
                .size(12)
                .width(Length::Fill),
            action_icon(Icon::Add, UiAction::NewEntry),
            action_icon(Icon::FolderAdd, UiAction::NewFolder),
            action_icon(Icon::Refresh, UiAction::Refresh),
        ]
        .align_y(iced::Alignment::Center)
        .spacing(2)
    ]
    .spacing(4)
    .padding([6, 4]);

    match state {
        TreeState::Loading => {
            rows = rows.push(text("Loading password-store tree…").size(13));
        }
        TreeState::Error(error) => {
            rows = rows.push(text(format!("Tree unavailable: {error}")).size(13));
        }
        TreeState::Empty => {
            rows = rows.push(text("This password store is empty.").size(13));
        }
        TreeState::Ready => {}
    }

    let selected = navigation.selected();
    for node in navigation.rows() {
        let expander: Element<'static, Message> = if node.id.is_directory() {
            icon_view(if node.expanded {
                Icon::ChevronDown
            } else {
                Icon::ChevronRight
            })
        } else {
            container(text(""))
                .width(Length::Fixed(16.0))
                .height(Length::Fixed(16.0))
                .into()
        };
        let mut flags = Vec::new();
        if node.indicators.has_conflict() {
            flags.push("conflict");
        }
        if node.indicators.is_changed() {
            flags.push("changed");
        }
        if node.indicators.is_locked() {
            flags.push("locked");
        }
        let suffix = if flags.is_empty() {
            String::new()
        } else {
            format!(" · {}", flags.join(", "))
        };
        let selected = selected == Some(&node.id);
        let id = node.id.clone();
        let kind = if node.id.is_directory() {
            Icon::Folder
        } else {
            Icon::Key
        };
        let count = node
            .id
            .is_directory()
            .then(|| text(node.entry_count.to_string()).size(12));
        let mut item_row = row![
            container(text("")).width(Length::Fixed((node.depth * 16) as f32)),
            expander,
            icon_view(kind),
            text(format!("{}{}", node.name, suffix)).width(Length::Fill),
        ]
        .align_y(iced::Alignment::Center)
        .spacing(4);
        if let Some(count) = count {
            item_row = item_row.push(count);
        }
        let item = button(item_row)
            .width(Length::Fill)
            .on_press(Message::SidebarActivate(id.clone()))
            .style(if selected {
                selected_button
            } else {
                button::text
            });
        rows = rows.push(mouse_area(item).on_right_press(Message::SidebarContext(id.clone())));
        if context_target == Some(&id) {
            rows = rows.push(
                row![
                    container(text("")).width(Length::Fixed(((node.depth + 1) * 16) as f32)),
                    icon_control(
                        Icon::Move,
                        action_hint(UiAction::MoveEntry),
                        Message::SidebarContextAction(id.clone(), UiAction::MoveEntry),
                    ),
                    icon_control(
                        Icon::Copy,
                        action_hint(UiAction::CopyEntry),
                        Message::SidebarContextAction(id.clone(), UiAction::CopyEntry),
                    ),
                    icon_control(
                        Icon::Delete,
                        action_hint(UiAction::DeleteEntry),
                        Message::SidebarContextAction(id, UiAction::DeleteEntry),
                    ),
                ]
                .spacing(2),
            );
        }
    }

    scrollable(rows)
        .id(sidebar_scroll_id())
        .height(Length::Fill)
        .into()
}

fn content_view(app: &App) -> Element<'_, Message> {
    let mut identity = row![]
        .align_y(iced::Alignment::Center)
        .spacing(6)
        .width(Length::Fill);
    if !app.entry_path.is_empty() {
        identity = identity.push(icon_view(Icon::Key));
    }
    identity = identity.push(
        text(if app.entry_path.is_empty() {
            if app.pane_focus == PaneFocus::Content {
                "CONTENT •"
            } else {
                "CONTENT"
            }
        } else {
            &app.entry_path
        })
        .size(15)
        .width(Length::Fill),
    );
    let header = row![
        identity,
        action_icon(Icon::Edit, UiAction::EditEntry),
        action_icon(Icon::Refresh, UiAction::ReloadEntry),
    ]
    .align_y(iced::Alignment::Center)
    .spacing(2);

    let body: Element<'_, Message> = if app.switching_vault {
        container(text(
            "Validating the selected password store and shared configuration…",
        ))
        .center(Length::Fill)
        .into()
    } else if !authentication_allows_content(&app.authentication) {
        container(text(
            "Protected entry content is locked. Select an entry to authenticate and open it.",
        ))
        .center(Length::Fill)
        .into()
    } else if app.saving {
        container(text("Saving without discarding the draft…"))
            .center(Length::Fill)
            .into()
    } else if let Some(editor) = &app.editor {
        match app.content_mode {
            ContentMode::Viewer => viewer_view(app, editor),
            ContentMode::Editor => editor_view(editor, app.conflict),
        }
    } else {
        container(text(
            "Select an entry in the navigation tree. Entry names remain visible while locked; protected content authenticates only when opened.",
        ))
        .center(Length::Fill)
        .into()
    };

    container(column![header, body].spacing(6).padding([6, 8]))
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
}

fn authentication_allows_content(authentication: &AuthenticationView) -> bool {
    matches!(authentication, AuthenticationView::Unlocked(_))
}

fn document_has_single_totp(document: &EntryDocument) -> bool {
    let mut otp = document
        .fields()
        .iter()
        .filter_map(|field| field.metadata().otp());
    otp.next()
        .is_some_and(|metadata| metadata.kind() == OtpKind::Totp)
        && otp.next().is_none()
}

#[derive(Debug, Eq, PartialEq)]
enum ViewerValue<'a> {
    Text(&'a str),
    Unavailable,
}

fn viewer_value(field: &EntryField) -> ViewerValue<'_> {
    std::str::from_utf8(field.value())
        .map(ViewerValue::Text)
        .unwrap_or(ViewerValue::Unavailable)
}

fn viewer_label(field: &EntryField) -> String {
    field.metadata().name().map_or_else(
        || match field.metadata().kind() {
            EntryFieldKind::Password => "Password".to_owned(),
            EntryFieldKind::Username => "Username".to_owned(),
            EntryFieldKind::Email => "Email".to_owned(),
            EntryFieldKind::Url => "URL".to_owned(),
            EntryFieldKind::OtpUri => "One-time password".to_owned(),
            EntryFieldKind::Field => "Field".to_owned(),
            EntryFieldKind::Note => "Note".to_owned(),
        },
        str::to_owned,
    )
}

fn viewer_diagnostic(diagnostic: EntryFieldDiagnostic) -> &'static str {
    match diagnostic {
        EntryFieldDiagnostic::MalformedOtpUri => {
            "Malformed OTP URI preserved losslessly; OTP metadata is unavailable."
        }
        EntryFieldDiagnostic::NonUtf8Value => {
            "Non-UTF-8 value preserved losslessly; text display is unavailable."
        }
    }
}

fn otp_code_text(display: &OtpDisplay) -> String {
    std::str::from_utf8(display.code.expose()).map_or_else(
        |_| "(unavailable)".to_owned(),
        |code| {
            let middle = code.len() / 2;
            if code.len() > 4 && code.is_char_boundary(middle) {
                format!("{} {}", &code[..middle], &code[middle..])
            } else {
                code.to_owned()
            }
        },
    )
}

fn matching_otp_display<'a>(app: &'a App, entry: &str) -> Option<&'a OtpDisplay> {
    app.sensitive
        .otp
        .as_ref()
        .filter(|display| display.entry == entry)
}

fn otp_progress(display: &OtpDisplay) -> Option<(u64, u64)> {
    display.validity.period().map(|period| {
        (
            display
                .remaining_at(display.observed_at)
                .unwrap_or_default()
                .min(period),
            period.max(1),
        )
    })
}

fn viewer_view<'a>(app: &'a App, editor: &'a EntryEditor) -> Element<'a, Message> {
    let entry = editor.entry();
    let mut rows = column![].spacing(8);
    if let Some(display) = matching_otp_display(app, &entry) {
        let mut code = column![
            text(match display.metadata.kind() {
                OtpKind::Totp => "Current one-time password",
                OtpKind::Hotp => "Generated one-time password",
            })
            .size(13),
            text(otp_code_text(display)).size(42),
        ]
        .align_x(iced::Alignment::Center)
        .spacing(2);
        if let Some((remaining, period)) = otp_progress(display) {
            code = code.push(text(format!("{remaining}s remaining"))).push(
                progress_bar(0.0..=period as f32, remaining as f32).girth(Length::Fixed(6.0)),
            );
        } else if let Some(counter) = display.validity.counter() {
            code = code.push(text(format!("HOTP counter {counter}")));
        }
        rows = rows.push(
            container(code)
                .padding([10, 12])
                .width(Length::Fill)
                .center_x(Length::Fill)
                .style(entry_value_style),
        );
    } else if app.otp_pending
        && editor
            .document()
            .fields()
            .iter()
            .any(|field| field.metadata().otp().is_some())
    {
        rows = rows.push(
            container(text("Generating one-time password…").size(15))
                .padding(10)
                .width(Length::Fill)
                .center_x(Length::Fill)
                .style(entry_value_style),
        );
    }
    rows =
        rows.push(text("Up/Down/Home/End select fields · ⌘C copies the selected value").size(12));

    for field in editor.document().display_fields() {
        let id = field.id();
        let label = viewer_label(field);
        let selected = editor.focused() == Some(id);
        let value = match viewer_value(field) {
            ViewerValue::Text("") => "(empty)",
            ViewerValue::Text(value) => value,
            ViewerValue::Unavailable => "(binary value)",
        };
        let copy_hint = format!("Copy {label} (⌘C)");
        let copy = entry_icon_control(
            Icon::Copy,
            copy_hint,
            Message::FieldAction(id, UiAction::CopyField),
        );
        let mut field_view = if value.contains('\n') {
            column![
                row![text(label).size(13).width(Length::Fill), copy,]
                    .align_y(iced::Alignment::Center)
                    .spacing(4),
                container(text(value).size(15).wrapping(text::Wrapping::WordOrGlyph),)
                    .padding(8)
                    .width(Length::Fill)
                    .style(entry_value_style),
            ]
            .spacing(4)
        } else {
            column![
                row![
                    text(label).size(13).width(Length::Fixed(ENTRY_LABEL_WIDTH)),
                    text(value)
                        .size(15)
                        .width(Length::Fill)
                        .wrapping(text::Wrapping::WordOrGlyph),
                    copy,
                ]
                .align_y(iced::Alignment::Center)
                .spacing(8),
            ]
        };
        if let Some(otp) = field.metadata().otp() {
            let cadence = otp.period().map_or_else(
                || format!("counter {}", otp.counter().unwrap_or_default()),
                |period| format!("{period}s period"),
            );
            field_view = field_view.push(
                text(format!(
                    "{:?} · {} · {} · {:?} · {} digits · {cadence}",
                    otp.kind(),
                    otp.issuer().unwrap_or("unknown issuer"),
                    otp.account(),
                    otp.algorithm(),
                    otp.digits(),
                ))
                .size(12),
            );
            field_view = field_view.push(
                container(
                    row![
                        entry_icon_control(
                            Icon::Refresh,
                            "Generate current OTP code".to_owned(),
                            Message::FieldAction(id, UiAction::GenerateOtp),
                        ),
                        entry_icon_control(
                            Icon::Copy,
                            "Copy current OTP code".to_owned(),
                            Message::FieldAction(id, UiAction::CopyOtp),
                        ),
                        entry_icon_control(
                            Icon::Link,
                            "Copy OTP provisioning URI".to_owned(),
                            Message::FieldAction(id, UiAction::CopyOtpUri),
                        ),
                        entry_icon_control(
                            Icon::Qr,
                            "Show OTP provisioning QR code".to_owned(),
                            Message::FieldAction(id, UiAction::ShowOtpQr),
                        ),
                    ]
                    .spacing(2),
                )
                .align_right(Length::Fill),
            );
        }
        if let Some(diagnostic) = field.metadata().diagnostic() {
            field_view = field_view.push(text(viewer_diagnostic(diagnostic)).size(12));
        }
        rows = rows.push(
            mouse_area(
                container(field_view)
                    .padding([7, 9])
                    .width(Length::Fill)
                    .style(move |theme| entry_field_style(theme, selected)),
            )
            .on_press(Message::SelectField(id)),
        );
    }

    container(scrollable(rows).id(viewer_scroll_id()).height(Length::Fill))
        .padding([8, 10])
        .width(Length::Fill)
        .height(Length::Fill)
        .style(entry_area_style)
        .into()
}

fn dirty_decision(editor: Option<&EntryEditor>) -> DirtyDecision {
    if editor.is_some_and(EntryEditor::is_dirty) {
        DirtyDecision::Confirm
    } else {
        DirtyDecision::Execute
    }
}

fn tree_state_from_result(result: Result<bool, String>) -> TreeState {
    match result {
        Ok(true) => TreeState::Empty,
        Ok(false) => TreeState::Ready,
        Err(error) => TreeState::Error(error),
    }
}

fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
    let mut fields = column![
        row![
            text(format!(
                "{}{}",
                editor.entry(),
                if editor.is_dirty() { " •" } else { "" }
            ))
            .size(16)
            .width(Length::Fill),
            entry_icon_control(
                Icon::Check,
                action_hint(UiAction::Save),
                Message::Action(UiAction::Save),
            ),
            entry_icon_control(Icon::Add, "Add field".to_owned(), Message::AddAfter(None),),
        ]
        .align_y(iced::Alignment::Center)
        .spacing(2)
    ]
    .spacing(10);

    if conflict {
        fields = fields.push(
            row![
                text("The stored entry changed. Reload it or keep the complete local draft."),
                button("Reload stored entry").on_press(Message::ReloadConflict),
                button("Keep draft").on_press(Message::KeepConflictDraft),
            ]
            .spacing(8),
        );
    }

    for field in editor.fields() {
        let id = field.id();
        let selected = editor.focused() == Some(id);
        let sensitive = field.metadata().sensitivity() == EntrySensitivity::Sensitive;
        let value = std::str::from_utf8(field.value()).ok();
        let label = field
            .metadata()
            .name()
            .map(str::to_owned)
            .unwrap_or_else(|| format!("{:?}", field.metadata().kind()));
        let mut actions = row![
            entry_icon_control(Icon::Up, "Move field up".to_owned(), Message::MoveUp(id)),
            entry_icon_control(
                Icon::Down,
                "Move field down".to_owned(),
                Message::MoveDown(id)
            ),
            entry_icon_control(
                Icon::Add,
                "Add field below".to_owned(),
                Message::AddAfter(Some(id)),
            ),
            entry_icon_control(Icon::Delete, "Remove field".to_owned(), Message::Remove(id)),
        ]
        .spacing(2);
        if sensitive && field.metadata().kind() != EntryFieldKind::OtpUri {
            actions = actions.push(entry_icon_control(
                Icon::Generate,
                "Generate password".to_owned(),
                Message::FieldAction(id, UiAction::GeneratePassword),
            ));
        }
        actions = actions.push(entry_icon_control(
            Icon::Copy,
            "Copy field value (⌘C)".to_owned(),
            Message::FieldAction(id, UiAction::CopyEditedField),
        ));

        let field_view = match value {
            Some(value) if value.contains('\n') => {
                let input: Element<'_, Message> = text_editor(
                    editor
                        .multiline_content(id)
                        .expect("multiline fields have one editor content"),
                )
                .id(editor_field_input_id(id))
                .height(Length::Fixed(120.0))
                .on_action(move |action| Message::FieldEdited(id, action))
                .style(entry_editor_style)
                .into();
                column![
                    row![text(label).size(14).width(Length::Fill), actions,]
                        .align_y(iced::Alignment::Center)
                        .spacing(4),
                    input,
                ]
                .spacing(4)
            }
            Some(value) => {
                let submit = if matches!(
                    field.metadata().kind(),
                    EntryFieldKind::Password | EntryFieldKind::OtpUri
                ) {
                    Message::AddAfter(Some(id))
                } else {
                    Message::AddFieldLine(id, 0)
                };
                let input: Element<'_, Message> = text_input("Entry value", value)
                    .id(editor_field_input_id(id))
                    .on_input(move |value| Message::FieldChanged(id, Zeroizing::new(value)))
                    .on_submit(submit)
                    .style(entry_input_style)
                    .into();
                column![
                    row![
                        text(label).size(14).width(Length::Fixed(ENTRY_LABEL_WIDTH)),
                        input,
                        actions,
                    ]
                    .align_y(iced::Alignment::Center)
                    .spacing(8),
                ]
            }
            None => {
                let input: Element<'_, Message> = text_input("Non-UTF-8 value preserved", "")
                    .style(entry_input_style)
                    .into();
                column![
                    row![
                        text(label).size(14).width(Length::Fixed(ENTRY_LABEL_WIDTH)),
                        input,
                        actions,
                    ]
                    .align_y(iced::Alignment::Center)
                    .spacing(8),
                ]
            }
        };
        fields = fields.push(
            mouse_area(
                container(field_view)
                    .padding([7, 9])
                    .width(Length::Fill)
                    .style(move |theme| entry_field_style(theme, selected)),
            )
            .on_press(Message::SelectField(id)),
        );
    }
    container(scrollable(fields).height(Length::Fill))
        .padding([8, 10])
        .width(Length::Fill)
        .height(Length::Fill)
        .style(entry_area_style)
        .into()
}

fn confirmation_view(action: &PendingAction) -> Element<'_, Message> {
    let description = match action {
        PendingAction::OpenVault(path) => format!("Open {}", path.display()),
        PendingAction::OpenEntry(entry) => format!("Open {entry}"),
        PendingAction::Reload(entry) => format!("Reload {entry}"),
        PendingAction::CloseWindow(_) => "Close IronStorage".to_owned(),
        PendingAction::ApplyRecipients(form) => match form.kind {
            RecipientWorkflowKind::InitializeStore => "Initialize the store".to_owned(),
            RecipientWorkflowKind::NewFolder => format!("Create folder {}", form.path),
        },
        PendingAction::CreateEntry(form) => format!("Create entry {}", form.path),
        PendingAction::SearchContents(request) => {
            format!("Search decrypted contents for {}", request.pattern)
        }
        PendingAction::Mutate(form) => format!(
            "{} {}",
            match form.kind {
                MutationKind::Move => "Move",
                MutationKind::Copy => "Copy",
                MutationKind::Delete => "Delete",
            },
            form.source.path().display()
        ),
        PendingAction::Git(request) => format!("Git {}", git_request_name(request)),
        PendingAction::ImportKdbx(form) => format!("Import {}", form.source),
    };
    container(
        column![
            text("Save changes?").size(26),
            text(format!(
                "{description} would discard the current structured editor draft."
            )),
            row![
                button("Save").on_press(Message::ConfirmSave),
                button("Discard").on_press(Message::ConfirmDiscard),
                button("Cancel").on_press(Message::CancelDiscard),
            ]
            .spacing(8),
        ]
        .spacing(12),
    )
    .center(Length::Fill)
    .into()
}

fn git_request_name(request: &DesktopGitRequest) -> &'static str {
    match request {
        DesktopGitRequest::Refresh => "status refresh",
        DesktopGitRequest::Pull => "pull",
        DesktopGitRequest::Push => "push",
        DesktopGitRequest::Sync => "synchronization",
        DesktopGitRequest::Resolve(_) => "conflict resolution",
    }
}

fn git_phase_name(phase: GitProgressPhase) -> &'static str {
    match phase {
        GitProgressPhase::Validating => "validating the repository and HTTPS remote",
        GitProgressPhase::Authenticating => "requesting secure HTTPS credentials",
        GitProgressPhase::Receiving => "receiving remote objects",
        GitProgressPhase::Integrating => "integrating fetched changes",
        GitProgressPhase::Sending => "sending local objects",
        GitProgressPhase::Refreshing => "refreshing repository state",
    }
}

fn git_outcome_message(outcome: &DesktopGitOutcome) -> String {
    match outcome {
        DesktopGitOutcome::Refreshed => "Git status and history refreshed.".to_owned(),
        DesktopGitOutcome::Pulled(outcome) => format!("Git pull completed: {outcome:?}."),
        DesktopGitOutcome::Pushed(outcome) => format!(
            "Pushed {} branch {} at {}.",
            outcome.remote(),
            outcome.branch(),
            &outcome.new_id()[..outcome.new_id().len().min(12)]
        ),
        DesktopGitOutcome::Synchronized { pull, push } => format!(
            "Git synchronization completed: {pull:?}; pushed {} at {}.",
            push.remote(),
            &push.new_id()[..push.new_id().len().min(12)]
        ),
        DesktopGitOutcome::Resolved(outcome) => {
            format!("Git conflicts resolved and committed: {outcome:?}.")
        }
    }
}

fn git_outcome_changes_worktree(outcome: &DesktopGitOutcome) -> bool {
    match outcome {
        DesktopGitOutcome::Pulled(outcome) => {
            !matches!(outcome, ironstorage::git::PullOutcome::UpToDate)
        }
        DesktopGitOutcome::Synchronized { pull, .. } => {
            !matches!(pull, ironstorage::git::PullOutcome::UpToDate)
        }
        DesktopGitOutcome::Resolved(_) => true,
        DesktopGitOutcome::Refreshed | DesktopGitOutcome::Pushed(_) => false,
    }
}

fn git_failure_message(error: &DesktopError) -> String {
    match error.git_error() {
        Some(GitError::NotRepository) => {
            "No embedded Git repository exists for this password store.".to_owned()
        }
        Some(GitError::ForbiddenRemoteUrl) => {
            "The repository remote must match the configured credential-free HTTPS URL. Fix the shared configuration or repository remote, then refresh.".to_owned()
        }
        Some(GitError::RemoteNotFound { name }) => {
            format!("The configured HTTPS remote {name} is missing from the repository.")
        }
        Some(GitError::CredentialsUnavailable) => {
            "HTTPS credentials are unavailable in secure storage for the configured server and application.".to_owned()
        }
        Some(GitError::CredentialAccessDenied) => {
            "Access to the HTTPS credential was denied; retry and approve the secure-storage request.".to_owned()
        }
        Some(GitError::CredentialCancelled) => {
            "The HTTPS credential request was cancelled; retry when ready.".to_owned()
        }
        Some(GitError::AuthenticationFailed) => {
            "The HTTPS server rejected the stored credential; update it in secure storage and retry.".to_owned()
        }
        Some(GitError::NetworkUnavailable) => {
            "The HTTPS Git server is unreachable; check the network and retry.".to_owned()
        }
        Some(GitError::TlsFailed) => {
            "TLS validation failed for the HTTPS Git server; verify its certificate and configured URL.".to_owned()
        }
        Some(GitError::Cancelled) => "The Git operation was cancelled safely.".to_owned(),
        Some(GitError::NonFastForward) => {
            "The push is not a fast-forward; pull and resolve remote changes before retrying."
                .to_owned()
        }
        Some(GitError::DirtyWorktree) => {
            "The repository has uncommitted changes; commit or restore them before synchronization."
                .to_owned()
        }
        Some(GitError::InvalidConflictResolution { path }) => format!(
            "The conflict choice for {} is no longer valid; refresh conflicts and choose again.",
            path.display()
        ),
        _ => error.to_string(),
    }
}

fn generation_view(form: &GenerateForm) -> Element<'_, Message> {
    let mut content = column![
        text(if form.replacement {
            "Replace with Generated Password"
        } else {
            "Generate Password"
        })
        .size(26),
        text("Password length"),
        text_input("25", &form.length)
            .on_input(Message::GenerateLengthChanged)
            .on_submit(Message::SubmitGenerate),
        button(text(format!(
            "[{}] Letters and digits only",
            if form.no_symbols { "x" } else { " " }
        )))
        .on_press(Message::ToggleGenerateSymbols),
    ]
    .spacing(12);
    if form.replacement {
        content = content.push(
            button(text(format!(
                "[{}] Replace the current field value",
                if form.confirmed { "x" } else { " " }
            )))
            .on_press(Message::ToggleGenerateConfirmation),
        );
    }
    if let Some(error) = &form.error {
        content = content.push(text(format!("Generation error: {error}")));
    }
    content = content
        .push(text(format!("Field {}", form.id.value())).size(12))
        .push(
            row![
                button(if form.replacement {
                    "Generate and Replace"
                } else {
                    "Generate"
                })
                .on_press(Message::SubmitGenerate),
                button("Cancel").on_press(Message::CancelGenerate),
            ]
            .spacing(8),
        );
    container(content).center(Length::Fill).into()
}

fn parse_generation_length(value: &str) -> Result<Option<NonZeroUsize>, String> {
    let length = value
        .trim()
        .parse::<usize>()
        .map_err(|_| "Password length must be a positive whole number.".to_owned())?;
    NonZeroUsize::new(length)
        .map(Some)
        .ok_or_else(|| "Password length must be greater than zero.".to_owned())
}

async fn load_authentication()
-> Result<(DesktopStorage, NativeAuthenticationSession, KeyInfo), String> {
    DesktopStorage::system()
        .map(|bootstrap| bootstrap.into_parts())
        .map_err(|error| error.to_string())
}

fn load_document(
    storage: &DesktopStorage,
    entry: &str,
    mut handle: NativeAuthenticationHandle,
) -> Result<EntryDocument, String> {
    storage
        .open_document(entry, &mut handle)
        .map_err(|error| error.to_string())
}

#[cfg(test)]
fn save_document(
    storage: &DesktopStorage,
    editor: &EntryEditor,
) -> Result<WriteOutcome, DesktopError> {
    storage.save_document(editor.document())
}

async fn copy_to_clipboard(
    value: SecretBytes,
    timeout: ironstorage::presentation::ClipboardTimeout,
    cancel: Arc<AtomicBool>,
) -> Result<String, String> {
    let mut clipboard =
        NativeClipboardManager::system(timeout).map_err(|error| error.to_string())?;
    let disposition = clipboard
        .copy_with(&value, |duration| {
            let deadline = Instant::now() + duration;
            while Instant::now() < deadline {
                if cancel.load(Ordering::Acquire) {
                    return ClipboardWait::Cancelled;
                }
                thread::sleep(
                    deadline
                        .saturating_duration_since(Instant::now())
                        .min(Duration::from_millis(50)),
                );
            }
            ClipboardWait::Elapsed
        })
        .map_err(|error| error.to_string())?;
    Ok(format!("Clipboard cleanup complete: {disposition:?}"))
}

fn current_unix_seconds() -> Result<u64, String> {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_secs())
        .map_err(|_| "the system clock is before the Unix epoch".to_owned())
}

fn poll_lease<B: SecretStoreBackend, C: AuthenticationClock>(
    session: &AuthenticationSession<B, C>,
    handle: &mut Option<AuthenticationHandle<B, C>>,
    sensitive: &mut SensitiveUiState,
) -> Result<LeasePoll, AuthenticationError> {
    if session.expire()? {
        *handle = None;
        sensitive.clear();
        return Ok(LeasePoll::Expired);
    }
    handle
        .as_ref()
        .map(AuthenticationHandle::remaining_time)
        .transpose()
        .map(|remaining| remaining.map_or(LeasePoll::Idle, LeasePoll::Active))
}

fn event_message(event: &Event) -> Option<Message> {
    if let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event {
        if key.as_ref() == keyboard::Key::Named(keyboard::key::Named::Escape) {
            return Some(Message::PaletteCancel);
        }
        if let Some(action) = action::shortcut_action(key, *modifiers) {
            return Some(Message::Action(action));
        }
        let navigation = match key.as_ref() {
            keyboard::Key::Named(keyboard::key::Named::ArrowUp) => NavigationKey::Previous,
            keyboard::Key::Named(keyboard::key::Named::ArrowDown) => NavigationKey::Next,
            keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => NavigationKey::Collapse,
            keyboard::Key::Named(keyboard::key::Named::ArrowRight) => NavigationKey::Expand,
            keyboard::Key::Named(keyboard::key::Named::Enter) => NavigationKey::Activate,
            keyboard::Key::Named(keyboard::key::Named::Home) => NavigationKey::First,
            keyboard::Key::Named(keyboard::key::Named::End) => NavigationKey::Last,
            _ => return is_deliberate_activity(event).then_some(Message::UserActivity),
        };
        return Some(Message::SidebarNavigate(navigation));
    }
    is_deliberate_activity(event).then_some(Message::UserActivity)
}

fn is_deliberate_activity(event: &Event) -> bool {
    matches!(
        event,
        Event::Keyboard(keyboard::Event::KeyPressed { .. })
            | Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. })
            | Event::Touch(
                touch::Event::FingerPressed { .. }
                    | touch::Event::FingerMoved { .. }
                    | touch::Event::FingerLifted { .. }
            )
    )
}

fn take_completion<T>(completion: &Arc<Mutex<Option<T>>>) -> Option<T> {
    completion.lock().ok()?.take()
}

#[cfg(test)]
mod tests {
    use std::{
        collections::BTreeMap,
        fs,
        path::Path,
        sync::{Arc, Mutex},
    };

    use iced::{Point, window};
    use ironstorage::{
        authentication::{AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT},
        crypto::{KeyStore, SecretProvider, SecretProviderError},
        git::{GitIdentity, GitRepository},
        repository::{EntryPath, Repository},
        secret_store::{
            SecretCachePolicy, SecretLocator, SecretProtection, SecretProtectionPolicy,
            SecretReference, SecretStore, SecretStoreError,
        },
    };

    use super::*;

    #[test]
    fn desktop_coverage_matrix_contains_every_registered_action() {
        let matrix = include_str!("../../../docs/desktop-audit.md");
        for spec in action::ACTIONS {
            assert!(
                matrix.contains(&format!("`{}`", spec.action.id())),
                "desktop parity matrix is missing {}",
                spec.action.id()
            );
        }
        for required_surface in [
            "Base pass",
            "Pass OTP",
            "Embedded Git",
            "Configuration and lock",
            "macOS",
            "Linux",
            "Windows",
        ] {
            assert!(
                matrix.contains(required_surface),
                "missing {required_surface}"
            );
        }
    }

    #[test]
    fn production_desktop_sources_preserve_security_and_architecture_boundaries() {
        let sources = [
            ("action.rs", include_str!("action.rs")),
            ("editor.rs", include_str!("editor.rs")),
            ("folder_picker.rs", include_str!("folder_picker.rs")),
            ("main.rs", include_str!("main.rs")),
            ("native_menu.rs", include_str!("native_menu.rs")),
            ("navigation.rs", include_str!("navigation.rs")),
            ("palette.rs", include_str!("palette.rs")),
        ];
        for (name, source) in sources {
            let production = source
                .split("#[cfg(test)]\nmod tests")
                .next()
                .unwrap_or(source);
            let forbidden_tokens = [
                ["std::", "process"].concat(),
                ["process", "::Command"].concat(),
                ["Command", "::new("].concat(),
                ["Repository", "::open"].concat(),
                ["GitRepository", "::"].concat(),
                ["OtpUri", "::parse"].concat(),
                ["qrcode", "::QrCode"].concat(),
                ["fs::", "write"].concat(),
                ["File", "::create"].concat(),
                ["OpenOptions", "::new"].concat(),
                ["println", "!("].concat(),
                ["eprintln", "!("].concat(),
                ["dbg", "!("].concat(),
                ["log", "::info"].concat(),
                ["log", "::debug"].concat(),
                ["log", "::error"].concat(),
                ["tracing", "::info"].concat(),
                ["tracing", "::debug"].concat(),
                ["tracing", "::error"].concat(),
                ["http", "://"].concat(),
                ["unsafe", " {"].concat(),
            ];
            for forbidden in &forbidden_tokens {
                assert!(
                    !production.contains(forbidden),
                    "{name} crosses the desktop architecture boundary with {forbidden}"
                );
            }
        }
    }

    #[test]
    fn window_contract_keeps_both_scrollable_panes_usable_at_narrow_size() {
        let settings = window_settings();
        assert_eq!(settings.size, Size::new(960.0, 680.0));
        assert_eq!(settings.min_size, Some(Size::new(480.0, 360.0)));
        let app = test_app(None);
        let _view = app.view();
    }

    #[test]
    fn utility_panels_share_the_compact_entry_palette() {
        let theme = Theme::Dark;
        assert_eq!(
            entry_area_style(&theme).background,
            Some(Background::Color(ENTRY_AREA_BACKGROUND))
        );
        assert_eq!(
            entry_field_style(&theme, false).background,
            Some(Background::Color(ENTRY_FIELD_BACKGROUND))
        );
        assert_eq!(
            entry_input_style(&theme, text_input::Status::Active).background,
            Background::Color(ENTRY_INPUT_BACKGROUND)
        );
        assert_eq!(
            entry_icon_button(&theme, button::Status::Hovered).background,
            Some(Background::Color(ENTRY_FIELD_ACTIVE_BACKGROUND))
        );

        let (_temporary, storage) = fixture_storage();
        let mut app = test_app(None);
        app.utility = Some(UtilityView::Settings(SettingsForm::new(&storage)));
        app.storage = Some(storage);
        assert_eq!(
            utility_title(app.utility.as_ref().expect("settings panel")),
            "Settings"
        );
        let _view = app.view();
    }

    #[derive(Clone, Default)]
    struct ManualClock(Arc<Mutex<Duration>>);

    impl ManualClock {
        fn advance(&self, duration: Duration) {
            *self.0.lock().expect("test clock") += duration;
        }
    }

    impl AuthenticationClock for ManualClock {
        fn now(&self) -> Duration {
            *self.0.lock().expect("test clock")
        }
    }

    #[derive(Clone, Default)]
    struct MemoryBackend(Arc<Mutex<BTreeMap<SecretLocator, SecretBytes>>>);

    impl SecretStoreBackend for MemoryBackend {
        fn create(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
            value: &[u8],
        ) -> Result<(), SecretStoreError> {
            self.0
                .lock()
                .expect("test backend")
                .insert(locator.clone(), SecretBytes::new(value.to_vec()));
            Ok(())
        }

        fn retrieve(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
        ) -> Result<SecretBytes, SecretStoreError> {
            self.0
                .lock()
                .expect("test backend")
                .get(locator)
                .map(|value| SecretBytes::new(value.expose().to_vec()))
                .ok_or(SecretStoreError::Missing)
        }

        fn replace(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
            value: &[u8],
        ) -> Result<(), SecretStoreError> {
            self.create(locator, SecretProtection::DeviceUnlocked, value)
        }

        fn delete(
            &self,
            locator: &SecretLocator,
            _protection: SecretProtection,
        ) -> Result<(), SecretStoreError> {
            self.0.lock().expect("test backend").remove(locator);
            Ok(())
        }

        fn lock(&self) -> Result<(), SecretStoreError> {
            Ok(())
        }

        fn unlock(&self) -> Result<(), SecretStoreError> {
            Ok(())
        }
    }

    struct FixtureSecrets;

    impl SecretProvider for FixtureSecrets {
        fn secret_for(&mut self, _key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
            Ok(SecretBytes::new(b"fixture-alice-passphrase".to_vec()))
        }
    }

    fn session(
        timeout: Duration,
    ) -> (
        AuthenticationSession<MemoryBackend, ManualClock>,
        KeyInfo,
        ManualClock,
    ) {
        let mut keys = KeyStore::new();
        let [key] = keys
            .import(include_bytes!(
                "../../../crates/storage/tests/fixtures/compatibility/keys/alice-secret.asc"
            ))
            .expect("fixture key")
            .try_into()
            .expect("one fixture key");
        let backend = MemoryBackend::default();
        let store = SecretStore::new(
            backend.clone(),
            SecretCachePolicy::Disabled,
            SecretProtectionPolicy::device_unlocked(),
        );
        store.unlock().expect("unlock fixture store");
        store
            .create(
                &SecretReference::openpgp_passphrase(key.fingerprint().as_str())
                    .expect("fixture reference"),
                SecretBytes::new(b"fixture-alice-passphrase".to_vec()),
            )
            .expect("provision fixture passphrase");
        store.lock().expect("lock fixture store");
        let clock = ManualClock::default();
        let session = AuthenticationSession::with_clock(
            backend,
            SecretProtectionPolicy::device_unlocked(),
            AuthenticationTimeout::new(timeout).expect("valid timeout"),
            clock.clone(),
        );
        (session, key, clock)
    }

    fn fixture_storage() -> (tempfile::TempDir, DesktopStorage) {
        let temporary = tempfile::tempdir().expect("temporary vault");
        let vault = temporary.path().join("vault");
        let native = temporary.path().join("native");
        fs::create_dir_all(&vault).expect("vault");
        fs::create_dir_all(&native).expect("native");
        let keys = Path::new(env!("CARGO_MANIFEST_DIR"))
            .join("../../crates/storage/tests/fixtures/compatibility/keys");
        fs::write(
            vault.join(".gpg-id"),
            b"7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30\n",
        )
        .expect("recipient policy");
        let config_path = temporary.path().join("config.toml");
        fs::write(
            &config_path,
            format!(
                "vault = {:?}\ndefault_key = \"7E5C5241B25F6FFAAD717EBFA132DCB2DC23AE30\"\nkey_material = {:?}\n",
                vault, keys
            ),
        )
        .expect("configuration");
        let storage = DesktopStorage::load(Some(&config_path)).expect("load configuration");
        (temporary, storage)
    }

    fn empty_editor(storage: &DesktopStorage, entry: &str) -> EntryEditor {
        let document = storage
            .open_document(entry, &mut FixtureSecrets)
            .expect("document");
        EntryEditor::new(document)
    }

    fn test_app(editor: Option<EntryEditor>) -> App {
        App {
            authentication: AuthenticationView::Locked,
            storage: None,
            session: None,
            key: None,
            handle: None,
            sensitive: SensitiveUiState::default(),
            git_control: None,
            git_progress: None,
            authentication_generation: 0,
            operation_generation: 0,
            tree_generation: 0,
            vault_generation: 0,
            settings_generation: 0,
            workflow_generation: 0,
            otp_generation: 0,
            otp_pending: false,
            selection_after_refresh: None,
            panes: pane_grid::State::with_configuration(pane_grid::Configuration::Split {
                axis: pane_grid::Axis::Vertical,
                ratio: 0.28,
                a: Box::new(pane_grid::Configuration::Pane(PaneKind::Sidebar)),
                b: Box::new(pane_grid::Configuration::Pane(PaneKind::Content)),
            }),
            pane_focus: PaneFocus::Sidebar,
            navigation: NavigationTree::default(),
            tree_state: TreeState::Loading,
            after_authentication: None,
            entry_path: String::new(),
            editor,
            content_mode: ContentMode::Viewer,
            saving: false,
            switching_vault: false,
            confirmation: None,
            after_save: None,
            generation_form: None,
            conflict: false,
            status: String::new(),
            open_menu: None,
            utility: None,
            context_target: None,
            palette: CommandPalette::default(),
            #[cfg(target_os = "macos")]
            native_menu: None,
        }
    }

    #[test]
    fn structured_fields_save_round_trip_and_stale_drafts_remain_recoverable() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "documents/editable");
        editor.add_after(None).expect("password line");
        let password = editor.fields()[0].id();
        editor.update_raw(password, b"password").expect("password");
        editor.add_after(Some(password)).expect("username line");
        let username = editor.fields()[1].id();
        editor
            .update_raw(username, b"username: alice")
            .expect("username");
        editor.add_after(Some(username)).expect("first note line");
        let first_note = editor.fields()[2].id();
        editor
            .update_raw(first_note, b"first note line")
            .expect("first note");
        editor
            .add_after(Some(first_note))
            .expect("second note line");
        let second_note = editor.fields()[3].id();
        editor
            .update_raw(second_note, b"second note line")
            .expect("second note");
        editor
            .add_after(Some(second_note))
            .expect("unknown field line");
        let unknown = editor.fields()[4].id();
        editor
            .update_raw(unknown, b"custom-field: opaque")
            .expect("unknown field");
        editor.move_up(unknown).expect("reorder unknown field");
        editor.add_after(None).expect("temporary line");
        let temporary = editor.fields().last().expect("temporary field").id();
        editor.remove(temporary).expect("remove temporary line");
        let expected =
            b"password\nusername: alice\n first note line\ncustom-field: opaque\n second note line";
        assert_eq!(editor.document().serialize().expose(), expected);
        save_document(&storage, &editor).expect("initial save");

        let reopened = empty_editor(&storage, "documents/editable");
        assert_eq!(reopened.document().serialize().expose(), expected);

        let mut winner = empty_editor(&storage, "documents/editable");
        let mut stale = empty_editor(&storage, "documents/editable");
        let winner_password = winner.fields()[0].id();
        winner
            .update_raw(winner_password, b"winner")
            .expect("winner edit");
        save_document(&storage, &winner).expect("winner save");
        let stale_password = stale.fields()[0].id();
        stale
            .update_raw(stale_password, b"complete stale draft")
            .expect("stale edit");
        let failure = save_document(&storage, &stale).expect_err("stale save");
        assert_eq!(failure.kind(), DesktopErrorKind::Conflict);
        assert!(stale.is_dirty());
        assert_eq!(
            stale.document().serialize().expose(),
            b"complete stale draft\nusername: alice\n first note line\ncustom-field: opaque\n second note line"
        );
    }

    #[test]
    fn viewer_exposes_every_unlocked_field_and_clears_it_on_lock() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "documents/viewer");
        for value in [
            b"password".as_slice(),
            b"username: alice",
            b"custom: first",
            b"custom: second",
            b"first note",
            b"second note",
            b"comments: Recovery codes:\none\ntwo",
            b"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example",
            b"otpauth://broken",
            b"binary: \xff",
        ] {
            let after = editor.fields().last().map(EntryField::id);
            editor.add_after(after).expect("add viewer field");
            let id = editor.fields().last().expect("new viewer field").id();
            editor.update_raw(id, value).expect("populate viewer field");
        }

        let fields = editor.fields();
        assert_eq!(fields[0].metadata().kind(), EntryFieldKind::Password);
        assert_eq!(fields[1].metadata().kind(), EntryFieldKind::Username);
        assert_eq!(fields[2].metadata().name(), Some("custom"));
        assert_eq!(fields[3].metadata().name(), Some("custom"));
        assert_eq!(viewer_label(&fields[2]), viewer_label(&fields[3]));
        assert_eq!(fields[4].metadata().kind(), EntryFieldKind::Note);
        assert_eq!(fields[5].metadata().kind(), EntryFieldKind::Note);
        assert_eq!(fields[6].metadata().name(), Some("comments"));
        assert_eq!(fields[6].value(), b"Recovery codes:\none\ntwo");
        assert!(fields[7].metadata().otp().is_some());
        assert_eq!(
            fields[8].metadata().diagnostic(),
            Some(EntryFieldDiagnostic::MalformedOtpUri)
        );
        assert_eq!(
            fields[9].metadata().diagnostic(),
            Some(EntryFieldDiagnostic::NonUtf8Value)
        );

        let password = fields[0].id();
        assert_eq!(
            viewer_value(&editor.fields()[0]),
            ViewerValue::Text("password")
        );
        assert_eq!(
            viewer_value(&editor.fields()[7]),
            ViewerValue::Text(
                "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example"
            )
        );
        assert_eq!(
            viewer_value(&editor.fields()[6]),
            ViewerValue::Text("Recovery codes:\none\ntwo")
        );
        assert_eq!(viewer_value(&editor.fields()[9]), ViewerValue::Unavailable);
        let comments = editor.fields()[6].id();
        editor
            .update_raw(comments, b"comments: Recovery codes:\nalpha\ntwo")
            .expect("update multiline field");
        editor
            .add_value_line_after(comments, 2)
            .expect("append multiline row");
        assert_eq!(editor.fields()[6].value(), b"Recovery codes:\nalpha\ntwo\n");
        assert_eq!(
            editor
                .multiline_content(comments)
                .expect("multiline editor")
                .text(),
            "Recovery codes:\nalpha\ntwo\n"
        );
        editor
            .edit_multiline(
                comments,
                text_editor::Action::Edit(text_editor::Edit::Insert('!')),
            )
            .expect("edit multiline content");
        assert_eq!(
            std::str::from_utf8(editor.fields()[6].value()).expect("UTF-8 field"),
            editor
                .multiline_content(comments)
                .expect("multiline editor")
                .text()
        );
        assert!(document_has_single_totp(editor.document()));
        assert_eq!(
            editor
                .document()
                .display_fields()
                .into_iter()
                .map(EntryField::id)
                .collect::<Vec<_>>(),
            [
                editor.fields()[1].id(),
                editor.fields()[0].id(),
                editor.fields()[7].id(),
                editor.fields()[8].id(),
                editor.fields()[4].id(),
                editor.fields()[5].id(),
                editor.fields()[6].id(),
                editor.fields()[9].id(),
                editor.fields()[2].id(),
                editor.fields()[3].id(),
            ]
        );
        editor.navigate(FieldNavigation::First);
        assert_eq!(editor.focused(), Some(editor.fields()[1].id()));
        editor.navigate(FieldNavigation::Next);
        assert_eq!(editor.focused(), Some(password));
        editor.navigate(FieldNavigation::Last);
        assert_eq!(editor.focused(), Some(editor.fields()[3].id()));

        assert!(!authentication_allows_content(&AuthenticationView::Locked));
        assert!(authentication_allows_content(
            &AuthenticationView::Unlocked(Duration::from_secs(1))
        ));
        let mut app = test_app(Some(editor));
        let _task = app.update(Message::BeginEdit);
        assert_eq!(app.content_mode, ContentMode::Viewer);
        app.authentication = AuthenticationView::Unlocked(Duration::from_secs(1));
        let _task = app.update(Message::BeginEdit);
        assert_eq!(app.content_mode, ContentMode::Editor);
        app.authentication_lost("locked".to_owned());
        assert!(app.editor.is_none());
    }

    #[test]
    fn every_destructive_path_uses_the_same_save_discard_cancel_guard() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "draft");
        editor.add_after(None).expect("line");
        assert_eq!(dirty_decision(Some(&editor)), DirtyDecision::Confirm);
        for action in [
            PendingAction::OpenVault(PathBuf::from("/selected-vault")),
            PendingAction::OpenEntry("other".to_owned()),
            PendingAction::Reload("draft".to_owned()),
            PendingAction::CloseWindow(window::Id::unique()),
            PendingAction::Git(DesktopGitRequest::Pull),
            PendingAction::Git(DesktopGitRequest::Sync),
            PendingAction::Git(DesktopGitRequest::Resolve(Vec::new())),
        ] {
            assert!(matches!(
                action,
                PendingAction::OpenVault(_)
                    | PendingAction::OpenEntry(_)
                    | PendingAction::Reload(_)
                    | PendingAction::CloseWindow(_)
                    | PendingAction::Git(_)
            ));
            assert_eq!(dirty_decision(Some(&editor)), DirtyDecision::Confirm);
        }
        assert_eq!(dirty_decision(None), DirtyDecision::Execute);

        let draft = editor.document().serialize().expose().to_vec();
        let mut app = test_app(Some(editor));
        let _task = app.update(Message::FolderPicked(Ok(None)));
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
        let selected_vault = PathBuf::from("/selected-vault");
        let _task = app.update(Message::FolderPicked(Ok(Some(selected_vault.clone()))));
        assert_eq!(
            app.confirmation,
            Some(PendingAction::OpenVault(selected_vault))
        );
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
        let _task = app.update(Message::CancelDiscard);
        assert!(app.confirmation.is_none());

        let _task = app.request_action(PendingAction::Git(DesktopGitRequest::Pull));
        assert_eq!(
            app.confirmation,
            Some(PendingAction::Git(DesktopGitRequest::Pull))
        );
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
        let _task = app.update(Message::CancelDiscard);

        let draft_id = TreeNodeId::Entry(EntryPath::parse("draft").expect("draft path"));
        let other_id = TreeNodeId::Entry(EntryPath::parse("other").expect("other path"));
        app.navigation.replace_test_nodes(vec![
            navigation::TestNode {
                id: draft_id,
                name: "draft".to_owned(),
                children: Vec::new(),
            },
            navigation::TestNode {
                id: other_id.clone(),
                name: "other".to_owned(),
                children: Vec::new(),
            },
        ]);
        assert!(app.navigation.select_entry_path("draft"));
        let intent = app.navigation.activate(other_id);
        let _task = app.handle_navigation(intent);
        assert_eq!(
            app.navigation.selected().map(TreeNodeId::path),
            Some(Path::new("draft"))
        );
        let _task = app.update(Message::CancelDiscard);
        assert!(app.confirmation.is_none());
        assert_eq!(
            app.editor
                .as_ref()
                .expect("cancel retains editor")
                .document()
                .serialize()
                .expose(),
            draft
        );

        let _task = app.request_action(PendingAction::OpenEntry("other".to_owned()));
        let _task = app.update(Message::ConfirmDiscard);
        assert!(app.editor.is_none());
        assert_eq!(
            app.after_authentication,
            Some(PendingAction::OpenEntry("other".to_owned()))
        );
    }

    #[test]
    fn shared_actions_cannot_bypass_authentication_or_dirty_confirmation() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "draft");
        editor.add_after(None).expect("dirty line");
        let mut app = test_app(Some(editor));
        app.content_mode = ContentMode::Editor;

        assert!(!action::enabled(UiAction::Save, app.action_context()));
        let _task = app.invoke_action(UiAction::Save);
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));

        app.authentication = AuthenticationView::Unlocked(Duration::from_secs(60));
        assert!(action::enabled(UiAction::Save, app.action_context()));
        let id = window::Id::unique();
        let _task = app.update(Message::WindowResolved(UiAction::CloseWindow, Some(id)));
        assert_eq!(app.confirmation, Some(PendingAction::CloseWindow(id)));
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));

        app.confirmation = None;
        app.switching_vault = true;
        let _task = app.update(Message::RequestClose(id));
        assert!(app.confirmation.is_none());
        assert!(app.status.contains("Wait for vault validation"));
    }

    #[test]
    fn command_palette_focus_matching_execution_cancellation_and_availability_are_shared() {
        let mut app = test_app(None);
        app.pane_focus = PaneFocus::Content;

        let _focus = app.update(Message::Action(UiAction::CommandPalette));
        assert!(app.palette.is_open());
        assert_eq!(app.palette.selected(), 0);
        let _task = app.update(Message::SidebarNavigate(NavigationKey::Next));
        assert_eq!(app.palette.selected(), 1);
        let _task = app.update(Message::SidebarNavigate(NavigationKey::Previous));
        assert_eq!(app.palette.selected(), 0);

        let _task = app.update(Message::PaletteQueryChanged("about".to_owned()));
        assert_eq!(app.palette.selected_action(), Some(UiAction::About));
        let _task = app.update(Message::SidebarNavigate(NavigationKey::Activate));
        assert!(!app.palette.is_open());
        assert!(matches!(app.utility, Some(UtilityView::About)));
        assert_eq!(app.pane_focus, PaneFocus::Content);

        app.utility = None;
        let _task = app.update(Message::Action(UiAction::CommandPalette));
        let _task = app.update(Message::PaletteQueryChanged("lock".to_owned()));
        assert_eq!(app.palette.selected_action(), Some(UiAction::Lock));
        let _task = app.update(Message::SidebarNavigate(NavigationKey::Activate));
        assert!(app.palette.is_open());
        assert!(app.status.contains("already locked"));
        let _restore = app.update(Message::PaletteCancel);
        assert!(!app.palette.is_open());
        assert!(app.palette.query().is_empty());
        assert_eq!(app.pane_focus, PaneFocus::Content);

        app.confirmation = Some(PendingAction::CloseWindow(window::Id::unique()));
        let _task = app.update(Message::Action(UiAction::CommandPalette));
        assert!(!app.palette.is_open());
        assert!(app.status.contains("Finish the current confirmation"));
        app.confirmation = None;

        let (_temporary, storage) = fixture_storage();
        app.storage = Some(storage);
        let _task = app.update(Message::Action(UiAction::CommandPalette));
        let _task = app.update(Message::PaletteInvoke(UiAction::Settings));
        assert!(!app.palette.is_open());
        assert!(matches!(app.utility, Some(UtilityView::Settings(_))));

        let escape = Event::Keyboard(keyboard::Event::KeyPressed {
            key: keyboard::Key::Named(keyboard::key::Named::Escape),
            modified_key: keyboard::Key::Named(keyboard::key::Named::Escape),
            physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
            location: keyboard::Location::Standard,
            modifiers: keyboard::Modifiers::NONE,
            text: None,
            repeat: false,
        });
        assert!(matches!(
            event_message(&escape),
            Some(Message::PaletteCancel)
        ));
    }

    #[test]
    fn settings_form_validates_and_storage_persists_atomically() {
        let (_temporary, storage) = fixture_storage();
        let before = fs::read(storage.config_source()).expect("original config");
        let mut form = SettingsForm::new(&storage);
        assert_eq!(form.vault, storage.vault().display().to_string());
        assert_eq!(form.default_key, storage.default_key());

        form.authentication_timeout = "not-a-number".to_owned();
        assert!(form.settings(&storage).is_err());
        form.authentication_timeout = "600".to_owned();
        form.default_key = "missing-key".to_owned();
        let invalid = form.settings(&storage).expect("syntactically valid form");
        assert!(storage.update_settings(invalid).is_err());
        assert_eq!(
            fs::read(storage.config_source()).expect("rejected config"),
            before
        );

        form.default_key = storage.default_key().to_owned();
        let updated = storage
            .update_settings(form.settings(&storage).expect("valid form"))
            .expect("persisted settings");
        assert_eq!(
            updated.authentication_timeout().duration(),
            Duration::from_secs(600)
        );
        let reloaded = DesktopStorage::load(Some(storage.config_source())).expect("reload config");
        assert_eq!(
            reloaded.authentication_timeout().duration(),
            Duration::from_secs(600)
        );
    }

    #[test]
    fn vault_switch_result_replaces_state_only_after_storage_success() {
        let (_temporary, storage) = fixture_storage();
        let mut failed = test_app(None);
        failed.storage = Some(storage.clone());
        failed.switching_vault = true;
        failed.vault_generation = 4;
        let original = failed.storage.as_ref().expect("storage").vault().to_owned();
        let _task = failed.update(Message::VaultSwitched {
            generation: 4,
            result: Box::new(Err("injected failure".to_owned())),
        });
        assert_eq!(failed.storage.as_ref().expect("storage").vault(), original);
        assert!(!failed.switching_vault);
        assert!(failed.status.contains("Current vault unchanged"));

        let mut editor = empty_editor(&storage, "draft");
        editor.add_after(None).expect("dirty line");
        let mut succeeded = test_app(Some(editor));
        succeeded.authentication = AuthenticationView::Unlocked(Duration::from_secs(60));
        succeeded.switching_vault = true;
        succeeded.vault_generation = 5;
        succeeded.operation_generation = 8;
        let _task = succeeded.update(Message::VaultSwitched {
            generation: 5,
            result: Box::new(Ok(storage)),
        });
        assert!(succeeded.editor.is_none());
        assert!(matches!(
            succeeded.authentication,
            AuthenticationView::Locked
        ));
        assert_eq!(succeeded.tree_state, TreeState::Loading);
        assert!(!succeeded.switching_vault);
        assert_eq!(succeeded.operation_generation, 9);
    }

    #[test]
    fn lock_and_expiry_drop_the_complete_editor_and_clipboard_state() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "dirty");
        editor.add_after(None).expect("dirty line");
        let mut app = test_app(Some(editor));
        app.sensitive.clipboard_cancel = Some(Arc::new(AtomicBool::new(false)));
        app.authentication_lost("locked".to_owned());
        assert!(app.editor.is_none());
        assert!(app.sensitive.clipboard_cancel.is_none());

        let (generation, cancel) = app.sensitive.begin_copy(Duration::from_secs(45));
        app.sensitive.clear();
        assert!(cancel.load(Ordering::Acquire));
        assert!(!app.sensitive.finish_copy(generation));

        let timeout = DEFAULT_AUTHENTICATION_TIMEOUT;
        let (session, key, clock) = session(timeout);
        let mut handle = Some(session.authenticate(&key).expect("authenticate"));
        let mut sensitive = SensitiveUiState {
            clipboard_cancel: Some(Arc::new(AtomicBool::new(false))),
            clipboard_generation: 0,
            ..SensitiveUiState::default()
        };
        clock.advance(timeout);
        assert_eq!(
            poll_lease(&session, &mut handle, &mut sensitive).expect("poll"),
            LeasePoll::Expired
        );
        assert!(handle.is_none());
        assert!(sensitive.clipboard_cancel.is_none());
    }

    #[test]
    fn storage_rfc_totp_countdown_does_not_renew_authentication_and_lock_clears_presentations() {
        let (_temporary, storage) = fixture_storage();
        let mut provider = FixtureSecrets;
        let mut document = storage
            .create_document("otp/rfc6238", &mut provider)
            .expect("create OTP document");
        document
            .add(
                0,
                ironstorage::document::EntryFieldDraft::line(b"password".to_vec())
                    .expect("password"),
            )
            .expect("password field");
        document
            .add(
                1,
                ironstorage::document::EntryFieldDraft::otp_uri(
                    b"otpauth://totp/RFC6238?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&algorithm=SHA1&digits=8&period=30".to_vec(),
                )
                .expect("RFC URI"),
            )
            .expect("OTP field");
        storage.save_document(&document).expect("save OTP fixture");
        let outcome = storage
            .otp_code("otp/rfc6238", 59, false, &mut provider)
            .expect("desktop OTP outcome");
        let (code, validity, metadata, _, _) = outcome.into_parts();
        assert_eq!(code.expose(), b"94287082");
        let display = OtpDisplay {
            entry: "otp/rfc6238".to_owned(),
            code,
            validity,
            metadata,
            observed_at: 59,
        };
        assert_eq!(display.remaining_at(59), Some(1));
        assert_eq!(display.remaining_at(60), Some(0));
        assert_eq!(otp_progress(&display), Some((1, 30)));
        assert_eq!(otp_code_text(&display), "9428 7082");

        let mut hotp_document = storage
            .create_document("otp/rfc4226", &mut provider)
            .expect("create HOTP document");
        hotp_document
            .add(
                0,
                ironstorage::document::EntryFieldDraft::line(b"password".to_vec())
                    .expect("password"),
            )
            .expect("password field");
        hotp_document
            .add(
                1,
                ironstorage::document::EntryFieldDraft::otp_uri(
                    b"otpauth://hotp/RFC4226?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&counter=0"
                        .to_vec(),
                )
                .expect("RFC HOTP URI"),
            )
            .expect("HOTP field");
        storage
            .save_document(&hotp_document)
            .expect("save HOTP fixture");
        let hotp = storage
            .otp_code("otp/rfc4226", 59, true, &mut provider)
            .expect("HOTP outcome");
        let mut app = test_app(None);
        app.otp_generation = 1;
        app.utility = Some(UtilityView::Otp(OtpForm::new("otp/rfc4226".to_owned())));
        let _task = app.update(Message::OtpFinished {
            generation: 1,
            completion: Arc::new(Mutex::new(Some(Ok(OtpTaskResult::Code {
                entry: "otp/rfc4226".to_owned(),
                copy: false,
                observed_at: 59,
                outcome: hotp,
            })))),
        });
        assert_eq!(
            app.sensitive
                .otp
                .as_ref()
                .expect("HOTP displayed")
                .code
                .expose(),
            b"287082"
        );
        assert!(app.status.contains("counter 1"));
        let refreshed = app.editor.as_ref().expect("refreshed HOTP entry");
        assert_eq!(refreshed.entry(), "otp/rfc4226");
        let refreshed_otp = refreshed
            .fields()
            .iter()
            .find_map(|field| field.metadata().otp())
            .expect("refreshed HOTP metadata");
        assert_eq!(refreshed_otp.kind(), OtpKind::Hotp);
        assert_eq!(refreshed_otp.counter(), Some(1));

        let timeout = Duration::from_secs(2);
        let (session, key, clock) = session(timeout);
        let mut handle = Some(session.authenticate(&key).expect("authenticate"));
        let mut sensitive = SensitiveUiState {
            otp: Some(display),
            otp_uri: Some(SecretBytes::new(b"otpauth://secret".to_vec())),
            otp_qr: Some(
                QrMatrix::encode(&SecretBytes::new(b"otpauth://secret".to_vec())).expect("QR"),
            ),
            ..SensitiveUiState::default()
        };
        clock.advance(Duration::from_secs(1));
        assert_eq!(
            poll_lease(&session, &mut handle, &mut sensitive).expect("countdown poll"),
            LeasePoll::Active(Duration::from_secs(1))
        );
        sensitive.otp.as_mut().expect("OTP display").observed_at = 60;
        clock.advance(Duration::from_secs(1));
        assert_eq!(
            poll_lease(&session, &mut handle, &mut sensitive).expect("expiry poll"),
            LeasePoll::Expired
        );
        assert!(handle.is_none());
        assert!(sensitive.otp.is_none());
        assert!(sensitive.otp_uri.is_none());
        assert!(sensitive.otp_qr.is_none());
    }

    #[test]
    fn deliberate_input_and_primary_save_are_distinct_from_passive_events() {
        assert!(!is_deliberate_activity(&Event::Window(
            window::Event::Focused
        )));
        assert!(!is_deliberate_activity(&Event::Mouse(
            mouse::Event::CursorMoved {
                position: Point::ORIGIN,
            }
        )));
        let click = Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left));
        assert!(matches!(event_message(&click), Some(Message::UserActivity)));
        let save = Event::Keyboard(keyboard::Event::KeyPressed {
            key: keyboard::Key::Character("s".into()),
            modified_key: keyboard::Key::Character("s".into()),
            physical_key: keyboard::key::Physical::Code(keyboard::key::Code::KeyS),
            location: keyboard::Location::Standard,
            modifiers: keyboard::Modifiers::COMMAND,
            text: Some("s".into()),
            repeat: false,
        });
        assert!(matches!(
            event_message(&save),
            Some(Message::Action(UiAction::Save))
        ));

        let tab = Event::Keyboard(keyboard::Event::KeyPressed {
            key: keyboard::Key::Named(keyboard::key::Named::Tab),
            modified_key: keyboard::Key::Named(keyboard::key::Named::Tab),
            physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Tab),
            location: keyboard::Location::Standard,
            modifiers: keyboard::Modifiers::NONE,
            text: None,
            repeat: false,
        });
        assert!(matches!(
            event_message(&tab),
            Some(Message::Action(UiAction::TogglePaneFocus))
        ));
    }

    #[test]
    fn pane_focus_and_storage_errors_are_handled_without_repository_access() {
        let mut app = test_app(None);
        assert_eq!(app.tree_state, TreeState::Loading);
        assert_eq!(tree_state_from_result(Ok(true)), TreeState::Empty);
        assert_eq!(tree_state_from_result(Ok(false)), TreeState::Ready);
        assert_eq!(
            tree_state_from_result(Err("fake error".to_owned())),
            TreeState::Error("fake error".to_owned())
        );
        assert!(matches!(app.authentication, AuthenticationView::Locked));
        assert_eq!(app.pane_focus, PaneFocus::Sidebar);
        let _task = app.update(Message::TogglePaneFocus);
        assert_eq!(app.pane_focus, PaneFocus::Content);

        app.tree_generation = 7;
        let completion = Arc::new(Mutex::new(Some(Err("injected tree failure".to_owned()))));
        let _task = app.update(Message::TreeLoaded {
            generation: 7,
            completion,
        });
        assert_eq!(
            app.tree_state,
            TreeState::Error("injected tree failure".to_owned())
        );
        assert!(app.navigation.is_empty());
    }

    #[test]
    fn recipient_forms_round_trip_storage_keys_and_create_upstream_policies() {
        let (_temporary, storage) = fixture_storage();
        let mut root = RecipientForm::new(
            RecipientWorkflowKind::InitializeStore,
            &storage,
            String::new(),
        )
        .expect("root initialization form");
        assert!(root.recipients.len() >= 2);
        let replacement_default = root
            .recipients
            .iter_mut()
            .find(|recipient| recipient.can_default && !recipient.default)
            .expect("second secret encryption key");
        replacement_default.selected = true;
        root.default_key
            .clone_from(&replacement_default.fingerprint);
        root.confirmed = true;
        let root_request = root.request().expect("root initialization request");
        fs::remove_file(storage.vault().join(".gpg-id")).expect("empty uninitialized store");
        let repository = Repository::open(storage.vault()).expect("repository");
        let git = GitRepository::init(&repository, GitIdentity::ironstorage()).expect("Git init");
        let commits_before = git.log(None).expect("initial log").len();
        let (initialized, root_outcome) = storage
            .apply_recipient_policy(&root_request, &root.default_key, &mut FixtureSecrets)
            .expect("root initialization");
        assert!(root_outcome.directory().as_path().as_os_str().is_empty());
        let git = GitRepository::open(&repository, GitIdentity::ironstorage()).expect("reopen Git");
        assert_eq!(
            git.log(None).expect("root init log").len(),
            commits_before + 1
        );
        assert!(git.status().expect("root init status").is_clean());
        assert_eq!(
            fs::read(storage.vault().join(".gpg-id")).expect("root policy bytes"),
            root_request
                .key_identities
                .iter()
                .map(|identity| format!("{identity}\n"))
                .collect::<String>()
                .as_bytes()
        );
        assert_eq!(initialized.default_key(), root.default_key);
        assert_eq!(
            DesktopStorage::load(Some(storage.config_source()))
                .expect("reloaded config")
                .default_key(),
            root.default_key
        );

        let mut form = RecipientForm::new(
            RecipientWorkflowKind::NewFolder,
            &initialized,
            "team/services".to_owned(),
        )
        .expect("recipient form");
        assert!(form.recipients.len() >= 2);
        let default = form
            .recipients
            .iter()
            .find(|recipient| recipient.default)
            .expect("configured default key");
        assert!(default.selected);
        assert!(form.request().is_err(), "replacement requires confirmation");
        form.confirmed = true;
        let request = form.request().expect("validated request");
        let (updated, outcome) = initialized
            .apply_recipient_policy(&request, &form.default_key, &mut FixtureSecrets)
            .expect("nested recipient policy");
        assert_eq!(outcome.directory().as_path(), Path::new("team/services"));
        assert_eq!(outcome.recipients().len(), 1);
        assert_eq!(updated.default_key(), form.default_key);
        assert_eq!(
            DesktopStorage::load(Some(initialized.config_source()))
                .expect("reloaded config")
                .default_key(),
            form.default_key
        );
        assert_eq!(
            fs::read(storage.vault().join("team/services/.gpg-id")).expect("policy bytes"),
            request
                .key_identities
                .iter()
                .map(|identity| format!("{identity}\n"))
                .collect::<String>()
                .as_bytes()
        );

        form.path = "../escape".to_owned();
        let invalid = form.request().expect("UI leaves path rules to storage");
        assert!(
            initialized
                .apply_recipient_policy(&invalid, &form.default_key, &mut FixtureSecrets)
                .is_err()
        );
        assert!(!storage.vault().join("escape").exists());
    }

    #[test]
    fn new_entry_drafts_generate_before_save_and_reject_collisions() {
        let (_temporary, storage) = fixture_storage();
        let form = NewEntryForm {
            path: "generated/account".to_owned(),
            generate: true,
            length: "32".to_owned(),
            no_symbols: true,
            running: false,
            error: None,
        };
        let password = form.password().expect("generated password");
        assert_eq!(password.expose().len(), 32);
        assert!(password.expose().iter().all(u8::is_ascii_alphanumeric));
        let document = storage
            .create_document(&form.path, &mut FixtureSecrets)
            .expect("new document");
        assert!(document.fields().is_empty());
        assert!(!storage.vault().join("generated/account.gpg").exists());

        let editor = EntryEditor::new_entry(document, password).expect("new entry editor");
        assert!(editor.is_dirty());
        assert_eq!(editor.fields().len(), 1);
        assert_eq!(
            editor.fields()[0].metadata().kind(),
            EntryFieldKind::Password
        );
        save_document(&storage, &editor).expect("save generated entry");
        assert!(storage.vault().join("generated/account.gpg").is_file());
        let collision = storage
            .create_document(&form.path, &mut FixtureSecrets)
            .expect_err("existing entry must not become a draft");
        assert_eq!(collision.kind(), DesktopErrorKind::EntryExists);
    }

    #[test]
    fn generated_replacements_confirm_and_update_only_the_selected_draft_field() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "replacement");
        editor.add_after(None).expect("password field");
        let password = editor.fields()[0].id();
        editor.update_raw(password, b"old password").expect("value");
        editor.add_after(Some(password)).expect("username field");
        let username = editor.fields()[1].id();
        editor
            .update_raw(username, b"username: alice")
            .expect("username");
        let username_before = editor.fields()[1].contents().expose().to_vec();
        let mut app = test_app(Some(editor));
        app.authentication = AuthenticationView::Unlocked(Duration::from_secs(60));
        app.content_mode = ContentMode::Editor;

        let _task = app.update(Message::FieldAction(password, UiAction::GeneratePassword));
        assert!(
            app.generation_form
                .as_ref()
                .is_some_and(|form| form.replacement)
        );
        let _task = app.update(Message::SubmitGenerate);
        assert!(
            app.generation_form
                .as_ref()
                .is_some_and(|form| form.error.is_some())
        );
        assert_eq!(
            app.editor.as_ref().expect("draft").fields()[0].value(),
            b"old password"
        );

        let _task = app.update(Message::GenerateLengthChanged("12".to_owned()));
        let _task = app.update(Message::ToggleGenerateSymbols);
        let _task = app.update(Message::ToggleGenerateConfirmation);
        let _task = app.update(Message::SubmitGenerate);
        let editor = app.editor.as_ref().expect("updated draft");
        assert!(app.generation_form.is_none());
        assert_eq!(editor.fields()[0].value().len(), 12);
        assert!(
            editor.fields()[0]
                .value()
                .iter()
                .all(u8::is_ascii_alphanumeric)
        );
        assert_eq!(
            editor
                .document()
                .field(username)
                .unwrap()
                .contents()
                .expose(),
            username_before
        );
    }

    #[test]
    fn creation_and_generation_state_preserve_drafts_on_cancel_or_failure() {
        let (_temporary, storage) = fixture_storage();
        let mut existing = empty_editor(&storage, "existing");
        existing.add_after(None).expect("existing draft field");
        let field_id = existing.fields()[0].id();
        let mut app = test_app(Some(existing));
        app.storage = Some(storage.clone());
        app.utility = Some(UtilityView::NewEntry(NewEntryForm::new(
            "cancelled".to_owned(),
        )));
        let _task = app.update(Message::DismissUtility);
        assert!(app.utility.is_none());
        assert!(app.editor.is_some());
        assert!(!storage.vault().join("cancelled.gpg").exists());

        let policy_before = fs::read(storage.vault().join(".gpg-id")).expect("policy before");
        app.utility = Some(UtilityView::Recipients(
            RecipientForm::new(
                RecipientWorkflowKind::InitializeStore,
                &storage,
                String::new(),
            )
            .expect("recipient form"),
        ));
        let _task = app.update(Message::DismissUtility);
        assert!(app.editor.is_some());
        assert_eq!(
            fs::read(storage.vault().join(".gpg-id")).expect("policy after cancel"),
            policy_before
        );

        app.utility = Some(UtilityView::NewEntry(NewEntryForm::new(
            "failed".to_owned(),
        )));
        app.workflow_generation = 4;
        let completion = Arc::new(Mutex::new(Some((
            SecretBytes::new(Vec::new()),
            Err("injected creation failure".to_owned()),
        ))));
        let _task = app.update(Message::CreateFinished {
            generation: 4,
            completion,
        });
        assert!(app.editor.is_some());
        assert!(matches!(
            app.utility,
            Some(UtilityView::NewEntry(NewEntryForm { error: Some(_), .. }))
        ));

        let mut generate = GenerateForm::new(field_id, true);
        assert!(generate.generate().is_err());
        generate.confirmed = true;
        generate.length = "0".to_owned();
        assert!(generate.generate().is_err());
        generate.length = "16".to_owned();
        assert_eq!(generate.generate().expect("replacement").expose().len(), 16);
    }

    #[test]
    fn desktop_search_and_mutations_use_typed_storage_contracts_and_clean_git_commits() {
        let (_temporary, storage) = fixture_storage();
        let repository = Repository::open(storage.vault()).expect("repository");
        GitRepository::init(&repository, GitIdentity::ironstorage()).expect("Git init");

        let mut source = empty_editor(&storage, "team/alpha");
        source.add_after(None).expect("password field");
        let password = source.fields()[0].id();
        source
            .update_raw(password, b"needle-secret")
            .expect("password value");
        save_document(&storage, &source).expect("source save");
        let commits_before_mutations = GitRepository::open(&repository, GitIdentity::ironstorage())
            .expect("Git after source save")
            .log(None)
            .expect("initial Git log")
            .len();

        let names = storage
            .find(&FindRequest {
                terms: vec!["alpha".to_owned()],
            })
            .expect("name search");
        assert_eq!(names.matches().len(), 1);
        assert_eq!(
            names.matches()[0].id(),
            &TreeNodeId::Entry(EntryPath::parse("team/alpha").expect("entry identity"))
        );

        let contents = storage
            .grep(
                &GrepRequest {
                    pattern: "needle-secret".to_owned(),
                    ignore_case: false,
                    invert_match: false,
                    line_number: true,
                    fixed_strings: true,
                },
                &mut FixtureSecrets,
            )
            .expect("decrypted search");
        assert_eq!(contents.entries().len(), 1);
        assert_eq!(contents.entries()[0].path().to_string(), "team/alpha");
        assert_eq!(contents.entries()[0].lines()[0].number(), 1);
        assert_eq!(
            contents.entries()[0].lines()[0].contents().expose(),
            b"needle-secret"
        );

        let copied = storage
            .mutate(
                &DesktopMutationRequest::Copy(CopyRequest {
                    source: "team/alpha".to_owned(),
                    destination: "team/beta".to_owned(),
                    force: false,
                }),
                OverwriteDecision::Decline,
                &mut FixtureSecrets,
            )
            .expect("copy");
        assert_eq!(copied.action(), MutationAction::Copy);
        assert_eq!(
            copied.selection(),
            Some(&MutationSelection::Entry(
                EntryPath::parse("team/beta").expect("copy selection")
            ))
        );
        assert!(
            storage
                .mutate(
                    &DesktopMutationRequest::Copy(CopyRequest {
                        source: "team/alpha".to_owned(),
                        destination: "team/beta".to_owned(),
                        force: false,
                    }),
                    OverwriteDecision::Decline,
                    &mut FixtureSecrets,
                )
                .is_err()
        );

        let moved = storage
            .mutate(
                &DesktopMutationRequest::Move(MoveRequest {
                    source: "team/beta".to_owned(),
                    destination: "team/gamma".to_owned(),
                    force: false,
                }),
                OverwriteDecision::Decline,
                &mut FixtureSecrets,
            )
            .expect("move");
        assert_eq!(moved.action(), MutationAction::Move);
        storage
            .mutate(
                &DesktopMutationRequest::Remove(RemoveRequest {
                    entry: "team/alpha".to_owned(),
                    recursive: false,
                    force: false,
                }),
                OverwriteDecision::Allow,
                &mut FixtureSecrets,
            )
            .expect("remove");
        assert!(!storage.vault().join("team/alpha.gpg").exists());
        assert!(!storage.vault().join("team/beta.gpg").exists());
        assert_eq!(
            storage
                .open_document("team/gamma", &mut FixtureSecrets)
                .expect("moved document")
                .serialize()
                .expose(),
            b"needle-secret"
        );
        let git = GitRepository::open(&repository, GitIdentity::ironstorage()).expect("Git reopen");
        assert!(git.status().expect("Git status").is_clean());
        assert_eq!(
            git.log(None).expect("Git log").len(),
            commits_before_mutations + 3
        );
    }

    #[test]
    fn desktop_git_status_is_typed_https_only_and_cancellation_safe() {
        let (temporary, _initial) = fixture_storage();
        let config_path = temporary.path().join("config.toml");
        let mut config = fs::read_to_string(&config_path).expect("configuration");
        config.push_str(
            "[[git.remotes]]\nname = \"origin\"\nurl = \"https://example.test/store.git\"\nserver_id = \"server\"\napplication_id = \"application\"\n",
        );
        fs::write(&config_path, config).expect("configured remote");
        let storage = DesktopStorage::load(Some(&config_path)).expect("reload remote");
        let repository = Repository::open(storage.vault()).expect("repository");
        let mut git = GitRepository::init(&repository, GitIdentity::ironstorage())
            .expect("initialize embedded Git");
        git.add_remote("origin", "https://example.test/store.git")
            .expect("HTTPS remote");

        let phases = Arc::new(Mutex::new(Vec::new()));
        let reported = Arc::clone(&phases);
        let control = GitOperationControl::new(move |phase| {
            reported.lock().expect("progress").push(phase);
        });
        let result = storage
            .git_operation(None, &DesktopGitRequest::Refresh, &control)
            .expect("status refresh");
        assert_eq!(result.outcome(), &DesktopGitOutcome::Refreshed);
        assert_eq!(result.snapshot().branch(), "main");
        assert!(result.snapshot().status().is_clean());
        assert_eq!(
            result.snapshot().remote().expect("remote").url(),
            "https://example.test/store.git"
        );
        assert_eq!(
            *phases.lock().expect("progress"),
            [GitProgressPhase::Validating]
        );

        let cancelled = GitOperationControl::default();
        cancelled.cancel();
        let error = storage
            .git_operation(None, &DesktopGitRequest::Refresh, &cancelled)
            .expect_err("cancel before repository access");
        assert_eq!(error.kind(), DesktopErrorKind::Git);
        assert_eq!(error.to_string(), "the Git operation was cancelled");
        assert_eq!(error.git_error(), Some(&GitError::Cancelled));
        assert_eq!(
            git_failure_message(&error),
            "The Git operation was cancelled safely."
        );

        let git_config = storage.vault().join(".git/config");
        let invalid = fs::read_to_string(&git_config)
            .expect("Git config")
            .replace(
                "https://example.test/store.git",
                "ssh://example.test/store.git",
            );
        fs::write(git_config, invalid).expect("hostile remote fixture");
        let error = storage
            .git_operation(
                None,
                &DesktopGitRequest::Refresh,
                &GitOperationControl::default(),
            )
            .expect_err("non-HTTPS remote rejection");
        assert_eq!(error.kind(), DesktopErrorKind::Git);
        assert_eq!(error.git_error(), Some(&GitError::ForbiddenRemoteUrl));
        assert_eq!(
            error.to_string(),
            "Git remotes must use credential-free HTTPS URLs"
        );

        assert!(DesktopGitRequest::Pull.requires_authentication());
        assert!(DesktopGitRequest::Pull.changes_worktree());
        assert!(!DesktopGitRequest::Push.changes_worktree());
    }

    #[test]
    fn search_and_mutation_forms_preserve_dirty_state_on_cancel_failure_and_lock() {
        let (_temporary, storage) = fixture_storage();
        let mut editor = empty_editor(&storage, "draft");
        editor.add_after(None).expect("dirty field");
        let draft = editor.document().serialize().expose().to_vec();
        let draft_id = TreeNodeId::Entry(EntryPath::parse("draft").expect("draft identity"));
        let mut app = test_app(Some(editor));
        app.storage = Some(storage.clone());
        app.navigation
            .replace_test_nodes(vec![navigation::TestNode {
                id: draft_id.clone(),
                name: "draft".to_owned(),
                children: Vec::new(),
            }]);
        assert!(app.navigation.select_id(&draft_id));

        let mut delete = MutationForm::new(MutationKind::Delete, draft_id.clone());
        assert!(delete.request().is_err());
        delete.confirmed = true;
        assert!(matches!(
            delete.request(),
            Ok(DesktopMutationRequest::Remove(RemoveRequest {
                recursive: false,
                ..
            }))
        ));
        app.utility = Some(UtilityView::Mutation(delete));
        let _task = app.update(Message::SubmitMutation);
        assert!(matches!(app.confirmation, Some(PendingAction::Mutate(_))));
        let _task = app.update(Message::CancelDiscard);
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
        assert_eq!(
            app.editor
                .as_ref()
                .expect("dirty editor")
                .document()
                .serialize()
                .expose(),
            draft
        );

        app.workflow_generation = 9;
        let _task = app.update(Message::MutationFinished {
            generation: 9,
            result: Box::new(Err("injected mutation failure".to_owned())),
        });
        assert_eq!(app.navigation.selected(), Some(&draft_id));
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
        assert!(matches!(
            app.utility,
            Some(UtilityView::Mutation(MutationForm { error: Some(_), .. }))
        ));
        let _task = app.update(Message::DismissUtility);
        assert!(app.utility.is_none());
        assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));

        let mut search = SearchForm::new(SearchMode::Contents);
        search.query = "needle".to_owned();
        search.ignore_case = true;
        search.invert_match = true;
        search.line_numbers = false;
        search.fixed_strings = true;
        assert_eq!(
            search.request().expect("content request"),
            SearchRequest::Contents(GrepRequest {
                pattern: "needle".to_owned(),
                ignore_case: true,
                invert_match: true,
                line_number: false,
                fixed_strings: true,
            })
        );
        let mut names = SearchForm::new(SearchMode::Names);
        names.terms.push("first term".to_owned());
        names.query = "second term".to_owned();
        assert_eq!(
            names.request().expect("name request"),
            SearchRequest::Names(FindRequest {
                terms: vec!["first term".to_owned(), "second term".to_owned()],
            })
        );
        search.results = Some(SearchResults::Contents(
            storage
                .grep(
                    &GrepRequest {
                        pattern: ".*".to_owned(),
                        ignore_case: false,
                        invert_match: false,
                        line_number: true,
                        fixed_strings: false,
                    },
                    &mut FixtureSecrets,
                )
                .expect("search results"),
        ));
        app.utility = Some(UtilityView::Search(search));
        app.authentication_lost("locked".to_owned());
        assert!(matches!(
            app.utility,
            Some(UtilityView::Search(SearchForm {
                results: None,
                error: Some(_),
                ..
            }))
        ));
    }

    #[test]
    fn kdbx_form_builds_a_confirmed_storage_request_and_is_cleared_on_lock() {
        let mut form = KdbxForm {
            source: "/tmp/passwords.kdbx".to_owned(),
            key_file: "/tmp/passwords.key".to_owned(),
            password: Zeroizing::new("database password".to_owned()),
            quick_add: true,
            confirmed: true,
            ..KdbxForm::default()
        };
        let (request, password) = form.request().expect("confirmed request");
        assert_eq!(
            request.source(),
            std::path::Path::new("/tmp/passwords.kdbx")
        );
        assert_eq!(
            request.key_file(),
            Some(std::path::Path::new("/tmp/passwords.key"))
        );
        assert_eq!(request.mode(), KdbxImportMode::QuickAdd);
        assert_eq!(password.expose(), b"database password");
        assert!(!format!("{form:?}").contains("database password"));

        form.running = true;
        let mut app = App::new().0;
        app.utility = Some(UtilityView::Kdbx(form));
        app.authentication_lost("locked".to_owned());
        let Some(UtilityView::Kdbx(form)) = app.utility else {
            panic!("expected KDBX form");
        };
        assert!(form.password.is_empty());
        assert!(!form.confirmed);
        assert!(!form.running);
    }
}
