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

//! Mechanical UniFFI exports for Apple presentation code.

use std::{
    error::Error,
    fmt,
    sync::{Arc, Mutex},
};

use ironstorage::{
    config::ConfigError,
    mobile::{self, MobileShellState as StorageShellState, MobileTab as StorageTab},
    mobile_authentication::{
        MobileAuthenticationError as StorageAuthenticationError,
        MobileAuthenticationErrorKind as StorageAuthenticationErrorKind,
        MobileAuthenticationState as StorageAuthenticationState,
        MobileEntryCopy as StorageEntryCopy, MobileEntryPresentation as StorageEntryPresentation,
    },
    mobile_entry::{
        MobileEntryEditorFieldKind as StorageEntryEditorFieldKind,
        MobileEntryEditorInput as StorageEntryEditorInput,
        MobileEntryEditorPage as StorageEntryEditorPage,
        MobileEntryEditorSession as StorageEntryEditorSession, MobileEntryPage as StorageEntryPage,
        MobileEntrySectionKind as StorageEntrySectionKind,
    },
    mobile_home::{
        self, MobileHomeChangeKind as StorageHomeChangeKind,
        MobileHomeChangeStatus as StorageHomeChangeStatus, MobileHomeError as StorageHomeError,
        MobileHomeErrorKind as StorageHomeErrorKind, MobileHomeFreshness as StorageHomeFreshness,
        MobileHomePhase as StorageHomePhase,
    },
    mobile_key_transfer::{
        MobileKeyTransferError as StorageKeyTransferError,
        MobileKeyTransferKey as StorageKeyTransferKey,
        MobileKeyTransferKind as StorageKeyTransferKind,
        MobileKeyTransferProgress as StorageKeyTransferProgress,
    },
    mobile_mutation::{
        MobileMutationAction as StorageMutationAction,
        MobileMutationOutcome as StorageMutationOutcome, MobileMutationPlan as StorageMutationPlan,
        MobileMutationRequest as StorageMutationRequest,
    },
    mobile_onboarding::{
        self, MobileOnboardingError as StorageOnboardingError,
        MobileOnboardingErrorKind as StorageOnboardingErrorKind,
        MobileOnboardingPhase as StorageOnboardingPhase,
    },
    mobile_passwords::{
        MobilePasswordError as StoragePasswordError,
        MobilePasswordErrorKind as StoragePasswordErrorKind,
        MobilePasswordRowKind as StoragePasswordRowKind,
    },
    mobile_totp::{
        MobileTotpDetail as StorageTotpDetail,
        MobileTotpDiscoveryPhase as StorageTotpDiscoveryPhase,
        MobileTotpOperation as StorageTotpOperation, MobileTotpPage as StorageTotpPage,
        MobileWatchSnapshotState as StorageWatchSnapshotState,
        MobileWatchSnapshotStatus as StorageWatchSnapshotStatus,
    },
};

uniffi::setup_scaffolding!();

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileTab {
    Home,
    Passwords,
    Search,
    Totp,
    Preferences,
}

impl From<StorageTab> for MobileTab {
    fn from(tab: StorageTab) -> Self {
        match tab {
            StorageTab::Home => Self::Home,
            StorageTab::Passwords => Self::Passwords,
            StorageTab::Search => Self::Search,
            StorageTab::Totp => Self::Totp,
            StorageTab::Preferences => Self::Preferences,
        }
    }
}

