//! Unbiased cryptographic password generation and storage orchestration.

use std::{collections::BTreeSet, error::Error, fmt, num::NonZeroUsize};

use rand::{CryptoRng, RngCore, rngs::OsRng};

use crate::{
    command::{GenerateRequest, GeneratedPresentation},
    crypto::{CryptoError, KeyStore, SecretProvider},
    recipient::SigningPolicy,
    repository::{EntryPath, Repository, RepositoryError, SecretBytes},
    write::{EntryCommitter, OverwriteDecision, VaultWriter, WriteError, WriteOutcome},
};

pub const DEFAULT_PASSWORD_LENGTH: usize = 25;
pub const MAX_PASSWORD_LENGTH: usize = 4096;
pub const DEFAULT_CHARACTER_SET: &str = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
pub const ALPHANUMERIC_CHARACTER_SET: &str =
    "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GeneratorConfig {
    default_length: NonZeroUsize,
    character_set: Vec<char>,
    alphanumeric_set: Vec<char>,
}

impl GeneratorConfig {
    pub fn new(default_length: usize, character_set: &str) -> Result<Self, GenerateError> {
        Ok(Self {
            default_length: validate_length(default_length)?,
            character_set: validate_character_set(character_set)?,
            alphanumeric_set: validate_character_set(ALPHANUMERIC_CHARACTER_SET)?,
        })
    }

    pub fn pass_defaults() -> Self {
        Self::new(DEFAULT_PASSWORD_LENGTH, DEFAULT_CHARACTER_SET)
            .expect("built-in generator defaults are valid")
    }

    pub fn default_length(&self) -> NonZeroUsize {
        self.default_length
    }

    pub fn character_set(&self) -> &[char] {
        &self.character_set
    }

    /// Generate a zeroizing password for an in-process structured editor
    /// without mutating a repository.
    pub fn generate_secret(
        &self,
        length: Option<NonZeroUsize>,
        no_symbols: bool,
    ) -> Result<SecretBytes, GenerateError> {
        self.generate_secret_with_rng(length, no_symbols, &mut OsRng)
    }

    fn generate_secret_with_rng<R: RngCore + CryptoRng>(
        &self,
        length: Option<NonZeroUsize>,
        no_symbols: bool,
        rng: &mut R,
    ) -> Result<SecretBytes, GenerateError> {
        let length = validate_length(length.unwrap_or(self.default_length).get())?;
        let characters = if no_symbols {
            &self.alphanumeric_set
        } else {
            &self.character_set
        };
        generate_password(rng, length, characters)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GeneratedChannel {
    Terminal,
    Clipboard,
    QrCode,
}

pub struct GenerateOutcome {
    password: SecretBytes,
    channel: GeneratedChannel,
    write: WriteOutcome,
}

impl GenerateOutcome {
    pub fn password(&self) -> &SecretBytes {
        &self.password
    }

    pub fn channel(&self) -> GeneratedChannel {
        self.channel
    }

    pub fn write(&self) -> &WriteOutcome {
        &self.write
    }
}

impl fmt::Debug for GenerateOutcome {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("GenerateOutcome")
            .field("password", &self.password)
            .field("channel", &self.channel)
            .field("write", &self.write)
            .finish()
    }
}

pub struct PasswordGenerator<'a> {
    repository: &'a Repository,
    keys: &'a KeyStore,
    config: GeneratorConfig,
}

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

    #[allow(clippy::too_many_arguments)]
    pub fn generate(
        &self,
        request: &GenerateRequest,
        overwrite: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl EntryCommitter,
    ) -> Result<GenerateOutcome, GenerateError> {
        self.generate_with_rng(request, overwrite, signing, provider, committer, &mut OsRng)
    }

    #[allow(clippy::too_many_arguments)]
    pub fn generate_with_rng<R: RngCore + CryptoRng>(
        &self,
        request: &GenerateRequest,
        overwrite: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl EntryCommitter,
        rng: &mut R,
    ) -> Result<GenerateOutcome, GenerateError> {
        if request.force && request.in_place {
            return Err(GenerateError::IncompatibleFlags);
        }
        let password =
            self.config
                .generate_secret_with_rng(request.length, request.no_symbols, rng)?;
        let path = EntryPath::parse(&request.entry)?;
        let contents = if request.in_place {
            let ciphertext = self.repository.read_entry(&path)?;
            let existing = self.keys.decrypt(&ciphertext, provider)?;
            replace_first_line(&password, &existing)
        } else {
            SecretBytes::new(password.expose().to_vec())
        };
        let write = VaultWriter::new(self.repository, self.keys).store_generated(
            &path,
            contents,
            request.force || request.in_place,
            overwrite,
            signing,
            committer,
        )?;
        Ok(GenerateOutcome {
            password,
            channel: match request.presentation {
                GeneratedPresentation::Terminal => GeneratedChannel::Terminal,
                GeneratedPresentation::Clipboard => GeneratedChannel::Clipboard,
                GeneratedPresentation::QrCode => GeneratedChannel::QrCode,
            },
            write,
        })
    }
}

