//! OS-backed secret storage, opaque references, and bounded in-memory caching.

use std::{
    collections::BTreeMap,
    error::Error,
    fmt,
    num::NonZeroUsize,
    sync::Mutex,
    time::{Duration, Instant},
};

use crate::{
    config::{ApplicationId, ServerId},
    crypto::{KeyInfo, SecretProvider, SecretProviderError},
    git::{GitCredential, GitCredentialProvider, GitError},
    repository::SecretBytes,
};

mod platform;

pub(crate) use platform::NativeSecretBackend;

const RECORD_MAGIC: &[u8] = b"IRONSTORAGE-SECRET\0";
const RECORD_VERSION: u8 = 1;
const MAX_SECRET_BYTES: usize = 1024;
const MAX_CACHE_LIFETIME: Duration = Duration::from_secs(15 * 60);
const MAX_CACHE_CAPACITY: usize = 128;
const OPENPGP_PASSPHRASE_SERVICE: &str = "de.rfc1437.ironstorage.openpgp-passphrase";
const HTTPS_GIT_SERVICE: &str = "de.rfc1437.ironstorage.https-git";

/// The purpose and stable, non-secret identity of an OS credential.
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct SecretReference {
    kind: SecretReferenceKind,
}

#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
enum SecretReferenceKind {
    OpenPgpPassphrase {
        fingerprint: String,
    },
    HttpsGitCredential {
        server_id: String,
        application_id: String,
        account: String,
    },
}

impl SecretReference {
    pub fn openpgp_passphrase(fingerprint: impl Into<String>) -> Result<Self, SecretStoreError> {
        let fingerprint = fingerprint.into();
        if !matches!(fingerprint.len(), 40 | 64)
            || !fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit())
        {
            return Err(SecretStoreError::InvalidReference);
        }
        Ok(Self {
            kind: SecretReferenceKind::OpenPgpPassphrase {
                fingerprint: fingerprint.to_ascii_uppercase(),
            },
        })
    }

    pub fn https_git_credential(
        server_id: impl Into<String>,
        application_id: impl Into<String>,
        account: impl Into<String>,
    ) -> Result<Self, SecretStoreError> {
        let server_id = server_id.into();
        let application_id = application_id.into();
        let account = account.into();
        validate_identifier(&server_id)?;
        validate_identifier(&application_id)?;
        if account.is_empty()
            || account.len() > 512
            || account.trim() != account
            || account.chars().any(char::is_control)
        {
            return Err(SecretStoreError::InvalidReference);
        }
        Ok(Self {
            kind: SecretReferenceKind::HttpsGitCredential {
                server_id,
                application_id,
                account,
            },
        })
    }

    pub fn account(&self) -> Option<&str> {
        match &self.kind {
            SecretReferenceKind::OpenPgpPassphrase { .. } => None,
            SecretReferenceKind::HttpsGitCredential { account, .. } => Some(account),
        }
    }

    fn locator(&self) -> SecretLocator {
        match &self.kind {
            SecretReferenceKind::OpenPgpPassphrase { fingerprint } => {
                SecretLocator::OpenPgpPassphrase {
                    fingerprint: fingerprint.clone(),
                }
            }
            SecretReferenceKind::HttpsGitCredential {
                server_id,
                application_id,
                ..
            } => SecretLocator::HttpsGitCredential {
                server_id: server_id.clone(),
                application_id: application_id.clone(),
            },
        }
    }
}

impl fmt::Debug for SecretReference {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self {
                kind: SecretReferenceKind::OpenPgpPassphrase { .. },
            } => formatter.write_str("SecretReference::OpenPgpPassphrase([REDACTED])"),
            Self {
                kind: SecretReferenceKind::HttpsGitCredential { .. },
            } => formatter.write_str("SecretReference::HttpsGitCredential([REDACTED])"),
        }
    }
}