impl From<MobileTab> for StorageTab {
    fn from(tab: MobileTab) -> Self {
        match tab {
            MobileTab::Home => Self::Home,
            MobileTab::Passwords => Self::Passwords,
            MobileTab::Search => Self::Search,
            MobileTab::Totp => Self::Totp,
            MobileTab::Preferences => Self::Preferences,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileShellState {
    Loading,
    Empty,
    Ready,
    Locked,
    Error,
}

impl From<StorageShellState> for MobileShellState {
    fn from(state: StorageShellState) -> Self {
        match state {
            StorageShellState::Loading => Self::Loading,
            StorageShellState::Empty => Self::Empty,
            StorageShellState::Ready => Self::Ready,
            StorageShellState::Locked => Self::Locked,
            StorageShellState::Error => Self::Error,
        }
    }
}

impl From<MobileShellState> for StorageShellState {
    fn from(state: MobileShellState) -> Self {
        match state {
            MobileShellState::Loading => Self::Loading,
            MobileShellState::Empty => Self::Empty,
            MobileShellState::Ready => Self::Ready,
            MobileShellState::Locked => Self::Locked,
            MobileShellState::Error => Self::Error,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobilePage {
    pub tab: MobileTab,
    pub title: String,
    pub system_image: String,
    pub selected_system_image: String,
    pub state: MobileShellState,
    pub state_title: String,
    pub state_detail: String,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileShell {
    pub selected_tab: MobileTab,
    pub pages: Vec<MobilePage>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileHomeFreshness {
    NeverRefreshed,
    Cached,
    Current,
}

impl From<StorageHomeFreshness> for MobileHomeFreshness {
    fn from(freshness: StorageHomeFreshness) -> Self {
        match freshness {
            StorageHomeFreshness::NeverRefreshed => Self::NeverRefreshed,
            StorageHomeFreshness::Cached => Self::Cached,
            StorageHomeFreshness::Current => Self::Current,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileHomeChangeKind {
    PasswordEntry,
    RecipientPolicy,
    RecipientSignature,
    RepositoryFile,
}

impl From<StorageHomeChangeKind> for MobileHomeChangeKind {
    fn from(kind: StorageHomeChangeKind) -> Self {
        match kind {
            StorageHomeChangeKind::PasswordEntry => Self::PasswordEntry,
            StorageHomeChangeKind::RecipientPolicy => Self::RecipientPolicy,
            StorageHomeChangeKind::RecipientSignature => Self::RecipientSignature,
            StorageHomeChangeKind::RepositoryFile => Self::RepositoryFile,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileHomeChangeStatus {
    Added,
    Modified,
    Deleted,
}

impl From<StorageHomeChangeStatus> for MobileHomeChangeStatus {
    fn from(status: StorageHomeChangeStatus) -> Self {
        match status {
            StorageHomeChangeStatus::Added => Self::Added,
            StorageHomeChangeStatus::Modified => Self::Modified,
            StorageHomeChangeStatus::Deleted => Self::Deleted,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileHomeSummaryRow {
    pub id: String,
    pub title: String,
    pub detail: String,
    pub system_image: String,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileHomeChange {
    pub id: String,
    pub title: String,
    pub detail: String,
    pub system_image: String,
    pub kind: MobileHomeChangeKind,
    pub status: MobileHomeChangeStatus,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileHomeCommit {
    pub id: String,
    pub title: String,
    pub detail: String,
    pub system_image: String,
    pub timestamp: i64,
    pub changes: Vec<MobileHomeChange>,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileHomeNotice {
    pub title: String,
    pub detail: String,
    pub system_image: String,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileHomePage {
    pub freshness: MobileHomeFreshness,
    pub refreshed_at: Option<i64>,
    pub summaries: Vec<MobileHomeSummaryRow>,
    pub incoming: Vec<MobileHomeCommit>,
    pub outgoing: Vec<MobileHomeCommit>,
    pub incoming_total: u32,
    pub outgoing_total: u32,
    pub notice: Option<MobileHomeNotice>,
}

impl From<mobile_home::MobileHomePage> for MobileHomePage {
    fn from(page: mobile_home::MobileHomePage) -> Self {
        Self {
            freshness: page.freshness().into(),
            refreshed_at: page.refreshed_at(),
            summaries: page
                .summaries()
                .iter()
                .map(|row| MobileHomeSummaryRow {
                    id: row.id().to_owned(),
                    title: row.title().to_owned(),
                    detail: row.detail().to_owned(),
                    system_image: row.system_image().to_owned(),
                })
                .collect(),
            incoming: page.incoming().iter().map(mobile_home_commit).collect(),
            outgoing: page.outgoing().iter().map(mobile_home_commit).collect(),
            incoming_total: u32::try_from(page.incoming_total()).unwrap_or(u32::MAX),
            outgoing_total: u32::try_from(page.outgoing_total()).unwrap_or(u32::MAX),
            notice: page.notice().map(|notice| MobileHomeNotice {
                title: notice.title().to_owned(),
                detail: notice.detail().to_owned(),
                system_image: notice.system_image().to_owned(),
            }),
        }
    }
}

fn mobile_home_commit(commit: &mobile_home::MobileHomeCommit) -> MobileHomeCommit {
    MobileHomeCommit {
        id: commit.id().to_owned(),
        title: commit.title().to_owned(),
        detail: commit.detail().to_owned(),
        system_image: commit.system_image().to_owned(),
        timestamp: commit.timestamp(),
        changes: commit
            .changes()
            .iter()
            .map(|change| MobileHomeChange {
                id: change.id().to_owned(),
                title: change.title().to_owned(),
                detail: change.detail().to_owned(),
                system_image: change.system_image().to_owned(),
                kind: change.kind().into(),
                status: change.status().into(),
            })
            .collect(),
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileHomePhase {
    Validating,
    Authenticating,
    Receiving,
    Integrating,
    Finishing,
}

impl From<StorageHomePhase> for MobileHomePhase {
    fn from(phase: StorageHomePhase) -> Self {
        match phase {
            StorageHomePhase::Validating => Self::Validating,
            StorageHomePhase::Authenticating => Self::Authenticating,
            StorageHomePhase::Receiving => Self::Receiving,
            StorageHomePhase::Integrating => Self::Integrating,
            StorageHomePhase::Finishing => Self::Finishing,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileHomeProgress {
    pub phase: MobileHomePhase,
    pub title: String,
    pub detail: String,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileHomeErrorKind {
    MissingConfiguration,
    Configuration,
    Authentication,
    Conflict,
    DirtyLocalChanges,
    Offline,
    Interrupted,
    SecureStorage,
    Repository,
    PartialProgress,
}

impl From<StorageHomeErrorKind> for MobileHomeErrorKind {
    fn from(kind: StorageHomeErrorKind) -> Self {
        match kind {
            StorageHomeErrorKind::MissingConfiguration => Self::MissingConfiguration,
            StorageHomeErrorKind::Configuration => Self::Configuration,
            StorageHomeErrorKind::Authentication => Self::Authentication,
            StorageHomeErrorKind::Conflict => Self::Conflict,
            StorageHomeErrorKind::DirtyLocalChanges => Self::DirtyLocalChanges,
            StorageHomeErrorKind::Offline => Self::Offline,
            StorageHomeErrorKind::Interrupted => Self::Interrupted,
            StorageHomeErrorKind::SecureStorage => Self::SecureStorage,
            StorageHomeErrorKind::Repository => Self::Repository,
            StorageHomeErrorKind::PartialProgress => Self::PartialProgress,
        }
    }
}

#[derive(Debug, uniffi::Error)]
pub enum MobileHomeFfiError {
    Failed {
        kind: MobileHomeErrorKind,
        title: String,
        detail: String,
    },
}

impl fmt::Display for MobileHomeFfiError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"),
        }
    }
}

impl Error for MobileHomeFfiError {}

impl From<StorageHomeError> for MobileHomeFfiError {
    fn from(error: StorageHomeError) -> Self {
        Self::Failed {
            kind: error.kind().into(),
            title: error.title().to_owned(),
            detail: error.detail().to_owned(),
        }
    }
}

#[derive(uniffi::Object)]
pub struct MobileHomeOperation {
    operation: mobile_home::MobileHomeOperation,
}

#[uniffi::export]
impl MobileHomeOperation {
    pub fn progress(&self) -> MobileHomeProgress {
        let progress = self.operation.progress();
        MobileHomeProgress {
            phase: progress.phase().into(),
            title: progress.title().to_owned(),
            detail: progress.detail().to_owned(),
        }
    }

    pub fn cached(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
        self.operation.cached().map(Into::into).map_err(Into::into)
    }

    pub fn refresh_if_stale(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
        self.operation
            .refresh_if_stale()
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn refresh(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
        self.operation.refresh().map(Into::into).map_err(Into::into)
    }

    pub fn pull(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
        self.operation.pull().map(Into::into).map_err(Into::into)
    }

    pub fn cancel(&self) {
        self.operation.cancel();
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobilePasswordRowKind {
    Directory,
    Entry,
}

impl From<StoragePasswordRowKind> for MobilePasswordRowKind {
    fn from(kind: StoragePasswordRowKind) -> Self {
        match kind {
            StoragePasswordRowKind::Directory => Self::Directory,
            StoragePasswordRowKind::Entry => Self::Entry,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobilePasswordRow {
    pub id: String,
    pub path: String,
    pub title: String,
    pub detail: String,
    pub system_image: String,
    pub kind: MobilePasswordRowKind,
}

#[derive(Clone, uniffi::Record)]
pub struct MobilePasswordPage {
    pub id: String,
    pub path: String,
    pub title: String,
    pub rows: Vec<MobilePasswordRow>,
}

impl From<ironstorage::mobile_passwords::MobilePasswordPage> for MobilePasswordPage {
    fn from(page: ironstorage::mobile_passwords::MobilePasswordPage) -> Self {
        Self {
            id: page.id().to_owned(),
            path: page.path().to_owned(),
            title: page.title().to_owned(),
            rows: page
                .rows()
                .iter()
                .map(|row| MobilePasswordRow {
                    id: row.id().to_owned(),
                    path: row.path().to_owned(),
                    title: row.title().to_owned(),
                    detail: row.detail().to_owned(),
                    system_image: row.system_image().to_owned(),
                    kind: row.kind().into(),
                })
                .collect(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobilePasswordErrorKind {
    MissingConfiguration,
    Configuration,
    InvalidPath,
    DirectoryMissing,
    Repository,
}

impl From<StoragePasswordErrorKind> for MobilePasswordErrorKind {
    fn from(kind: StoragePasswordErrorKind) -> Self {
        match kind {
            StoragePasswordErrorKind::MissingConfiguration => Self::MissingConfiguration,
            StoragePasswordErrorKind::Configuration => Self::Configuration,
            StoragePasswordErrorKind::InvalidPath => Self::InvalidPath,
            StoragePasswordErrorKind::DirectoryMissing => Self::DirectoryMissing,
            StoragePasswordErrorKind::Repository => Self::Repository,
        }
    }
}

#[derive(Debug, uniffi::Error)]
pub enum MobilePasswordFfiError {
    Failed {
        kind: MobilePasswordErrorKind,
        title: String,
        detail: String,
    },
}

impl fmt::Display for MobilePasswordFfiError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"),
        }
    }
}

impl Error for MobilePasswordFfiError {}

impl From<StoragePasswordError> for MobilePasswordFfiError {
    fn from(error: StoragePasswordError) -> Self {
        Self::Failed {
            kind: error.kind().into(),
            title: error.title().to_owned(),
            detail: error.detail().to_owned(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileAuthenticationErrorKind {
    PassphraseRequired,
    InvalidPassphrase,
    Cancelled,
    BiometryUnavailable,
    Configuration,
    KeyMaterial,
    Entry,
    SecureStorage,
    Expired,
    Conflict,
}

impl From<StorageAuthenticationErrorKind> for MobileAuthenticationErrorKind {
    fn from(kind: StorageAuthenticationErrorKind) -> Self {
        match kind {
            StorageAuthenticationErrorKind::PassphraseRequired => Self::PassphraseRequired,
            StorageAuthenticationErrorKind::InvalidPassphrase => Self::InvalidPassphrase,
            StorageAuthenticationErrorKind::Cancelled => Self::Cancelled,
            StorageAuthenticationErrorKind::BiometryUnavailable => Self::BiometryUnavailable,
            StorageAuthenticationErrorKind::Configuration => Self::Configuration,
            StorageAuthenticationErrorKind::KeyMaterial => Self::KeyMaterial,
            StorageAuthenticationErrorKind::Entry => Self::Entry,
            StorageAuthenticationErrorKind::SecureStorage => Self::SecureStorage,
            StorageAuthenticationErrorKind::Expired => Self::Expired,
            StorageAuthenticationErrorKind::Conflict => Self::Conflict,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileAuthenticationState {
    pub unlocked: bool,
    pub biometric_unlock_enabled: bool,
    pub remaining_seconds: u64,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileGitIdentity {
    pub name: String,
    pub email: String,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileMutationAction {
    Move,
    Copy,
    Delete,
}

impl From<StorageMutationAction> for MobileMutationAction {
    fn from(action: StorageMutationAction) -> Self {
        match action {
            StorageMutationAction::Move => Self::Move,
            StorageMutationAction::Copy => Self::Copy,
            StorageMutationAction::Delete => Self::Delete,
        }
    }
}

impl From<MobileMutationAction> for StorageMutationAction {
    fn from(action: MobileMutationAction) -> Self {
        match action {
            MobileMutationAction::Move => Self::Move,
            MobileMutationAction::Copy => Self::Copy,
            MobileMutationAction::Delete => Self::Delete,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileMutationDestination {
    pub path: String,
    pub title: String,
    pub detail: String,
    pub requires_overwrite: bool,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileMutationPlan {
    pub action: MobileMutationAction,
    pub source: String,
    pub source_title: String,
    pub revision: String,
    pub destinations: Vec<MobileMutationDestination>,
    pub has_open_editor: bool,
    pub has_dirty_editor: bool,
}

impl From<StorageMutationPlan> for MobileMutationPlan {
    fn from(plan: StorageMutationPlan) -> Self {
        Self {
            action: plan.action().into(),
            source: plan.source().to_owned(),
            source_title: plan.source_title().to_owned(),
            revision: plan.revision().to_owned(),
            destinations: plan
                .destinations()
                .iter()
                .map(|destination| MobileMutationDestination {
                    path: destination.path().to_owned(),
                    title: destination.title().to_owned(),
                    detail: destination.detail().to_owned(),
                    requires_overwrite: destination.requires_overwrite(),
                })
                .collect(),
            has_open_editor: plan.has_open_editor(),
            has_dirty_editor: plan.has_dirty_editor(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileMutationRequest {
    pub action: MobileMutationAction,
    pub source: String,
    pub revision: String,
    pub destination: Option<String>,
    pub confirmed: bool,
    pub overwrite: bool,
    pub discard_editor: bool,
}

impl From<MobileMutationRequest> for StorageMutationRequest {
    fn from(request: MobileMutationRequest) -> Self {
        Self {
            action: request.action.into(),
            source: request.source,
            revision: request.revision,
            destination: request.destination,
            confirmed: request.confirmed,
            overwrite: request.overwrite,
            discard_editor: request.discard_editor,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileMutationOutcome {
    pub action: MobileMutationAction,
    pub source: String,
    pub destination: Option<String>,
    pub title: String,
    pub detail: String,
}

impl From<StorageMutationOutcome> for MobileMutationOutcome {
    fn from(outcome: StorageMutationOutcome) -> Self {
        Self {
            action: outcome.action().into(),
            source: outcome.source().to_owned(),
            destination: outcome.destination().map(str::to_owned),
            title: outcome.title().to_owned(),
            detail: outcome.detail().to_owned(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileEntrySectionKind {
    Password,
    Details,
    OneTimePassword,
    Notes,
}

impl From<StorageEntrySectionKind> for MobileEntrySectionKind {
    fn from(kind: StorageEntrySectionKind) -> Self {
        match kind {
            StorageEntrySectionKind::Password => Self::Password,
            StorageEntrySectionKind::Details => Self::Details,
            StorageEntrySectionKind::OneTimePassword => Self::OneTimePassword,
            StorageEntrySectionKind::Notes => Self::Notes,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryField {
    pub id: u64,
    pub label: String,
    pub system_image: String,
    pub value: Option<String>,
    pub masked_value: String,
    pub detail: Option<String>,
    pub diagnostic: Option<String>,
    pub sensitive: bool,
    pub multiline: bool,
    pub selectable: bool,
    pub editable: bool,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntrySection {
    pub kind: MobileEntrySectionKind,
    pub title: String,
    pub fields: Vec<MobileEntryField>,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryPage {
    pub id: String,
    pub title: String,
    pub sections: Vec<MobileEntrySection>,
}

impl From<StorageEntryPage> for MobileEntryPage {
    fn from(page: StorageEntryPage) -> Self {
        Self {
            id: page.id().to_owned(),
            title: page.title().to_owned(),
            sections: page
                .sections()
                .iter()
                .map(|section| MobileEntrySection {
                    kind: section.kind().into(),
                    title: section.title().to_owned(),
                    fields: section
                        .fields()
                        .iter()
                        .map(|field| MobileEntryField {
                            id: field.id(),
                            label: field.label().to_owned(),
                            system_image: field.system_image().to_owned(),
                            value: field.value().map(str::to_owned),
                            masked_value: field.masked_value().to_owned(),
                            detail: field.detail().map(str::to_owned),
                            diagnostic: field.diagnostic().map(str::to_owned),
                            sensitive: field.sensitive(),
                            multiline: field.multiline(),
                            selectable: field.selectable(),
                            editable: field.editable(),
                        })
                        .collect(),
                })
                .collect(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryCopy {
    pub value: String,
    pub timeout_seconds: u64,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileWatchSnapshotState {
    Unavailable,
    Pending,
}

impl From<StorageWatchSnapshotState> for MobileWatchSnapshotState {
    fn from(state: StorageWatchSnapshotState) -> Self {
        match state {
            StorageWatchSnapshotState::Unavailable => Self::Unavailable,
            StorageWatchSnapshotState::Pending => Self::Pending,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileWatchSnapshotStatus {
    pub state: MobileWatchSnapshotState,
    pub detail: String,
}

impl From<&StorageWatchSnapshotStatus> for MobileWatchSnapshotStatus {
    fn from(status: &StorageWatchSnapshotStatus) -> Self {
        Self {
            state: status.state().into(),
            detail: status.detail().to_owned(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileTotpRow {
    pub path: String,
    pub issuer: Option<String>,
    pub account: String,
    pub title: String,
    pub detail: String,
    pub shared_with_watch: bool,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileTotpPage {
    pub rows: Vec<MobileTotpRow>,
    pub unavailable_entries: u32,
    pub cache_notice: Option<String>,
    pub watch: MobileWatchSnapshotStatus,
}

impl From<StorageTotpPage> for MobileTotpPage {
    fn from(page: StorageTotpPage) -> Self {
        Self {
            rows: page
                .rows()
                .iter()
                .map(|row| MobileTotpRow {
                    path: row.path().to_owned(),
                    issuer: row.issuer().map(str::to_owned),
                    account: row.account().to_owned(),
                    title: row.title().to_owned(),
                    detail: row.detail().to_owned(),
                    shared_with_watch: row.shared_with_watch(),
                })
                .collect(),
            unavailable_entries: page.unavailable_entries(),
            cache_notice: page.cache_notice().map(str::to_owned),
            watch: page.watch().into(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileTotpDiscoveryPhase {
    Preparing,
    Inspecting,
    Saving,
    Complete,
    Cancelled,
}

impl From<StorageTotpDiscoveryPhase> for MobileTotpDiscoveryPhase {
    fn from(phase: StorageTotpDiscoveryPhase) -> Self {
        match phase {
            StorageTotpDiscoveryPhase::Preparing => Self::Preparing,
            StorageTotpDiscoveryPhase::Inspecting => Self::Inspecting,
            StorageTotpDiscoveryPhase::Saving => Self::Saving,
            StorageTotpDiscoveryPhase::Complete => Self::Complete,
            StorageTotpDiscoveryPhase::Cancelled => Self::Cancelled,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileTotpDiscoveryProgress {
    pub phase: MobileTotpDiscoveryPhase,
    pub total: u32,
    pub inspected: u32,
    pub cache_hits: u32,
    pub matches: u32,
    pub unavailable: u32,
}

#[derive(uniffi::Object)]
pub struct MobileTotpOperation {
    operation: StorageTotpOperation,
}

#[uniffi::export]
impl MobileTotpOperation {
    pub fn progress(&self) -> MobileTotpDiscoveryProgress {
        let progress = self.operation.progress();
        MobileTotpDiscoveryProgress {
            phase: progress.phase().into(),
            total: progress.total(),
            inspected: progress.inspected(),
            cache_hits: progress.cache_hits(),
            matches: progress.matches(),
            unavailable: progress.unavailable(),
        }
    }

    pub fn cancel(&self) {
        self.operation.cancel();
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileTotpDetail {
    pub path: String,
    pub issuer: Option<String>,
    pub account: String,
    pub code: String,
    pub valid_until: u64,
    pub period: u64,
    pub shared_with_watch: bool,
    pub watch: MobileWatchSnapshotStatus,
}

impl From<StorageTotpDetail> for MobileTotpDetail {
    fn from(detail: StorageTotpDetail) -> Self {
        Self {
            path: detail.path().to_owned(),
            issuer: detail.issuer().map(str::to_owned),
            account: detail.account().to_owned(),
            code: String::from_utf8(detail.code().expose().to_vec())
                .expect("storage-generated TOTP codes are ASCII"),
            valid_until: detail.valid_until(),
            period: detail.period(),
            shared_with_watch: detail.shared_with_watch(),
            watch: detail.watch().into(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryPresentation {
    pub page: MobileEntryPage,
    pub totp: Option<MobileTotpDetail>,
    pub cache_notice: Option<String>,
}

impl From<StorageEntryPresentation> for MobileEntryPresentation {
    fn from(presentation: StorageEntryPresentation) -> Self {
        let (page, totp, cache_notice) = presentation.into_parts();
        Self {
            page: page.into(),
            totp: totp.map(Into::into),
            cache_notice,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileEntryEditorFieldKind {
    Password,
    Field,
    Note,
    OtpUri,
}

impl From<StorageEntryEditorFieldKind> for MobileEntryEditorFieldKind {
    fn from(kind: StorageEntryEditorFieldKind) -> Self {
        match kind {
            StorageEntryEditorFieldKind::Password => Self::Password,
            StorageEntryEditorFieldKind::Field => Self::Field,
            StorageEntryEditorFieldKind::Note => Self::Note,
            StorageEntryEditorFieldKind::OtpUri => Self::OtpUri,
        }
    }
}

impl From<MobileEntryEditorFieldKind> for StorageEntryEditorFieldKind {
    fn from(kind: MobileEntryEditorFieldKind) -> Self {
        match kind {
            MobileEntryEditorFieldKind::Password => Self::Password,
            MobileEntryEditorFieldKind::Field => Self::Field,
            MobileEntryEditorFieldKind::Note => Self::Note,
            MobileEntryEditorFieldKind::OtpUri => Self::OtpUri,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryEditorField {
    pub id: u64,
    pub kind: MobileEntryEditorFieldKind,
    pub name: Option<String>,
    pub label: String,
    pub system_image: String,
    pub value: Option<String>,
    pub masked_value: String,
    pub diagnostic: Option<String>,
    pub sensitive: bool,
    pub multiline: bool,
    pub name_editable: bool,
    pub removable: bool,
    pub reorderable: bool,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryEditorPage {
    pub path: String,
    pub title: String,
    pub fields: Vec<MobileEntryEditorField>,
    pub dirty: bool,
    pub creating: bool,
    pub default_password_length: u32,
    pub maximum_password_length: u32,
}

impl From<&StorageEntryEditorPage> for MobileEntryEditorPage {
    fn from(page: &StorageEntryEditorPage) -> Self {
        Self {
            path: page.path().to_owned(),
            title: page.title().to_owned(),
            fields: page
                .fields()
                .iter()
                .map(|field| MobileEntryEditorField {
                    id: field.id(),
                    kind: field.kind().into(),
                    name: field.name().map(str::to_owned),
                    label: field.label().to_owned(),
                    system_image: field.system_image().to_owned(),
                    value: field.value().map(str::to_owned),
                    masked_value: field.masked_value().to_owned(),
                    diagnostic: field.diagnostic().map(str::to_owned),
                    sensitive: field.sensitive(),
                    multiline: field.multiline(),
                    name_editable: field.name_editable(),
                    removable: field.removable(),
                    reorderable: field.reorderable(),
                })
                .collect(),
            dirty: page.dirty(),
            creating: page.creating(),
            default_password_length: page.default_password_length(),
            maximum_password_length: page.maximum_password_length(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryEditorSession {
    pub id: u64,
    pub page: MobileEntryEditorPage,
}

impl From<StorageEntryEditorSession> for MobileEntryEditorSession {
    fn from(session: StorageEntryEditorSession) -> Self {
        Self {
            id: session.id(),
            page: session.page().into(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileEntryEditorInput {
    pub id: u64,
    pub name: Option<String>,
    pub value: Option<String>,
}

impl From<MobileEntryEditorInput> for StorageEntryEditorInput {
    fn from(input: MobileEntryEditorInput) -> Self {
        Self::new(input.id, input.name, input.value)
    }
}

impl From<StorageEntryCopy> for MobileEntryCopy {
    fn from(copy: StorageEntryCopy) -> Self {
        Self {
            value: copy.value().to_owned(),
            timeout_seconds: copy.timeout_seconds(),
        }
    }
}

impl From<StorageAuthenticationState> for MobileAuthenticationState {
    fn from(state: StorageAuthenticationState) -> Self {
        Self {
            unlocked: state.unlocked(),
            biometric_unlock_enabled: state.biometric_unlock_enabled(),
            remaining_seconds: state.remaining_seconds(),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileKeyTransferKind {
    Public,
    Private,
}

impl From<StorageKeyTransferKind> for MobileKeyTransferKind {
    fn from(kind: StorageKeyTransferKind) -> Self {
        match kind {
            StorageKeyTransferKind::Public => Self::Public,
            StorageKeyTransferKind::Private => Self::Private,
        }
    }
}

impl From<MobileKeyTransferKind> for StorageKeyTransferKind {
    fn from(kind: MobileKeyTransferKind) -> Self {
        match kind {
            MobileKeyTransferKind::Public => Self::Public,
            MobileKeyTransferKind::Private => Self::Private,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileKeyTransferKey {
    pub fingerprint: String,
    pub title: String,
    pub detail: String,
    pub kind: MobileKeyTransferKind,
    pub requires_passphrase: bool,
}

impl From<&StorageKeyTransferKey> for MobileKeyTransferKey {
    fn from(key: &StorageKeyTransferKey) -> Self {
        Self {
            fingerprint: key.fingerprint().to_owned(),
            title: key.title().to_owned(),
            detail: key.detail().to_owned(),
            kind: key.kind().into(),
            requires_passphrase: key.requires_passphrase(),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileKeyTransferFrame {
    pub sequence: u32,
    pub total: u32,
    pub width: u32,
    pub modules: Vec<u8>,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileKeyTransferExport {
    pub key: MobileKeyTransferKey,
    pub frames: Vec<MobileKeyTransferFrame>,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileKeyTransferProgress {
    pub received: u32,
    pub total: u32,
    pub duplicate: bool,
    pub key: Option<MobileKeyTransferKey>,
}

impl From<StorageKeyTransferProgress> for MobileKeyTransferProgress {
    fn from(progress: StorageKeyTransferProgress) -> Self {
        Self {
            received: u32::try_from(progress.received()).unwrap_or(u32::MAX),
            total: u32::try_from(progress.total()).unwrap_or(u32::MAX),
            duplicate: progress.duplicate(),
            key: progress.key().map(Into::into),
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileKeyTransferOutcome {
    pub title: String,
    pub detail: String,
}

#[derive(Debug, uniffi::Error)]
pub enum MobileKeyTransferFfiError {
    Failed { message: String },
}

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

impl Error for MobileKeyTransferFfiError {}

impl From<StorageKeyTransferError> for MobileKeyTransferFfiError {
    fn from(error: StorageKeyTransferError) -> Self {
        Self::Failed {
            message: error.to_string(),
        }
    }
}

#[derive(uniffi::Object)]
pub struct MobileKeyTransfer {
    service: ironstorage::mobile_key_transfer::MobileKeyTransferService,
}

#[uniffi::export]
impl MobileKeyTransfer {
    pub fn keys(&self) -> Vec<MobileKeyTransferKey> {
        self.service.keys().iter().map(Into::into).collect()
    }

    pub fn export(
        &self,
        fingerprint: String,
        kind: MobileKeyTransferKind,
        passphrase: Option<String>,
    ) -> Result<MobileKeyTransferExport, MobileKeyTransferFfiError> {
        let exported = self.service.export(
            &fingerprint,
            kind.into(),
            passphrase.map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
        )?;
        let frames = exported
            .frames()
            .iter()
            .map(|frame| {
                let matrix = frame.matrix();
                let mut modules = Vec::with_capacity(matrix.width() * matrix.width());
                for y in 0..matrix.width() {
                    for x in 0..matrix.width() {
                        modules.push(u8::from(matrix.is_dark(x, y).unwrap_or(false)));
                    }
                }
                MobileKeyTransferFrame {
                    sequence: u32::try_from(frame.sequence()).unwrap_or(u32::MAX),
                    total: u32::try_from(frame.total()).unwrap_or(u32::MAX),
                    width: u32::try_from(matrix.width()).unwrap_or(u32::MAX),
                    modules,
                }
            })
            .collect();
        Ok(MobileKeyTransferExport {
            key: exported.key().into(),
            frames,
        })
    }

    pub fn importer(&self) -> Arc<MobileKeyTransferImport> {
        Arc::new(MobileKeyTransferImport {
            importer: Mutex::new(self.service.importer()),
        })
    }
}

#[derive(uniffi::Object)]
pub struct MobileKeyTransferImport {
    importer: Mutex<ironstorage::mobile_key_transfer::MobileKeyTransferImport>,
}

#[uniffi::export]
impl MobileKeyTransferImport {
    pub fn add_frame(
        &self,
        payload: String,
    ) -> Result<MobileKeyTransferProgress, MobileKeyTransferFfiError> {
        self.importer
            .lock()
            .map_err(|_| MobileKeyTransferFfiError::Failed {
                message: "the key-transfer session is unavailable".to_owned(),
            })?
            .add_frame(ironstorage::repository::SecretBytes::new(
                payload.into_bytes(),
            ))
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn import(
        &self,
        passphrase: Option<String>,
        make_default: bool,
    ) -> Result<MobileKeyTransferOutcome, MobileKeyTransferFfiError> {
        let outcome = self
            .importer
            .lock()
            .map_err(|_| MobileKeyTransferFfiError::Failed {
                message: "the key-transfer session is unavailable".to_owned(),
            })?
            .import(
                passphrase
                    .map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
                make_default,
            )?;
        Ok(MobileKeyTransferOutcome {
            title: outcome.title().to_owned(),
            detail: outcome.detail().to_owned(),
        })
    }
}

#[derive(Debug, uniffi::Error)]
pub enum MobileAuthenticationFfiError {
    Failed {
        kind: MobileAuthenticationErrorKind,
        title: String,
        detail: String,
    },
}

impl fmt::Display for MobileAuthenticationFfiError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"),
        }
    }
}

impl Error for MobileAuthenticationFfiError {}

impl From<StorageAuthenticationError> for MobileAuthenticationFfiError {
    fn from(error: StorageAuthenticationError) -> Self {
        Self::Failed {
            kind: error.kind().into(),
            title: error.title().to_owned(),
            detail: error.detail().to_owned(),
        }
    }
}

#[derive(uniffi::Object)]
pub struct MobileAuthentication {
    authentication: ironstorage::mobile_authentication::MobileAuthentication,
}

#[uniffi::export]
impl MobileAuthentication {
    pub fn state(&self) -> Result<MobileAuthenticationState, MobileAuthenticationFfiError> {
        self.authentication
            .state()
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn git_identity(&self) -> Result<MobileGitIdentity, MobileAuthenticationFfiError> {
        self.authentication
            .git_identity()
            .map(|identity| MobileGitIdentity {
                name: identity.name().to_owned(),
                email: identity.email().to_owned(),
            })
            .map_err(Into::into)
    }

    pub fn set_git_identity(
        &self,
        name: String,
        email: String,
    ) -> Result<MobileGitIdentity, MobileAuthenticationFfiError> {
        self.authentication
            .set_git_identity(name, email)
            .map(|identity| MobileGitIdentity {
                name: identity.name().to_owned(),
                email: identity.email().to_owned(),
            })
            .map_err(Into::into)
    }

    pub fn unlock_entry(
        &self,
        path: String,
        passphrase: Option<String>,
    ) -> Result<MobileAuthenticationState, MobileAuthenticationFfiError> {
        self.authentication
            .unlock_entry(
                &path,
                passphrase
                    .map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
            )
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn set_biometric_unlock(
        &self,
        enabled: bool,
    ) -> Result<MobileAuthenticationState, MobileAuthenticationFfiError> {
        self.authentication
            .set_biometric_unlock(enabled)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationFfiError> {
        self.authentication
            .touch_user_activity()
            .map_err(Into::into)
    }

    pub fn entry_page(
        &self,
        path: String,
        unix_seconds: u64,
    ) -> Result<MobileEntryPresentation, MobileAuthenticationFfiError> {
        self.authentication
            .entry_page(&path, unix_seconds)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn reveal_entry_field(
        &self,
        path: String,
        field: u64,
    ) -> Result<String, MobileAuthenticationFfiError> {
        self.authentication
            .reveal_entry_field(&path, field)
            .map_err(Into::into)
    }

    pub fn copy_entry_field(
        &self,
        path: String,
        field: u64,
    ) -> Result<MobileEntryCopy, MobileAuthenticationFfiError> {
        self.authentication
            .copy_entry_field(&path, field)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn prepare_entry_mutation(
        &self,
        path: String,
        action: MobileMutationAction,
    ) -> Result<MobileMutationPlan, MobileAuthenticationFfiError> {
        self.authentication
            .prepare_entry_mutation(&path, action.into())
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn perform_entry_mutation(
        &self,
        request: MobileMutationRequest,
    ) -> Result<MobileMutationOutcome, MobileAuthenticationFfiError> {
        self.authentication
            .perform_entry_mutation(request.into())
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn replace_entry_field(
        &self,
        path: String,
        field: u64,
        value: String,
    ) -> Result<MobileEntryPage, MobileAuthenticationFfiError> {
        self.authentication
            .replace_entry_field(&path, field, value)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn begin_entry_editor(
        &self,
        path: String,
    ) -> Result<MobileEntryEditorSession, MobileAuthenticationFfiError> {
        self.authentication
            .begin_entry_editor(&path)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn unlock_totp(
        &self,
        passphrase: Option<String>,
    ) -> Result<MobileAuthenticationState, MobileAuthenticationFfiError> {
        self.authentication
            .unlock_totp(
                passphrase
                    .map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
            )
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn totp_page(
        &self,
        operation: Arc<MobileTotpOperation>,
    ) -> Result<MobileTotpPage, MobileAuthenticationFfiError> {
        self.authentication
            .totp_page(&operation.operation)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn cached_totp_page(&self) -> Result<Option<MobileTotpPage>, MobileAuthenticationFfiError> {
        self.authentication
            .cached_totp_page()
            .map(|page| page.map(Into::into))
            .map_err(Into::into)
    }

    pub fn search_cached_totp_page(
        &self,
        query: String,
    ) -> Result<Option<MobileTotpPage>, MobileAuthenticationFfiError> {
        self.authentication
            .search_cached_totp_page(&query)
            .map(|page| page.map(Into::into))
            .map_err(Into::into)
    }

    pub fn totp_detail(
        &self,
        path: String,
        unix_seconds: u64,
    ) -> Result<MobileTotpDetail, MobileAuthenticationFfiError> {
        self.authentication
            .totp_detail(&path, unix_seconds)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn copy_totp_code(
        &self,
        path: String,
        unix_seconds: u64,
    ) -> Result<MobileEntryCopy, MobileAuthenticationFfiError> {
        self.authentication
            .copy_totp_code(&path, unix_seconds)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn set_totp_watch_shared(
        &self,
        path: String,
        shared: bool,
        unix_seconds: u64,
    ) -> Result<MobileTotpDetail, MobileAuthenticationFfiError> {
        self.authentication
            .set_totp_watch_shared(&path, shared, unix_seconds)
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn begin_create_entry(
        &self,
        directory: String,
        name: String,
        passphrase: Option<String>,
    ) -> Result<MobileEntryEditorSession, MobileAuthenticationFfiError> {
        self.authentication
            .begin_create_entry(
                &directory,
                &name,
                passphrase
                    .map(|value| ironstorage::repository::SecretBytes::new(value.into_bytes())),
            )
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn entry_editor(
        &self,
        editor: u64,
    ) -> Result<MobileEntryEditorPage, MobileAuthenticationFfiError> {
        self.authentication
            .entry_editor(editor)
            .map(|page| (&page).into())
            .map_err(Into::into)
    }

    pub fn update_entry_editor(
        &self,
        editor: u64,
        fields: Vec<MobileEntryEditorInput>,
    ) -> Result<MobileEntryEditorPage, MobileAuthenticationFfiError> {
        self.authentication
            .update_entry_editor(editor, fields.into_iter().map(Into::into).collect())
            .map(|page| (&page).into())
            .map_err(Into::into)
    }

    pub fn add_entry_editor_field(
        &self,
        editor: u64,
        kind: MobileEntryEditorFieldKind,
        name: Option<String>,
        value: String,
    ) -> Result<MobileEntryEditorPage, MobileAuthenticationFfiError> {
        self.authentication
            .add_entry_editor_field(editor, kind.into(), name, value)
            .map(|page| (&page).into())
            .map_err(Into::into)
    }

    pub fn remove_entry_editor_field(
        &self,
        editor: u64,
        field: u64,
    ) -> Result<MobileEntryEditorPage, MobileAuthenticationFfiError> {
        self.authentication
            .remove_entry_editor_field(editor, field)
            .map(|page| (&page).into())
            .map_err(Into::into)
    }

    pub fn reorder_entry_editor_field(
        &self,
        editor: u64,
        field: u64,
        index: u32,
    ) -> Result<MobileEntryEditorPage, MobileAuthenticationFfiError> {
        self.authentication
            .reorder_entry_editor_field(editor, field, index as usize)
            .map(|page| (&page).into())
            .map_err(Into::into)
    }

    pub fn generate_entry_editor_password(
        &self,
        editor: u64,
        length: Option<u32>,
        no_symbols: bool,
    ) -> Result<MobileEntryEditorPage, MobileAuthenticationFfiError> {
        self.authentication
            .generate_entry_editor_password(editor, length, no_symbols)
            .map(|page| (&page).into())
            .map_err(Into::into)
    }

    pub fn save_entry_editor(
        &self,
        editor: u64,
        fields: Vec<MobileEntryEditorInput>,
    ) -> Result<MobileEntryPage, MobileAuthenticationFfiError> {
        self.authentication
            .save_entry_editor(editor, fields.into_iter().map(Into::into).collect())
            .map(Into::into)
            .map_err(Into::into)
    }

    pub fn discard_entry_editor(&self, editor: u64) -> Result<(), MobileAuthenticationFfiError> {
        self.authentication
            .discard_entry_editor(editor)
            .map_err(Into::into)
    }

    pub fn manual_lock(&self) -> Result<(), MobileAuthenticationFfiError> {
        self.authentication.manual_lock().map_err(Into::into)
    }

    pub fn cancel(&self) -> Result<(), MobileAuthenticationFfiError> {
        self.authentication.cancel().map_err(Into::into)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileOnboardingPhase {
    Validating,
    Authenticating,
    Receiving,
    Integrating,
    Finishing,
}

impl From<StorageOnboardingPhase> for MobileOnboardingPhase {
    fn from(phase: StorageOnboardingPhase) -> Self {
        match phase {
            StorageOnboardingPhase::Validating => Self::Validating,
            StorageOnboardingPhase::Authenticating => Self::Authenticating,
            StorageOnboardingPhase::Receiving => Self::Receiving,
            StorageOnboardingPhase::Integrating => Self::Integrating,
            StorageOnboardingPhase::Finishing => Self::Finishing,
        }
    }
}

#[derive(Clone, uniffi::Record)]
pub struct MobileOnboardingProgress {
    pub phase: MobileOnboardingPhase,
    pub title: String,
    pub detail: String,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileOnboardingDiscovery {
    pub branches: Vec<String>,
    pub selected_branch: u32,
}

#[derive(Clone, uniffi::Record)]
pub struct MobileOnboardingOutcome {
    pub title: String,
    pub detail: String,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileOnboardingErrorKind {
    InvalidInput,
    UnsupportedRemote,
    Authentication,
    Repository,
    ExistingClone,
    Interrupted,
    SecureStorage,
    Configuration,
    AlreadyConfigured,
}

impl From<StorageOnboardingErrorKind> for MobileOnboardingErrorKind {
    fn from(kind: StorageOnboardingErrorKind) -> Self {
        match kind {
            StorageOnboardingErrorKind::InvalidInput => Self::InvalidInput,
            StorageOnboardingErrorKind::UnsupportedRemote => Self::UnsupportedRemote,
            StorageOnboardingErrorKind::Authentication => Self::Authentication,
            StorageOnboardingErrorKind::Repository => Self::Repository,
            StorageOnboardingErrorKind::ExistingClone => Self::ExistingClone,
            StorageOnboardingErrorKind::Interrupted => Self::Interrupted,
            StorageOnboardingErrorKind::SecureStorage => Self::SecureStorage,
            StorageOnboardingErrorKind::Configuration => Self::Configuration,
            StorageOnboardingErrorKind::AlreadyConfigured => Self::AlreadyConfigured,
        }
    }
}

#[derive(Debug, uniffi::Error)]
pub enum MobileOnboardingFfiError {
    Failed {
        kind: MobileOnboardingErrorKind,
        title: String,
        detail: String,
    },
}

impl fmt::Display for MobileOnboardingFfiError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Failed { title, detail, .. } => write!(formatter, "{title}: {detail}"),
        }
    }
}

impl Error for MobileOnboardingFfiError {}

impl From<StorageOnboardingError> for MobileOnboardingFfiError {
    fn from(error: StorageOnboardingError) -> Self {
        Self::Failed {
            kind: error.kind().into(),
            title: error.title().to_owned(),
            detail: error.detail().to_owned(),
        }
    }
}

#[derive(uniffi::Object)]
pub struct MobileOnboardingOperation {
    operation: mobile_onboarding::MobileOnboardingOperation,
    request: mobile_onboarding::MobileOnboardingRequest,
}

#[uniffi::export]
impl MobileOnboardingOperation {
    pub fn progress(&self) -> MobileOnboardingProgress {
        let progress = self.operation.progress();
        MobileOnboardingProgress {
            phase: progress.phase().into(),
            title: progress.title().to_owned(),
            detail: progress.detail().to_owned(),
        }
    }

    pub fn discover(&self) -> Result<MobileOnboardingDiscovery, MobileOnboardingFfiError> {
        let discovery = self.operation.discover(&self.request)?;
        Ok(MobileOnboardingDiscovery {
            branches: discovery.branches().to_vec(),
            selected_branch: u32::try_from(discovery.selected_branch()).unwrap_or_default(),
        })
    }

    pub fn setup(
        &self,
        branch: String,
        use_existing: bool,
    ) -> Result<MobileOnboardingOutcome, MobileOnboardingFfiError> {
        let outcome = self.operation.setup(&self.request, &branch, use_existing)?;
        Ok(MobileOnboardingOutcome {
            title: outcome.title().to_owned(),
            detail: outcome.detail().to_owned(),
        })
    }

    pub fn cancel(&self) {
        self.operation.cancel();
    }
}

impl From<mobile::MobileShell> for MobileShell {
    fn from(shell: mobile::MobileShell) -> Self {
        Self {
            selected_tab: shell.selected_tab().into(),
            pages: shell
                .pages()
                .iter()
                .map(|page| MobilePage {
                    tab: page.tab().into(),
                    title: page.title().to_owned(),
                    system_image: page.system_image().to_owned(),
                    selected_system_image: page.selected_system_image().to_owned(),
                    state: page.state().into(),
                    state_title: page.state_title().to_owned(),
                    state_detail: page.state_detail().to_owned(),
                })
                .collect(),
        }
    }
}

#[derive(Debug, uniffi::Error)]
pub enum MobilePreferenceError {
    Configuration { message: String },
}

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

impl Error for MobilePreferenceError {}

impl From<ConfigError> for MobilePreferenceError {
    fn from(error: ConfigError) -> Self {
        Self::Configuration {
            message: error.to_string(),
        }
    }
}

#[uniffi::export]
pub fn product_name() -> String {
    ironstorage::PRODUCT_NAME.to_owned()
}

#[uniffi::export]
pub fn mobile_shell() -> MobileShell {
    mobile::MobileShell::load().into()
}

#[uniffi::export]
pub fn mobile_shell_fixture(state: MobileShellState) -> MobileShell {
    mobile::MobileShell::fixture(state.into()).into()
}

#[uniffi::export]
pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceError> {
    mobile::store_selected_tab(tab.into()).map_err(Into::into)
}

#[uniffi::export]
pub fn mobile_home_operation() -> Arc<MobileHomeOperation> {
    Arc::new(MobileHomeOperation {
        operation: mobile_home::MobileHomeOperation::default(),
    })
}

#[uniffi::export]
pub fn mobile_totp_operation() -> Arc<MobileTotpOperation> {
    Arc::new(MobileTotpOperation {
        operation: StorageTotpOperation::default(),
    })
}

#[uniffi::export]
pub fn mobile_password_page(
    path: Option<String>,
) -> Result<MobilePasswordPage, MobilePasswordFfiError> {
    ironstorage::mobile_passwords::MobilePasswordPage::load(path.as_deref())
        .map(Into::into)
        .map_err(Into::into)
}

#[uniffi::export]
pub fn mobile_password_search(query: String) -> Result<MobilePasswordPage, MobilePasswordFfiError> {
    ironstorage::mobile_passwords::MobilePasswordPage::search(&query)
        .map(Into::into)
        .map_err(Into::into)
}

#[uniffi::export]
pub fn mobile_authentication() -> Result<Arc<MobileAuthentication>, MobileAuthenticationFfiError> {
    Ok(Arc::new(MobileAuthentication {
        authentication: ironstorage::mobile_authentication::MobileAuthentication::load()?,
    }))
}

#[uniffi::export]
pub fn mobile_key_transfer() -> Result<Arc<MobileKeyTransfer>, MobileKeyTransferFfiError> {
    Ok(Arc::new(MobileKeyTransfer {
        service: ironstorage::mobile_key_transfer::MobileKeyTransferService::load()?,
    }))
}

#[uniffi::export]
pub fn mobile_onboarding_operation(
    server_url: String,
    account: String,
    repository_path: String,
    application_token: String,
) -> Result<Arc<MobileOnboardingOperation>, MobileOnboardingFfiError> {
    Ok(Arc::new(MobileOnboardingOperation {
        operation: mobile_onboarding::MobileOnboardingOperation::default(),
        request: mobile_onboarding::MobileOnboardingRequest::new(
            server_url,
            account,
            repository_path,
            application_token.into_bytes(),
        )?,
    }))
}

#[uniffi::export]
pub fn replace_configured_mobile_application_token(
    account: String,
    application_token: String,
) -> Result<(), MobileOnboardingFfiError> {
    mobile_onboarding::replace_configured_application_token(
        account,
        application_token.into_bytes(),
    )?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{
        MobileHomePhase, MobileOnboardingErrorKind, MobileOnboardingFfiError, MobileShellState,
        MobileTab,
    };

    #[test]
    fn bridge_reads_product_name_from_storage_crate() {
        assert_eq!(super::product_name(), ironstorage::PRODUCT_NAME);
    }

    #[test]
    fn bridge_exposes_every_view_ready_shell_fixture() {
        for state in [
            MobileShellState::Loading,
            MobileShellState::Empty,
            MobileShellState::Ready,
            MobileShellState::Locked,
            MobileShellState::Error,
        ] {
            let shell = super::mobile_shell_fixture(state);
            assert_eq!(shell.selected_tab, MobileTab::Home);
            assert_eq!(shell.pages.len(), 5);
            assert!(shell.pages.iter().all(|page| page.state == state));
        }
    }

    #[test]
    fn bridge_rejects_non_https_before_creating_an_operation() {
        let error = match super::mobile_onboarding_operation(
            "ssh://example.test".to_owned(),
            "alice".to_owned(),
            "team/passwords".to_owned(),
            "DO-NOT-RENDER".to_owned(),
        ) {
            Ok(_) => panic!("SSH must be rejected"),
            Err(error) => error,
        };
        match error {
            MobileOnboardingFfiError::Failed { kind, detail, .. } => {
                assert_eq!(kind, MobileOnboardingErrorKind::UnsupportedRemote);
                assert!(!detail.contains("DO-NOT-RENDER"));
            }
        }
    }

    #[test]
    fn home_operation_exposes_typed_progress_and_cancellation() {
        let operation = super::mobile_home_operation();
        assert_eq!(operation.progress().phase, MobileHomePhase::Validating);
        operation.cancel();
        assert_eq!(operation.progress().phase, MobileHomePhase::Validating);
    }
}
