//! `pass-otp` compatible URI handling, code generation, and repository mutation.

use std::{error::Error, fmt, ops::Range, str, sync::Mutex};

use data_encoding::BASE32_NOPAD;
use hmac::Hmac;
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use zeroize::Zeroize as _;

use crate::{
    command::{OtpAppendRequest, OtpInputSource, OtpInsertRequest},
    crypto::{CryptoError, KeyStore, SecretProvider},
    git::{AutomaticEntryCommitter, GitError, GitIdentity},
    recipient::{RecipientPolicyError, RecipientPolicyManager, SigningPolicy},
    repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes},
    write::{EntryAction, EntryCommit, EntryCommitError, EntryCommitter, OverwriteDecision},
};

const DEFAULT_DIGITS: u32 = 6;
const DEFAULT_PERIOD: u64 = 30;
static OTP_MUTATION_LOCK: Mutex<()> = Mutex::new(());

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpKind {
    Totp,
    Hotp,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpAlgorithm {
    Sha1,
    Sha256,
    Sha512,
}

/// A validated key URI whose encoded and decoded secrets zeroize on drop.
pub struct OtpUri {
    encoded: SecretBytes,
    kind: OtpKind,
    secret: SecretBytes,
    issuer: Option<String>,
    account: String,
    algorithm: OtpAlgorithm,
    digits: u32,
    period: Option<u64>,
    counter: Option<u64>,
    counter_value: Option<Range<usize>>,
}

impl OtpUri {
    pub fn parse(encoded: SecretBytes) -> Result<Self, OtpError> {
        let text = str::from_utf8(encoded.expose()).map_err(|_| OtpError::InvalidUri)?;
        if text.is_empty()
            || text
                .bytes()
                .any(|byte| byte.is_ascii_control() || byte == b' ')
        {
            return Err(OtpError::InvalidUri);
        }
        let remainder = text
            .strip_prefix("otpauth://")
            .ok_or(OtpError::InvalidScheme)?;
        let (authority_and_label, query) = remainder
            .split_once('?')
            .ok_or(OtpError::MissingParameters)?;
        if query.is_empty() || query.contains('#') {
            return Err(OtpError::MissingParameters);
        }
        let (kind_text, raw_label) = authority_and_label
            .split_once('/')
            .ok_or(OtpError::MissingAccount)?;
        let kind = match kind_text {
            "totp" => OtpKind::Totp,
            "hotp" => OtpKind::Hotp,
            _ => return Err(OtpError::UnsupportedType),
        };
        if raw_label.is_empty() || raw_label.contains('/') {
            return Err(OtpError::MissingAccount);
        }
        let label = decode_component(raw_label)?;
        let (label_issuer, account) = split_label(&label)?;

        let query_offset = text.len() - query.len();
        let mut secret = None;
        let mut issuer = None;
        let mut algorithm = None;
        let mut digits = None;
        let mut period = None;
        let mut counter = None;
        let mut counter_value = None;
        let mut offset = 0;
        for parameter in query.split('&') {
            if parameter.is_empty() {
                return Err(OtpError::InvalidParameter);
            }
            let (name, raw_value) = parameter
                .split_once('=')
                .ok_or(OtpError::InvalidParameter)?;
            if name.is_empty() || raw_value.is_empty() {
                return Err(OtpError::InvalidParameter);
            }
            match name {
                "secret" => {
                    set_once(&mut secret, decode_secret(raw_value)?, OtpParameter::Secret)?;
                }
                "issuer" => {
                    let value = decode_component(raw_value)?;
                    if value.is_empty() || value.contains(':') {
                        return Err(OtpError::InvalidIssuer);
                    }
                    set_once(&mut issuer, value, OtpParameter::Issuer)?;
                }
                "algorithm" => {
                    let value = decode_ascii(raw_value)?;
                    let value = match value.to_ascii_uppercase().as_str() {
                        "SHA1" => OtpAlgorithm::Sha1,
                        "SHA256" => OtpAlgorithm::Sha256,
                        "SHA512" => OtpAlgorithm::Sha512,
                        _ => return Err(OtpError::InvalidAlgorithm),
                    };
                    set_once(&mut algorithm, value, OtpParameter::Algorithm)?;
                }
                "digits" => {
                    let value = parse_number(raw_value).ok_or(OtpError::InvalidDigits)?;
                    if !matches!(value, 6 | 8) {
                        return Err(OtpError::InvalidDigits);
                    }
                    set_once(&mut digits, value as u32, OtpParameter::Digits)?;
                }
                "period" => {
                    let value = parse_number(raw_value).ok_or(OtpError::InvalidPeriod)?;
                    if value == 0 {
                        return Err(OtpError::InvalidPeriod);
                    }
                    set_once(&mut period, value, OtpParameter::Period)?;
                }
                "counter" => {
                    let value = parse_number(raw_value).ok_or(OtpError::InvalidCounter)?;
                    set_once(&mut counter, value, OtpParameter::Counter)?;
                    let start = query_offset + offset + name.len() + 1;
                    counter_value = Some(start..start + raw_value.len());
                }
                _ => {}
            }
            offset += parameter.len() + 1;
        }

        let secret = secret.ok_or(OtpError::MissingSecret)?;
        if let (Some(label), Some(parameter)) = (label_issuer.as_ref(), issuer.as_ref())
            && label != parameter
        {
            return Err(OtpError::IssuerMismatch);
        }
        let issuer = issuer.or(label_issuer);
        let algorithm = algorithm.unwrap_or(OtpAlgorithm::Sha1);
        let digits = digits.unwrap_or(DEFAULT_DIGITS);
        let (period, counter, counter_value) = match kind {
            OtpKind::Totp => {
                if counter.is_some() {
                    return Err(OtpError::UnexpectedCounter);
                }
                (Some(period.unwrap_or(DEFAULT_PERIOD)), None, None)
            }
            OtpKind::Hotp => {
                if period.is_some() {
                    return Err(OtpError::UnexpectedPeriod);
                }
                (
                    None,
                    Some(counter.ok_or(OtpError::MissingCounter)?),
                    counter_value,
                )
            }
        };

        Ok(Self {
            encoded,
            kind,
            secret,
            issuer,
            account,
            algorithm,
            digits,
            period,
            counter,
            counter_value,
        })
    }

    pub fn parse_str(encoded: &str) -> Result<Self, OtpError> {
        Self::parse(SecretBytes::new(encoded.as_bytes().to_vec()))
    }

    pub fn from_input(source: &OtpInputSource, input: OtpInput) -> Result<Self, OtpError> {
        match source {
            OtpInputSource::Uri => Self::parse(input.into_secret()),
            OtpInputSource::Secret { issuer, account } => {
                build_secret_uri(input.into_secret(), issuer.as_deref(), account.as_deref())
            }
        }
    }

    pub fn encoded(&self) -> &SecretBytes {
        &self.encoded
    }

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

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

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

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

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

    pub fn period(&self) -> Option<u64> {
        self.period
    }

    pub fn counter(&self) -> Option<u64> {
        self.counter
    }

    pub fn derived_entry(&self) -> Result<EntryPath, OtpError> {
        let path = self.issuer.as_ref().map_or_else(
            || self.account.clone(),
            |issuer| format!("{issuer}/{}", self.account),
        );
        EntryPath::parse(&path).map_err(Into::into)
    }

    pub fn code_at(&self, unix_seconds: u64) -> Result<SecretBytes, OtpError> {
        let period = self.period.ok_or(OtpError::NotTotp)?;
        self.code_for_counter(unix_seconds / period)
    }

    pub fn code_for_counter(&self, counter: u64) -> Result<SecretBytes, OtpError> {
        let message = counter.to_be_bytes();
        let mut digest = match self.algorithm {
            OtpAlgorithm::Sha1 => hmac_digest::<Hmac<Sha1>>(self.secret.expose(), &message)?,
            OtpAlgorithm::Sha256 => hmac_digest::<Hmac<Sha256>>(self.secret.expose(), &message)?,
            OtpAlgorithm::Sha512 => hmac_digest::<Hmac<Sha512>>(self.secret.expose(), &message)?,
        };
        let offset = usize::from(digest[digest.len() - 1] & 0x0f);
        let binary = (u32::from(digest[offset]) & 0x7f) << 24
            | u32::from(digest[offset + 1]) << 16
            | u32::from(digest[offset + 2]) << 8
            | u32::from(digest[offset + 3]);
        digest.zeroize();
        let modulus = 10_u32.pow(self.digits);
        let code = format!("{:0width$}", binary % modulus, width = self.digits as usize);
        Ok(SecretBytes::new(code.into_bytes()))
    }

    fn incremented_hotp(&self) -> Result<(u64, Self), OtpError> {
        let counter = self.counter.ok_or(OtpError::NotHotp)?;
        let incremented = counter.checked_add(1).ok_or(OtpError::CounterOverflow)?;
        let range = self.counter_value.clone().ok_or(OtpError::MissingCounter)?;
        let mut encoded = Vec::with_capacity(self.encoded.expose().len() + 1);
        encoded.extend_from_slice(&self.encoded.expose()[..range.start]);
        encoded.extend_from_slice(incremented.to_string().as_bytes());
        encoded.extend_from_slice(&self.encoded.expose()[range.end..]);
        Ok((incremented, Self::parse(SecretBytes::new(encoded))?))
    }
}

impl fmt::Debug for OtpUri {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("OtpUri")
            .field("encoded", &"[REDACTED]")
            .field("kind", &self.kind)
            .field("secret", &"[REDACTED]")
            .field("issuer", &self.issuer)
            .field("account", &self.account)
            .field("algorithm", &self.algorithm)
            .field("digits", &self.digits)
            .field("period", &self.period)
            .field("counter", &self.counter)
            .finish()
    }
}

