//! Insert and concurrency-safe in-process edit sessions.

use std::{error::Error, fmt};

use crate::{
    command::{EditRequest, InsertInput, InsertRequest},
    crypto::{CryptoError, KeyStore, SecretProvider},
    recipient::{RecipientPolicyError, RecipientPolicyManager, SigningPolicy},
    repository::{EncryptedEntry, EntryPath, Repository, RepositoryError, SecretBytes},
};
use zeroize::Zeroize;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum OverwriteDecision {
    Allow,
    Decline,
}

pub struct InsertContent {
    mode: InsertInput,
    secret: SecretBytes,
}

impl InsertContent {
    pub fn hidden(mut first: Vec<u8>, mut confirmation: Vec<u8>) -> Result<Self, WriteError> {
        if let Err(error) = validate_single_line(&first) {
            first.zeroize();
            confirmation.zeroize();
            return Err(error);
        }
        if let Err(error) = validate_single_line(&confirmation) {
            first.zeroize();
            confirmation.zeroize();
            return Err(error);
        }
        if first != confirmation {
            first.zeroize();
            confirmation.zeroize();
            return Err(WriteError::ConfirmationMismatch);
        }
        if first.is_empty() {
            confirmation.zeroize();
            return Err(WriteError::EmptySingleLine);
        }
        confirmation.zeroize();
        Ok(Self {
            mode: InsertInput::HiddenConfirmed,
            secret: SecretBytes::new(first),
        })
    }

    pub fn echoed(mut line: Vec<u8>) -> Result<Self, WriteError> {
        if let Err(error) = validate_single_line(&line) {
            line.zeroize();
            return Err(error);
        }
        if line.is_empty() {
            return Err(WriteError::EmptySingleLine);
        }
        Ok(Self {
            mode: InsertInput::EchoedLine,
            secret: SecretBytes::new(line),
        })
    }

    pub fn multiline(contents: Vec<u8>) -> Self {
        Self {
            mode: InsertInput::Multiline,
            secret: SecretBytes::new(contents),
        }
    }

