//! Hierarchical `.gpg-id` policy and rollback-safe selective reencryption.

use std::{
    error::Error,
    fmt,
    path::{Path, PathBuf},
};

use crate::{
    command::InitRequest,
    crypto::{CryptoError, DetachedSignatureBytes, KeyHandle, KeyStore, SecretProvider},
    repository::{
        DirectoryPath, EncryptedEntry, EntryPath, Repository, RepositoryError, RepositorySnapshot,
    },
};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SigningPolicy {
    signer: KeyHandle,
    trusted_signers: Vec<KeyHandle>,
}

impl SigningPolicy {
    pub fn new(signer: KeyHandle, trusted_signers: Vec<KeyHandle>) -> Self {
        Self {
            signer,
            trusted_signers,
        }
    }

    pub fn signer(&self) -> &KeyHandle {
        &self.signer
    }

    pub fn trusted_signers(&self) -> &[KeyHandle] {
        &self.trusted_signers
    }
}

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyCommit {
    directory: DirectoryPath,
    action: PolicyAction,
    changed_entries: Vec<EntryPath>,
    policy_changed: bool,
    signature_changed: bool,
    message: String,
}

impl PolicyCommit {
    pub fn directory(&self) -> &DirectoryPath {
        &self.directory
    }

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

    pub fn changed_entries(&self) -> &[EntryPath] {
        &self.changed_entries
    }

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

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

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