pub struct OtpInput {
    secret: SecretBytes,
}

impl OtpInput {
    pub fn hidden(mut first: Vec<u8>, mut confirmation: Vec<u8>) -> Result<Self, OtpError> {
        if let Err(error) = validate_input(&first) {
            first.zeroize();
            confirmation.zeroize();
            return Err(error);
        }
        if let Err(error) = validate_input(&confirmation) {
            first.zeroize();
            confirmation.zeroize();
            return Err(error);
        }
        if first != confirmation {
            first.zeroize();
            confirmation.zeroize();
            return Err(OtpError::ConfirmationMismatch);
        }
        confirmation.zeroize();
        Ok(Self {
            secret: SecretBytes::new(first),
        })
    }

    pub fn line(mut line: Vec<u8>) -> Result<Self, OtpError> {
        if let Err(error) = validate_input(&line) {
            line.zeroize();
            return Err(error);
        }
        Ok(Self {
            secret: SecretBytes::new(line),
        })
    }

    fn into_secret(self) -> SecretBytes {
        self.secret
    }
}

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

pub struct OtpInsertPlan {
    path: EntryPath,
    uri: OtpUri,
    original: Option<EncryptedEntry>,
    force: bool,
    confirm_path: bool,
}

impl OtpInsertPlan {
    pub fn path(&self) -> &EntryPath {
        &self.path
    }

