/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::collections::BTreeMap;
use std::collections::HashSet;
use std::env;
use std::fmt;
use std::fmt::Write as _;
#[cfg(unix)]
use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use std::vec;

use anyhow::anyhow;
use anyhow::Context;
use atomicfile::atomic_write;
use byteorder::BigEndian;
use byteorder::ReadBytesExt;
use edenfs_config::EdenFsConfig;
use edenfs_error::EdenFsError;
use edenfs_error::Result;
use edenfs_error::ResultExt;
use edenfs_utils::path_from_bytes;
#[cfg(windows)]
use edenfs_utils::strip_unc_prefix;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::Serializer;
use thrift_types::edenfs::errors::eden_service::PrefetchFilesError;
use thrift_types::edenfs::types::Glob;
use thrift_types::edenfs::types::GlobParams;
use thrift_types::edenfs::types::MountInfo;
use thrift_types::edenfs::types::MountState;
use thrift_types::edenfs::types::PredictiveFetch;
use thrift_types::edenfs::types::PrefetchParams;
use thrift_types::fbthrift::ApplicationExceptionErrorCode;
use toml::value::Value;
use uuid::Uuid;

use crate::redirect::deserialize_redirections;
use crate::redirect::Redirection;
use crate::redirect::RedirectionType;
use crate::redirect::REPO_SOURCE;
use crate::EdenFsInstance;

// files in the client directory (aka data_dir aka state_dir)
const MOUNT_CONFIG: &str = "config.toml";
const SNAPSHOT: &str = "SNAPSHOT";

// Magical snapshot strings
const SNAPSHOT_MAGIC_1: &[u8] = b"eden\x00\x00\x00\x01";
const SNAPSHOT_MAGIC_2: &[u8] = b"eden\x00\x00\x00\x02";
const SNAPSHOT_MAGIC_3: &[u8] = b"eden\x00\x00\x00\x03";
const SNAPSHOT_MAGIC_4: &[u8] = b"eden\x00\x00\x00\x04";

const SUPPORTED_REPOS: &[&str] = &["git", "hg", "recas"];
const SUPPORTED_MOUNT_PROTOCOLS: &[&str] = &["fuse", "nfs", "prjfs"];

#[derive(Deserialize, Serialize, Debug)]
struct Repository {
    path: PathBuf,

    #[serde(rename = "type", deserialize_with = "deserialize_repo_type")]
    repo_type: String,

    #[serde(default = "default_guid")]
    guid: Uuid,

    #[serde(
        deserialize_with = "deserialize_protocol",
        default = "default_protocol"
    )]
    protocol: String,

    #[serde(rename = "case-sensitive", default = "default_case_sensitive")]
    case_sensitive: bool,

    #[serde(rename = "require-utf8-path", default = "default_require_utf8_path")]
    require_utf8_path: bool,

    #[serde(
        rename = "enable-sqlite-overlay",
        default = "default_sqlite_overlay",
        deserialize_with = "deserialize_sqlite_overlay"
    )]
    enable_sqlite_overlay: bool,

    #[serde(rename = "use-write-back-cache", default)]
    use_write_back_cache: bool,
}

fn default_sqlite_overlay() -> bool {
    cfg!(target_os = "windows")
}

fn deserialize_sqlite_overlay<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
    D: Deserializer<'de>,
{
    let s = bool::deserialize(deserializer)?;

    if cfg!(target_os = "windows") {
        Ok(true)
    } else {
        Ok(s)
    }
}

fn deserialize_repo_type<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;

    if SUPPORTED_REPOS.iter().any(|v| v == &s) {
        Ok(s)
    } else {
        Err(serde::de::Error::custom(format!(
            "Unsupported value: `{}`. Must be one of: {}",
            s,
            SUPPORTED_REPOS.join(", ")
        )))
    }
}

fn deserialize_protocol<'de, D>(deserializer: D) -> Result<String, D::Error>
where
    D: Deserializer<'de>,
{
    let s = String::deserialize(deserializer)?;

    if SUPPORTED_MOUNT_PROTOCOLS.iter().any(|v| v == &s) {
        Ok(s)
    } else {
        Err(serde::de::Error::custom(format!(
            "Unsupported value: `{}`. Must be one of: {}",
            s,
            SUPPORTED_MOUNT_PROTOCOLS.join(", ")
        )))
    }
}

fn default_protocol() -> String {
    if cfg!(windows) {
        "prjfs".to_string()
    } else {
        "fuse".to_string()
    }
}

fn default_guid() -> Uuid {
    Uuid::new_v4()
}

fn default_case_sensitive() -> bool {
    cfg!(target_os = "linux")
}

fn default_require_utf8_path() -> bool {
    // Existing repositories may have non-utf8 files, thus allow them by default
    true
}

#[derive(Deserialize, Serialize, Debug)]
struct PrefetchProfiles {
    #[serde(deserialize_with = "deserialize_active", default)]
    pub active: Vec<String>,
}

impl PrefetchProfiles {
    fn push(&mut self, profile: &str) {
        self.active.push(profile.into());
    }
}