/// A backend key. Git accounts live inside the protected record so a configured
/// server/application pair can retrieve its account without TOML metadata.
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub enum SecretLocator {
    OpenPgpPassphrase {
        fingerprint: String,
    },
    HttpsGitCredential {
        server_id: String,
        application_id: String,
    },
}

impl fmt::Debug for SecretLocator {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::OpenPgpPassphrase { .. } => {
                formatter.write_str("SecretLocator::OpenPgpPassphrase([REDACTED])")
            }
            Self::HttpsGitCredential { .. } => {
                formatter.write_str("SecretLocator::HttpsGitCredential([REDACTED])")
            }
        }
    }
}

impl SecretLocator {
    fn service_and_user(&self) -> (&'static str, String) {
        match self {
            Self::OpenPgpPassphrase { fingerprint } => {
                (OPENPGP_PASSPHRASE_SERVICE, fingerprint.clone())
            }
            Self::HttpsGitCredential {
                server_id,
                application_id,
            } => (HTTPS_GIT_SERVICE, format!("{server_id}/{application_id}")),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecretProtection {
    DeviceUnlocked,
    RequireUserPresence,
    BiometryCurrentSet,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SecretProtectionPolicy {
    openpgp: SecretProtection,
    git: SecretProtection,
}

impl SecretProtectionPolicy {
    pub const fn new(openpgp: SecretProtection, git: SecretProtection) -> Self {
        Self { openpgp, git }
    }

    pub const fn device_unlocked() -> Self {
        Self::new(
            SecretProtection::DeviceUnlocked,
            SecretProtection::DeviceUnlocked,
        )
    }

    pub const fn user_presence_for_openpgp() -> Self {
        Self::new(
            SecretProtection::RequireUserPresence,
            SecretProtection::DeviceUnlocked,
        )
    }

    pub const fn current_biometry_for_openpgp() -> Self {
        Self::new(
            SecretProtection::BiometryCurrentSet,
            SecretProtection::DeviceUnlocked,
        )
    }

    fn for_reference(self, reference: &SecretReference) -> SecretProtection {
        match &reference.kind {
            SecretReferenceKind::OpenPgpPassphrase { .. } => self.openpgp,
            SecretReferenceKind::HttpsGitCredential { .. } => self.git,
        }
    }

    fn for_locator(self, locator: &SecretLocator) -> SecretProtection {
        match locator {
            SecretLocator::OpenPgpPassphrase { .. } => self.openpgp,
            SecretLocator::HttpsGitCredential { .. } => self.git,
        }
    }
}

impl Default for SecretProtectionPolicy {
    fn default() -> Self {
        Self::device_unlocked()
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecretCachePolicy {
    Disabled,
    Timed {
        lifetime: Duration,
        capacity: NonZeroUsize,
    },
}

impl SecretCachePolicy {
    pub fn timed(lifetime: Duration, capacity: NonZeroUsize) -> Result<Self, SecretStoreError> {
        if lifetime.is_zero()
            || lifetime > MAX_CACHE_LIFETIME
            || capacity.get() > MAX_CACHE_CAPACITY
        {
            return Err(SecretStoreError::InvalidCachePolicy);
        }
        Ok(Self::Timed { lifetime, capacity })
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SecretStoreError {
    InvalidReference,
    InvalidCachePolicy,
    AlreadyExists,
    Missing,
    Locked,
    Denied,
    Cancelled,
    Corrupted,
    UnsupportedProtection,
    Unavailable,
}

impl fmt::Display for SecretStoreError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = match self {
            Self::InvalidReference => "the secret reference is invalid",
            Self::InvalidCachePolicy => "the secret cache policy is invalid",
            Self::AlreadyExists => "the referenced secret already exists",
            Self::Missing => "the referenced secret does not exist",
            Self::Locked => "the secret store is locked",
            Self::Denied => "access to the secret store was denied",
            Self::Cancelled => "secret-store authentication was cancelled",
            Self::Corrupted => "the stored secret record is corrupted",
            Self::UnsupportedProtection => {
                "the requested secret protection is unsupported on this platform"
            }
            Self::Unavailable => "the operating-system secret store is unavailable",
        };
        formatter.write_str(message)
    }
}

impl Error for SecretStoreError {}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OpenPgpPassphrasePromptError {
    Cancelled,
    Unavailable,
}

/// Collects an OpenPGP passphrase for storage-owned provisioning.
///
/// Implementations are presentation adapters only. Storage decides when a
/// prompt is required and persists the candidate only after the crypto layer
/// confirms that it successfully unlocked the requested key.
pub trait OpenPgpPassphrasePrompt: Send + Sync {
    fn request_passphrase(
        &mut self,
        key: &KeyInfo,
    ) -> Result<SecretBytes, OpenPgpPassphrasePromptError>;
}

/// Mockable contract implemented by each operating-system adapter.
pub trait SecretStoreBackend: Send + Sync {
    fn create(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
        value: &[u8],
    ) -> Result<(), SecretStoreError>;
    fn retrieve(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
    ) -> Result<SecretBytes, SecretStoreError>;
    fn replace(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
        value: &[u8],
    ) -> Result<(), SecretStoreError>;
    fn delete(
        &self,
        locator: &SecretLocator,
        protection: SecretProtection,
    ) -> Result<(), SecretStoreError>;
    fn lock(&self) -> Result<(), SecretStoreError> {
        Ok(())
    }
    fn unlock(&self) -> Result<(), SecretStoreError> {
        Ok(())
    }
}

struct CachedSecret {
    value: SecretBytes,
    expires_at: Instant,
    sequence: u64,
}

struct StoreState {
    unlocked: bool,
    sequence: u64,
    cache: BTreeMap<SecretLocator, CachedSecret>,
    pending_openpgp: Option<PendingOpenPgpPassphrase>,
}

struct PendingOpenPgpPassphrase {
    reference: SecretReference,
    value: SecretBytes,
}

pub struct SecretStore<B> {
    backend: B,
    cache_policy: SecretCachePolicy,
    protections: SecretProtectionPolicy,
    state: Mutex<StoreState>,
    openpgp_prompt: Option<Box<dyn OpenPgpPassphrasePrompt>>,
}

impl<B> fmt::Debug for SecretStore<B> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("SecretStore")
            .field("cache_policy", &self.cache_policy)
            .field("protections", &self.protections)
            .field("contents", &"[REDACTED]")
            .finish()
    }
}

impl<B: SecretStoreBackend> SecretStore<B> {
    pub fn new(
        backend: B,
        cache_policy: SecretCachePolicy,
        protections: SecretProtectionPolicy,
    ) -> Self {
        Self {
            backend,
            cache_policy,
            protections,
            state: Mutex::new(StoreState {
                unlocked: false,
                sequence: 0,
                cache: BTreeMap::new(),
                pending_openpgp: None,
            }),
            openpgp_prompt: None,
        }
    }

    pub fn with_openpgp_passphrase_prompt(
        mut self,
        prompt: impl OpenPgpPassphrasePrompt + 'static,
    ) -> Self {
        self.openpgp_prompt = Some(Box::new(prompt));
        self
    }

    pub fn is_locked(&self) -> bool {
        self.state.lock().map_or(true, |state| !state.unlocked)
    }

    pub fn unlock(&self) -> Result<(), SecretStoreError> {
        let mut state = self
            .state
            .lock()
            .map_err(|_| SecretStoreError::Unavailable)?;
        self.backend.unlock()?;
        state.unlocked = true;
        Ok(())
    }

    pub fn lock(&self) -> Result<(), SecretStoreError> {
        let mut state = self
            .state
            .lock()
            .map_err(|_| SecretStoreError::Unavailable)?;
        state.unlocked = false;
        state.cache.clear();
        state.pending_openpgp = None;
        self.backend.lock()
    }

    pub fn create(
        &self,
        reference: &SecretReference,
        value: SecretBytes,
    ) -> Result<(), SecretStoreError> {
        validate_secret(&value)?;
        let locator = reference.locator();
        let encoded = encode_record(reference, &value)?;
        let mut state = self.unlocked_state()?;
        self.backend.create(
            &locator,
            self.protections.for_reference(reference),
            encoded.expose(),
        )?;
        self.cache_insert(&mut state, locator, encoded);
        Ok(())
    }

    pub fn retrieve(&self, reference: &SecretReference) -> Result<SecretBytes, SecretStoreError> {
        let locator = reference.locator();
        let mut state = self.unlocked_state()?;
        let encoded = match self.cache_get(&mut state, &locator) {
            Some(value) => value,
            None => self
                .backend
                .retrieve(&locator, self.protections.for_reference(reference))?,
        };
        let record = decode_record(encoded)?;
        if &record.reference != reference {
            return Err(SecretStoreError::Missing);
        }
        let output = copy_secret(&record.value);
        self.cache_insert(
            &mut state,
            locator,
            encode_record(&record.reference, &record.value)?,
        );
        Ok(output)
    }

    pub fn replace(
        &self,
        reference: &SecretReference,
        value: SecretBytes,
    ) -> Result<(), SecretStoreError> {
        validate_secret(&value)?;
        let locator = reference.locator();
        let encoded = encode_record(reference, &value)?;
        let mut state = self.unlocked_state()?;
        self.require_exact_record(reference, &locator)?;
        self.backend.replace(
            &locator,
            self.protections.for_reference(reference),
            encoded.expose(),
        )?;
        self.cache_insert(&mut state, locator, encoded);
        Ok(())
    }

    /// Persist a verified OpenPGP passphrase without first reading the old item.
    /// This lets Apple replace an item invalidated by biometric enrollment changes.
    pub(crate) fn store_openpgp_passphrase(
        &self,
        reference: &SecretReference,
        value: SecretBytes,
    ) -> Result<(), SecretStoreError> {
        if !matches!(
            reference.kind,
            SecretReferenceKind::OpenPgpPassphrase { .. }
        ) {
            return Err(SecretStoreError::InvalidReference);
        }
        validate_secret(&value)?;
        let locator = reference.locator();
        let encoded = encode_record(reference, &value)?;
        let mut state = self.unlocked_state()?;
        match self.backend.replace(
            &locator,
            self.protections.for_reference(reference),
            encoded.expose(),
        ) {
            Ok(()) => {}
            Err(SecretStoreError::Missing) => self.backend.create(
                &locator,
                self.protections.for_reference(reference),
                encoded.expose(),
            )?,
            Err(error) => return Err(error),
        }
        self.cache_insert(&mut state, locator, encoded);
        Ok(())
    }

    /// Create or replace one HTTPS Git account/token record without exposing
    /// the previously stored account or token to a frontend.
    pub fn store_https_git_credential(
        &self,
        server: &ServerId,
        application: &ApplicationId,
        account: impl Into<String>,
        value: SecretBytes,
    ) -> Result<(), SecretStoreError> {
        validate_secret(&value)?;
        let reference =
            SecretReference::https_git_credential(server.as_str(), application.as_str(), account)?;
        let locator = reference.locator();
        let encoded = encode_record(&reference, &value)?;
        let mut state = self.unlocked_state()?;
        match self
            .backend
            .retrieve(&locator, self.protections.for_reference(&reference))
        {
            Ok(existing) => {
                let existing = decode_record(existing)?;
                if existing.reference.locator() != locator {
                    return Err(SecretStoreError::Corrupted);
                }
                self.backend.replace(
                    &locator,
                    self.protections.for_reference(&reference),
                    encoded.expose(),
                )?;
            }
            Err(SecretStoreError::Missing) => self.backend.create(
                &locator,
                self.protections.for_reference(&reference),
                encoded.expose(),
            )?,
            Err(error) => return Err(error),
        }
        self.cache_insert(&mut state, locator, encoded);
        Ok(())
    }

    pub fn delete(&self, reference: &SecretReference) -> Result<(), SecretStoreError> {
        let locator = reference.locator();
        let mut state = self.unlocked_state()?;
        self.require_exact_record(reference, &locator)?;
        self.backend
            .delete(&locator, self.protections.for_reference(reference))?;
        state.cache.remove(&locator);
        Ok(())
    }

    pub(crate) fn delete_openpgp_passphrase(
        &self,
        reference: &SecretReference,
    ) -> Result<(), SecretStoreError> {
        if !matches!(
            reference.kind,
            SecretReferenceKind::OpenPgpPassphrase { .. }
        ) {
            return Err(SecretStoreError::InvalidReference);
        }
        let locator = reference.locator();
        let mut state = self.unlocked_state()?;
        self.backend
            .delete(&locator, self.protections.for_reference(reference))?;
        state.cache.remove(&locator);
        Ok(())
    }

    fn retrieve_git_record(
        &self,
        server: &ServerId,
        application: &ApplicationId,
    ) -> Result<SecretRecord, SecretStoreError> {
        let locator = SecretLocator::HttpsGitCredential {
            server_id: server.as_str().to_owned(),
            application_id: application.as_str().to_owned(),
        };
        let mut state = self.unlocked_state()?;
        if let Some(value) = self.cache_get(&mut state, &locator) {
            return decode_record(value);
        }
        let encoded = self
            .backend
            .retrieve(&locator, self.protections.for_locator(&locator))?;
        let record = decode_record(encoded)?;
        if record.reference.locator() != locator {
            return Err(SecretStoreError::Corrupted);
        }
        self.cache_insert(
            &mut state,
            locator,
            encode_record(&record.reference, &record.value)?,
        );
        Ok(record)
    }

    fn require_exact_record(
        &self,
        reference: &SecretReference,
        locator: &SecretLocator,
    ) -> Result<(), SecretStoreError> {
        let encoded = self
            .backend
            .retrieve(locator, self.protections.for_reference(reference))?;
        let record = decode_record(encoded)?;
        if &record.reference != reference {
            return Err(SecretStoreError::Missing);
        }
        Ok(())
    }

    fn unlocked_state(&self) -> Result<std::sync::MutexGuard<'_, StoreState>, SecretStoreError> {
        let state = self
            .state
            .lock()
            .map_err(|_| SecretStoreError::Unavailable)?;
        if !state.unlocked {
            return Err(SecretStoreError::Locked);
        }
        Ok(state)
    }

    fn clear_pending_openpgp(&self, reference: &SecretReference) {
        if let Ok(mut state) = self.state.lock()
            && state
                .pending_openpgp
                .as_ref()
                .is_some_and(|pending| &pending.reference == reference)
        {
            state.pending_openpgp = None;
        }
    }

    fn cache_get(&self, state: &mut StoreState, locator: &SecretLocator) -> Option<SecretBytes> {
        let SecretCachePolicy::Timed { .. } = self.cache_policy else {
            return None;
        };
        let now = Instant::now();
        state.cache.retain(|_, entry| entry.expires_at > now);
        state
            .cache
            .get(locator)
            .map(|entry| copy_secret(&entry.value))
    }

    fn cache_insert(&self, state: &mut StoreState, locator: SecretLocator, value: SecretBytes) {
        let SecretCachePolicy::Timed { lifetime, capacity } = self.cache_policy else {
            return;
        };
        let now = Instant::now();
        state.cache.retain(|_, entry| entry.expires_at > now);
        state.sequence = state.sequence.wrapping_add(1);
        let sequence = state.sequence;
        state.cache.insert(
            locator,
            CachedSecret {
                value,
                expires_at: now + lifetime,
                sequence,
            },
        );
        while state.cache.len() > capacity.get() {
            let oldest = state
                .cache
                .iter()
                .min_by_key(|(_, entry)| entry.sequence)
                .map(|(locator, _)| locator.clone());
            if let Some(oldest) = oldest {
                state.cache.remove(&oldest);
            }
        }
    }
}

pub type NativeSecretStore = SecretStore<NativeSecretBackend>;

impl NativeSecretStore {
    pub fn system(
        cache_policy: SecretCachePolicy,
        protections: SecretProtectionPolicy,
    ) -> Result<Self, SecretStoreError> {
        Ok(Self::new(
            NativeSecretBackend::new()?,
            cache_policy,
            protections,
        ))
    }
}

impl<B: SecretStoreBackend> SecretProvider for SecretStore<B> {
    fn secret_for(&mut self, key: &KeyInfo) -> Result<SecretBytes, SecretProviderError> {
        let reference = SecretReference::openpgp_passphrase(key.fingerprint().as_str())
            .map_err(|_| SecretProviderError::Unavailable)?;
        self.clear_pending_openpgp(&reference);
        match self.retrieve(&reference) {
            Ok(secret) => Ok(secret),
            Err(SecretStoreError::Missing) if self.openpgp_prompt.is_some() => {
                let candidate = self
                    .openpgp_prompt
                    .as_mut()
                    .expect("checked prompt")
                    .request_passphrase(key)
                    .map_err(prompt_error)?;
                validate_secret(&candidate).map_err(|_| SecretProviderError::PersistenceFailed)?;
                let output = copy_secret(&candidate);
                let mut state = self
                    .state
                    .lock()
                    .map_err(|_| SecretProviderError::Unavailable)?;
                if !state.unlocked {
                    return Err(SecretProviderError::Unavailable);
                }
                state.pending_openpgp = Some(PendingOpenPgpPassphrase {
                    reference,
                    value: candidate,
                });
                Ok(output)
            }
            Err(error) => Err(provider_error(error)),
        }
    }

    fn secret_accepted(&mut self, key: &KeyInfo) -> Result<(), SecretProviderError> {
        let reference = SecretReference::openpgp_passphrase(key.fingerprint().as_str())
            .map_err(|_| SecretProviderError::Unavailable)?;
        let pending = {
            let mut state = self
                .state
                .lock()
                .map_err(|_| SecretProviderError::Unavailable)?;
            match state.pending_openpgp.take() {
                Some(pending) if pending.reference == reference => Some(pending),
                Some(pending) => {
                    state.pending_openpgp = Some(pending);
                    None
                }
                None => None,
            }
        };
        if let Some(pending) = pending {
            self.create(&pending.reference, pending.value)
                .map_err(persistence_error)?;
        }
        Ok(())
    }

    fn secret_rejected(&mut self, key: &KeyInfo) {
        if let Ok(reference) = SecretReference::openpgp_passphrase(key.fingerprint().as_str()) {
            self.clear_pending_openpgp(&reference);
        }
    }
}

impl<B: SecretStoreBackend> GitCredentialProvider for SecretStore<B> {
    fn credential(
        &self,
        server: &ServerId,
        application: &ApplicationId,
    ) -> Result<GitCredential, GitError> {
        let record = self
            .retrieve_git_record(server, application)
            .map_err(git_provider_error)?;
        let Some(account) = record.reference.account() else {
            return Err(GitError::CredentialsUnavailable);
        };
        if record.value.expose().contains(&b'\n')
            || record.value.expose().contains(&b'\r')
            || record.value.expose().contains(&0)
        {
            return Err(GitError::CredentialsUnavailable);
        }
        GitCredential::new(account, record.value.expose().to_vec())
    }
}

fn provider_error(error: SecretStoreError) -> SecretProviderError {
    match error {
        SecretStoreError::Missing => SecretProviderError::Missing,
        SecretStoreError::Denied => SecretProviderError::Denied,
        SecretStoreError::Cancelled => SecretProviderError::Cancelled,
        _ => SecretProviderError::Unavailable,
    }
}

fn prompt_error(error: OpenPgpPassphrasePromptError) -> SecretProviderError {
    match error {
        OpenPgpPassphrasePromptError::Cancelled => SecretProviderError::Cancelled,
        OpenPgpPassphrasePromptError::Unavailable => SecretProviderError::Unavailable,
    }
}

fn persistence_error(error: SecretStoreError) -> SecretProviderError {
    match error {
        SecretStoreError::Denied => SecretProviderError::Denied,
        SecretStoreError::Cancelled => SecretProviderError::Cancelled,
        SecretStoreError::Unavailable | SecretStoreError::Locked => {
            SecretProviderError::Unavailable
        }
        _ => SecretProviderError::PersistenceFailed,
    }
}

fn git_provider_error(error: SecretStoreError) -> GitError {
    match error {
        SecretStoreError::Cancelled => GitError::CredentialCancelled,
        SecretStoreError::Denied => GitError::CredentialAccessDenied,
        _ => GitError::CredentialsUnavailable,
    }
}

struct SecretRecord {
    reference: SecretReference,
    value: SecretBytes,
}

fn encode_record(
    reference: &SecretReference,
    value: &SecretBytes,
) -> Result<SecretBytes, SecretStoreError> {
    validate_secret(value)?;
    let mut encoded = Vec::with_capacity(RECORD_MAGIC.len() + value.expose().len() + 1024);
    encoded.extend_from_slice(RECORD_MAGIC);
    encoded.push(RECORD_VERSION);
    match &reference.kind {
        SecretReferenceKind::OpenPgpPassphrase { fingerprint } => {
            encoded.push(1);
            write_field(&mut encoded, fingerprint.as_bytes())?;
        }
        SecretReferenceKind::HttpsGitCredential {
            server_id,
            application_id,
            account,
        } => {
            encoded.push(2);
            write_field(&mut encoded, server_id.as_bytes())?;
            write_field(&mut encoded, application_id.as_bytes())?;
            write_field(&mut encoded, account.as_bytes())?;
        }
    }
    let length = u32::try_from(value.expose().len()).map_err(|_| SecretStoreError::Corrupted)?;
    encoded.extend_from_slice(&length.to_be_bytes());
    encoded.extend_from_slice(value.expose());
    Ok(SecretBytes::new(encoded))
}

fn decode_record(encoded: SecretBytes) -> Result<SecretRecord, SecretStoreError> {
    let bytes = encoded.expose();
    let Some(mut remainder) = bytes.strip_prefix(RECORD_MAGIC) else {
        return Err(SecretStoreError::Corrupted);
    };
    let Some((&version, rest)) = remainder.split_first() else {
        return Err(SecretStoreError::Corrupted);
    };
    if version != RECORD_VERSION {
        return Err(SecretStoreError::Corrupted);
    }
    let Some((&kind, rest)) = rest.split_first() else {
        return Err(SecretStoreError::Corrupted);
    };
    remainder = rest;
    let reference = match kind {
        1 => {
            let (fingerprint, rest) = read_field(remainder)?;
            remainder = rest;
            SecretReference::openpgp_passphrase(read_text(fingerprint)?)
                .map_err(|_| SecretStoreError::Corrupted)?
        }
        2 => {
            let (server_id, rest) = read_field(remainder)?;
            let (application_id, rest) = read_field(rest)?;
            let (account, rest) = read_field(rest)?;
            remainder = rest;
            SecretReference::https_git_credential(
                read_text(server_id)?,
                read_text(application_id)?,
                read_text(account)?,
            )
            .map_err(|_| SecretStoreError::Corrupted)?
        }
        _ => return Err(SecretStoreError::Corrupted),
    };
    if remainder.len() < 4 {
        return Err(SecretStoreError::Corrupted);
    }
    let length = u32::from_be_bytes(
        remainder[..4]
            .try_into()
            .map_err(|_| SecretStoreError::Corrupted)?,
    ) as usize;
    let value = &remainder[4..];
    if length == 0 || length != value.len() || length > MAX_SECRET_BYTES {
        return Err(SecretStoreError::Corrupted);
    }
    Ok(SecretRecord {
        reference,
        value: SecretBytes::new(value.to_vec()),
    })
}

fn write_field(output: &mut Vec<u8>, field: &[u8]) -> Result<(), SecretStoreError> {
    let length = u16::try_from(field.len()).map_err(|_| SecretStoreError::InvalidReference)?;
    output.extend_from_slice(&length.to_be_bytes());
    output.extend_from_slice(field);
    Ok(())
}

fn read_field(input: &[u8]) -> Result<(&[u8], &[u8]), SecretStoreError> {
    if input.len() < 2 {
        return Err(SecretStoreError::Corrupted);
    }
    let length = u16::from_be_bytes(
        input[..2]
            .try_into()
            .map_err(|_| SecretStoreError::Corrupted)?,
    ) as usize;
    if input.len() < 2 + length {
        return Err(SecretStoreError::Corrupted);
    }
    Ok((&input[2..2 + length], &input[2 + length..]))
}

fn read_text(input: &[u8]) -> Result<String, SecretStoreError> {
    std::str::from_utf8(input)
        .map(str::to_owned)
        .map_err(|_| SecretStoreError::Corrupted)
}

fn validate_identifier(value: &str) -> Result<(), SecretStoreError> {
    if value.is_empty()
        || value.len() > 128
        || !value
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
    {
        return Err(SecretStoreError::InvalidReference);
    }
    Ok(())
}

fn validate_secret(value: &SecretBytes) -> Result<(), SecretStoreError> {
    if value.expose().is_empty() || value.expose().len() > MAX_SECRET_BYTES {
        return Err(SecretStoreError::InvalidReference);
    }
    Ok(())
}

fn copy_secret(value: &SecretBytes) -> SecretBytes {
    SecretBytes::new(value.expose().to_vec())
}

#[cfg(test)]
mod tests {
    use super::{
        HTTPS_GIT_SERVICE, OPENPGP_PASSPHRASE_SERVICE, RECORD_MAGIC, RECORD_VERSION, SecretBytes,
        SecretLocator, SecretStoreError, decode_record,
    };

    fn openpgp_record(fingerprint: &[u8], secret: &[u8]) -> SecretBytes {
        let mut record = Vec::new();
        record.extend_from_slice(RECORD_MAGIC);
        record.push(RECORD_VERSION);
        record.push(1);
        record.extend_from_slice(&(fingerprint.len() as u16).to_be_bytes());
        record.extend_from_slice(fingerprint);
        record.extend_from_slice(&(secret.len() as u32).to_be_bytes());
        record.extend_from_slice(secret);
        SecretBytes::new(record)
    }

    #[test]
    fn decoder_classifies_invalid_stored_fields_as_corruption() {
        assert!(matches!(
            decode_record(openpgp_record(b"not-a-fingerprint", b"secret")),
            Err(SecretStoreError::Corrupted)
        ));
        assert!(matches!(
            decode_record(openpgp_record(
                b"0123456789ABCDEF0123456789ABCDEF01234567",
                b""
            )),
            Err(SecretStoreError::Corrupted)
        ));
    }

    #[test]
    fn native_service_names_use_the_canonical_bundle_identifier() {
        let openpgp = SecretLocator::OpenPgpPassphrase {
            fingerprint: "0123456789ABCDEF0123456789ABCDEF01234567".to_owned(),
        };
        let git = SecretLocator::HttpsGitCredential {
            server_id: "server".to_owned(),
            application_id: "application".to_owned(),
        };
        assert_eq!(
            openpgp.service_and_user(),
            (
                OPENPGP_PASSPHRASE_SERVICE,
                "0123456789ABCDEF0123456789ABCDEF01234567".to_owned()
            )
        );
        assert_eq!(
            git.service_and_user(),
            (HTTPS_GIT_SERVICE, "server/application".to_owned())
        );
        assert!(OPENPGP_PASSPHRASE_SERVICE.starts_with("de.rfc1437.ironstorage."));
        assert!(HTTPS_GIT_SERVICE.starts_with("de.rfc1437.ironstorage."));
    }
}