    pub fn requires_path_confirmation(&self) -> bool {
        self.confirm_path
    }

    pub fn requires_overwrite_confirmation(&self) -> bool {
        self.original.is_some() && !self.force
    }
}

impl fmt::Debug for OtpInsertPlan {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("OtpInsertPlan")
            .field("path", &self.path)
            .field("uri", &self.uri)
            .field("has_original", &self.original.is_some())
            .field("force", &self.force)
            .field("confirm_path", &self.confirm_path)
            .finish()
    }
}

pub struct OtpAppendSession {
    path: EntryPath,
    original: EncryptedEntry,
    plaintext: SecretBytes,
    existing: Option<Range<usize>>,
    source: OtpInputSource,
    force: bool,
}

impl OtpAppendSession {
    pub fn path(&self) -> &EntryPath {
        &self.path
    }

    pub fn requires_replace_confirmation(&self) -> bool {
        self.existing.is_some() && !self.force
    }
}

impl fmt::Debug for OtpAppendSession {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("OtpAppendSession")
            .field("path", &self.path)
            .field("plaintext", &"[REDACTED]")
            .field("has_uri", &self.existing.is_some())
            .field("source", &self.source)
            .field("force", &self.force)
            .finish()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OtpWriteOutcome {
    path: EntryPath,
    replaced: bool,
}

impl OtpWriteOutcome {
    pub fn path(&self) -> &EntryPath {
        &self.path
    }

    pub fn replaced(&self) -> bool {
        self.replaced
    }
}

pub struct OtpCodeOutcome {
    code: SecretBytes,
    validity: OtpCodeValidity,
}

/// Storage-owned validity information for presenting an OTP code.
///
/// Frontends use this value instead of deriving TOTP periods or inferring HOTP
/// behavior from display strings. A timed code carries its exclusive Unix-time
/// boundary and complete period for progress presentation, while a
/// counter-based code identifies the committed HOTP counter.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpCodeValidity {
    Timed { valid_until: u64, period: u64 },
    CounterBased { counter: u64 },
}

impl OtpCodeValidity {
    pub fn valid_until(self) -> Option<u64> {
        match self {
            Self::Timed { valid_until, .. } => Some(valid_until),
            Self::CounterBased { .. } => None,
        }
    }

    pub fn counter(self) -> Option<u64> {
        match self {
            Self::Timed { .. } => None,
            Self::CounterBased { counter } => Some(counter),
        }
    }

    pub fn period(self) -> Option<u64> {
        match self {
            Self::Timed { period, .. } => Some(period),
            Self::CounterBased { .. } => None,
        }
    }

    pub fn remaining_at(self, unix_seconds: u64) -> Option<u64> {
        self.valid_until()
            .map(|valid_until| valid_until.saturating_sub(unix_seconds))
    }
}

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

    pub fn counter(&self) -> Option<u64> {
        self.validity.counter()
    }

    pub fn valid_until(&self) -> Option<u64> {
        self.validity.valid_until()
    }

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

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

