//! Rollback-safe remove, move, and copy transactions.

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

use crate::{
    command::{CopyRequest, MoveRequest, RemoveRequest},
    crypto::{KeyStore, SecretProvider},
    recipient::{RecipientPolicyManager, SigningPolicy},
    repository::{
        DirectoryPath, EncryptedEntry, EntryPath, Repository, RepositoryError, ResolvedObject,
    },
    write::OverwriteDecision,
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MutationAction {
    Remove,
    Move,
    Copy,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeCommit {
    action: MutationAction,
    source: String,
    destination: Option<String>,
    changed_paths: Vec<std::path::PathBuf>,
    message: String,
}

impl TreeCommit {
    pub fn action(&self) -> MutationAction {
        self.action
    }
    pub fn source(&self) -> &str {
        &self.source
    }
    pub fn destination(&self) -> Option<&str> {
        self.destination.as_deref()
    }
    pub fn changed_paths(&self) -> &[std::path::PathBuf] {
        &self.changed_paths
    }
    pub fn message(&self) -> &str {
        &self.message
    }
}

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

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

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

impl Error for TreeCommitError {}

/// Atomically records a completed tree mutation.
///
/// An error must mean that no commit was created. `TreeMutator` restores the
/// filesystem transaction when an error is returned.
pub trait TreeCommitter {
    fn commit(&mut self, change: &TreeCommit) -> Result<(), TreeCommitError>;
}

#[derive(Default)]
pub struct NoGitTreeCommitter;

impl TreeCommitter for NoGitTreeCommitter {
    fn commit(&mut self, _change: &TreeCommit) -> Result<(), TreeCommitError> {
        Ok(())
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MutationOutcome {
    action: MutationAction,
    entries: Vec<EntryPath>,
    selection: Option<MutationSelection>,
}

impl MutationOutcome {
    pub fn action(&self) -> MutationAction {
        self.action
    }
    pub fn entries(&self) -> &[EntryPath] {
        &self.entries
    }
    pub fn selection(&self) -> Option<&MutationSelection> {
        self.selection.as_ref()
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MutationSelection {
    Entry(EntryPath),
    Directory(DirectoryPath),
}

impl MutationSelection {
    pub fn path(&self) -> &std::path::Path {
        match self {
            Self::Entry(path) => path.as_path(),
            Self::Directory(path) => path.as_path(),
        }
    }

    pub fn is_directory(&self) -> bool {
        matches!(self, Self::Directory(_))
    }

    pub fn display_path(&self) -> String {
        match self {
            Self::Entry(path) => path.to_string(),
            Self::Directory(path) => path.to_string(),
        }
    }
}

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

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

    pub fn remove(
        &self,
        request: &RemoveRequest,
        decision: OverwriteDecision,
        committer: &mut impl TreeCommitter,
    ) -> Result<MutationOutcome, MutationError> {
        let snapshot = self.repository.snapshot()?;
        let resolved = snapshot.resolve(&request.entry)?;
        if !request.force && decision == OverwriteDecision::Decline {
            return Err(MutationError::Cancelled);
        }
        let (entries, policies, directories, cleanup, display) = match resolved {
            ResolvedObject::Entry(record) => (
                vec![EntryState::source(
                    record.path().clone(),
                    self.repository.read_entry(record.path())?,
                )],
                Vec::new(),
                Vec::new(),
                record.path().parent_directory(),
                record.path().to_string(),
            ),
            ResolvedObject::Directory(record) => {
                if record.path().as_path().as_os_str().is_empty() {
                    return Err(MutationError::RootMutation);
                }
                if !request.recursive {
                    return Err(MutationError::RecursiveRequired);
                }
                self.reject_unmanaged(record.path())?;
                (
                    self.source_entries(record.path())?,
                    self.source_policies(record.path())?,
                    self.source_directories(record.path())?,
                    record.path().clone(),
                    format!("{}/", record.path()),
                )
            }
        };
        if let Err(operation) = self.remove_sources(&entries, &policies, &directories) {
            self.restore_sources(&entries, &policies, &directories)?;
            return Err(operation);
        }
        let change = TreeCommit {
            action: MutationAction::Remove,
            source: display.clone(),
            destination: None,
            changed_paths: entries
                .iter()
                .map(|entry| entry.source.encrypted_relative_path())
                .chain(policies.iter().flat_map(|policy| {
                    [
                        policy.directory.as_path().join(".gpg-id"),
                        policy.directory.as_path().join(".gpg-id.sig"),
                    ]
                }))
                .collect(),
            message: format!("Remove {display} from store."),
        };
        if let Err(error) = committer.commit(&change) {
            self.restore_sources(&entries, &policies, &directories)?;
            return Err(MutationError::Commit(error));
        }
        self.repository.cleanup_empty_directories(&cleanup)?;
        Ok(MutationOutcome {
            action: MutationAction::Remove,
            entries: entries.into_iter().map(|entry| entry.source).collect(),
            selection: None,
        })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn copy(
        &self,
        request: &CopyRequest,
        decision: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl TreeCommitter,
    ) -> Result<MutationOutcome, MutationError> {
        self.transfer(
            &request.source,
            &request.destination,
            request.force,
            decision,
            false,
            signing,
            provider,
            committer,
        )
    }

    #[allow(clippy::too_many_arguments)]
    pub fn move_tree(
        &self,
        request: &MoveRequest,
        decision: OverwriteDecision,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl TreeCommitter,
    ) -> Result<MutationOutcome, MutationError> {
        self.transfer(
            &request.source,
            &request.destination,
            request.force,
            decision,
            true,
            signing,
            provider,
            committer,
        )
    }

    #[allow(clippy::too_many_arguments)]
    fn transfer(
        &self,
        source: &str,
        destination: &str,
        force: bool,
        decision: OverwriteDecision,
        moving: bool,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl TreeCommitter,
    ) -> Result<MutationOutcome, MutationError> {
        let snapshot = self.repository.snapshot()?;
        let resolved = snapshot.resolve(source)?;
        let requested_destination = DirectoryPath::parse(destination)?;
        let destination_is_directory = snapshot
            .directories()
            .any(|record| record.path() == &requested_destination);
        if has_trailing_separator(destination) && !destination_is_directory {
            return Err(MutationError::DestinationDirectoryMissing {
                directory: requested_destination,
            });
        }
        let (
            source_entries,
            source_policies,
            source_directories,
            source_root,
            destination_root,
            destination_entry,
        ) = match resolved {
            ResolvedObject::Entry(record) => (
                vec![EntryState::source(
                    record.path().clone(),
                    self.repository.read_entry(record.path())?,
                )],
                Vec::new(),
                Vec::new(),
                record.path().parent_directory(),
                if destination_is_directory {
                    requested_destination.clone()
                } else {
                    EntryPath::parse(destination)?.parent_directory()
                },
                Some(if destination_is_directory {
                    let name = record
                        .path()
                        .as_path()
                        .file_name()
                        .expect("validated entry has a file name");
                    EntryPath::parse(requested_destination.as_path().join(name))?
                } else {
                    EntryPath::parse(destination)?
                }),
            ),
            ResolvedObject::Directory(record) => {
                if record.path().as_path().as_os_str().is_empty() {
                    return Err(MutationError::RootMutation);
                }
                let destination = if destination_is_directory {
                    let name = record
                        .path()
                        .as_path()
                        .file_name()
                        .expect("non-root directory has a file name");
                    DirectoryPath::parse(requested_destination.as_path().join(name))?
                } else {
                    requested_destination
                };
                if destination == *record.path() {
                    return Err(MutationError::SameObject);
                }
                if destination.as_path().starts_with(record.path().as_path()) {
                    return Err(MutationError::DestinationInsideSource);
                }
                if snapshot
                    .entries()
                    .any(|entry| entry.path().as_path() == destination.as_path())
                {
                    return Err(MutationError::DestinationTypeCollision {
                        path: destination.as_path().to_owned(),
                    });
                }
                self.reject_unmanaged(record.path())?;
                (
                    self.source_entries(record.path())?,
                    self.source_policies(record.path())?,
                    self.source_directories(record.path())?,
                    record.path().clone(),
                    destination,
                    None,
                )
            }
        };
        if destination_entry
            .as_ref()
            .is_some_and(|destination| destination == &source_entries[0].source)
        {
            return Err(MutationError::SameObject);
        }
        let mut entries = Vec::new();
        for source_entry in source_entries {
            let destination_path = if let Some(destination) = &destination_entry {
                destination.clone()
            } else {
                let relative = source_entry
                    .source
                    .as_path()
                    .strip_prefix(source_root.as_path())
                    .expect("source prefix");
                EntryPath::parse(destination_root.as_path().join(relative))?
            };
            if snapshot
                .directories()
                .any(|directory| directory.path().as_path() == destination_path.as_path())
            {
                return Err(MutationError::DestinationTypeCollision {
                    path: destination_path.as_path().to_owned(),
                });
            }
            let old_destination = match self.repository.read_entry(&destination_path) {
                Ok(ciphertext) => Some(ciphertext),
                Err(RepositoryError::NotFound { .. }) => None,
                Err(error) => return Err(error.into()),
            };
            if old_destination.is_some() && !force && decision == OverwriteDecision::Decline {
                return Err(MutationError::Cancelled);
            }
            let recipients = if destination_entry.is_none()
                && snapshot
                    .recipient_policy(&source_entry.source)
                    .is_some_and(|policy| {
                        policy
                            .directory()
                            .as_path()
                            .starts_with(source_root.as_path())
                    }) {
                RecipientPolicyManager::new(self.repository, self.keys)
                    .resolve_for_entry(&source_entry.source, signing)?
            } else {
                RecipientPolicyManager::new(self.repository, self.keys)
                    .resolve_for_entry(&destination_path, signing)?
            };
            let replacement = if self
                .keys
                .is_encrypted_for(&source_entry.ciphertext, recipients.recipients())?
            {
                source_entry.ciphertext.clone()
            } else {
                let plaintext = self.keys.decrypt(&source_entry.ciphertext, provider)?;
                self.keys.encrypt(plaintext, recipients.recipients())?
            };
            entries.push(TransferEntry {
                source: source_entry,
                destination: destination_path,
                replacement,
                old_destination,
            });
        }
        let mut policies = Vec::new();
        for policy in source_policies {
            let relative = policy
                .directory
                .as_path()
                .strip_prefix(source_root.as_path())
                .expect("policy prefix");
            let destination_directory =
                DirectoryPath::parse(destination_root.as_path().join(relative))?;
            let old_recipients = self
                .repository
                .read_policy_file(&destination_directory, false)?;
            let old_signature = self
                .repository
                .read_policy_file(&destination_directory, true)?;
            if old_recipients.is_some() || old_signature.is_some() {
                return Err(MutationError::UnsafePolicyOverwrite {
                    directory: destination_directory,
                });
            }
            policies.push(TransferPolicy {
                source: policy,
                destination: destination_directory,
            });
        }

        let source_entries = entries
            .iter()
            .map(|entry| entry.source.clone())
            .collect::<Vec<_>>();
        let source_policies = policies
            .iter()
            .map(|policy| policy.source.clone())
            .collect::<Vec<_>>();
        let destination_directories = if let Some(entry) = &destination_entry {
            vec![entry.parent_directory()]
        } else {
            source_directories
                .iter()
                .map(|directory| {
                    let relative = directory
                        .as_path()
                        .strip_prefix(source_root.as_path())
                        .expect("directory prefix");
                    DirectoryPath::parse(destination_root.as_path().join(relative))
                })
                .collect::<Result<Vec<_>, _>>()?
        };
        let mut created_directories = Vec::new();
        let applied = (|| -> Result<(), MutationError> {
            for directory in &destination_directories {
                created_directories.extend(self.repository.ensure_directory(directory)?);
            }
            for entry in &entries {
                self.repository
                    .write_entry(&entry.destination, &entry.replacement)?;
            }
            for policy in &policies {
                self.repository.replace_policy_file(
                    &policy.destination,
                    false,
                    Some(&policy.source.recipients),
                )?;
                if let Some(signature) = &policy.source.signature {
                    self.repository.replace_policy_file(
                        &policy.destination,
                        true,
                        Some(signature),
                    )?;
                }
            }
            if moving {
                self.remove_sources(&source_entries, &source_policies, &source_directories)?;
            }
            Ok(())
        })();
        if let Err(operation) = applied {
            if moving {
                self.restore_sources(&source_entries, &source_policies, &source_directories)?;
            }
            self.restore_destinations(&entries, &policies)?;
            self.remove_created_directories(&created_directories)?;
            return Err(operation);
        }
        let action = if moving {
            MutationAction::Move
        } else {
            MutationAction::Copy
        };
        let verb = if moving { "Rename" } else { "Copy" };
        let mut changed_paths = entries
            .iter()
            .map(|entry| entry.destination.encrypted_relative_path())
            .chain(policies.iter().flat_map(|policy| {
                [
                    policy.destination.as_path().join(".gpg-id"),
                    policy.destination.as_path().join(".gpg-id.sig"),
                ]
            }))
            .collect::<Vec<_>>();
        if moving {
            changed_paths.extend(
                source_entries
                    .iter()
                    .map(|entry| entry.source.encrypted_relative_path()),
            );
            changed_paths.extend(source_policies.iter().flat_map(|policy| {
                [
                    policy.directory.as_path().join(".gpg-id"),
                    policy.directory.as_path().join(".gpg-id.sig"),
                ]
            }));
        }
        let change = TreeCommit {
            action,
            source: source.to_owned(),
            destination: Some(destination.to_owned()),
            changed_paths,
            message: format!("{verb} {source} to {destination}."),
        };
        if let Err(error) = committer.commit(&change) {
            if moving {
                self.restore_sources(&source_entries, &source_policies, &source_directories)?;
            }
            self.restore_destinations(&entries, &policies)?;
            self.remove_created_directories(&created_directories)?;
            return Err(MutationError::Commit(error));
        }
        if moving {
            self.repository.cleanup_empty_directories(&source_root)?;
        }
        let selection = destination_entry.map_or_else(
            || MutationSelection::Directory(destination_root),
            MutationSelection::Entry,
        );
        Ok(MutationOutcome {
            action,
            entries: entries.into_iter().map(|entry| entry.destination).collect(),
            selection: Some(selection),
        })
    }

    fn source_entries(&self, root: &DirectoryPath) -> Result<Vec<EntryState>, MutationError> {
        self.repository
            .snapshot()?
            .entries()
            .filter(|entry| entry.path().as_path().starts_with(root.as_path()))
            .map(|entry| {
                Ok(EntryState::source(
                    entry.path().clone(),
                    self.repository.read_entry(entry.path())?,
                ))
            })
            .collect()
    }

    fn source_policies(&self, root: &DirectoryPath) -> Result<Vec<PolicyState>, MutationError> {
        self.repository
            .snapshot()?
            .recipient_policies()
            .filter(|policy| policy.directory().as_path().starts_with(root.as_path()))
            .map(|policy| {
                Ok(PolicyState {
                    directory: policy.directory().clone(),
                    recipients: self
                        .repository
                        .read_policy_file(policy.directory(), false)?
                        .expect("snapshot policy exists"),
                    signature: self.repository.read_policy_file(policy.directory(), true)?,
                })
            })
            .collect()
    }

    fn source_directories(
        &self,
        root: &DirectoryPath,
    ) -> Result<Vec<DirectoryPath>, MutationError> {
        Ok(self
            .repository
            .snapshot()?
            .directories()
            .filter(|directory| directory.path().as_path().starts_with(root.as_path()))
            .map(|directory| directory.path().clone())
            .collect())
    }

    fn reject_unmanaged(&self, root: &DirectoryPath) -> Result<(), MutationError> {
        let snapshot = self.repository.snapshot()?;
        if let Some(file) = snapshot
            .auxiliary_files()
            .find(|file| file.path().starts_with(root.as_path()))
        {
            return Err(MutationError::UnsupportedAuxiliary {
                path: file.path().to_owned(),
            });
        }
        if let Some(repository) = snapshot
            .git_repositories()
            .find(|repository| repository.work_tree().as_path().starts_with(root.as_path()))
        {
            return Err(MutationError::UnsupportedGitRepository {
                path: repository.git_directory().to_owned(),
            });
        }
        Ok(())
    }

    fn remove_sources(
        &self,
        entries: &[EntryState],
        policies: &[PolicyState],
        directories: &[DirectoryPath],
    ) -> Result<(), MutationError> {
        for entry in entries.iter().rev() {
            self.repository.remove_entry(&entry.source)?;
        }
        for policy in policies.iter().rev() {
            if policy.signature.is_some() {
                self.repository
                    .replace_policy_file(&policy.directory, true, None)?;
            }
            self.repository
                .replace_policy_file(&policy.directory, false, None)?;
        }
        for directory in directories.iter().rev() {
            if !self.repository.remove_empty_directory(directory)? {
                return Err(MutationError::TreeChanged {
                    path: directory.as_path().to_owned(),
                });
            }
        }
        Ok(())
    }

    fn restore_sources(
        &self,
        entries: &[EntryState],
        policies: &[PolicyState],
        directories: &[DirectoryPath],
    ) -> Result<(), MutationError> {
        for directory in directories {
            self.repository.ensure_directory(directory)?;
        }
        for policy in policies {
            self.repository.replace_policy_file(
                &policy.directory,
                false,
                Some(&policy.recipients),
            )?;
            if let Some(signature) = &policy.signature {
                self.repository
                    .replace_policy_file(&policy.directory, true, Some(signature))?;
            }
        }
        for entry in entries {
            self.repository
                .write_entry(&entry.source, &entry.ciphertext)?;
        }
        Ok(())
    }

    fn remove_created_directories(
        &self,
        directories: &[DirectoryPath],
    ) -> Result<(), MutationError> {
        for directory in directories.iter().rev() {
            if !self.repository.remove_empty_directory(directory)? {
                return Err(MutationError::TreeChanged {
                    path: directory.as_path().to_owned(),
                });
            }
        }
        Ok(())
    }

    fn restore_destinations(
        &self,
        entries: &[TransferEntry],
        policies: &[TransferPolicy],
    ) -> Result<(), MutationError> {
        for policy in policies.iter().rev() {
            if policy.source.signature.is_some() {
                self.repository
                    .replace_policy_file(&policy.destination, true, None)?;
            }
            self.repository
                .replace_policy_file(&policy.destination, false, None)?;
        }
        for entry in entries.iter().rev() {
            if let Some(original) = &entry.old_destination {
                self.repository.write_entry(&entry.destination, original)?;
            } else {
                match self.repository.remove_entry(&entry.destination) {
                    Ok(_) | Err(RepositoryError::NotFound { .. }) => {}
                    Err(error) => return Err(error.into()),
                }
            }
        }
        Ok(())
    }
}

#[derive(Clone)]
struct EntryState {
    source: EntryPath,
    ciphertext: EncryptedEntry,
}

impl EntryState {
    fn source(source: EntryPath, ciphertext: EncryptedEntry) -> Self {
        Self { source, ciphertext }
    }
}

#[derive(Clone)]
struct PolicyState {
    directory: DirectoryPath,
    recipients: Vec<u8>,
    signature: Option<Vec<u8>>,
}

struct TransferEntry {
    source: EntryState,
    destination: EntryPath,
    replacement: EncryptedEntry,
    old_destination: Option<EncryptedEntry>,
}

struct TransferPolicy {
    source: PolicyState,
    destination: DirectoryPath,
}

#[derive(Debug)]
pub enum MutationError {
    Repository(RepositoryError),
    Recipient(crate::recipient::RecipientPolicyError),
    Crypto(crate::crypto::CryptoError),
    Cancelled,
    RecursiveRequired,
    RootMutation,
    SameObject,
    DestinationInsideSource,
    DestinationDirectoryMissing { directory: DirectoryPath },
    DestinationTypeCollision { path: std::path::PathBuf },
    UnsafePolicyOverwrite { directory: DirectoryPath },
    UnsupportedAuxiliary { path: std::path::PathBuf },
    UnsupportedGitRepository { path: std::path::PathBuf },
    TreeChanged { path: std::path::PathBuf },
    Commit(TreeCommitError),
}

impl fmt::Display for MutationError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Repository(error) => error.fmt(formatter),
            Self::Recipient(error) => error.fmt(formatter),
            Self::Crypto(error) => error.fmt(formatter),
            Self::Cancelled => formatter.write_str("tree mutation was cancelled"),
            Self::RecursiveRequired => {
                formatter.write_str("directory removal requires --recursive")
            }
            Self::RootMutation => formatter.write_str("the password-store root cannot be mutated"),
            Self::SameObject => formatter.write_str("source and destination are the same object"),
            Self::DestinationInsideSource => {
                formatter.write_str("destination is inside source subtree")
            }
            Self::DestinationDirectoryMissing { directory } => write!(
                formatter,
                "trailing-slash destination directory does not exist: {directory}"
            ),
            Self::DestinationTypeCollision { path } => write!(
                formatter,
                "directory destination collides with password entry: {}",
                path.display()
            ),
            Self::UnsafePolicyOverwrite { directory } => write!(
                formatter,
                "destination recipient policy already exists at {directory}"
            ),
            Self::UnsupportedAuxiliary { path } => write!(
                formatter,
                "subtree contains unsupported auxiliary file {}",
                path.display()
            ),
            Self::UnsupportedGitRepository { path } => write!(
                formatter,
                "subtree contains a nested Git repository at {}",
                path.display()
            ),
            Self::TreeChanged { path } => write!(
                formatter,
                "password-store subtree changed during mutation: {}",
                path.display()
            ),
            Self::Commit(error) => write!(formatter, "cannot commit tree mutation: {error}"),
        }
    }
}

fn has_trailing_separator(path: &str) -> bool {
    path.ends_with('/') || (cfg!(windows) && path.ends_with('\\'))
}

impl Error for MutationError {}

impl From<RepositoryError> for MutationError {
    fn from(error: RepositoryError) -> Self {
        Self::Repository(error)
    }
}
impl From<crate::recipient::RecipientPolicyError> for MutationError {
    fn from(error: crate::recipient::RecipientPolicyError) -> Self {
        Self::Recipient(error)
    }
}
impl From<crate::crypto::CryptoError> for MutationError {
    fn from(error: crate::crypto::CryptoError) -> Self {
        Self::Crypto(error)
    }
}