fn deserialize_active<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
    D: Deserializer<'de>,
{
    let unvalidated_arr: Vec<Value> = Vec::deserialize(deserializer)?;
    let mut arr = Vec::new();
    for val in unvalidated_arr {
        if let Some(s) = val.as_str() {
            arr.push(s.to_string());
        } else {
            return Err(serde::de::Error::custom(format!(
                "Unsupported [profiles] active type {}. Must be string.",
                val
            )));
        }
    }

    Ok(arr)
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct PredictivePrefetch {
    #[serde(default)]
    predictive_prefetch_active: bool,

    #[serde(default)]
    predictive_prefetch_num_dirs: u32,
}

#[derive(Deserialize, Serialize, Debug)]
pub struct CheckoutConfig {
    repository: Repository,

    #[serde(deserialize_with = "deserialize_redirections")]
    redirections: BTreeMap<PathBuf, RedirectionType>,

    profiles: Option<PrefetchProfiles>,

    #[serde(rename = "predictive-prefetch", default)]
    predictive_prefetch: Option<PredictivePrefetch>,
}

impl CheckoutConfig {
    /// Reads checkout config information from config.toml and
    /// returns an Err if it is not properly formatted or does not exist.
    pub fn parse_config(state_dir: PathBuf) -> Result<CheckoutConfig> {
        let config_path = state_dir.join(MOUNT_CONFIG);
        let content = String::from_utf8(std::fs::read(config_path).from_err()?).from_err()?;
        let config: CheckoutConfig = toml::from_str(&content).from_err()?;
        Ok(config)
    }

    pub fn print_prefetch_profiles(&self) {
        if let Some(profiles) = &self.profiles {
            for s in profiles.active.iter() {
                println!("{}", s);
            }
        }
    }

    pub fn get_prefetch_profiles(&self) -> Result<&Vec<String>> {
        if let Some(profiles) = &self.profiles {
            Ok(&profiles.active)
        } else {
            Err(EdenFsError::Other(anyhow!(
                "Cannot get active prefetch profiles for {}",
                self.repository.path.display()
            )))
        }
    }

    pub fn contains_prefetch_profile(&self, profile: &str) -> bool {
        if let Some(profiles) = &self.profiles {
            profiles.active.iter().any(|x| x == profile)
        } else {
            false
        }
    }

    pub fn predictive_prefetch_is_active(&self) -> bool {
        if let Some(config) = &self.predictive_prefetch {
            config.predictive_prefetch_active
        } else {
            false
        }
    }

    pub fn get_predictive_num_dirs(&self) -> u32 {
        if let Some(config) = &self.predictive_prefetch {
            config.predictive_prefetch_num_dirs
        } else {
            0
        }
    }

    pub fn remove_prefetch_profile(&mut self, profile: &str, config_dir: PathBuf) -> Result<()> {
        if let Some(profiles) = &mut self.profiles {
            if profiles.active.iter().any(|x| x == profile) {
                profiles.active.retain(|x| *x != *profile);
                self.save_config(config_dir.clone()).with_context(|| {
                    anyhow!(
                        "failed to save config in the given config_dir: {}",
                        &config_dir.display()
                    )
                })?;
            }
        };
        Ok(())
    }

    pub fn update_redirections(
        &mut self,
        config_dir: &Path,
        redirs: &BTreeMap<PathBuf, Redirection>,
    ) -> Result<()> {
        self.redirections.clear();
        for (_, redir) in redirs.iter() {
            if redir.source != REPO_SOURCE {
                self.redirections
                    .insert(redir.repo_path(), redir.redir_type);
            }
        }
        self.save_config(config_dir.into())?;
        Ok(())
    }

    /// Store information about the mount in the config.toml file.
    pub fn save_config(&mut self, state_dir: PathBuf) -> Result<()> {
        let toml_out = &toml::to_string(&self).with_context(|| {
            anyhow!(
                "could not toml-ize checkout config for repo '{}'",
                self.repository.path.display()
            )
        })?;
        let config_path = state_dir.join(MOUNT_CONFIG);
        // set default permissions to 0o644 (420 in decimal)
        #[cfg(windows)]
        let perm = 0o664;

        #[cfg(not(windows))]
        let perm = std::fs::metadata(&config_path)
            .map(|meta| meta.permissions().mode())
            .unwrap_or(0o664);

        atomic_write(config_path.as_path(), perm, true, |f| {
            f.write_all(toml_out.as_bytes())?;
            Ok(())
        })
        .from_err()?;
        Ok(())
    }

    /// Add a profile to the config (read the config file and write it back
    /// with profile added). Returns true if we should fetch, false otherwise.
    pub fn activate_profile(
        &mut self,
        profile: &str,
        config_dir: PathBuf,
        force_fetch: &bool,
    ) -> Result<bool> {
        if let Some(profiles) = &mut self.profiles {
            if profiles.active.iter().any(|x| x == profile) {
                // The profile is already activated so we don't need to update the profile list,
                // but we want to return a success so we continue with the fetch
                if *force_fetch {
                    return Ok(true);
                }
                eprintln!("{} is already an active prefetch profile", profile);
                return Ok(false);
            }
            profiles.push(profile);
            self.save_config(config_dir.clone()).with_context(|| {
                anyhow!(
                    "failed to save config in the given config_dir: {}",
                    &config_dir.display()
                )
            })?;
        }
        Ok(true)
    }

    /// Switch on predictive prefetch profiles (read the config file and write
    /// it back with predictive_prefetch_profiles_active set to True, set or
    /// update predictive_prefetch_num_dirs if specified).
    pub fn activate_predictive_profile(
        &mut self,
        config_dir: PathBuf,
        num_dirs: u32,
    ) -> Result<()> {
        if let Some(profiles) = &mut self.predictive_prefetch {
            if profiles.predictive_prefetch_active
                && num_dirs == profiles.predictive_prefetch_num_dirs
            {
                return Err(EdenFsError::Other(anyhow!(
                    "Predictive prefetch profiles are already activated \
                            with {} directories configured.",
                    num_dirs
                )));
            }
            profiles.predictive_prefetch_active = true;
            profiles.predictive_prefetch_num_dirs = num_dirs;
            self.save_config(config_dir.clone()).with_context(|| {
                anyhow!(
                    "failed to save config in the given config_dir: {}",
                    &config_dir.display()
                )
            })?;
        }
        Ok(())
    }

    /// Remove a profile to the config (read the config file and write it back
    /// with profile added).
    pub fn deactivate_profile(&mut self, profile: &str, config_dir: PathBuf) -> Result<()> {
        if let Some(profiles) = &mut self.profiles {
            if !profiles.active.iter().any(|x| x == profile) {
                return Err(EdenFsError::Other(anyhow!(
                    "Profile {} was not deactivated since it wasn't active.",
                    profile
                )));
            }
            profiles.active.retain(|x| *x != *profile);
            self.save_config(config_dir.clone()).with_context(|| {
                anyhow!(
                    "failed to save config in the given config_dir: {}",
                    &config_dir.display()
                )
            })?;
        };
        Ok(())
    }

    /// Switch off predictive prefetch profiles (read the config file and write
    /// it back with predictive_profile_profiles_active set to false. Also
    /// set predictive_prefetch_num_dirs to 0).
    pub fn deactivate_predictive_profile(&mut self, config_dir: PathBuf) -> Result<()> {
        if let Some(profiles) = &mut self.predictive_prefetch {
            if !profiles.predictive_prefetch_active {
                return Err(EdenFsError::Other(anyhow!(
                    "Predictive prefetch profile was not deactivated since it \
                    wasn't active."
                )));
            }
            profiles.predictive_prefetch_active = false;
            profiles.predictive_prefetch_num_dirs = 0;
            self.save_config(config_dir.clone()).with_context(|| {
                anyhow!(
                    "failed to save config in the given config_dir: {}",
                    &config_dir.display()
                )
            })?;
        };
        Ok(())
    }
}

pub struct SnapshotState {
    pub working_copy_parent: String,
    pub last_checkout_hash: String,
}

impl SnapshotState {
    fn new(working_copy_parent: String, last_checkout_hash: String) -> Self {
        Self {
            working_copy_parent,
            last_checkout_hash,
        }
    }
}

fn is_unknown_method_error(error: &PrefetchFilesError) -> bool {
    if let PrefetchFilesError::ApplicationException(ref e) = error {
        e.type_ == ApplicationExceptionErrorCode::UnknownMethod
    } else {
        false
    }
}

/// Represents an edenfs checkout with mount information as well as information from configuration
#[derive(Serialize)]
pub struct EdenFsCheckout {
    /// E.g., /data/sandcastle/boxes/fbsource
    #[serde(skip)]
    path: PathBuf,
    /// E.g., /home/unixname/local/.eden/clients/fbsource
    data_dir: PathBuf,
    /// This is None when it's just configured but not actively mounted in eden
    #[serde(serialize_with = "serialize_state")]
    state: Option<MountState>,
    /// If this is false, that means this model is only populated with mount info from edenfs
    /// As opposed to being populated with information from the configuration & live mount info.
    configured: bool,
    backing_repo: Option<PathBuf>,
    #[serde(skip)]
    pub(crate) redirections: Option<BTreeMap<PathBuf, RedirectionType>>,
}

impl EdenFsCheckout {
    pub fn path(&self) -> PathBuf {
        self.path.clone()
    }

    pub fn data_dir(&self) -> PathBuf {
        self.data_dir.clone()
    }

    pub fn fsck_dir(&self) -> PathBuf {
        self.data_dir.join("fsck")
    }

    fn encode_hex(bytes: &[u8]) -> String {
        let mut s = String::with_capacity(bytes.len() * 2);
        for &b in bytes {
            write!(&mut s, "{:02x}", b).unwrap();
        }
        s
    }

    /// Returns a SnapshotState representing EdenFS working copy parent as well as the last checked
    /// out revision.
    pub fn get_snapshot(&self) -> Result<SnapshotState> {
        let snapshot_path = self.data_dir.join(SNAPSHOT);
        let mut f = File::open(&snapshot_path).from_err()?;
        let mut header = [0u8; 8];
        f.read(&mut header).from_err()?;
        if header == SNAPSHOT_MAGIC_1 {
            let mut snapshot = [0u8; 20];
            f.read(&mut snapshot).from_err()?;
            let decoded = EdenFsCheckout::encode_hex(&snapshot);
            Ok(SnapshotState::new(decoded.clone(), decoded))
        } else if header == SNAPSHOT_MAGIC_2 {
            let body_length = f.read_u32::<BigEndian>().from_err()?;
            let mut buf = vec![0u8; body_length as usize];
            f.read_exact(&mut buf).from_err()?;
            let decoded = std::str::from_utf8(&buf).from_err()?.to_string();
            Ok(SnapshotState::new(decoded.clone(), decoded))
        } else if header == SNAPSHOT_MAGIC_3 {
            let _pid = f.read_u32::<BigEndian>().from_err()?;

            let from_length = f.read_u32::<BigEndian>().from_err()?;
            let mut from_buf = vec![0u8; from_length as usize];
            f.read_exact(&mut from_buf).from_err()?;

            let to_length = f.read_u32::<BigEndian>().from_err()?;
            let mut to_buf = vec![0u8; to_length as usize];
            f.read_exact(&mut to_buf).from_err()?;

            // TODO(xavierd): return a proper object that the caller could use.
            Err(EdenFsError::Other(anyhow!(
                "A checkout operation is ongoing from {} to {}",
                std::str::from_utf8(&from_buf).from_err()?,
                std::str::from_utf8(&to_buf).from_err()?
            )))
        } else if header == SNAPSHOT_MAGIC_4 {
            let working_copy_parent_length = f.read_u32::<BigEndian>().from_err()?;
            let mut working_copy_parent_buf = vec![0u8; working_copy_parent_length as usize];
            f.read_exact(&mut working_copy_parent_buf).from_err()?;

            let checked_out_length = f.read_u32::<BigEndian>().from_err()?;
            let mut checked_out_buf = vec![0u8; checked_out_length as usize];
            f.read_exact(&mut checked_out_buf).from_err()?;

            Ok(SnapshotState::new(
                std::str::from_utf8(&working_copy_parent_buf)
                    .from_err()?
                    .to_string(),
                std::str::from_utf8(&checked_out_buf)
                    .from_err()?
                    .to_string(),
            ))
        } else {
            Err(EdenFsError::Other(anyhow!(
                "SNAPSHOT file has invalid header"
            )))
        }
    }

    pub fn backing_repo(&self) -> Option<PathBuf> {
        self.backing_repo.clone()
    }

    fn from_mount_info(path: PathBuf, thrift_mount: MountInfo) -> Result<EdenFsCheckout> {
        Ok(EdenFsCheckout {
            path,
            data_dir: path_from_bytes(&thrift_mount.edenClientPath)?,
            state: Some(thrift_mount.state),
            configured: false,
            backing_repo: match thrift_mount.backingRepoPath {
                Some(path_string) => Some(path_from_bytes(&path_string)?),
                None => None,
            },
            redirections: None,
        })
    }

    fn from_config(path: PathBuf, data_dir: PathBuf, config: CheckoutConfig) -> EdenFsCheckout {
        EdenFsCheckout {
            path,
            data_dir,
            state: None,
            configured: true,
            backing_repo: Some(config.repository.path.clone()),
            redirections: Some(config.redirections),
        }
    }

    fn update_with_config(&mut self, config: CheckoutConfig) {
        if self.backing_repo.is_none() {
            self.backing_repo = Some(config.repository.path.clone());
        }
        self.configured = true;
    }

    pub fn get_contents_for_profile(
        &self,
        profile: &String,
        silent: bool,
    ) -> Result<HashSet<String>> {
        const RELATIVE_PROFILES_LOCATION: &str = "xplat/scm/prefetch_profiles/profiles";
        let profile_path = self.path.join(RELATIVE_PROFILES_LOCATION).join(profile);

        if !profile_path.exists() {
            if !silent {
                eprintln!(
                    "Profile '{}' not found for checkout {}.",
                    profile,
                    self.path().display()
                );
            }
            return Ok(HashSet::new());
        }

        let file = File::open(&profile_path).with_context(|| {
            anyhow!("Sparse profile '{}' does not exist", profile_path.display())
        })?;
        Ok(BufReader::new(file)
            .lines()
            .collect::<std::io::Result<HashSet<_>>>()
            .with_context(|| {
                anyhow!(
                    "Cannot read conents for prefetch profile '{}'",
                    profile_path.display()
                )
            })?)
    }

    /// Function to actually cause the prefetch, can be called on a background
    /// process or in the main process.
    /// Only print here if silent is False, as that could send messages
    /// randomly to stdout.
    pub async fn make_prefetch_request(
        &self,
        instance: &EdenFsInstance,
        all_profile_contents: HashSet<String>,
        directories_only: bool,
        silent: bool,
        revisions: Option<&Vec<String>>,
        predict_revisions: bool,
        background: bool,
        predictive: bool,
        predictive_num_dirs: u32,
    ) -> Result<Glob> {
        let mut commit_vec = vec![];
        if predict_revisions {
            // The arc and hg commands need to be run in the mount mount, so we need
            // to change the working path if it is not within the mount.
            let cwd = env::current_dir().context("Unable to get current working directory")?;
            let mut changed_dir = false;
            if find_checkout(instance, &cwd).is_err() {
                println!("Setting the current working directory");
                env::set_current_dir(&self.path).with_context(|| {
                    anyhow!(
                        "failed to change working directory to '{}'",
                        self.path.display()
                    )
                })?;
                changed_dir = true;
            }

            let output = Command::new("arc")
                .arg("stable")
                .arg("best")
                .arg("--verbose")
                .arg("error")
                .output()
                .with_context(|| {
                    anyhow!("Failed to execute subprocess `arc stable best --verbose error`")
                })?;
            if !output.status.success() {
                return Err(EdenFsError::Other(anyhow!(
                    "Unable to predict commits to prefetch, error finding bookmark \
            to prefetch: {}",
                    String::from_utf8_lossy(output.stderr.as_slice())
                )));
            }

            let bookmark = String::from_utf8_lossy(output.stdout.as_slice());
            let bookmark = bookmark.trim();

            let output = Command::new("hg")
                .arg("log")
                .arg("-r")
                .arg(bookmark)
                .arg("-T")
                .arg("{node}")
                .output()
                .with_context(|| {
                    anyhow!(
                        "Failed to execute subprocess `hg log -r {} -T {{node}}`",
                        bookmark
                    )
                })?;

            if !output.status.success() {
                return Err(EdenFsError::Other(anyhow!(
                    "Unable to predict commits to prefetch, error converting \
                bookmark to commit: {}",
                    String::from_utf8_lossy(output.stderr.as_slice())
                )));
            }

            // If we changed directories to run the subcommands, we should switch
            // back to our previous location
            if changed_dir {
                env::set_current_dir(&cwd)
                    .context("failed to change back to old working directory")?;
            }

            let commit = String::from_utf8_lossy(output.stdout.as_slice());
            let commit = commit.trim().as_bytes().to_vec();
            commit_vec.push(commit);
        }

        if let Some(revs) = revisions {
            for rev in revs {
                let commit = rev.trim().as_bytes().to_vec();
                commit_vec.push(commit);
            }
        }

        let client = instance.connect(None).await?;
        let mnt_pt = self
            .path
            .to_str()
            .context("failed to get mount point as str")?
            .as_bytes()
            .to_vec();
        if predictive {
            let num_dirs = if predictive_num_dirs != 0 {
                predictive_num_dirs
                    .try_into()
                    .with_context(|| {
                        anyhow!("could not convert u32 ({}) to i32", predictive_num_dirs)
                    })
                    .ok()
            } else {
                None
            };
            let predictive_params = PredictiveFetch {
                numTopDirectories: num_dirs,
                ..Default::default()
            };
            let glob_params = GlobParams {
                mountPoint: mnt_pt,
                includeDotfiles: false,
                prefetchFiles: !directories_only,
                suppressFileList: silent,
                revisions: commit_vec,
                background,
                predictiveGlob: Some(predictive_params),
                ..Default::default()
            };
            let res = client.predictiveGlobFiles(&glob_params).await;
            Ok(res.context("Failed predictiveGlobFiles() thrift call")?)
        } else {
            let profile_set = all_profile_contents.into_iter().collect::<Vec<_>>();
            let prefetch_params = PrefetchParams {
                mountPoint: mnt_pt.clone(),
                globs: profile_set.clone(),
                directoriesOnly: directories_only,
                revisions: commit_vec.clone(),
                background,
                ..Default::default()
            };
            let res = client.prefetchFiles(&prefetch_params).await;

            match res {
                Ok(_) => Ok(Glob::default()),
                Err(error) => {
                    if is_unknown_method_error(&error) {
                        let glob_params = GlobParams {
                            mountPoint: mnt_pt,
                            globs: profile_set,
                            includeDotfiles: false,
                            prefetchFiles: !directories_only,
                            suppressFileList: silent,
                            revisions: commit_vec,
                            background,
                            ..Default::default()
                        };
                        let glob_res = client.globFiles(&glob_params).await;
                        Ok(glob_res.context("Failed globFiles() thrift call")?)
                    } else {
                        Err(EdenFsError::Other(error.into()))
                    }
                }
            }
        }
    }

    pub fn should_prefetch_profiles(config: &EdenFsConfig) -> bool {
        config.prefetch_profiles.prefetching_enabled.unwrap_or(true)
    }

    pub fn should_prefetch_predictive_profiles(config: &EdenFsConfig) -> bool {
        config
            .prefetch_profiles
            .predictive_prefetching_enabled
            .unwrap_or(false)
    }

    pub async fn prefetch_profiles(
        &self,
        instance: &EdenFsInstance,
        profiles: &Vec<String>,
        background: bool,
        directories_only: bool,
        silent: bool,
        revisions: Option<&Vec<String>>,
        predict_revisions: bool,
        predictive: bool,
        predictive_num_dirs: u32,
    ) -> Result<Vec<Glob>> {
        let mut profiles_to_fetch = profiles.clone();

        let config = instance
            .get_config()
            .context("unable to load configuration")?;

        if predictive && !EdenFsCheckout::should_prefetch_predictive_profiles(&config) {
            if !silent {
                eprintln!(
                    "Skipping Predictive Prefetch Profiles fetch due to global kill switch. \
                    This means prefetch-profiles.predictive-prefetching-enabled is not set in \
                    the EdenFS configs.",
                );
            } else {
                return Ok(vec![Glob::default()]);
            }
        }

        if !EdenFsCheckout::should_prefetch_profiles(&config) && !predictive {
            if !silent {
                eprintln!(
                    "Skipping Prefetch Profiles fetch due to global kill switch. \
                    This means prefetch-profiles.prefetching-enabled is not set in \
                    the EdenFS configs."
                );
            }
            return Ok(vec![Glob::default()]);
        }

        let mut profile_contents = HashSet::new();
        let mut glob_results = vec![];

        if !predictive {
            // special trees prefetch profile which fetches all of the trees in the repo, kick this
            // off before activating the rest of the prefetch profiles
            let tree_profile = "trees";
            if profiles_to_fetch.iter().any(|x| x == tree_profile) {
                profiles_to_fetch.retain(|x| *x != *tree_profile);
                let mut profile_set = HashSet::new();
                profile_set.insert("**/*".to_owned());

                let blob_res = self
                    .make_prefetch_request(
                        instance,
                        profile_set,
                        true, // only prefetch directories
                        silent,
                        revisions.clone(),
                        predict_revisions,
                        background,
                        predictive,
                        predictive_num_dirs,
                    )
                    .await
                    .with_context(|| anyhow!("make_prefetch_request() failed, returning early"))?;
                glob_results.push(blob_res);
                if profiles_to_fetch.is_empty() {
                    return Ok(glob_results);
                }
            }

            for profile in profiles_to_fetch {
                let res = self
                    .get_contents_for_profile(&profile, silent)
                    .with_context(|| {
                        anyhow!("failed to get contents of prefetch profile {}", &profile)
                    })?;
                profile_contents.extend(res);
            }
        }
        let blob_res = self
            .make_prefetch_request(
                instance,
                profile_contents,
                directories_only,
                silent,
                revisions,
                predict_revisions,
                background,
                predictive,
                predictive_num_dirs,
            )
            .await
            .with_context(|| anyhow!("make_prefetch_request() failed, returning early"))?;
        glob_results.push(blob_res);
        Ok(glob_results)
    }
}

fn serialize_state<S>(field: &Option<MountState>, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.serialize_str(&match *field {
        Some(state) => {
            format!("{}", state)
        }
        None => "NOT_RUNNING".to_string(),
    })
}

impl fmt::Display for EdenFsCheckout {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let suffix = if self.configured {
            ""
        } else {
            " (unconfigured)"
        };

        let state_str = match self.state {
            Some(state) => {
                if state == MountState::RUNNING {
                    String::new()
                } else {
                    format!(" ({})", state)
                }
            }
            None => " (not mounted)".to_string(),
        };

        write!(f, "{}{}{}", self.path.display(), state_str, suffix)
    }
}

