//! Storage-owned preparation and execution of native mobile entry mutations.

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

use data_encoding::HEXLOWER;
use sha2::{Digest as _, Sha256};

use crate::{
    command::{CopyRequest, MoveRequest, RemoveRequest},
    crypto::{KeyStore, SecretProvider},
    mutation::{MutationError, TreeCommitter, TreeMutator},
    read::hidden_path,
    repository::{DirectoryPath, EntryPath, Repository, RepositoryError},
    write::OverwriteDecision,
};

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileMutationDestination {
    path: String,
    title: String,
    detail: String,
    requires_overwrite: bool,
}

impl MobileMutationDestination {
    pub fn path(&self) -> &str {
        &self.path
    }

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

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

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileMutationPlan {
    action: MobileMutationAction,
    source: String,
    source_title: String,
    revision: String,
    destinations: Vec<MobileMutationDestination>,
    has_open_editor: bool,
    has_dirty_editor: bool,
}

impl MobileMutationPlan {
    pub fn action(&self) -> MobileMutationAction {
        self.action
    }

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

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

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

    pub fn destinations(&self) -> &[MobileMutationDestination] {
        &self.destinations
    }

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

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

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileMutationRequest {
    pub action: MobileMutationAction,
    pub source: String,
    pub revision: String,
    pub destination: Option<String>,
    pub confirmed: bool,
    pub overwrite: bool,
    pub discard_editor: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileMutationOutcome {
    action: MobileMutationAction,
    source: String,
    destination: Option<String>,
    title: String,
    detail: String,
}

impl MobileMutationOutcome {
    pub fn action(&self) -> MobileMutationAction {
        self.action
    }

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

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

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

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

pub struct MobileMutationService<'a> {
    repository: &'a Repository,
}

impl<'a> MobileMutationService<'a> {
    pub fn new(repository: &'a Repository) -> Self {
        Self { repository }
    }

    pub fn prepare(
        &self,
        source: &str,
        action: MobileMutationAction,
        has_open_editor: bool,
        has_dirty_editor: bool,
    ) -> Result<MobileMutationPlan, MobileMutationError> {
        let source = EntryPath::parse(source)?;
        if hidden_path(source.as_path()) {
            return Err(MobileMutationError::InvalidSource {
                path: source.to_string(),
            });
        }
        let ciphertext = self.repository.read_entry(&source)?;
        let snapshot = self.repository.snapshot()?;
        let source_title = source
            .as_path()
            .file_name()
            .and_then(|name| name.to_str())
            .expect("a string entry path has a UTF-8 file name")
            .to_owned();
        let destinations = if action == MobileMutationAction::Delete {
            Vec::new()
        } else {
            snapshot
                .directories()
                .filter(|directory| !hidden_path(directory.path().as_path()))
                .filter_map(|directory| {
                    let destination = EntryPath::parse(
                        directory
                            .path()
                            .as_path()
                            .join(source.as_path().file_name()?),
                    )
                    .ok()?;
                    (destination != source).then(|| MobileMutationDestination {
                        path: directory_path(directory.path()),
                        title: directory_title(directory.path()),
                        detail: directory_detail(directory.path()),
                        requires_overwrite: snapshot
                            .entries()
                            .any(|entry| entry.path() == &destination),
                    })
                })
                .collect()
        };
        Ok(MobileMutationPlan {
            action,
            source: source.to_string(),
            source_title,
            revision: revision(ciphertext.as_bytes()),
            destinations,
            has_open_editor,
            has_dirty_editor,
        })
    }

    pub fn perform(
        &self,
        request: &MobileMutationRequest,
        has_open_editor: bool,
        keys: &KeyStore,
        provider: &mut impl SecretProvider,
        committer: &mut impl TreeCommitter,
    ) -> Result<MobileMutationOutcome, MobileMutationError> {
        let destination = self.preflight(request, has_open_editor)?;
        let mutator = TreeMutator::new(self.repository, keys);
        let overwrite = if request.overwrite {
            OverwriteDecision::Allow
        } else {
            OverwriteDecision::Decline
        };
        let outcome = match request.action {
            MobileMutationAction::Delete => mutator.remove(
                &RemoveRequest {
                    entry: request.source.clone(),
                    recursive: false,
                    force: false,
                },
                OverwriteDecision::Allow,
                committer,
            )?,
            MobileMutationAction::Move => mutator.move_tree(
                &MoveRequest {
                    source: request.source.clone(),
                    destination: destination.expect("move destination was validated"),
                    force: request.overwrite,
                },
                overwrite,
                None,
                provider,
                committer,
            )?,
            MobileMutationAction::Copy => mutator.copy(
                &CopyRequest {
                    source: request.source.clone(),
                    destination: destination.expect("copy destination was validated"),
                    force: request.overwrite,
                },
                overwrite,
                None,
                provider,
                committer,
            )?,
        };
        let destination = outcome
            .selection()
            .map(|selection| selection.display_path());
        let (title, detail) = match request.action {
            MobileMutationAction::Move => (
                "Password Moved",
                format!(
                    "Moved {} to {}.",
                    request.source,
                    destination.as_deref().unwrap_or("the selected folder")
                ),
            ),
            MobileMutationAction::Copy => (
                "Password Copied",
                format!(
                    "Copied {} to {}.",
                    request.source,
                    destination.as_deref().unwrap_or("the selected folder")
                ),
            ),
            MobileMutationAction::Delete => {
                ("Password Deleted", format!("Deleted {}.", request.source))
            }
        };
        Ok(MobileMutationOutcome {
            action: request.action,
            source: request.source.clone(),
            destination,
            title: title.to_owned(),
            detail,
        })
    }

    fn preflight(
        &self,
        request: &MobileMutationRequest,
        has_open_editor: bool,
    ) -> Result<Option<String>, MobileMutationError> {
        let source = EntryPath::parse(&request.source)?;
        if hidden_path(source.as_path()) {
            return Err(MobileMutationError::InvalidSource {
                path: source.to_string(),
            });
        }
        let current = self.repository.read_entry(&source)?;
        if revision(current.as_bytes()) != request.revision {
            return Err(MobileMutationError::StaleEntry {
                path: source.to_string(),
            });
        }
        if has_open_editor && !request.discard_editor {
            return Err(MobileMutationError::EditorOpen {
                path: source.to_string(),
            });
        }
        if request.action == MobileMutationAction::Delete {
            if !request.confirmed {
                return Err(MobileMutationError::ConfirmationRequired {
                    path: source.to_string(),
                });
            }
            return Ok(None);
        }
        let destination = request
            .destination
            .as_deref()
            .ok_or(MobileMutationError::DestinationRequired)?;
        let destination = DirectoryPath::parse(destination)?;
        if hidden_path(destination.as_path())
            || !self
                .repository
                .snapshot()?
                .directories()
                .any(|directory| directory.path() == &destination)
        {
            return Err(MobileMutationError::InvalidDestination {
                path: directory_path(&destination),
            });
        }
        let destination_entry = EntryPath::parse(
            destination
                .as_path()
                .join(source.as_path().file_name().expect("validated source name")),
        )?;
        if destination_entry == source {
            return Err(MobileMutationError::SameDestination);
        }
        let destination_exists = match self.repository.read_entry(&destination_entry) {
            Ok(_) => true,
            Err(RepositoryError::NotFound { .. }) => false,
            Err(error) => return Err(error.into()),
        };
        if destination_exists && !request.overwrite {
            return Err(MobileMutationError::OverwriteRequired {
                path: destination_entry.to_string(),
            });
        }
        let destination = directory_path(&destination);
        Ok(Some(if destination.is_empty() {
            destination
        } else {
            format!("{destination}/")
        }))
    }
}

fn revision(ciphertext: &[u8]) -> String {
    HEXLOWER.encode(&Sha256::digest(ciphertext))
}

fn directory_path(path: &DirectoryPath) -> String {
    path.as_path().to_string_lossy().into_owned()
}

fn directory_title(path: &DirectoryPath) -> String {
    path.as_path()
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or("Password Store")
        .to_owned()
}

fn directory_detail(path: &DirectoryPath) -> String {
    if path.as_path().as_os_str().is_empty() {
        "Store root".to_owned()
    } else {
        path.to_string()
    }
}

#[derive(Debug)]
pub enum MobileMutationError {
    Repository(RepositoryError),
    Mutation(MutationError),
    InvalidSource { path: String },
    StaleEntry { path: String },
    EditorOpen { path: String },
    ConfirmationRequired { path: String },
    DestinationRequired,
    InvalidDestination { path: String },
    SameDestination,
    OverwriteRequired { path: String },
}

impl MobileMutationError {
    pub fn is_conflict(&self) -> bool {
        matches!(
            self,
            Self::StaleEntry { .. }
                | Self::EditorOpen { .. }
                | Self::OverwriteRequired { .. }
                | Self::Mutation(MutationError::TreeChanged { .. } | MutationError::Commit(_))
        )
    }

    pub fn title(&self) -> &'static str {
        match self {
            Self::StaleEntry { .. } => "Password Entry Changed",
            Self::EditorOpen { .. } => "Password Editor Is Open",
            Self::ConfirmationRequired { .. } => "Delete Confirmation Required",
            Self::DestinationRequired => "Destination Required",
            Self::InvalidDestination { .. } | Self::SameDestination => "Destination Is Invalid",
            Self::OverwriteRequired { .. } => "Password Already Exists",
            Self::InvalidSource { .. } => "Password Entry Is Unavailable",
            Self::Repository(_) | Self::Mutation(_) => "Password Action Failed",
        }
    }
}

impl fmt::Display for MobileMutationError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Repository(error) => error.fmt(formatter),
            Self::Mutation(error) => error.fmt(formatter),
            Self::InvalidSource { path } => {
                write!(formatter, "the selected entry is not mutable: {path}")
            }
            Self::StaleEntry { path } => write!(
                formatter,
                "{path} changed after the action began; refresh and try again"
            ),
            Self::EditorOpen { path } => write!(
                formatter,
                "close or explicitly discard the open editor for {path}"
            ),
            Self::ConfirmationRequired { path } => {
                write!(formatter, "confirm deletion of {path}")
            }
            Self::DestinationRequired => formatter.write_str("select a password folder"),
            Self::InvalidDestination { path } => {
                write!(
                    formatter,
                    "the selected password folder is unavailable: {path}"
                )
            }
            Self::SameDestination => {
                formatter.write_str("the source is already in the selected folder")
            }
            Self::OverwriteRequired { path } => {
                write!(
                    formatter,
                    "confirm replacement of the existing entry at {path}"
                )
            }
        }
    }
}