impl fmt::Debug for OtpCodeOutcome {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("OtpCodeOutcome")
            .field("code", &"[REDACTED]")
            .field("validity", &self.validity)
            .finish()
    }
}

pub struct OtpService<'a> {
    repository: &'a Repository,
    keys: &'a KeyStore,
}

impl<'a> OtpService<'a> {
    pub fn new(repository: &'a Repository, keys: &'a KeyStore) -> Self {
        Self { repository, keys }
    }

    pub fn validate(encoded: &str) -> Result<(), OtpError> {
        OtpUri::parse_str(encoded).map(|_| ())
    }

    pub fn validate_input(input: OtpInput) -> Result<(), OtpError> {
        OtpUri::parse(input.into_secret()).map(|_| ())
    }

    pub fn prepare_insert(
        &self,
        request: &OtpInsertRequest,
        input: OtpInput,
    ) -> Result<OtpInsertPlan, OtpError> {
        let uri = OtpUri::from_input(&request.source, input)?;
        let confirm_path = request.entry.is_none();
        let path = request
            .entry
            .as_deref()
            .map(parse_entry)
            .transpose()?
            .map_or_else(|| uri.derived_entry(), Ok)?;
        let original = match self.repository.read_entry(&path) {
            Ok(original) => Some(original),
            Err(RepositoryError::NotFound { .. }) => None,
            Err(error) => return Err(error.into()),
        };
        Ok(OtpInsertPlan {
            path,
            uri,
            original,
            force: request.force,
            confirm_path,
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn finish_insert(
        &self,
        plan: OtpInsertPlan,
        path_decision: OverwriteDecision,
        overwrite: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<OtpWriteOutcome, OtpError> {
        if plan.confirm_path && path_decision == OverwriteDecision::Decline
            || plan.original.is_some() && !plan.force && overwrite == OverwriteDecision::Decline
        {
            return Err(OtpError::Cancelled);
        }
        let mut plaintext = Vec::with_capacity(plan.uri.encoded().expose().len() + 1);
        plaintext.extend_from_slice(plan.uri.encoded().expose());
        plaintext.push(b'\n');
        let replaced = plan.original.is_some();
        self.store(
            &plan.path,
            SecretBytes::new(plaintext),
            plan.original.as_ref(),
            EntryAction::Insert,
            format!("Add OTP secret for {} to store.", plan.path),
            signing,
            committer,
        )?;
        Ok(OtpWriteOutcome {
            path: plan.path,
            replaced,
        })
    }

    pub fn begin_append(
        &self,
        request: &OtpAppendRequest,
        provider: &mut impl SecretProvider,
    ) -> Result<OtpAppendSession, OtpError> {
        let path = parse_entry(&request.entry)?;
        let original = self.repository.read_entry(&path)?;
        let plaintext = self.keys.decrypt(&original, provider)?;
        let existing = find_uri(&plaintext, &path)?.map(|(range, _)| range);
        Ok(OtpAppendSession {
            path,
            original,
            plaintext,
            existing,
            source: request.source.clone(),
            force: request.force,
        })
    }

    pub fn finish_append(
        &self,
        session: OtpAppendSession,
        input: OtpInput,
        replace: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<OtpWriteOutcome, OtpError> {
        if session.existing.is_some() && !session.force && replace == OverwriteDecision::Decline {
            return Err(OtpError::Cancelled);
        }
        let uri = OtpUri::from_input(&session.source, input)?;
        let (replacement, replaced, message) = if let Some(range) = &session.existing {
            let mut replacement = Vec::with_capacity(
                session.plaintext.expose().len() - range.len() + uri.encoded().expose().len(),
            );
            replacement.extend_from_slice(&session.plaintext.expose()[..range.start]);
            replacement.extend_from_slice(uri.encoded().expose());
            replacement.extend_from_slice(&session.plaintext.expose()[range.end..]);
            (
                replacement,
                true,
                format!("Replace OTP secret for {}.", session.path),
            )
        } else {
            let mut replacement = Vec::with_capacity(
                session.plaintext.expose().len() + uri.encoded().expose().len() + 2,
            );
            replacement.extend_from_slice(session.plaintext.expose());
            if !replacement.is_empty() && !replacement.ends_with(b"\n") {
                replacement.push(b'\n');
            }
            replacement.extend_from_slice(uri.encoded().expose());
            replacement.push(b'\n');
            (
                replacement,
                false,
                format!("Append OTP secret for {}.", session.path),
            )
        };
        self.store(
            &session.path,
            SecretBytes::new(replacement),
            Some(&session.original),
            EntryAction::Edit,
            message,
            signing,
            committer,
        )?;
        Ok(OtpWriteOutcome {
            path: session.path,
            replaced,
        })
    }

    pub fn uri(&self, entry: &str, provider: &mut impl SecretProvider) -> Result<OtpUri, OtpError> {
        let path = parse_entry(entry)?;
        let ciphertext = self.repository.read_entry(&path)?;
        self.uri_encrypted(&path, &ciphertext, provider)
    }

    pub(crate) fn uri_encrypted(
        &self,
        path: &EntryPath,
        ciphertext: &EncryptedEntry,
        provider: &mut impl SecretProvider,
    ) -> Result<OtpUri, OtpError> {
        let plaintext = self.keys.decrypt(ciphertext, provider)?;
        find_uri(&plaintext, path)?
            .map(|(_, uri)| uri)
            .ok_or_else(|| OtpError::MissingUri {
                entry: path.clone(),
            })
    }

    pub fn code(
        &self,
        entry: &str,
        unix_seconds: u64,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl EntryCommitter,
    ) -> Result<OtpCodeOutcome, OtpError> {
        let (path, original, plaintext, range, uri) = self.load_code_entry(entry, provider)?;
        self.finish_code(
            path,
            original,
            plaintext,
            range,
            uri,
            unix_seconds,
            signing,
            committer,
        )
    }

    /// Generate a code with storage-owned lazy Git selection. TOTP is
    /// read-only and never opens Git; HOTP opens the innermost repository only
    /// after the token has been decrypted and identified as counter based.
    pub fn code_automatic(
        &self,
        entry: &str,
        unix_seconds: u64,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
    ) -> Result<OtpCodeOutcome, OtpError> {
        let (path, original, plaintext, range, uri) = self.load_code_entry(entry, provider)?;
        if uri.kind() == OtpKind::Totp {
            let period = uri.period().ok_or(OtpError::NotTotp)?;
            return Ok(OtpCodeOutcome {
                code: uri.code_at(unix_seconds)?,
                validity: OtpCodeValidity::Timed {
                    valid_until: (unix_seconds / period)
                        .checked_add(1)
                        .and_then(|counter| counter.checked_mul(period))
                        .ok_or(OtpError::CounterOverflow)?,
                    period,
                },
            });
        }
        let entry = path.to_string();
        let mut committer = AutomaticEntryCommitter::for_entry(
            self.repository,
            &entry,
            GitIdentity::ironstorage(),
        )?;
        self.finish_code(
            path,
            original,
            plaintext,
            range,
            uri,
            unix_seconds,
            signing,
            &mut committer,
        )
    }

    fn load_code_entry(
        &self,
        entry: &str,
        provider: &mut impl SecretProvider,
    ) -> Result<(EntryPath, EncryptedEntry, SecretBytes, Range<usize>, OtpUri), OtpError> {
        let path = parse_entry(entry)?;
        let original = self.repository.read_entry(&path)?;
        let plaintext = self.keys.decrypt(&original, provider)?;
        let (range, uri) = find_uri(&plaintext, &path)?.ok_or_else(|| OtpError::MissingUri {
            entry: path.clone(),
        })?;
        Ok((path, original, plaintext, range, uri))
    }

    #[allow(clippy::too_many_arguments)]
    fn finish_code(
        &self,
        path: EntryPath,
        original: EncryptedEntry,
        plaintext: SecretBytes,
        range: Range<usize>,
        uri: OtpUri,
        unix_seconds: u64,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<OtpCodeOutcome, OtpError> {
        match uri.kind() {
            OtpKind::Totp => {
                let period = uri.period().ok_or(OtpError::NotTotp)?;
                Ok(OtpCodeOutcome {
                    code: uri.code_at(unix_seconds)?,
                    validity: OtpCodeValidity::Timed {
                        valid_until: {
                            (unix_seconds / period)
                                .checked_add(1)
                                .and_then(|counter| counter.checked_mul(period))
                                .ok_or(OtpError::CounterOverflow)?
                        },
                        period,
                    },
                })
            }
            OtpKind::Hotp => {
                let (counter, incremented) = uri.incremented_hotp()?;
                let code = incremented.code_for_counter(counter)?;
                let mut replacement = Vec::with_capacity(
                    plaintext.expose().len() - range.len() + incremented.encoded().expose().len(),
                );
                replacement.extend_from_slice(&plaintext.expose()[..range.start]);
                replacement.extend_from_slice(incremented.encoded().expose());
                replacement.extend_from_slice(&plaintext.expose()[range.end..]);
                self.store(
                    &path,
                    SecretBytes::new(replacement),
                    Some(&original),
                    EntryAction::Edit,
                    format!("Increment HOTP counter for {path}."),
                    signing,
                    committer,
                )?;
                Ok(OtpCodeOutcome {
                    code,
                    validity: OtpCodeValidity::CounterBased { counter },
                })
            }
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn store(
        &self,
        path: &EntryPath,
        plaintext: SecretBytes,
        original: Option<&EncryptedEntry>,
        action: EntryAction,
        message: String,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<(), OtpError> {
        let _mutation = OTP_MUTATION_LOCK
            .lock()
            .map_err(|_| OtpError::MutationLockUnavailable)?;
        let current = match self.repository.read_entry(path) {
            Ok(current) => Some(current),
            Err(RepositoryError::NotFound { .. }) => None,
            Err(error) => return Err(error.into()),
        };
        if current.as_ref() != original {
            return Err(OtpError::ConcurrentModification {
                entry: path.clone(),
            });
        }
        let recipients = RecipientPolicyManager::new(self.repository, self.keys)
            .resolve_for_entry(path, signing)?;
        let ciphertext = self.keys.encrypt(plaintext, recipients.recipients())?;
        self.repository.write_entry(path, &ciphertext)?;
        let change = EntryCommit::new(path.clone(), action, message);
        if let Err(operation) = committer.commit(&change) {
            if let Err(rollback) = self.restore(path, original) {
                return Err(OtpError::RollbackFailed {
                    operation,
                    rollback,
                });
            }
            return Err(OtpError::Commit(operation));
        }
        Ok(())
    }

    fn restore(
        &self,
        path: &EntryPath,
        original: Option<&EncryptedEntry>,
    ) -> Result<(), RepositoryError> {
        if let Some(original) = original {
            self.repository.write_entry(path, original)
        } else {
            self.repository.remove_entry(path)?;
            self.repository
                .cleanup_empty_directories(&path.parent_directory())
                .map(|_| ())
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OtpParameter {
    Secret,
    Issuer,
    Algorithm,
    Digits,
    Period,
    Counter,
}

#[derive(Debug)]
pub enum OtpError {
    Repository(RepositoryError),
    Crypto(CryptoError),
    RecipientPolicy(RecipientPolicyError),
    Git(GitError),
    InvalidUri,
    InvalidScheme,
    UnsupportedType,
    MissingParameters,
    MissingAccount,
    InvalidLabel,
    InvalidIssuer,
    MissingSecret,
    InvalidSecret,
    InvalidParameter,
    DuplicateParameter(OtpParameter),
    IssuerMismatch,
    InvalidAlgorithm,
    InvalidDigits,
    InvalidPeriod,
    InvalidCounter,
    MissingCounter,
    UnexpectedCounter,
    UnexpectedPeriod,
    NotTotp,
    NotHotp,
    CounterOverflow,
    EmptyInput,
    InvalidInput,
    ConfirmationMismatch,
    MissingUri {
        entry: EntryPath,
    },
    AmbiguousUri {
        entry: EntryPath,
    },
    Cancelled,
    MutationLockUnavailable,
    ConcurrentModification {
        entry: EntryPath,
    },
    Commit(EntryCommitError),
    RollbackFailed {
        operation: EntryCommitError,
        rollback: RepositoryError,
    },
}

impl fmt::Display for OtpError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Repository(error) => error.fmt(formatter),
            Self::Crypto(error) => error.fmt(formatter),
            Self::RecipientPolicy(error) => error.fmt(formatter),
            Self::Git(error) => error.fmt(formatter),
            Self::InvalidUri => formatter.write_str("OTP key URI is not valid UTF-8 URI text"),
            Self::InvalidScheme => formatter.write_str("OTP key URI must use the otpauth scheme"),
            Self::UnsupportedType => formatter.write_str("OTP key URI type must be totp or hotp"),
            Self::MissingParameters => formatter.write_str("OTP key URI parameters are missing"),
            Self::MissingAccount => formatter.write_str("OTP key URI account is missing"),
            Self::InvalidLabel => formatter.write_str("OTP key URI label is invalid"),
            Self::InvalidIssuer => formatter.write_str("OTP key URI issuer is invalid"),
            Self::MissingSecret => formatter.write_str("OTP key URI secret is missing"),
            Self::InvalidSecret => formatter.write_str("OTP key URI secret is not valid Base32"),
            Self::InvalidParameter => formatter.write_str("OTP key URI parameter is invalid"),
            Self::DuplicateParameter(parameter) => {
                write!(
                    formatter,
                    "OTP key URI has a duplicate {parameter:?} parameter"
                )
            }
            Self::IssuerMismatch => {
                formatter.write_str("OTP key URI label and parameter issuers do not match")
            }
            Self::InvalidAlgorithm => formatter.write_str("OTP algorithm is invalid"),
            Self::InvalidDigits => formatter.write_str("OTP digit count must be 6 or 8"),
            Self::InvalidPeriod => formatter.write_str("TOTP period must be a positive integer"),
            Self::InvalidCounter => formatter.write_str("HOTP counter must be an integer"),
            Self::MissingCounter => formatter.write_str("HOTP counter is missing"),
            Self::UnexpectedCounter => formatter.write_str("TOTP URI cannot contain a counter"),
            Self::UnexpectedPeriod => formatter.write_str("HOTP URI cannot contain a period"),
            Self::NotTotp => formatter.write_str("the OTP token is not time based"),
            Self::NotHotp => formatter.write_str("the OTP token is not counter based"),
            Self::CounterOverflow => formatter.write_str("HOTP counter cannot be incremented"),
            Self::EmptyInput => formatter.write_str("OTP input may not be empty"),
            Self::InvalidInput => formatter.write_str("OTP input must be a single line"),
            Self::ConfirmationMismatch => {
                formatter.write_str("OTP input confirmation does not match")
            }
            Self::MissingUri { entry } => write!(formatter, "OTP key URI not found in {entry}"),
            Self::AmbiguousUri { entry } => {
                write!(formatter, "multiple OTP key URIs found in {entry}")
            }
            Self::Cancelled => formatter.write_str("OTP mutation was declined"),
            Self::MutationLockUnavailable => {
                formatter.write_str("OTP mutation serialization is unavailable")
            }
            Self::ConcurrentModification { entry } => {
                write!(formatter, "OTP entry changed concurrently: {entry}")
            }
            Self::Commit(error) => write!(formatter, "cannot commit OTP mutation: {error}"),
            Self::RollbackFailed {
                operation,
                rollback,
            } => write!(
                formatter,
                "OTP commit failed ({operation}) and repository rollback failed ({rollback})"
            ),
        }
    }
}

impl Error for OtpError {}

impl From<RepositoryError> for OtpError {
    fn from(error: RepositoryError) -> Self {
        Self::Repository(error)
    }
}

impl From<CryptoError> for OtpError {
    fn from(error: CryptoError) -> Self {
        Self::Crypto(error)
    }
}

impl From<RecipientPolicyError> for OtpError {
    fn from(error: RecipientPolicyError) -> Self {
        Self::RecipientPolicy(error)
    }
}

impl From<GitError> for OtpError {
    fn from(error: GitError) -> Self {
        Self::Git(error)
    }
}

fn validate_input(input: &[u8]) -> Result<(), OtpError> {
    if input.is_empty() {
        Err(OtpError::EmptyInput)
    } else if input.iter().any(|byte| matches!(byte, b'\n' | b'\r')) {
        Err(OtpError::InvalidInput)
    } else {
        Ok(())
    }
}

fn parse_entry(input: &str) -> Result<EntryPath, OtpError> {
    EntryPath::parse(input.trim_end_matches('/')).map_err(Into::into)
}

fn set_once<T>(
    destination: &mut Option<T>,
    value: T,
    parameter: OtpParameter,
) -> Result<(), OtpError> {
    if destination.replace(value).is_some() {
        Err(OtpError::DuplicateParameter(parameter))
    } else {
        Ok(())
    }
}

fn split_label(label: &str) -> Result<(Option<String>, String), OtpError> {
    if label.is_empty() || label.chars().any(char::is_control) {
        return Err(OtpError::InvalidLabel);
    }
    if let Some((issuer, account)) = label.split_once(':') {
        let account = account.trim_start_matches(' ');
        if issuer.is_empty() || account.is_empty() || account.contains(':') {
            return Err(OtpError::InvalidLabel);
        }
        Ok((Some(issuer.to_owned()), account.to_owned()))
    } else {
        Ok((None, label.to_owned()))
    }
}

fn decode_ascii(value: &str) -> Result<String, OtpError> {
    let decoded = decode_component(value)?;
    if decoded.is_ascii() {
        Ok(decoded)
    } else {
        Err(OtpError::InvalidParameter)
    }
}

fn decode_component(value: &str) -> Result<String, OtpError> {
    let bytes = decode_component_bytes(value)?;
    let decoded = String::from_utf8(bytes).map_err(|_| OtpError::InvalidParameter)?;
    if decoded.chars().any(char::is_control) {
        Err(OtpError::InvalidParameter)
    } else {
        Ok(decoded)
    }
}

fn decode_component_bytes(value: &str) -> Result<Vec<u8>, OtpError> {
    let bytes = value.as_bytes();
    let mut decoded = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        match bytes[index] {
            b'%' => {
                let high = bytes.get(index + 1).and_then(|byte| hex(*byte));
                let low = bytes.get(index + 2).and_then(|byte| hex(*byte));
                let (Some(high), Some(low)) = (high, low) else {
                    decoded.zeroize();
                    return Err(OtpError::InvalidParameter);
                };
                decoded.push(high << 4 | low);
                index += 3;
            }
            b'+' => {
                decoded.push(b' ');
                index += 1;
            }
            byte => {
                decoded.push(byte);
                index += 1;
            }
        }
    }
    Ok(decoded)
}

fn hex(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => Some(byte - b'0'),
        b'a'..=b'f' => Some(byte - b'a' + 10),
        b'A'..=b'F' => Some(byte - b'A' + 10),
        _ => None,
    }
}

fn decode_secret(raw: &str) -> Result<SecretBytes, OtpError> {
    let mut encoded = decode_component_bytes(raw)?;
    for byte in &mut encoded {
        byte.make_ascii_uppercase();
    }
    if let Some(padding_start) = encoded.iter().position(|byte| *byte == b'=') {
        let padding = encoded.len() - padding_start;
        let expected = match padding_start % 8 {
            2 => 6,
            4 => 4,
            5 => 3,
            7 => 1,
            _ => 0,
        };
        if encoded[padding_start..].iter().any(|byte| *byte != b'=')
            || encoded.len() % 8 != 0
            || padding != expected
        {
            encoded.zeroize();
            return Err(OtpError::InvalidSecret);
        }
        encoded.truncate(padding_start);
    }
    if encoded.is_empty() {
        encoded.zeroize();
        return Err(OtpError::InvalidSecret);
    }
    let decoded = BASE32_NOPAD
        .decode(&encoded)
        .map_err(|_| OtpError::InvalidSecret);
    encoded.zeroize();
    let decoded = decoded?;
    if decoded.is_empty() {
        Err(OtpError::InvalidSecret)
    } else {
        Ok(SecretBytes::new(decoded))
    }
}

fn parse_number(value: &str) -> Option<u64> {
    if value.bytes().all(|byte| byte.is_ascii_digit()) {
        value.parse().ok()
    } else {
        None
    }
}

fn build_secret_uri(
    secret: SecretBytes,
    issuer: Option<&str>,
    account: Option<&str>,
) -> Result<OtpUri, OtpError> {
    let secret_text = str::from_utf8(secret.expose()).map_err(|_| OtpError::InvalidSecret)?;
    decode_secret(secret_text)?;
    let issuer = issuer.filter(|value| !value.is_empty());
    let account = account.filter(|value| !value.is_empty());
    if issuer.is_none() && account.is_none() {
        return Err(OtpError::MissingAccount);
    }
    let encoded_issuer = issuer.map(percent_encode);
    let encoded_account = account.map(percent_encode);
    let label = match (&encoded_issuer, &encoded_account) {
        (Some(issuer), Some(account)) => format!("{issuer}:{account}"),
        (Some(issuer), None) => issuer.clone(),
        (None, Some(account)) => account.clone(),
        (None, None) => unreachable!("an issuer or account was required"),
    };
    let mut uri = format!("otpauth://totp/{label}?secret={secret_text}");
    if let Some(issuer) = encoded_issuer {
        uri.push_str("&issuer=");
        uri.push_str(&issuer);
    }
    OtpUri::parse(SecretBytes::new(uri.into_bytes()))
}

fn percent_encode(value: &str) -> String {
    let mut encoded = String::new();
    for byte in value.as_bytes() {
        match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'.' | b'~' | b'_' | b'-' => {
                encoded.push(char::from(*byte));
            }
            b' ' => encoded.push('+'),
            byte => {
                use fmt::Write as _;
                write!(encoded, "%{byte:02X}").expect("writing to String cannot fail");
            }
        }
    }
    encoded
}

fn hmac_digest<M>(key: &[u8], message: &[u8]) -> Result<Vec<u8>, OtpError>
where
    M: hmac::digest::Mac + hmac::digest::KeyInit,
{
    let mut mac =
        <M as hmac::digest::Mac>::new_from_slice(key).map_err(|_| OtpError::InvalidSecret)?;
    mac.update(message);
    let mut output = mac.finalize().into_bytes();
    let digest = output.to_vec();
    output.fill(0);
    Ok(digest)
}

pub(crate) fn find_uri(
    plaintext: &SecretBytes,
    entry: &EntryPath,
) -> Result<Option<(Range<usize>, OtpUri)>, OtpError> {
    let mut found = None;
    let mut start = 0;
    while start < plaintext.expose().len() {
        let tail = &plaintext.expose()[start..];
        let line_length = tail
            .iter()
            .position(|byte| *byte == b'\n')
            .unwrap_or(tail.len());
        let end = start + line_length;
        let line = &plaintext.expose()[start..end];
        if line.starts_with(b"otpauth://") {
            let uri = OtpUri::parse(SecretBytes::new(line.to_vec()))?;
            if found.is_some() {
                return Err(OtpError::AmbiguousUri {
                    entry: entry.clone(),
                });
            }
            found = Some((start..end, uri));
        }
        if end == plaintext.expose().len() {
            break;
        }
        start = end + 1;
    }
    Ok(found)
}