/// Return information about all checkouts defined in EdenFS's configuration files
/// and all information about mounted checkouts from the eden daemon
pub async fn get_mounts(instance: &EdenFsInstance) -> Result<BTreeMap<PathBuf, EdenFsCheckout>> {
    // Get all configured checkout info (including not mounted / not active ones) from configs
    let mut configs: Vec<(PathBuf, PathBuf, CheckoutConfig)> = Vec::new();
    for (mount_path, client_name) in instance.get_configured_mounts_map()? {
        configs.push((
            mount_path,
            instance.config_directory(&client_name),
            CheckoutConfig::parse_config(instance.config_directory(&client_name))?,
        ));
    }

    // Get active mounted checkouts info from eden daemon
    let client = instance.connect(Some(Duration::from_secs(3))).await;
    let mounted_checkouts = match client {
        Ok(client) => Some(client.listMounts().await.from_err()?),
        Err(_) => None, // eden daemon not running
    };

    // Combine mount info from active mounts and mount info from config files
    let mut mount_points = BTreeMap::new();
    if let Some(mounts) = mounted_checkouts {
        for thrift_mount in mounts {
            let path = path_from_bytes(&thrift_mount.mountPoint)?;
            mount_points.insert(
                path.clone(),
                EdenFsCheckout::from_mount_info(path.clone(), thrift_mount)?,
            );
        }
    }

    for (path, data_dir, config) in configs {
        match mount_points.get_mut(&path) {
            Some(mount_info) => {
                mount_info.update_with_config(config);
            }
            None => {
                mount_points.insert(
                    path.clone(),
                    EdenFsCheckout::from_config(path.clone(), data_dir, config),
                );
            }
        };
    }

    Ok(mount_points)
}