impl Error for MobileMutationError {}

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

impl From<MutationError> for MobileMutationError {
    fn from(error: MutationError) -> Self {
        Self::Mutation(error)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::{
        MobileMutationAction, MobileMutationError, MobileMutationRequest, MobileMutationService,
    };
    use crate::repository::Repository;

    #[test]
    fn plans_destinations_collisions_hidden_paths_and_stale_revisions()
    -> Result<(), Box<dyn std::error::Error>> {
        let temporary = tempdir()?;
        fs::create_dir_all(temporary.path().join("Personal"))?;
        fs::create_dir_all(temporary.path().join("Work"))?;
        fs::create_dir_all(temporary.path().join(".extensions"))?;
        fs::write(temporary.path().join("Personal/bank.gpg"), b"source")?;
        fs::write(temporary.path().join("Work/bank.gpg"), b"collision")?;
        let repository = Repository::open(temporary.path())?;
        let service = MobileMutationService::new(&repository);

        let plan = service.prepare("Personal/bank", MobileMutationAction::Move, true, true)?;
        assert!(plan.has_open_editor());
        assert!(plan.has_dirty_editor());
        assert!(!plan.revision().is_empty());
        assert!(plan.destinations().iter().all(|destination| {
            destination.path() != "Personal" && !destination.path().starts_with(".extensions")
        }));
        assert!(
            plan.destinations()
                .iter()
                .any(|destination| destination.path() == "" && !destination.requires_overwrite())
        );
        assert!(
            plan.destinations()
                .iter()
                .any(|destination| destination.path() == "Work" && destination.requires_overwrite())
        );

        fs::write(temporary.path().join("Personal/bank.gpg"), b"changed")?;
        let changed = service.prepare("Personal/bank", MobileMutationAction::Move, false, false)?;
        assert_ne!(plan.revision(), changed.revision());

        let stale = MobileMutationRequest {
            action: MobileMutationAction::Move,
            source: plan.source().to_owned(),
            revision: plan.revision().to_owned(),
            destination: Some("Work".to_owned()),
            confirmed: false,
            overwrite: true,
            discard_editor: true,
        };
        assert!(matches!(
            service.preflight(&stale, false),
            Err(MobileMutationError::StaleEntry { .. })
        ));

        let delete = MobileMutationRequest {
            action: MobileMutationAction::Delete,
            source: changed.source().to_owned(),
            revision: changed.revision().to_owned(),
            destination: None,
            confirmed: false,
            overwrite: false,
            discard_editor: false,
        };
        assert!(matches!(
            service.preflight(&delete, false),
            Err(MobileMutationError::ConfirmationRequired { .. })
        ));
        assert!(matches!(
            service.preflight(
                &MobileMutationRequest {
                    confirmed: true,
                    ..delete.clone()
                },
                true
            ),
            Err(MobileMutationError::EditorOpen { .. })
        ));

        let collision = MobileMutationRequest {
            action: MobileMutationAction::Copy,
            source: changed.source().to_owned(),
            revision: changed.revision().to_owned(),
            destination: Some("Work".to_owned()),
            confirmed: false,
            overwrite: false,
            discard_editor: false,
        };
        assert!(matches!(
            service.preflight(&collision, false),
            Err(MobileMutationError::OverwriteRequired { .. })
        ));
        Ok(())
    }
}