    pub fn expose(&self) -> &[u8] {
        self.secret.expose()
    }

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

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

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EntryAction {
    Insert,
    Edit,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryCommit {
    path: EntryPath,
    action: EntryAction,
    message: String,
}

impl EntryCommit {
    pub(crate) fn new(path: EntryPath, action: EntryAction, message: String) -> Self {
        Self {
            path,
            action,
            message,
        }
    }

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

    pub fn action(&self) -> EntryAction {
        self.action
    }

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EntryCommitError(String);

impl EntryCommitError {
    pub fn new(message: impl Into<String>) -> Self {
        Self(message.into())
    }
}

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

impl Error for EntryCommitError {}

pub trait EntryCommitter {
    fn commit(&mut self, change: &EntryCommit) -> Result<(), EntryCommitError>;
}

#[derive(Default)]
pub struct NoGitEntryCommitter;

impl EntryCommitter for NoGitEntryCommitter {
    fn commit(&mut self, _change: &EntryCommit) -> Result<(), EntryCommitError> {
        Ok(())
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WriteOutcome {
    path: EntryPath,
    action: EntryAction,
}

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

    pub fn action(&self) -> EntryAction {
        self.action
    }
}

pub struct EditSession {
    path: EntryPath,
    original_ciphertext: Option<EncryptedEntry>,
    plaintext: SecretBytes,
}

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

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

    pub(crate) fn original_ciphertext(&self) -> Option<&EncryptedEntry> {
        self.original_ciphertext.as_ref()
    }
}

impl fmt::Debug for EditSession {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("EditSession")
            .field("path", &self.path)
            .field("plaintext", &self.plaintext)
            .finish_non_exhaustive()
    }
}

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

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

    /// Report whether a logical entry already exists so a frontend can ask for
    /// confirmation without inspecting the password-store filesystem itself.
    pub fn entry_exists(&self, entry: &str) -> Result<bool, WriteError> {
        let path = EntryPath::parse(entry)?;
        match self.repository.read_entry(&path) {
            Ok(_) => Ok(true),
            Err(RepositoryError::NotFound { .. }) => Ok(false),
            Err(error) => Err(error.into()),
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub fn insert(
        &self,
        request: &InsertRequest,
        contents: InsertContent,
        overwrite: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<WriteOutcome, WriteError> {
        let path = EntryPath::parse(&request.entry)?;
        if contents.mode != request.input {
            return Err(WriteError::InputModeMismatch);
        }
        let original = match self.repository.read_entry(&path) {
            Ok(original) => Some(original),
            Err(RepositoryError::NotFound { .. }) => None,
            Err(error) => return Err(error.into()),
        };
        if original.is_some() && !request.force && overwrite == OverwriteDecision::Decline {
            return Err(WriteError::Cancelled);
        }
        let recipients = RecipientPolicyManager::new(self.repository, self.keys)
            .resolve_for_entry(&path, signing)?;
        let ciphertext = self
            .keys
            .encrypt(contents.into_secret(), recipients.recipients())?;
        self.repository.write_entry(&path, &ciphertext)?;
        let change = EntryCommit::new(
            path.clone(),
            EntryAction::Insert,
            format!("Add given password for {path} to store."),
        );
        if let Err(error) = committer.commit(&change) {
            if let Err(rollback) = self.restore(&path, original.as_ref()) {
                return Err(WriteError::RollbackFailed {
                    operation: error,
                    rollback,
                });
            }
            return Err(WriteError::Commit(error));
        }
        Ok(WriteOutcome {
            path,
            action: EntryAction::Insert,
        })
    }

    pub fn begin_edit(
        &self,
        request: &EditRequest,
        provider: &mut impl SecretProvider,
    ) -> Result<EditSession, WriteError> {
        let path = EntryPath::parse(&request.entry)?;
        let original_ciphertext = match self.repository.read_entry(&path) {
            Ok(ciphertext) => Some(ciphertext),
            Err(RepositoryError::NotFound { .. }) => None,
            Err(error) => return Err(error.into()),
        };
        let plaintext = if let Some(ciphertext) = &original_ciphertext {
            self.keys.decrypt(ciphertext, provider)?
        } else {
            SecretBytes::new(Vec::new())
        };
        Ok(EditSession {
            path,
            original_ciphertext,
            plaintext,
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn store_generated(
        &self,
        path: &EntryPath,
        contents: SecretBytes,
        force: bool,
        overwrite: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<WriteOutcome, WriteError> {
        let original = match self.repository.read_entry(path) {
            Ok(original) => Some(original),
            Err(RepositoryError::NotFound { .. }) => None,
            Err(error) => return Err(error.into()),
        };
        if original.is_some() && !force && overwrite == OverwriteDecision::Decline {
            return Err(WriteError::Cancelled);
        }
        let recipients = RecipientPolicyManager::new(self.repository, self.keys)
            .resolve_for_entry(path, signing)?;
        let ciphertext = self.keys.encrypt(contents, recipients.recipients())?;
        self.repository.write_entry(path, &ciphertext)?;
        let change = EntryCommit::new(
            path.clone(),
            EntryAction::Insert,
            format!("Add generated password for {path}."),
        );
        if let Err(error) = committer.commit(&change) {
            if let Err(rollback) = self.restore(path, original.as_ref()) {
                return Err(WriteError::RollbackFailed {
                    operation: error,
                    rollback,
                });
            }
            return Err(WriteError::Commit(error));
        }
        Ok(WriteOutcome {
            path: path.clone(),
            action: EntryAction::Insert,
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn finish_edit(
        &self,
        session: EditSession,
        replacement: SecretBytes,
        editor_name: &str,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<WriteOutcome, WriteError> {
        self.finish_edit_recoverable(&session, replacement, editor_name, signing, committer)
    }

    /// Finish an edit while retaining the session when validation, encryption,
    /// conflict detection, or commit orchestration fails.
    #[allow(clippy::too_many_arguments)]
    pub fn finish_edit_recoverable(
        &self,
        session: &EditSession,
        replacement: SecretBytes,
        editor_name: &str,
        signing: Option<&SigningPolicy>,
        committer: &mut impl EntryCommitter,
    ) -> Result<WriteOutcome, WriteError> {
        if replacement.expose() == session.plaintext.expose() {
            return Err(WriteError::Unchanged);
        }
        let concurrent = match (
            session.original_ciphertext.as_ref(),
            self.repository.read_entry(&session.path),
        ) {
            (Some(original), Ok(current)) => &current != original,
            (None, Err(RepositoryError::NotFound { .. })) => false,
            (Some(_), Err(RepositoryError::NotFound { .. })) | (None, Ok(_)) => true,
            (_, Err(error)) => return Err(error.into()),
        };
        if concurrent {
            return Err(WriteError::ConcurrentModification {
                path: session.path.clone(),
            });
        }
        let recipients = RecipientPolicyManager::new(self.repository, self.keys)
            .resolve_for_entry(&session.path, signing)?;
        let ciphertext = self.keys.encrypt(replacement, recipients.recipients())?;
        self.repository.write_entry(&session.path, &ciphertext)?;
        let change = EntryCommit::new(
            session.path.clone(),
            EntryAction::Edit,
            format!("Edit password for {} using {}.", session.path, editor_name),
        );
        if let Err(error) = committer.commit(&change) {
            if let Err(rollback) = self.restore(&session.path, session.original_ciphertext.as_ref())
            {
                return Err(WriteError::RollbackFailed {
                    operation: error,
                    rollback,
                });
            }
            return Err(WriteError::Commit(error));
        }
        Ok(WriteOutcome {
            path: session.path.clone(),
            action: EntryAction::Edit,
        })
    }

    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())?;
        }
        Ok(())
    }
}

fn validate_single_line(line: &[u8]) -> Result<(), WriteError> {
    if line.iter().any(|byte| matches!(byte, b'\n' | b'\r')) {
        Err(WriteError::InvalidSingleLine)
    } else {
        Ok(())
    }
}

#[derive(Debug)]
pub enum WriteError {
    Repository(RepositoryError),
    Crypto(CryptoError),
    RecipientPolicy(RecipientPolicyError),
    ConfirmationMismatch,
    EmptySingleLine,
    InvalidSingleLine,
    InputModeMismatch,
    Cancelled,
    Unchanged,
    ConcurrentModification {
        path: EntryPath,
    },
    Commit(EntryCommitError),
    RollbackFailed {
        operation: EntryCommitError,
        rollback: RepositoryError,
    },
}

impl fmt::Display for WriteError {
    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::ConfirmationMismatch => {
                formatter.write_str("password confirmation does not match")
            }
            Self::EmptySingleLine => formatter.write_str("password may not be empty"),
            Self::InvalidSingleLine => {
                formatter.write_str("single-line input contains a line break")
            }
            Self::InputModeMismatch => {
                formatter.write_str("insert content does not match the requested input mode")
            }
            Self::Cancelled => formatter.write_str("entry overwrite was declined"),
            Self::Unchanged => formatter.write_str("entry was not changed"),
            Self::ConcurrentModification { path } => {
                write!(formatter, "entry changed during editing: {path}")
            }
            Self::Commit(error) => write!(formatter, "cannot commit entry mutation: {error}"),
            Self::RollbackFailed {
                operation,
                rollback,
            } => write!(
                formatter,
                "entry commit failed ({operation}) and repository rollback failed ({rollback})"
            ),
        }
    }
}

impl Error for WriteError {}

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

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

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