#[cfg(windows)]
#[derive(Deserialize)]
struct WindowsEdenConfigInner {
    root: PathBuf,
    client: PathBuf,
}

#[cfg(windows)]
#[derive(Deserialize)]
struct WindowsEdenConfig {
    #[serde(rename = "Config")]
    config: WindowsEdenConfigInner,
}

#[cfg(windows)]
fn get_checkout_root_state(path: &Path) -> Result<(Option<PathBuf>, Option<PathBuf>)> {
    let mut checkout_root = None;
    let mut checkout_state_dir = None;

    // On Windows, walk backwards through the path until you find the `.eden` folder
    let mut curr_dir = Some(path.clone());
    while let Some(candidate_dir) = curr_dir {
        if candidate_dir.join(".eden").exists() {
            let config_file = candidate_dir.join(".eden").join("config");
            let config = std::fs::read_to_string(config_file).from_err()?;
            let config = toml::from_str::<WindowsEdenConfig>(&config).from_err()?;
            checkout_root = Some(config.config.root);
            checkout_state_dir = Some(config.config.client);
            break;
        } else {
            curr_dir = candidate_dir.parent();
        }
    }
    Ok((checkout_root, checkout_state_dir))
}

#[cfg(not(windows))]
fn get_checkout_root_state(path: &Path) -> Result<(Option<PathBuf>, Option<PathBuf>)> {
    // We will get an error if any of these symlinks do not exist
    let eden_socket_path = fs::read_link(path.join(".eden").join("socket"));
    if eden_socket_path.is_ok() {
        let checkout_root = fs::read_link(path.join(".eden").join("root")).ok();
        let checkout_state_dir = fs::read_link(path.join(".eden").join("client")).ok();
        Ok((checkout_root, checkout_state_dir))
    } else {
        Ok((None, None))
    }
}

