//! Storage-owned service boundary for the Iced desktop presentation adapter.

use std::{
    error::Error,
    fmt,
    path::Path,
    sync::{Arc, OnceLock},
};

use crate::{
    authentication::{
        AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
    },
    command::{
        CopyRequest, FindRequest, GrepRequest, InitRequest, MoveRequest, OtpInputSource,
        OtpInsertRequest, RemoveRequest,
    },
    config::{Config, ConfigSettings, EditorCommand},
    crypto::{KeyInfo, KeyStore, SecretProvider},
    document::{
        DocumentError, EntryDocument, EntryDocumentService, EntryFieldDraft, EntryFieldId,
        EntryFieldKind,
    },
    git::{
        AutomaticEntryCommitter, AutomaticPolicyCommitter, AutomaticTreeCommitter,
        EmbeddedFetchTransport, GitConflict, GitConflictResolution, GitError, GitIdentity,
        GitOperationControl, GitProgressPhase, GitRepository, GitSnapshot, PullOutcome,
        PushOutcome, ReqwestGitTransport,
    },
    kdbx::{KdbxImportOutcome, KdbxImportRequest, KdbxImporter},
    mutation::{MutationOutcome, TreeMutator},
    otp::{OtpAlgorithm, OtpCodeValidity, OtpInput, OtpKind, OtpService, OtpUri},
    presentation::{ClipboardTimeout, QrMatrix},
    read::{FindResults, GrepResults, TreeModel, VaultReader},
    recipient::{
        PolicyCommit, PolicyCommitError, PolicyCommitter, RecipientPolicyManager,
        RecipientPolicyOutcome,
    },
    repository::{DirectoryPath, Repository, SecretBytes},
    secret_store::SecretProtectionPolicy,
    write::{OverwriteDecision, VaultWriter, WriteError, WriteOutcome},
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DesktopErrorKind {
    Configuration,
    Authentication,
    KeyMaterial,
    Repository,
    Read,
    Document,
    Git,
    Conflict,
    Unchanged,
    MissingDefaultKey,
    EntryExists,
    Mutation,
    Otp,
    Import,
}

#[derive(Debug)]
pub struct DesktopError {
    kind: DesktopErrorKind,
    message: String,
    conflicts: Vec<GitConflict>,
    git: Option<GitError>,
}

impl DesktopError {
    pub fn kind(&self) -> DesktopErrorKind {
        self.kind
    }

    pub fn conflicts(&self) -> &[GitConflict] {
        &self.conflicts
    }

    pub fn git_error(&self) -> Option<&GitError> {
        self.git.as_ref()
    }

    fn new(kind: DesktopErrorKind, error: impl fmt::Display) -> Self {
        Self {
            kind,
            message: error.to_string(),
            conflicts: Vec::new(),
            git: None,
        }
    }

    fn git(error: GitError) -> Self {
        let (kind, conflicts) = match &error {
            GitError::MergeConflicts { conflicts } => {
                (DesktopErrorKind::Conflict, conflicts.clone())
            }
            _ => (DesktopErrorKind::Git, Vec::new()),
        };
        Self {
            kind,
            message: error.to_string(),
            conflicts,
            git: Some(error),
        }
    }

    fn document(error: DocumentError) -> Self {
        let kind = match &error {
            DocumentError::Write(WriteError::ConcurrentModification { .. }) => {
                DesktopErrorKind::Conflict
            }
            DocumentError::Write(WriteError::Unchanged) => DesktopErrorKind::Unchanged,
            _ => DesktopErrorKind::Document,
        };
        Self::new(kind, error)
    }
}

impl fmt::Display for DesktopError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl Error for DesktopError {}

#[derive(Clone, Debug)]
pub struct DesktopStorage {
    config: Config,
    keys: Arc<OnceLock<KeyStore>>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DesktopMutationRequest {
    Remove(RemoveRequest),
    Move(MoveRequest),
    Copy(CopyRequest),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DesktopGitRequest {
    Refresh,
    Pull,
    Push,
    Sync,
    Resolve(Vec<GitConflictResolution>),
}

impl DesktopGitRequest {
    pub fn requires_authentication(&self) -> bool {
        !matches!(self, Self::Refresh)
    }

    pub fn changes_worktree(&self) -> bool {
        matches!(self, Self::Pull | Self::Sync | Self::Resolve(_))
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DesktopGitOutcome {
    Refreshed,
    Pulled(PullOutcome),
    Pushed(PushOutcome),
    Synchronized {
        pull: PullOutcome,
        push: PushOutcome,
    },
    Resolved(PullOutcome),
}

#[derive(Clone, Debug)]
pub struct DesktopGitResult {
    outcome: DesktopGitOutcome,
    snapshot: GitSnapshot,
    tree: Option<TreeModel>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DesktopOtpMetadata {
    kind: OtpKind,
    issuer: Option<String>,
    account: String,
    algorithm: OtpAlgorithm,
    digits: u32,
}

impl DesktopOtpMetadata {
    pub fn kind(&self) -> OtpKind {
        self.kind
    }

    pub fn issuer(&self) -> Option<&str> {
        self.issuer.as_deref()
    }

    pub fn account(&self) -> &str {
        &self.account
    }

    pub fn algorithm(&self) -> OtpAlgorithm {
        self.algorithm
    }

    pub fn digits(&self) -> u32 {
        self.digits
    }
}

pub struct DesktopOtpCode {
    code: SecretBytes,
    validity: OtpCodeValidity,
    metadata: DesktopOtpMetadata,
    tree: Option<TreeModel>,
    document: Option<EntryDocument>,
}

impl DesktopOtpCode {
    pub fn code(&self) -> &SecretBytes {
        &self.code
    }

    pub fn validity(&self) -> OtpCodeValidity {
        self.validity
    }

    pub fn metadata(&self) -> &DesktopOtpMetadata {
        &self.metadata
    }

    pub fn into_parts(
        self,
    ) -> (
        SecretBytes,
        OtpCodeValidity,
        DesktopOtpMetadata,
        Option<TreeModel>,
        Option<EntryDocument>,
    ) {
        (
            self.code,
            self.validity,
            self.metadata,
            self.tree,
            self.document,
        )
    }
}

pub struct DesktopOtpUri {
    payload: SecretBytes,
    matrix: Option<QrMatrix>,
}

impl DesktopOtpUri {
    pub fn payload(&self) -> &SecretBytes {
        &self.payload
    }

    pub fn matrix(&self) -> Option<&QrMatrix> {
        self.matrix.as_ref()
    }

    pub fn into_parts(self) -> (SecretBytes, Option<QrMatrix>) {
        (self.payload, self.matrix)
    }
}

pub struct DesktopOtpMutation {
    entry: String,
    document: EntryDocument,
    tree: TreeModel,
}

impl DesktopOtpMutation {
    pub fn entry(&self) -> &str {
        &self.entry
    }

    pub fn into_parts(self) -> (String, EntryDocument, TreeModel) {
        (self.entry, self.document, self.tree)
    }
}

impl DesktopGitResult {
    pub fn outcome(&self) -> &DesktopGitOutcome {
        &self.outcome
    }

    pub fn snapshot(&self) -> &GitSnapshot {
        &self.snapshot
    }

    pub fn into_parts(self) -> (DesktopGitOutcome, GitSnapshot, Option<TreeModel>) {
        (self.outcome, self.snapshot, self.tree)
    }
}

impl DesktopMutationRequest {
    pub fn source(&self) -> &str {
        match self {
            Self::Remove(request) => &request.entry,
            Self::Move(request) => &request.source,
            Self::Copy(request) => &request.source,
        }
    }
}

impl DesktopStorage {
    pub fn load(explicit: Option<&Path>) -> Result<Self, DesktopError> {
        let config = Config::load(explicit)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error))?;
        Ok(Self {
            config,
            keys: Arc::new(OnceLock::new()),
        })
    }

    pub fn system() -> Result<DesktopBootstrap, DesktopError> {
        Self::load(None)?.bootstrap()
    }

    pub fn bootstrap(self) -> Result<DesktopBootstrap, DesktopError> {
        let keys = self.keys()?;
        let handle = keys
            .resolve(self.config.default_key().as_str())
            .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?;
        let key = keys
            .infos()
            .find(|key| key.fingerprint() == handle.fingerprint())
            .ok_or_else(|| {
                DesktopError::new(
                    DesktopErrorKind::MissingDefaultKey,
                    "the configured GPG key is unavailable",
                )
            })?;
        let authentication = NativeAuthenticationSession::system(
            SecretProtectionPolicy::default(),
            self.config.authentication_timeout(),
        )
        .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        Ok(DesktopBootstrap {
            storage: self,
            authentication,
            key,
        })
    }

    pub fn clipboard_timeout(&self) -> ClipboardTimeout {
        self.config.clipboard_timeout()
    }

    pub fn authentication_timeout(&self) -> AuthenticationTimeout {
        self.config.authentication_timeout()
    }

    pub fn vault(&self) -> &Path {
        self.config.vault()
    }

    pub fn config_source(&self) -> &Path {
        self.config.source()
    }

    pub fn default_key(&self) -> &str {
        self.config.default_key().as_str()
    }

    pub fn configured_editor(&self) -> Option<&EditorCommand> {
        self.config.configured_editor()
    }

    pub fn settings(&self) -> ConfigSettings {
        self.config.settings()
    }

    pub fn update_settings(&self, settings: ConfigSettings) -> Result<Self, DesktopError> {
        let storage = self.validate_settings(settings)?;
        storage.persist()?;
        Ok(storage)
    }

    pub fn update_settings_and_bootstrap(
        &self,
        settings: ConfigSettings,
    ) -> Result<DesktopBootstrap, DesktopError> {
        let bootstrap = self.validate_settings(settings)?.bootstrap()?;
        bootstrap.storage.persist()?;
        Ok(bootstrap)
    }

    fn validate_settings(&self, mut settings: ConfigSettings) -> Result<Self, DesktopError> {
        let repository = Repository::open(settings.vault())
            .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))?;
        settings.set_vault(repository.root_path().to_owned());
        let config = self
            .config
            .with_settings(settings)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error))?;
        let storage = Self {
            config,
            keys: Arc::clone(&self.keys),
        };
        let keys = storage.keys()?;
        keys.resolve(storage.config.default_key().as_str())
            .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?;
        storage.tree()?;
        Ok(storage)
    }

    fn persist(&self) -> Result<(), DesktopError> {
        self.config
            .persist()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Configuration, error))
    }

    /// Validate a selected folder through the same repository and key path as
    /// normal desktop reads, then atomically update the shared configuration.
    pub fn switch_vault(&self, vault: &Path) -> Result<Self, DesktopError> {
        let mut settings = self.settings();
        settings.set_vault(vault.to_owned());
        self.update_settings(settings)
    }

    pub fn tree(&self) -> Result<TreeModel, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        VaultReader::new(&repository, keys)
            .list(&DirectoryPath::root())
            .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
    }

    pub fn git_operation(
        &self,
        handle: Option<&NativeAuthenticationHandle>,
        request: &DesktopGitRequest,
        control: &GitOperationControl,
    ) -> Result<DesktopGitResult, DesktopError> {
        control
            .report(GitProgressPhase::Validating)
            .map_err(DesktopError::git)?;
        let repository = self.repository()?;
        let git = GitRepository::open(&repository, GitIdentity::ironstorage())
            .map_err(DesktopError::git)?;
        let configured = || {
            self.config.git_remote(None).ok_or_else(|| {
                DesktopError::new(
                    DesktopErrorKind::Configuration,
                    "no HTTPS Git remote is configured",
                )
            })
        };
        let authenticated = || {
            let handle = handle.ok_or_else(|| {
                DesktopError::new(
                    DesktopErrorKind::Authentication,
                    "authentication is required for HTTPS Git credentials",
                )
            })?;
            handle
                .ensure_active()
                .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
            Ok(handle)
        };
        let (outcome, changed_tree) = match request {
            DesktopGitRequest::Refresh => (DesktopGitOutcome::Refreshed, false),
            DesktopGitRequest::Pull => {
                let outcome = git
                    .pull_with_transport_controlled(
                        configured()?,
                        None,
                        authenticated()?,
                        &EmbeddedFetchTransport,
                        control,
                    )
                    .map_err(DesktopError::git)?;
                (DesktopGitOutcome::Pulled(outcome), true)
            }
            DesktopGitRequest::Push => {
                let outcome = git
                    .push_with_transport_controlled(
                        configured()?,
                        None,
                        authenticated()?,
                        &ReqwestGitTransport,
                        control,
                    )
                    .map_err(DesktopError::git)?;
                (DesktopGitOutcome::Pushed(outcome), false)
            }
            DesktopGitRequest::Sync => {
                let (pull, push) = git
                    .sync_controlled(configured()?, authenticated()?, control)
                    .map_err(DesktopError::git)?;
                (DesktopGitOutcome::Synchronized { pull, push }, true)
            }
            DesktopGitRequest::Resolve(resolutions) => {
                let _handle = authenticated()?;
                control
                    .report(GitProgressPhase::Integrating)
                    .map_err(DesktopError::git)?;
                let outcome = git
                    .resolve_fetched(configured()?, None, resolutions)
                    .map_err(DesktopError::git)?;
                (DesktopGitOutcome::Resolved(outcome), true)
            }
        };
        let snapshot = git
            .snapshot(self.config.git_remote(None), 10)
            .map_err(DesktopError::git)?;
        let tree = changed_tree.then(|| self.tree()).transpose()?;
        Ok(DesktopGitResult {
            outcome,
            snapshot,
            tree,
        })
    }

    pub fn find(&self, request: &FindRequest) -> Result<FindResults, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        VaultReader::new(&repository, keys)
            .find(&request.terms)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
    }

    pub fn grep_active(
        &self,
        handle: &NativeAuthenticationHandle,
        request: &GrepRequest,
    ) -> Result<GrepResults, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.grep(request, &mut provider)
    }

    pub fn grep(
        &self,
        request: &GrepRequest,
        provider: &mut impl SecretProvider,
    ) -> Result<GrepResults, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        VaultReader::new(&repository, keys)
            .grep(request, provider)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))
    }

    pub fn import_kdbx_active(
        &self,
        handle: &NativeAuthenticationHandle,
        request: &KdbxImportRequest,
        password: SecretBytes,
    ) -> Result<(KdbxImportOutcome, TreeModel), DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let repository = self.repository()?;
        let keys = self.keys()?;
        let mut provider = handle.clone();
        let outcome = KdbxImporter::new(&repository, keys)
            .import(request, password, &mut provider, GitIdentity::ironstorage())
            .map_err(|error| DesktopError::new(DesktopErrorKind::Import, error))?;
        let tree = VaultReader::new(&repository, keys)
            .list(&DirectoryPath::root())
            .map_err(|error| DesktopError::new(DesktopErrorKind::Read, error))?;
        Ok((outcome, tree))
    }

    pub fn otp_code_active(
        &self,
        handle: &NativeAuthenticationHandle,
        entry: &str,
        unix_seconds: u64,
        confirm_hotp: bool,
    ) -> Result<DesktopOtpCode, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.otp_code(entry, unix_seconds, confirm_hotp, &mut provider)
    }

    pub fn otp_code(
        &self,
        entry: &str,
        unix_seconds: u64,
        confirm_hotp: bool,
        provider: &mut impl SecretProvider,
    ) -> Result<DesktopOtpCode, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        let service = OtpService::new(&repository, keys);
        let uri = service
            .uri(entry, provider)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        if uri.kind() == OtpKind::Hotp && !confirm_hotp {
            return Err(DesktopError::new(
                DesktopErrorKind::Otp,
                "HOTP generation requires explicit confirmation because it commits the advanced counter",
            ));
        }
        let metadata = DesktopOtpMetadata {
            kind: uri.kind(),
            issuer: uri.issuer().map(str::to_owned),
            account: uri.account().to_owned(),
            algorithm: uri.algorithm(),
            digits: uri.digits(),
        };
        let outcome = service
            .code_automatic(entry, unix_seconds, None, provider)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        let validity = outcome.validity();
        let changed = validity.counter().is_some();
        let tree = changed.then(|| self.tree()).transpose()?;
        let document = changed
            .then(|| {
                EntryDocumentService::new(&repository, keys)
                    .open(entry, provider)
                    .map_err(DesktopError::document)
            })
            .transpose()?;
        Ok(DesktopOtpCode {
            code: SecretBytes::new(outcome.code().expose().to_vec()),
            validity,
            metadata,
            tree,
            document,
        })
    }

    pub fn otp_uri_active(
        &self,
        handle: &NativeAuthenticationHandle,
        entry: &str,
        qr: bool,
    ) -> Result<DesktopOtpUri, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.otp_uri(entry, qr, &mut provider)
    }

    pub fn otp_uri(
        &self,
        entry: &str,
        qr: bool,
        provider: &mut impl SecretProvider,
    ) -> Result<DesktopOtpUri, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        let uri = OtpService::new(&repository, keys)
            .uri(entry, provider)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        let payload = SecretBytes::new(uri.encoded().expose().to_vec());
        let matrix = qr
            .then(|| QrMatrix::encode(&payload))
            .transpose()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        Ok(DesktopOtpUri { payload, matrix })
    }

    pub fn import_otp_active(
        &self,
        handle: &NativeAuthenticationHandle,
        entry: &str,
        uri: SecretBytes,
        replace: OverwriteDecision,
    ) -> Result<DesktopOtpMutation, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.import_otp(entry, uri, replace, &mut provider)
    }

    pub fn import_otp_qr_active(
        &self,
        handle: &NativeAuthenticationHandle,
        entry: &str,
        image: SecretBytes,
        replace: OverwriteDecision,
    ) -> Result<DesktopOtpMutation, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.import_otp_qr(entry, image, replace, &mut provider)
    }

    pub fn import_otp_qr(
        &self,
        entry: &str,
        image: SecretBytes,
        replace: OverwriteDecision,
        provider: &mut impl SecretProvider,
    ) -> Result<DesktopOtpMutation, DesktopError> {
        let uri = QrMatrix::decode_image(&image)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        self.import_otp(entry, uri, replace, provider)
    }

    pub fn import_otp(
        &self,
        entry: &str,
        uri: SecretBytes,
        replace: OverwriteDecision,
        provider: &mut impl SecretProvider,
    ) -> Result<DesktopOtpMutation, DesktopError> {
        let parsed =
            OtpUri::parse(uri).map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        let encoded = parsed.encoded().expose().to_vec();
        let repository = self.repository()?;
        let keys = self.keys()?;
        let exists = VaultWriter::new(&repository, keys)
            .entry_exists(entry)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        if exists {
            let mut document = EntryDocumentService::new(&repository, keys)
                .open(entry, provider)
                .map_err(DesktopError::document)?;
            if let Some(field) = otp_field(&document)? {
                if replace == OverwriteDecision::Decline {
                    return Err(DesktopError::new(
                        DesktopErrorKind::EntryExists,
                        format!("entry already contains an OTP URI: {entry}"),
                    ));
                }
                document
                    .replace_field_value(field, encoded.clone())
                    .map_err(DesktopError::document)?;
            } else {
                let index = document.fields().len();
                document
                    .add(
                        index,
                        EntryFieldDraft::otp_uri(encoded.clone())
                            .map_err(DesktopError::document)?,
                    )
                    .map_err(DesktopError::document)?;
            }
            self.save_document(&document)?;
            let document = EntryDocumentService::new(&repository, keys)
                .open(entry, provider)
                .map_err(DesktopError::document)?;
            return Ok(DesktopOtpMutation {
                entry: entry.to_owned(),
                document,
                tree: self.tree()?,
            });
        }
        let request = OtpInsertRequest {
            entry: Some(entry.to_owned()),
            force: false,
            echo: true,
            source: OtpInputSource::Uri,
        };
        let input = OtpInput::line(encoded)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        let service = OtpService::new(&repository, keys);
        let plan = service
            .prepare_insert(&request, input)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        let mut committer =
            AutomaticEntryCommitter::for_entry(&repository, entry, GitIdentity::ironstorage())
                .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
        service
            .finish_insert(
                plan,
                OverwriteDecision::Allow,
                OverwriteDecision::Decline,
                None,
                &mut committer,
            )
            .map_err(|error| DesktopError::new(DesktopErrorKind::Otp, error))?;
        let document = EntryDocumentService::new(&repository, keys)
            .open(entry, provider)
            .map_err(DesktopError::document)?;
        Ok(DesktopOtpMutation {
            entry: entry.to_owned(),
            document,
            tree: self.tree()?,
        })
    }

    pub fn remove_otp_active(
        &self,
        handle: &NativeAuthenticationHandle,
        entry: &str,
    ) -> Result<DesktopOtpMutation, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.remove_otp(entry, &mut provider)
    }

    pub fn remove_otp(
        &self,
        entry: &str,
        provider: &mut impl SecretProvider,
    ) -> Result<DesktopOtpMutation, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        let mut document = EntryDocumentService::new(&repository, keys)
            .open(entry, provider)
            .map_err(DesktopError::document)?;
        let field = otp_field(&document)?.ok_or_else(|| {
            DesktopError::new(
                DesktopErrorKind::Otp,
                format!("entry does not contain an OTP URI: {entry}"),
            )
        })?;
        document.remove(field).map_err(DesktopError::document)?;
        self.save_document(&document)?;
        let document = EntryDocumentService::new(&repository, keys)
            .open(entry, provider)
            .map_err(DesktopError::document)?;
        Ok(DesktopOtpMutation {
            entry: entry.to_owned(),
            document,
            tree: self.tree()?,
        })
    }

    pub fn mutate_active(
        &self,
        handle: &NativeAuthenticationHandle,
        request: &DesktopMutationRequest,
        overwrite: OverwriteDecision,
    ) -> Result<MutationOutcome, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.mutate(request, overwrite, &mut provider)
    }

    pub fn mutate(
        &self,
        request: &DesktopMutationRequest,
        overwrite: OverwriteDecision,
        provider: &mut impl SecretProvider,
    ) -> Result<MutationOutcome, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        let mut committer = AutomaticTreeCommitter::for_source(
            &repository,
            request.source(),
            GitIdentity::ironstorage(),
        )
        .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
        let mutator = TreeMutator::new(&repository, keys);
        match request {
            DesktopMutationRequest::Remove(request) => {
                mutator.remove(request, overwrite, &mut committer)
            }
            DesktopMutationRequest::Move(request) => {
                mutator.move_tree(request, overwrite, None, provider, &mut committer)
            }
            DesktopMutationRequest::Copy(request) => {
                mutator.copy(request, overwrite, None, provider, &mut committer)
            }
        }
        .map_err(|error| DesktopError::new(DesktopErrorKind::Mutation, error))
    }

    /// Enumerate storage-validated encryption keys for recipient selection.
    pub fn key_infos(&self) -> Result<Vec<KeyInfo>, DesktopError> {
        Ok(self.keys()?.infos().filter(KeyInfo::can_encrypt).collect())
    }

    /// Apply an upstream-compatible root or nested `.gpg-id` policy while the
    /// shared authentication lease is active.
    pub fn apply_active_recipient_policy(
        &self,
        handle: &NativeAuthenticationHandle,
        request: &InitRequest,
        default_key: &str,
    ) -> Result<(Self, RecipientPolicyOutcome), DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.apply_recipient_policy(request, default_key, &mut provider)
    }

    pub fn apply_recipient_policy(
        &self,
        request: &InitRequest,
        default_key: &str,
        provider: &mut impl SecretProvider,
    ) -> Result<(Self, RecipientPolicyOutcome), DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        let default = keys
            .infos()
            .find(|key| key.fingerprint().as_str() == default_key)
            .ok_or_else(|| {
                DesktopError::new(
                    DesktopErrorKind::MissingDefaultKey,
                    "the selected default GPG key is unavailable",
                )
            })?;
        if !default.has_secret() {
            return Err(DesktopError::new(
                DesktopErrorKind::MissingDefaultKey,
                "the selected default GPG key has no secret key material",
            ));
        }
        let mut settings = self.settings();
        settings.set_default_key(default_key.to_owned());
        let replacement = self.validate_settings(settings)?;
        let directory = request.path.as_deref().unwrap_or_default();
        let committer = AutomaticPolicyCommitter::for_directory(
            &repository,
            directory,
            GitIdentity::ironstorage(),
        )
        .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
        let mut committer = ConfigPolicyCommitter {
            git: committer,
            previous: &self.config,
            replacement: &replacement.config,
            persisted: false,
        };
        let outcome = RecipientPolicyManager::new(&repository, keys)
            .apply_init(request, None, provider, &mut committer)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))?;
        if !committer.persisted {
            replacement.persist()?;
        }
        Ok((replacement, outcome))
    }

    /// Open a collision-free in-memory draft. No repository bytes are written
    /// until the structured document is explicitly saved.
    pub fn create_active_document(
        &self,
        handle: &NativeAuthenticationHandle,
        entry: &str,
    ) -> Result<EntryDocument, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        let mut provider = handle.clone();
        self.create_document(entry, &mut provider)
    }

    pub fn create_document(
        &self,
        entry: &str,
        provider: &mut impl SecretProvider,
    ) -> Result<EntryDocument, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        if VaultWriter::new(&repository, keys)
            .entry_exists(entry)
            .map_err(|error| DesktopError::new(DesktopErrorKind::Document, error))?
        {
            return Err(DesktopError::new(
                DesktopErrorKind::EntryExists,
                format!("password-store entry already exists: {entry}"),
            ));
        }
        EntryDocumentService::new(&repository, keys)
            .open(entry, provider)
            .map_err(DesktopError::document)
    }

    pub fn open_document(
        &self,
        entry: &str,
        secrets: &mut impl SecretProvider,
    ) -> Result<EntryDocument, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        EntryDocumentService::new(&repository, keys)
            .open(entry, secrets)
            .map_err(DesktopError::document)
    }

    pub fn save_document(&self, document: &EntryDocument) -> Result<WriteOutcome, DesktopError> {
        let repository = self.repository()?;
        let keys = self.keys()?;
        let entry = document.path().to_string();
        let mut committer =
            AutomaticEntryCommitter::for_entry(&repository, &entry, GitIdentity::ironstorage())
                .map_err(|error| DesktopError::new(DesktopErrorKind::Git, error))?;
        EntryDocumentService::new(&repository, keys)
            .save_recoverable(document, None, &mut committer)
            .map_err(DesktopError::document)
    }

    pub fn save_active_document(
        &self,
        handle: &NativeAuthenticationHandle,
        document: &EntryDocument,
    ) -> Result<WriteOutcome, DesktopError> {
        handle
            .ensure_active()
            .map_err(|error| DesktopError::new(DesktopErrorKind::Authentication, error))?;
        self.save_document(document)
    }

    fn repository(&self) -> Result<Repository, DesktopError> {
        Repository::open(self.config.vault())
            .map_err(|error| DesktopError::new(DesktopErrorKind::Repository, error))
    }

    fn keys(&self) -> Result<&KeyStore, DesktopError> {
        if let Some(keys) = self.keys.get() {
            return Ok(keys);
        }
        let keys = KeyStore::load(self.config.key_material())
            .map_err(|error| DesktopError::new(DesktopErrorKind::KeyMaterial, error))?;
        let _already_initialized = self.keys.set(keys);
        Ok(self
            .keys
            .get()
            .expect("the key store was initialized by this or another caller"))
    }
}