fn generate_password<R: RngCore + CryptoRng>(
    rng: &mut R,
    length: NonZeroUsize,
    characters: &[char],
) -> Result<SecretBytes, GenerateError> {
    if characters.is_empty() {
        return Err(GenerateError::EmptyCharacterSet);
    }
    let mut password = String::with_capacity(length.get());
    for _ in 0..length.get() {
        let range = characters.len() as u64;
        let unbiased_limit = u64::MAX - (u64::MAX % range);
        let value = loop {
            let mut bytes = [0_u8; 8];
            rng.try_fill_bytes(&mut bytes)
                .map_err(|_| GenerateError::RandomnessUnavailable)?;
            let value = u64::from_le_bytes(bytes);
            if value < unbiased_limit {
                break value;
            }
        };
        password.push(characters[(value % range) as usize]);
    }
    Ok(SecretBytes::new(password.into_bytes()))
}

fn replace_first_line(password: &SecretBytes, existing: &SecretBytes) -> SecretBytes {
    let suffix = existing
        .expose()
        .iter()
        .position(|byte| *byte == b'\n')
        .map_or(&[][..], |index| &existing.expose()[index..]);
    let mut replacement = Vec::with_capacity(password.expose().len() + suffix.len());
    replacement.extend_from_slice(password.expose());
    replacement.extend_from_slice(suffix);
    SecretBytes::new(replacement)
}

fn validate_length(length: usize) -> Result<NonZeroUsize, GenerateError> {
    let length = NonZeroUsize::new(length).ok_or(GenerateError::InvalidLength)?;
    if length.get() > MAX_PASSWORD_LENGTH {
        return Err(GenerateError::InvalidLength);
    }
    Ok(length)
}

fn validate_character_set(character_set: &str) -> Result<Vec<char>, GenerateError> {
    let characters = character_set.chars().collect::<Vec<_>>();
    if characters.is_empty() {
        return Err(GenerateError::EmptyCharacterSet);
    }
    let mut unique = BTreeSet::new();
    if characters
        .iter()
        .any(|character| character.is_control() || !unique.insert(*character))
    {
        return Err(GenerateError::InvalidCharacterSet);
    }
    Ok(characters)
}

#[derive(Debug)]
pub enum GenerateError {
    Repository(RepositoryError),
    Crypto(CryptoError),
    Write(WriteError),
    InvalidLength,
    EmptyCharacterSet,
    InvalidCharacterSet,
    IncompatibleFlags,
    RandomnessUnavailable,
}

impl fmt::Display for GenerateError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Repository(error) => error.fmt(formatter),
            Self::Crypto(error) => error.fmt(formatter),
            Self::Write(error) => error.fmt(formatter),
            Self::InvalidLength => formatter.write_str("password length is invalid"),
            Self::EmptyCharacterSet => formatter.write_str("password character set is empty"),
            Self::InvalidCharacterSet => formatter.write_str("password character set is invalid"),
            Self::IncompatibleFlags => {
                formatter.write_str("--force and --in-place cannot be combined")
            }
            Self::RandomnessUnavailable => {
                formatter.write_str("operating-system randomness is unavailable")
            }
        }
    }
}

impl Error for GenerateError {}

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

impl From<WriteError> for GenerateError {
    fn from(error: WriteError) -> Self {
        Self::Write(error)
    }
}

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