    pub fn changed_paths(&self) -> Vec<PathBuf> {
        let mut paths = self
            .changed_entries
            .iter()
            .map(|entry| {
                let mut path = entry.as_path().to_owned();
                let mut name = path
                    .file_name()
                    .expect("validated entry has a file name")
                    .to_os_string();
                name.push(".gpg");
                path.set_file_name(name);
                path
            })
            .collect::<Vec<_>>();
        if self.policy_changed {
            paths.push(self.directory.as_path().join(".gpg-id"));
        }
        if self.signature_changed {
            paths.push(self.directory.as_path().join(".gpg-id.sig"));
        }
        paths.sort();
        paths
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PolicyCommitError {
    message: String,
}

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

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

impl Error for PolicyCommitError {}

/// Boundary implemented by embedded Git support. Returning an error promises that the
/// committer also restored any staging state it changed; repository bytes are restored here.
pub trait PolicyCommitter {
    fn commit(&mut self, change: &PolicyCommit) -> Result<(), PolicyCommitError>;
}

/// Use when the affected path is not inside a Git repository.
#[derive(Default)]
pub struct NoGitCommitter;

impl PolicyCommitter for NoGitCommitter {
    fn commit(&mut self, _change: &PolicyCommit) -> Result<(), PolicyCommitError> {
        Ok(())
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EffectiveRecipients {
    policy_directory: DirectoryPath,
    recipients: Vec<KeyHandle>,
}

impl EffectiveRecipients {
    pub fn policy_directory(&self) -> &DirectoryPath {
        &self.policy_directory
    }

    pub fn recipients(&self) -> &[KeyHandle] {
        &self.recipients
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RecipientPolicyOutcome {
    directory: DirectoryPath,
    action: PolicyAction,
    recipients: Vec<KeyHandle>,
    reencrypted_entries: Vec<EntryPath>,
    committed: bool,
}

impl RecipientPolicyOutcome {
    pub fn directory(&self) -> &DirectoryPath {
        &self.directory
    }

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

    pub fn recipients(&self) -> &[KeyHandle] {
        &self.recipients
    }

    pub fn reencrypted_entries(&self) -> &[EntryPath] {
        &self.reencrypted_entries
    }

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

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

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

    /// Resolve and, when configured, authenticate the nearest ancestor policy for an entry.
    pub fn resolve_for_entry(
        &self,
        entry: &EntryPath,
        signing: Option<&SigningPolicy>,
    ) -> Result<EffectiveRecipients, RecipientPolicyError> {
        let snapshot = self.repository.snapshot()?;
        let policy = snapshot.recipient_policy(entry).ok_or_else(|| {
            RecipientPolicyError::MissingRecipientPolicy {
                entry: entry.clone(),
            }
        })?;
        let recipients = self.load_policy(policy.directory(), signing)?;
        Ok(EffectiveRecipients {
            policy_directory: policy.directory().clone(),
            recipients,
        })
    }

    /// Apply `pass init`, including an empty single identity for policy removal.
    pub fn apply_init(
        &self,
        request: &InitRequest,
        signing: Option<&SigningPolicy>,
        provider: &mut impl SecretProvider,
        committer: &mut impl PolicyCommitter,
    ) -> Result<RecipientPolicyOutcome, RecipientPolicyError> {
        let directory = DirectoryPath::parse(request.path.as_deref().unwrap_or_default())?;
        if request.key_identities.is_empty() {
            return Err(RecipientPolicyError::MissingIdentities);
        }
        let remove = request.key_identities.len() == 1 && request.key_identities[0].is_empty();
        let action = if remove {
            PolicyAction::Remove
        } else {
            PolicyAction::Set
        };
        let snapshot = self.repository.snapshot()?;
        let original_policy = self.repository.read_policy_file(&directory, false)?;
        if remove && original_policy.is_none() {
            return Err(RecipientPolicyError::PolicyNotFound {
                directory: directory.clone(),
            });
        }
        let original_signature = self.repository.read_policy_file(&directory, true)?;

        let (replacement_policy, replacement_signature, requested_recipients) = if remove {
            (None, original_signature.clone(), Vec::new())
        } else {
            let contents = serialize_identities(&request.key_identities);
            let recipients = self.keys.resolve_recipients(&contents)?;
            let signature = if let Some(signing) = signing {
                let signature = self.keys.sign(&contents, signing.signer(), provider)?;
                self.keys
                    .verify(&contents, &signature, signing.trusted_signers())?;
                Some(signature.into_bytes())
            } else {
                original_signature.clone()
            };
            (Some(contents), signature, recipients)
        };

        let controlled_entries = snapshot
            .entries()
            .filter(|record| entry_is_controlled_by(record.path(), &directory, &snapshot))
            .collect::<Vec<_>>();
        let inherited = if remove && !controlled_entries.is_empty() {
            nearest_parent_policy(&snapshot, &directory)
                .map(|policy_directory| {
                    self.load_policy(&policy_directory, signing)
                        .map(|recipients| (policy_directory, recipients))
                })
                .transpose()?
        } else {
            None
        };
        let mut entries = Vec::new();
        for record in controlled_entries {
            let recipients = if remove {
                inherited
                    .as_ref()
                    .map(|(_, recipients)| recipients)
                    .ok_or_else(|| RecipientPolicyError::MissingRecipientPolicy {
                        entry: record.path().clone(),
                    })?
            } else {
                &requested_recipients
            };
            let original = self.repository.read_entry(record.path())?;
            if self.keys.is_encrypted_for(&original, recipients)? {
                continue;
            }
            let plaintext = self.keys.decrypt(&original, provider)?;
            let replacement = self.keys.encrypt(plaintext, recipients)?;
            entries.push(EntryMutation {
                path: record.path().clone(),
                original,
                replacement,
            });
        }

        let policy_changed = original_policy != replacement_policy;
        let signature_changed = original_signature != replacement_signature;
        let changed_entries = entries
            .iter()
            .map(|entry| entry.path.clone())
            .collect::<Vec<_>>();
        let change = PolicyCommit {
            directory: directory.clone(),
            action,
            changed_entries: changed_entries.clone(),
            policy_changed,
            signature_changed,
            message: commit_message(action, &directory, &request.key_identities),
        };
        if !policy_changed && !signature_changed && entries.is_empty() {
            return Ok(RecipientPolicyOutcome {
                directory,
                action,
                recipients: requested_recipients,
                reencrypted_entries: Vec::new(),
                committed: false,
            });
        }

        let applied = self.apply_files(
            &directory,
            &entries,
            policy_changed
                .then_some(replacement_policy.as_deref())
                .flatten(),
            policy_changed && replacement_policy.is_none(),
            signature_changed
                .then_some(replacement_signature.as_deref())
                .flatten(),
            signature_changed && replacement_signature.is_none(),
        );
        if let Err(operation) = applied {
            return self.rollback_error(
                operation.into(),
                &directory,
                &entries,
                original_policy.as_deref(),
                original_signature.as_deref(),
                policy_changed,
                signature_changed,
            );
        }
        if remove && let Err(operation) = self.repository.cleanup_empty_directories(&directory) {
            return self.rollback_error(
                operation.into(),
                &directory,
                &entries,
                original_policy.as_deref(),
                original_signature.as_deref(),
                policy_changed,
                signature_changed,
            );
        }
        if let Err(error) = committer.commit(&change) {
            return self.rollback_error(
                RecipientPolicyError::Commit(error),
                &directory,
                &entries,
                original_policy.as_deref(),
                original_signature.as_deref(),
                policy_changed,
                signature_changed,
            );
        }
        Ok(RecipientPolicyOutcome {
            directory,
            action,
            recipients: if remove {
                inherited.map_or_else(Vec::new, |(_, recipients)| recipients)
            } else {
                requested_recipients
            },
            reencrypted_entries: changed_entries,
            committed: true,
        })
    }

    fn load_policy(
        &self,
        directory: &DirectoryPath,
        signing: Option<&SigningPolicy>,
    ) -> Result<Vec<KeyHandle>, RecipientPolicyError> {
        let contents = self
            .repository
            .read_policy_file(directory, false)?
            .ok_or_else(|| RecipientPolicyError::PolicyNotFound {
                directory: directory.clone(),
            })?;
        if let Some(signing) = signing {
            let signature = self
                .repository
                .read_policy_file(directory, true)?
                .ok_or_else(|| RecipientPolicyError::MissingSignature {
                    directory: directory.clone(),
                })?;
            self.keys.verify(
                &contents,
                &DetachedSignatureBytes::new(signature),
                signing.trusted_signers(),
            )?;
        }
        Ok(self.keys.resolve_recipients(&contents)?)
    }

    fn apply_files(
        &self,
        directory: &DirectoryPath,
        entries: &[EntryMutation],
        policy: Option<&[u8]>,
        remove_policy: bool,
        signature: Option<&[u8]>,
        remove_signature: bool,
    ) -> Result<(), RepositoryError> {
        for entry in entries {
            self.repository
                .write_entry(&entry.path, &entry.replacement)?;
        }
        if policy.is_some() || remove_policy {
            self.repository
                .replace_policy_file(directory, false, policy)?;
        }
        if signature.is_some() || remove_signature {
            self.repository
                .replace_policy_file(directory, true, signature)?;
        }
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    fn rollback_error<T>(
        &self,
        operation: RecipientPolicyError,
        directory: &DirectoryPath,
        entries: &[EntryMutation],
        original_policy: Option<&[u8]>,
        original_signature: Option<&[u8]>,
        policy_changed: bool,
        signature_changed: bool,
    ) -> Result<T, RecipientPolicyError> {
        let mut rollback = None;
        if signature_changed
            && let Err(error) =
                self.repository
                    .replace_policy_file(directory, true, original_signature)
        {
            rollback.get_or_insert(error);
        }
        if policy_changed
            && let Err(error) =
                self.repository
                    .replace_policy_file(directory, false, original_policy)
        {
            rollback.get_or_insert(error);
        }
        for entry in entries.iter().rev() {
            if let Err(error) = self.repository.write_entry(&entry.path, &entry.original) {
                rollback.get_or_insert(error);
            }
        }
        if original_policy.is_none() {
            let _ = self.repository.cleanup_empty_directories(directory);
        }
        match rollback {
            Some(rollback) => Err(RecipientPolicyError::RollbackFailed {
                operation: Box::new(operation),
                rollback,
            }),
            None => Err(operation),
        }
    }
}

struct EntryMutation {
    path: EntryPath,
    original: EncryptedEntry,
    replacement: EncryptedEntry,
}

fn serialize_identities(identities: &[String]) -> Vec<u8> {
    let mut contents = identities.join("\n").into_bytes();
    contents.push(b'\n');
    contents
}

fn entry_is_controlled_by(
    entry: &EntryPath,
    target: &DirectoryPath,
    snapshot: &RepositorySnapshot,
) -> bool {
    let parent = entry.as_path().parent().unwrap_or_else(|| Path::new(""));
    if !parent.starts_with(target.as_path()) {
        return false;
    }
    !snapshot.recipient_policies().any(|policy| {
        policy.directory() != target
            && policy.directory().as_path().starts_with(target.as_path())
            && parent.starts_with(policy.directory().as_path())
    })
}

fn nearest_parent_policy(
    snapshot: &RepositorySnapshot,
    directory: &DirectoryPath,
) -> Option<DirectoryPath> {
    let mut current = directory.as_path().parent();
    while let Some(path) = current {
        let candidate = DirectoryPath::parse(path).expect("ancestor of validated directory");
        if snapshot.recipient_policy_at(&candidate).is_some() {
            return Some(candidate);
        }
        if path.as_os_str().is_empty() {
            break;
        }
        current = path.parent();
    }
    None
}

fn commit_message(
    action: PolicyAction,
    directory: &DirectoryPath,
    identities: &[String],
) -> String {
    let suffix = if directory.as_path().as_os_str().is_empty() {
        String::new()
    } else {
        format!(" ({directory})")
    };
    match action {
        PolicyAction::Set => format!(
            "Set GPG id to {}{suffix} and selectively reencrypt password store.",
            identities.join(", ")
        ),
        PolicyAction::Remove => {
            format!(
                "Deinitialize recipient policy{suffix} and selectively reencrypt password store."
            )
        }
    }
}

#[derive(Debug)]
pub enum RecipientPolicyError {
    Repository(RepositoryError),
    Crypto(CryptoError),
    MissingIdentities,
    PolicyNotFound {
        directory: DirectoryPath,
    },
    MissingRecipientPolicy {
        entry: EntryPath,
    },
    MissingSignature {
        directory: DirectoryPath,
    },
    Commit(PolicyCommitError),
    RollbackFailed {
        operation: Box<RecipientPolicyError>,
        rollback: RepositoryError,
    },
}

impl fmt::Display for RecipientPolicyError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Repository(error) => error.fmt(formatter),
            Self::Crypto(error) => error.fmt(formatter),
            Self::MissingIdentities => formatter.write_str("at least one GPG identity is required"),
            Self::PolicyNotFound { directory } => {
                write!(formatter, "recipient policy does not exist at {directory}")
            }
            Self::MissingRecipientPolicy { entry } => {
                write!(formatter, "no recipient policy applies to entry {entry}")
            }
            Self::MissingSignature { directory } => {
                write!(
                    formatter,
                    "recipient policy at {directory} has no signature"
                )
            }
            Self::Commit(error) => write!(formatter, "cannot commit recipient policy: {error}"),
            Self::RollbackFailed {
                operation,
                rollback,
            } => write!(
                formatter,
                "recipient policy operation failed ({operation}) and rollback failed ({rollback})"
            ),
        }
    }
}

impl Error for RecipientPolicyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            Self::Repository(error) => Some(error),
            Self::Crypto(error) => Some(error),
            Self::Commit(error) => Some(error),
            Self::RollbackFailed { operation, .. } => Some(operation),
            _ => None,
        }
    }
}

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

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