fn otp_field(document: &EntryDocument) -> Result<Option<EntryFieldId>, DesktopError> {
    let mut fields = document
        .fields()
        .iter()
        .filter(|field| field.metadata().kind() == EntryFieldKind::OtpUri)
        .map(|field| field.id());
    let first = fields.next();
    if fields.next().is_some() {
        return Err(DesktopError::new(
            DesktopErrorKind::Otp,
            format!("entry contains multiple OTP URIs: {}", document.path()),
        ));
    }
    Ok(first)
}

struct ConfigPolicyCommitter<'a> {
    git: AutomaticPolicyCommitter,
    previous: &'a Config,
    replacement: &'a Config,
    persisted: bool,
}

impl PolicyCommitter for ConfigPolicyCommitter<'_> {
    fn commit(&mut self, change: &PolicyCommit) -> Result<(), PolicyCommitError> {
        self.replacement
            .persist()
            .map_err(|error| PolicyCommitError::new(error.to_string()))?;
        match self.git.commit(change) {
            Ok(()) => {
                self.persisted = true;
                Ok(())
            }
            Err(operation) => match self.previous.persist() {
                Ok(()) => Err(operation),
                Err(rollback) => Err(PolicyCommitError::new(format!(
                    "recipient-policy commit failed ({operation}) and configuration rollback failed ({rollback})"
                ))),
            },
        }
    }
}

pub struct DesktopBootstrap {
    storage: DesktopStorage,
    authentication: NativeAuthenticationSession,
    key: KeyInfo,
}

impl DesktopBootstrap {
    pub fn into_parts(self) -> (DesktopStorage, NativeAuthenticationSession, KeyInfo) {
        (self.storage, self.authentication, self.key)
    }
}