/// If the path provided is an eden checkout, this returns an object representing that checkout.
/// Otherwise, if the path provided is not an eden checkout, this returns an EdenFsError.
pub fn find_checkout(instance: &EdenFsInstance, path: &Path) -> Result<EdenFsCheckout> {
    // Resolve symlinks and get absolute path
    let path = path.canonicalize().from_err()?;
    #[cfg(windows)]
    let path = strip_unc_prefix(path);

    // Check if it is a mounted checkout
    let (checkout_root, checkout_state_dir) = get_checkout_root_state(&path)?;

    if checkout_root.is_none() {
        // Find `checkout_path` that `path` is a sub path of
        let all_checkouts = instance.get_configured_mounts_map()?;
        if let Some(item) = all_checkouts
            .iter()
            .find(|&(checkout_path, _)| path.starts_with(checkout_path))
        {
            let (checkout_path, checkout_name) = item;
            let checkout_state_dir = instance.config_directory(checkout_name);
            Ok(EdenFsCheckout::from_config(
                PathBuf::from(checkout_path),
                checkout_state_dir.clone(),
                CheckoutConfig::parse_config(checkout_state_dir)?,
            ))
        } else {
            Err(EdenFsError::Other(anyhow!(
                "Checkout path {} is not handled by EdenFS",
                path.display()
            )))
        }
    } else if checkout_state_dir.is_none() {
        let all_checkouts = instance.get_configured_mounts_map()?;
        let checkout_path = checkout_root.unwrap();
        if let Some(checkout_name) = all_checkouts.get(&checkout_path) {
            let checkout_state_dir = instance.config_directory(checkout_name);
            Ok(EdenFsCheckout::from_config(
                checkout_path,
                checkout_state_dir.clone(),
                CheckoutConfig::parse_config(checkout_state_dir)?,
            ))
        } else {
            Err(EdenFsError::Other(anyhow!(
                "unknown checkout {}",
                checkout_path.display()
            )))
        }
    } else {
        Ok(EdenFsCheckout::from_config(
            checkout_root.unwrap(),
            checkout_state_dir.as_ref().unwrap().clone(),
            CheckoutConfig::parse_config(checkout_state_dir.unwrap())?,
        ))
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;
    use std::fs::File;
    use std::io::Write;
    use std::path::Path;
    use std::path::PathBuf;

    use anyhow::anyhow;
    use anyhow::Context;
    use edenfs_error::EdenFsError;
    use edenfs_error::Result;
    use edenfs_error::ResultExt;
    use tempfile::tempdir;
    use tempfile::TempDir;

    use crate::checkout::CheckoutConfig;
    use crate::checkout::RedirectionType;
    use crate::checkout::MOUNT_CONFIG;
    use crate::checkout::REPO_SOURCE;
    use crate::redirect::Redirection;

    // path and type are required... /tmp/ is probably the safest place to use as the path
    const DEFAULT_CHECKOUT_CONFIG: &str = r#"
    [repository]
    path = "/tmp/"
    type = "hg"

    [redirections]

    [profiles]

    [predictive-prefetch]"#;

    /// creates a checkout config and returns the tempdir in which the config is located
    fn create_test_checkout_config() -> Result<TempDir> {
        let temp_dir = tempdir().context("couldn't create temp dir")?;
        let mut config_file = File::create(temp_dir.path().join(MOUNT_CONFIG)).from_err()?;
        config_file
            .write_all(DEFAULT_CHECKOUT_CONFIG.as_bytes())
            .from_err()?;
        Ok(temp_dir)
    }

    /// Takes a (repo_path, redir_type) pair and checks if a checkout config contains that redirection
    fn checkout_config_contains_redirection(
        repo_path: &Path,
        redir_type: RedirectionType,
        config_dir: &Path,
    ) -> Result<()> {
        let config = CheckoutConfig::parse_config(config_dir.into())?;
        let config_redir_type = config.redirections.get(repo_path);
        match config_redir_type {
            Some(r_type) => {
                if *r_type != redir_type {
                    Err(EdenFsError::Other(anyhow!(
                        "Redirection type did not match the redirection type in the config"
                    )))
                } else {
                    Ok(())
                }
            }
            None => Err(EdenFsError::Other(anyhow!(
                "Did not find redirection in checkout config"
            ))),
        }
    }

    fn update_and_test_redirection(
        redir: Redirection,
        config_dir: &Path,
        should_be_inserted: bool,
    ) -> Result<()> {
        // parse config from test_path
        let mut config = CheckoutConfig::parse_config(config_dir.into())?;

        // create map of redirections we want to add to the checkout config
        let mut redir_map: BTreeMap<PathBuf, Redirection> = BTreeMap::new();

        // We need to track repo_path and redir type to confirm the redirection was added correctly
        let repo_path = redir.repo_path();
        let redir_type = redir.redir_type;

        // insert the redirection and update the checkout config
        redir_map.insert(repo_path.clone(), redir);
        config
            .update_redirections(config_dir, &redir_map)
            .expect("failed to update checkout config");

        // check if redirection was inserted (or not inserted) correctly
        if should_be_inserted {
            checkout_config_contains_redirection(&repo_path, redir_type, config_dir)
        } else {
            // In some cases, we don't want the checkout to be inserted (when source == REPO_SOURCE)
            // In that case, we should fail if we find the redirection in the checkout config
            match checkout_config_contains_redirection(&repo_path, redir_type, config_dir) {
                Ok(_) => Err(EdenFsError::Other(anyhow!(
                    "Redirection was present in config when it shouldn't have been present"
                ))),
                Err(_) => Ok(()),
            }
        }
    }

    #[test]
    fn test_update_redirections() {
        let config_test_dir =
            create_test_checkout_config().expect("failed to create test checkout config");
        let config_dir = config_test_dir.path();

        // test inserting a sylink redirection (this should be inserted because source != REPO_SOURCE)
        let redir1 = Redirection {
            repo_path: PathBuf::from("test_path"),
            redir_type: RedirectionType::Symlink,
            target: None,
            source: "NotARepoSource".into(),
            state: None,
        };
        assert!(update_and_test_redirection(redir1, config_dir, true).is_ok());

        // test inserting a symlink redirection whose source == REPO_SOURCE
        let redir2 = Redirection {
            repo_path: PathBuf::from("test_path2"),
            redir_type: RedirectionType::Symlink,
            target: None,
            source: REPO_SOURCE.into(),
            state: None,
        };
        assert!(update_and_test_redirection(redir2, config_dir, false).is_ok());

        // test inserting a bind redirection whose source != REPO_SOURCE
        let redir3 = Redirection {
            repo_path: PathBuf::from("test_path3"),
            redir_type: RedirectionType::Bind,
            target: None,
            source: "NotARepoSource".into(),
            state: None,
        };
        assert!(update_and_test_redirection(redir3, config_dir, true).is_ok());

        // test inserting a bind redirection whose source == REPO_SOURCE
        let redir4 = Redirection {
            repo_path: PathBuf::from("test_path4"),
            redir_type: RedirectionType::Bind,
            target: None,
            source: REPO_SOURCE.into(),
            state: None,
        };
        assert!(update_and_test_redirection(redir4, config_dir, false).is_ok());
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

//! EdenFsInstance - manages EdenFS resources besides Thrift connection (managed by
//! [`EdenFsClient`]).

use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;

use anyhow::anyhow;
use anyhow::Context;
use edenfs_config::EdenFsConfig;
use edenfs_error::EdenFsError;
use edenfs_error::Result;
use edenfs_error::ResultExt;
use edenfs_utils::get_executable;
#[cfg(windows)]
use edenfs_utils::strip_unc_prefix;
#[cfg(fbcode_build)]
use fbinit::expect_init;
use fbthrift_socket::SocketTransport;
use once_cell::sync::OnceCell;
#[cfg(fbcode_build)]
use thrift_streaming::streaming_eden_service::StreamStartStatusError;
#[cfg(fbcode_build)]
use thrift_streaming_thriftclients::build_StreamingEdenService_client;
use thrift_types::edenfs::client::EdenService;
use thrift_types::edenfs::types::DaemonInfo;
use thrift_types::fb303_core::types::fb303_status;
use thrift_types::fbthrift::binary_protocol::BinaryProtocol;
use thrift_types::fbthrift::ApplicationExceptionErrorCode;
#[cfg(fbcode_build)]
use thriftclient::ThriftChannelBuilder;
#[cfg(fbcode_build)]
use thriftclient::TransportType;
use tokio_uds_compat::UnixStream;
use tracing::event;
use tracing::Level;

use crate::EdenFsClient;
#[cfg(fbcode_build)]
use crate::StartStatusStream;
#[cfg(fbcode_build)]
use crate::StreamingEdenFsClient;

// We should create a single EdenFsInstance when parsing EdenFs commands and utilize
// EdenFsInstance::global() whenever we need to access it. This way we can avoid passing an
// EdenFsInstance through every subcommand
static INSTANCE: OnceCell<EdenFsInstance> = OnceCell::new();

/// These paths are relative to the user's client directory.
const CLIENTS_DIR: &str = "clients";
const CONFIG_JSON: &str = "config.json";

#[derive(Debug)]
pub struct EdenFsInstance {
    config_dir: PathBuf,
    etc_eden_dir: PathBuf,
    home_dir: Option<PathBuf>,
}

impl EdenFsInstance {
    pub fn global() -> &'static EdenFsInstance {
        INSTANCE.get().expect("EdenFsInstance is not initialized")
    }

    pub fn init(config_dir: PathBuf, etc_eden_dir: PathBuf, home_dir: Option<PathBuf>) {
        event!(
            Level::TRACE,
            ?config_dir,
            ?etc_eden_dir,
            ?home_dir,
            "Creating EdenFsInstance"
        );
        INSTANCE
            .set(Self {
                config_dir,
                etc_eden_dir,
                home_dir,
            })
            .expect("should be able to initialize EdenfsInstance")
    }

    pub fn get_config(&self) -> Result<EdenFsConfig> {
        edenfs_config::load_config(
            &self.etc_eden_dir,
            self.home_dir.as_ref().map(|x| x.as_ref()),
        )
    }

    pub fn get_user_home_dir(&self) -> Option<&PathBuf> {
        self.home_dir.as_ref()
    }

    async fn _connect(&self, socket_path: &PathBuf) -> Result<EdenFsClient> {
        let stream = UnixStream::connect(&socket_path)
            .await
            .map_err(EdenFsError::ThriftIoError)?;
        let transport = SocketTransport::new(stream);
        let client = <dyn EdenService>::new(BinaryProtocol, transport);

        Ok(client)
    }

    pub async fn connect(&self, timeout: Option<Duration>) -> Result<EdenFsClient> {
        let socket_path = self.config_dir.join("socket");

        let connect = self._connect(&socket_path);
        let res = if let Some(timeout) = timeout {
            tokio::time::timeout(timeout, connect)
                .await
                .map_err(|_| EdenFsError::ThriftConnectionTimeout(socket_path))?
        } else {
            connect.await
        };

        res
    }

    #[cfg(fbcode_build)]
    pub async fn _connect_streaming(&self, socket_path: &PathBuf) -> Result<StreamingEdenFsClient> {
        let client = build_StreamingEdenService_client(
            ThriftChannelBuilder::from_path(expect_init(), socket_path)?
                .with_transport_type(TransportType::Rocket)
                .with_secure(false),
        )?;
        Ok(client)
    }

    #[cfg(fbcode_build)]
    pub async fn connect_streaming(
        &self,
        timeout: Option<Duration>,
    ) -> Result<StreamingEdenFsClient> {
        let socket_path = self.config_dir.join("socket");
        let client = self._connect_streaming(&socket_path);

        if let Some(timeout) = timeout {
            tokio::time::timeout(timeout, client)
                .await
                .map_err(|_| EdenFsError::ThriftConnectionTimeout(socket_path))?
        } else {
            client.await
        }
    }

    #[cfg(windows)]
    fn pidfile(&self) -> PathBuf {
        self.config_dir.join("pid")
    }

    #[cfg(unix)]
    fn pidfile(&self) -> PathBuf {
        self.config_dir.join("lock")
    }

    /// Read the pid from the EdenFS lockfile
    fn pid(&self) -> Result<sysinfo::Pid, anyhow::Error> {
        let pidfile = self.pidfile();
        let pid_bytes = std::fs::read(&pidfile)
            .with_context(|| format!("Unable to read from pid file '{}'", pidfile.display()))?;
        let pid_str =
            std::str::from_utf8(&pid_bytes).context("Unable to parse pid file as UTF-8 string")?;

        pid_str
            .trim()
            .parse()
            .with_context(|| format!("Unable to parse pid file content: '{}'", pid_str))
    }

    /// Retrieving running EdenFS process status based on lock file
    pub fn status_from_lock(&self) -> Result<i32, anyhow::Error> {
        let pid = self.pid()?;

        let exe = match get_executable(pid) {
            Some(exe) => exe,
            None => {
                tracing::debug!("PID {} is not running", pid);
                return Err(anyhow!("EdenFS is not running"));
            }
        };
        let name = match exe.file_name() {
            Some(name) => name.to_string_lossy(),
            None => {
                tracing::debug!("Unable to retrieve information about PID {}", pid);
                return Err(anyhow!("EdenFS is not running"));
            }
        };

        tracing::trace!(?name, "executable name");

        if name == "edenfs"
            || name == "fake_edenfs"
            || (cfg!(windows) && name.ends_with("edenfs.exe"))
        {
            Err(anyhow!(
                "EdenFS's Thrift server does not appear to be running, \
                but the process is still alive (PID={})",
                pid
            ))
        } else {
            Err(anyhow!("EdenFS is not running"))
        }
    }

    pub async fn get_health(&self, timeout: Option<Duration>) -> Result<DaemonInfo> {
        let client = self
            .connect(timeout.or_else(|| Some(Duration::from_secs(3))))
            .await
            .context("Unable to connect to EdenFS daemon")?;
        event!(Level::DEBUG, "connected to EdenFS daemon");
        client.getDaemonInfo().await.from_err()
    }

    #[cfg(fbcode_build)]
    pub async fn get_health_with_startup_updates_included(
        &self,
        timeout: Duration,
    ) -> Result<(DaemonInfo, StartStatusStream)> {
        let client = self
            .connect_streaming(Some(timeout))
            .await
            .context("Unable to connect to EdenFS daemon")?;
        let result = client.streamStartStatus().await;
        match result {
            Err(StreamStartStatusError::ApplicationException(e))
                if e.type_ == ApplicationExceptionErrorCode::UnknownMethod =>
            {
                Err(EdenFsError::UnknownMethod(e.message))
            }
            r => r.from_err(),
        }
    }

    /// Returns a map of mount paths to mount names
    /// as defined in EdenFS's config.json.
    pub fn get_configured_mounts_map(&self) -> Result<BTreeMap<PathBuf, String>, anyhow::Error> {
        let directory_map = self.config_dir.join(CONFIG_JSON);
        match std::fs::read_to_string(&directory_map) {
            Ok(buff) => {
                let string_map = serde_json::from_str::<BTreeMap<String, String>>(&buff)
                    .with_context(|| format!("Failed to parse directory map: {:?}", &buff))?;
                Ok(string_map
                    .into_iter()
                    .map(|(key, val)| (key.into(), val))
                    .collect())
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(BTreeMap::new()),
            Err(e) => Err(e)
                .with_context(|| format!("Failed to read directory map from {:?}", directory_map)),
        }
    }

    pub fn clients_dir(&self) -> PathBuf {
        self.config_dir.join(CLIENTS_DIR)
    }

    pub fn logs_dir(&self) -> PathBuf {
        self.config_dir.join("logs")
    }

    pub fn storage_dir(&self) -> PathBuf {
        self.config_dir.join("storage")
    }

    pub fn client_name(&self, path: &Path) -> Result<String> {
        // Resolve symlinks and get absolute path
        let path = path.canonicalize().from_err()?;
        #[cfg(windows)]
        let path = strip_unc_prefix(path);

        // Find `checkout_path` that `path` is a sub path of
        let all_checkouts = self.get_configured_mounts_map()?;
        if let Some(item) = all_checkouts
            .iter()
            .find(|&(checkout_path, _)| path.starts_with(checkout_path))
        {
            let (_, checkout_name) = item;
            Ok(checkout_name.clone())
        } else {
            Err(EdenFsError::Other(anyhow!(
                "Checkout path {} is not handled by EdenFS",
                path.display()
            )))
        }
    }

    pub fn config_directory(&self, client_name: &str) -> PathBuf {
        self.clients_dir().join(client_name)
    }
}

pub trait DaemonHealthy {
    fn is_healthy(&self) -> bool;
}

impl DaemonHealthy for DaemonInfo {
    fn is_healthy(&self) -> bool {
        self.status
            .map_or_else(|| false, |val| val == fb303_status::ALIVE)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::sync::Arc;

use futures::stream::BoxStream;
#[cfg(fbcode_build)]
use thrift_streaming::client::StreamingEdenService;
#[cfg(fbcode_build)]
use thrift_streaming::errors::streaming_eden_service::StreamStartStatusStreamError;
#[cfg(fbcode_build)]
use thrift_streaming::types::EdenStartStatusUpdate;
use thrift_types::edenfs::client::EdenService;

pub mod checkout;
pub mod instance;
mod mounttable;
pub mod redirect;

pub use instance::DaemonHealthy;
pub use instance::EdenFsInstance;

pub type EdenFsClient = Arc<dyn EdenService + Sync>;

#[cfg(fbcode_build)]
pub type StreamingEdenFsClient = Arc<dyn StreamingEdenService + Sync>;
#[cfg(fbcode_build)]
pub type StartStatusStream =
    BoxStream<'static, Result<EdenStartStatusUpdate, StreamStartStatusStreamError>>;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This software may be used and distributed according to the terms of the
 * GNU General Public License version 2.
 */

use std::path::PathBuf;

use anyhow::anyhow;
use edenfs_error::EdenFsError;
use edenfs_error::Result;
use edenfs_error::ResultExt;
use regex::Regex;
use subprocess::Exec;
use subprocess::Redirection;

#[derive(Debug, PartialEq)]
pub(crate) struct MountTableInfo {
    device: String,
    mount_point: PathBuf,
    vfstype: String,
}

impl MountTableInfo {
    pub(crate) fn mount_point(&self) -> PathBuf {
        self.mount_point.clone()
    }
}

fn parse_linux_mtab(mtab_string: String) -> Vec<MountTableInfo> {
    let mut mounts = Vec::new();
    for line in mtab_string.trim().lines() {
        let entries: Vec<&str> = line.split_ascii_whitespace().collect();
        if entries.len() != 6 {
            eprintln!(
                "mount table line `{}` has {} entries instead of 6",
                line,
                entries.len()
            );
        } else if let [device, mount_point, vfstype, _opts, _freq, _passno] = &entries[..] {
            mounts.push(MountTableInfo {
                device: String::from(*device),
                mount_point: PathBuf::from(*mount_point),
                vfstype: String::from(*vfstype),
            });
        }
    }
    mounts
}

fn parse_macos_mtab(mtab_string: String) -> Vec<MountTableInfo> {
    let mut mounts = Vec::new();
    let mount_regex = Regex::new(r"^(\S+) on (.+) \(([^,]+),.*\)$")
        .expect("Expect each macos mtab to follow a specific format.");
    for line in mtab_string.split('\n') {
        for caps in mount_regex.captures_iter(line) {
            mounts.push(MountTableInfo {
                device: String::from(&caps[1]),
                mount_point: PathBuf::from(&caps[2]),
                vfstype: String::from(&caps[3]),
            });
        }
    }
    mounts
}

/// Returns the list of system mounts
pub(crate) fn read_mount_table() -> Result<Vec<MountTableInfo>> {
    if cfg!(target_os = "linux") {
        Ok(parse_linux_mtab(
            std::fs::read_to_string(PathBuf::from("/proc/self/mounts")).from_err()?,
        ))
    } else if cfg!(target_os = "macos") {
        // Specifying the path is important, as sudo may have munged the path
        // such that /sbin is not part of it any longer
        let output = Exec::cmd("/sbin/mount")
            .stdout(Redirection::Pipe)
            .stderr(Redirection::Pipe)
            .capture()
            .from_err()?;

        if output.success() {
            Ok(parse_macos_mtab(output.stdout_str()))
        } else {
            Err(EdenFsError::Other(anyhow!(
                "Failed to execute /sbin/mount, stderr: {}",
                output.stderr_str()
            )))
        }
    } else {
        Ok(Vec::new())
    }
}

#[test]
fn test_parse_linux_mtab() {
    let contents = "
homedir.eden.com:/home109/chadaustin/public_html /mnt/public/chadaustin nfs rw,context=user_u:object_r:user_home_dir_t,relatime,vers=3,rsize=65536,wsize=65536,namlen=255,soft,nosharecache,proto=tcp6,timeo=100,retrans=2,sec=krb5i,mountaddr=2401:db00:fffe:1007:face:0000:0:4007,mountvers=3,mountport=635,mountproto=udp6,local_lock=none,addr=2401:db00:fffe:1007:0000:b00c:0:4007 0 0
squashfuse_ll /mnt/xarfuse/uid-0/2c071047-ns-4026531840 fuse.squashfuse_ll rw,nosuid,nodev,relatime,user_id=0,group_id=0 0 0
bogus line here
edenfs: /tmp/eden_test.4rec6drf/mounts/main fuse rw,nosuid,relatime,user_id=138655,group_id=100,default_permissions,allow_other 0 0
".to_string();
    let mount_infos = parse_linux_mtab(contents);
    assert_eq!(3, mount_infos.len());
    assert_eq!("edenfs:", mount_infos[2].device);
    assert_eq!(
        PathBuf::from("/tmp/eden_test.4rec6drf/mounts/main"),
        mount_infos[2].mount_point
    );
    assert_eq!("fuse", mount_infos[2].vfstype);
}

#[test]
fn test_parse_mtab_macos() {
    let contents = "
/dev/disk1s1 on / (apfs, local, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk1s4 on /private/var/vm (apfs, local, noexec, journaled, noatime, nobrowse)
map -hosts on /net (autofs, nosuid, automounted, nobrowse)
map auto_home on /home (autofs, automounted, nobrowse)
map -fstab on /Network/Servers (autofs, automounted, nobrowse)
eden@osxfuse0 on /Users/wez/fbsource (osxfuse_eden, nosuid, synchronous)
"
    .to_string();

    let expected = vec![
        MountTableInfo {
            device: "/dev/disk1s1".to_string(),
            mount_point: PathBuf::from("/"),
            vfstype: "apfs".to_string(),
        },
        MountTableInfo {
            device: "devfs".to_string(),
            mount_point: PathBuf::from("/dev"),
            vfstype: "devfs".to_string(),
        },
        MountTableInfo {
            device: "/dev/disk1s4".to_string(),
            mount_point: PathBuf::from("/private/var/vm"),
            vfstype: "apfs".to_string(),
        },
        MountTableInfo {
            device: "eden@osxfuse0".to_string(),
            mount_point: PathBuf::from("/Users/wez/fbsource"),
            vfstype: "osxfuse_eden".to_string(),
        },
    ];
    let actual = parse_macos_mtab(contents);

    assert_eq!(expected.len(), actual.len());
    assert!(expected.iter().zip(&actual).all(|(a, b)| *a == *b));
}