/*
 * 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.
 */

//! edenfsctl redirect

use std::collections::BTreeMap;
#[cfg(target_os = "macos")]
use std::ffi::OsStr;
#[cfg(target_os = "macos")]
use std::io::IsTerminal;
use std::path::Path;
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use std::process::Command;
use std::str::FromStr;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
#[cfg(target_os = "macos")]
use dialoguer::Confirm;
use edenfs_client::checkout::find_checkout;
use edenfs_client::checkout::CheckoutConfig;
use edenfs_client::redirect::get_configured_redirections;
use edenfs_client::redirect::get_effective_redirections;
use edenfs_client::redirect::try_add_redirection;
use edenfs_client::redirect::Redirection;
use edenfs_client::redirect::RedirectionState;
use edenfs_client::redirect::RedirectionType;
#[cfg(target_os = "macos")]
use edenfs_client::redirect::APFS_HELPER;
use edenfs_client::redirect::REPO_SOURCE;
use edenfs_client::EdenFsInstance;
use hg_util::path::expand_path;
use tabular::row;
use tabular::Table;

use crate::util::expand_path_or_cwd;
use crate::util::remove_trailing_slash;
use crate::ExitCode;
use crate::Subcommand;

#[derive(Parser, Debug)]
#[clap(name = "redirect")]
#[clap(about = "List and manipulate redirected paths")]
pub enum RedirectCmd {
    #[clap(about = "List redirections")]
    List {
        #[clap(long, help = "The EdenFS mount point path.")]
        mount: Option<PathBuf>,
        #[clap(long, help = "output in json rather than human readable text")]
        json: bool,
    },
    #[clap(about = "Add or change a redirection")]
    Add {
        #[clap(long, parse(from_str = expand_path), help = "The EdenFS mount point path.")]
        mount: Option<PathBuf>,
        #[clap(parse(from_str = expand_path), index = 1, help = "The path in the repo which should be redirected")]
        repo_path: PathBuf,
        #[clap(index = 2, help = "The type of the redirection", possible_values = ["bind", "symlink"])]
        redir_type: String,
        #[clap(
            long,
            help = "Unmount and re-bind mount any bind mount redirections to ensure that they are \
            pointing to the right place. This is not the default behavior in the interest of \
            preserving kernel caches."
        )]
        force_remount_bind_mounts: bool,
        #[clap(
            long,
            help = "force the bind mount to fail if it would overwrite a pre-existing directory"
        )]
        strict: bool,
    },
    #[clap(
        about = "Unmount all effective redirection configuration, but preserve the configuration \
        so that a subsequent fixup will restore it"
    )]
    Unmount {
        #[clap(long, parse(from_str = expand_path), help = "The EdenFS mount point path.")]
        mount: Option<PathBuf>,
    },
    #[clap(about = "Delete a redirection")]
    Del {
        #[clap(long, parse(from_str = expand_path), help = "The EdenFS mount point path.")]
        mount: Option<PathBuf>,
        #[clap(parse(from_str = expand_path), index = 1, help = "The path in the repo which should no longer be redirected")]
        repo_path: PathBuf,
    },
    #[clap(
        about = "Fixup redirection configuration; redirect things that should be redirected and \
        remove things that should not be redirected"
    )]
    Fixup {
        #[clap(long, parse(from_str = expand_path), help = "The EdenFS mount point path.")]
        mount: Option<PathBuf>,
        #[clap(
            long,
            help = "Unmount and re-bind mount any bind mount redirections to ensure that they are \
            pointing to the right place. This is not the default behavior in the interest of \
            preserving kernel caches"
        )]
        force_remount_bind_mounts: bool,
        #[clap(
            long,
            help = "By default, paths from all sources are fixed. Setting this flag to true will \
            fix paths only from the .eden-redirections source."
        )]
        only_repo_source: bool,
    },
    #[clap(about = "Delete stale apfs volumes")]
    CleanupApfs {},
}

impl RedirectCmd {
    fn print_redirection_table(
        &self,
        redirections: BTreeMap<PathBuf, Redirection>,
    ) -> Result<ExitCode> {
        let mut table = Table::new("{:<}    {:<}    {:<}    {:<}    {:<}");
        table.add_row(row!("REPO_PATH", "TYPE", "TARGET", "SOURCE", "STATE"));
        for redir in redirections.into_values() {
            table.add_row(row!(
                redir.repo_path().display(),
                redir.redir_type,
                redir
                    .target
                    .map(|x| x.display().to_string())
                    .unwrap_or_default(),
                redir.source,
                redir.state.unwrap_or(RedirectionState::UnknownMount),
            ));
        }
        println!("{}", table);
        Ok(0)
    }

    fn print_redirection_json(
        &self,
        redirections: BTreeMap<PathBuf, Redirection>,
    ) -> Result<ExitCode> {
        let json_out = serde_json::to_string(&redirections.into_values().collect::<Vec<_>>())
            .with_context(|| anyhow!("could not serialize redirections",))?;
        println!("{}", json_out);
        Ok(0)
    }

    async fn list(&self, mount: Option<PathBuf>, json: bool) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let mount = match mount {
            Some(provided) => provided,
            None => expand_path_or_cwd("").with_context(|| {
                anyhow!("could not infer mount: could not determine current working directory")
            })?,
        };
        let checkout = find_checkout(instance, &mount)?;
        let mut redirections = get_effective_redirections(&checkout).with_context(|| {
            anyhow!(
                "Unable to retrieve redirections for checkout '{}'",
                mount.display()
            )
        })?;

        redirections
            .values_mut()
            .map(|v| v.update_target_abspath(&checkout))
            .collect::<Result<Vec<()>, _>>()
            .with_context(|| anyhow!("failed to expand redirection target path"))?;

        if json {
            self.print_redirection_json(redirections)
        } else {
            self.print_redirection_table(redirections)
        }
    }

    async fn add(
        &self,
        mount: Option<PathBuf>,
        repo_path: &Path,
        redir_type: &str,
        force_remount_bind_mounts: bool,
        strict: bool,
    ) -> Result<ExitCode> {
        let repo_path = remove_trailing_slash(repo_path);
        let redir_type = RedirectionType::from_str(redir_type)?;
        let instance = EdenFsInstance::global();
        let mount = match mount {
            Some(provided) => provided,
            None => expand_path_or_cwd("").with_context(|| {
                anyhow!("could not infer mount: could not determine current working directory")
            })?,
        };
        let client_name = instance.client_name(&mount)?;
        let config_dir = instance.config_directory(&client_name);
        let checkout = find_checkout(instance, &mount)?;
        try_add_redirection(
            &checkout,
            &config_dir,
            &repo_path,
            redir_type,
            force_remount_bind_mounts,
            strict,
        )
        .await
        .with_context(|| {
            format!(
                "Could not add redirection {} of type {}",
                &repo_path.display(),
                redir_type,
            )
        })?;
        Ok(0)
    }

    async fn unmount(&self, mount: Option<PathBuf>) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let mount = match mount {
            Some(provided) => provided,
            None => expand_path_or_cwd("").with_context(|| {
                anyhow!("could not infer mount: could not determine current working directory")
            })?,
        };
        let checkout = find_checkout(instance, &mount)?;
        let redirs = get_effective_redirections(&checkout).with_context(|| {
            anyhow!(
                "Could not get effective redirections for checkout {}",
                checkout.path().display()
            )
        })?;
        for redir in redirs.values() {
            redir
                .remove_existing(&checkout, false)
                .await
                .with_context(|| {
                    anyhow!(
                        "failed to remove existing redirection {}",
                        redir.repo_path.display()
                    )
                })?;
        }

        // recompute the current state and catch any failures
        let recomputed_redirs = get_effective_redirections(&checkout).with_context(|| {
            anyhow!(
                "Could not get effective redirections for checkout {}",
                checkout.path().display()
            )
        })?;

        for redir in recomputed_redirs.values() {
            if !redir
                .state
                .as_ref()
                .map_or(true, |v| RedirectionState::MatchesConfiguration != *v)
            {
                eprintln!("error: at least one redirection does not match its configuration");
                return Ok(1);
            }
        }
        Ok(0)
    }

    async fn del(&self, mount: Option<PathBuf>, repo_path: &Path) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let mount = match mount {
            Some(provided) => provided,
            None => expand_path_or_cwd("").with_context(|| {
                anyhow!("could not infer mount: could not determine current working directory")
            })?,
        };
        let checkout = find_checkout(instance, &mount)?;
        let client_name = instance.client_name(&mount)?;
        let config_dir = instance.config_directory(&client_name);
        let mut redirs = get_configured_redirections(&checkout).with_context(|| {
            anyhow!(
                "Could not get configured redirections for checkout {}",
                checkout.path().display()
            )
        })?;

        // Note that we're deliberately not using the same validation logic
        // for args.repo_path that we do for the add case for now so that we
        // provide a way to remove bogus redirection paths.  After we've deployed
        // the improved `add` validation for a while, we can use it here also.
        if let Some(redir) = redirs.get(repo_path) {
            redir
                .remove_existing(&checkout, false)
                .await
                .with_context(|| {
                    format!(
                        "Failed to remove existing redirection {}",
                        repo_path.display()
                    )
                })?;
            redirs.remove(repo_path);
            let mut checkout_config = CheckoutConfig::parse_config(config_dir.clone())
                .with_context(|| {
                    format!(
                        "Failed to parse checkout config using config dir {}",
                        &config_dir.display()
                    )
                })?;
            checkout_config
                .update_redirections(&config_dir, &redirs)
                .with_context(|| {
                    format!(
                        "Failed to update redirections for checkout {}",
                        checkout.path().display()
                    )
                })?;
            return Ok(0);
        }

        let effective_redirs = get_effective_redirections(&checkout).with_context(|| {
            anyhow!(
                "Could not get configured redirections for checkout {}",
                checkout.path().display()
            )
        })?;
        if let Some(effective_redir) = effective_redirs.get(repo_path) {
            // This path isn't possible to trigger until we add profiles,
            // but let's be ready for it anyway.
            println!(
                "error: {} is defined by {} and cannot be removed using `edenfsctl redirect del {}`",
                repo_path.display(),
                &effective_redir.source,
                repo_path.display()
            );
            return Ok(1);
        }
        println!("{} is not a known redirection", repo_path.display());
        Ok(1)
    }

    async fn fixup(
        &self,
        mount: Option<PathBuf>,
        force_remount_bind_mounts: bool,
        only_repo_source: bool,
    ) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let mount = match mount {
            Some(provided) => provided,
            None => expand_path_or_cwd("").with_context(|| {
                anyhow!("could not infer mount: could not determine current working directory")
            })?,
        };
        let checkout = find_checkout(instance, &mount)?;
        let redirs = get_effective_redirections(&checkout).with_context(|| {
            anyhow!(
                "Could not get configured redirections for checkout {}",
                checkout.path().display()
            )
        })?;

        for redir in redirs.values() {
            if redir.state == Some(RedirectionState::MatchesConfiguration)
                && !(force_remount_bind_mounts && redir.redir_type == RedirectionType::Bind)
            {
                tracing::debug!(
                    ?redir,
                    "not fixing since it's already matching configuration"
                );
                continue;
            }

            if only_repo_source && redir.source != REPO_SOURCE {
                tracing::debug!(?redir, "not fixing due to not from repo source");
                continue;
            }

            eprintln!("Fixing {}", redir.repo_path.display());
            if let Err(e) = redir.remove_existing(&checkout, false).await {
                eprintln!(
                    "Unable to remove redirection {}... this isn't necessarily an error: {}",
                    redir.repo_path.display(),
                    e
                )
            }

            if redir.redir_type == RedirectionType::Unknown {
                tracing::debug!(?redir, "not fixing due to unknown redirection type");
                continue;
            }

            if let Err(e) = redir.apply(&checkout).await {
                eprintln!(
                    "Unable to apply redirection {}: {}",
                    redir.repo_path.display(),
                    e
                );
            }
        }

        let effective_redirs = get_effective_redirections(&checkout).with_context(|| {
            anyhow!(
                "Failed to get effective redirections for checkout {}",
                checkout.path().display()
            )
        })?;
        for redir in effective_redirs.values() {
            if let Some(state) = &redir.state {
                if *state != RedirectionState::MatchesConfiguration {
                    // When --only-repo-source is passed, we may fail to fixup some redirections.
                    // This scenario is ok and should not be considered a failure.
                    if !only_repo_source || redir.source == REPO_SOURCE {
                        return Ok(1);
                    }
                }
            }
        }
        Ok(0)
    }

    #[cfg(not(target_os = "macos"))]
    async fn cleanup_apfs(&self) -> Result<ExitCode> {
        Err(anyhow!("Cannot run cleanup-apfs: Unsupported Platform"))
    }

    #[cfg(target_os = "macos")]
    async fn cleanup_apfs(&self) -> Result<ExitCode> {
        match Redirection::have_apfs_helper() {
            Err(e) => return Err(anyhow!("Cannot run cleanup-apfs: {}", e)),
            Ok(res) => {
                if !res {
                    return Err(anyhow!(
                        "Cannot run cleanup-apfs: {} does not exist",
                        APFS_HELPER
                    ));
                }
            }
        }

        let instance = EdenFsInstance::global();
        let mounts = instance
            .get_configured_mounts_map()
            .with_context(|| anyhow!("could not get configured mounts map for EdenFS instance"))?;

        let mut args: Vec<&OsStr> = vec!["list-stale-volumes", "--json"]
            .into_iter()
            .map(OsStr::new)
            .collect::<Vec<_>>();
        args.extend(mounts.keys().map(|m| m.as_os_str()));
        let output = Command::new(APFS_HELPER)
            .args(&args)
            .output()
            .with_context(|| {
                format!(
                    "Failed to execute apfs_helper cmd: `{} {:?}`.",
                    APFS_HELPER,
                    args.join(OsStr::new(" ")),
                )
            })?;
        if !output.status.success() {
            return Err(anyhow!(
                "failed to fetch stale volumes. stderr: {}\n stdout: {}",
                String::from_utf8_lossy(&output.stderr),
                String::from_utf8_lossy(&output.stdout)
            ));
        }

        let stale_output = std::str::from_utf8(&output.stdout).with_context(|| {
            anyhow!("Failed to convert list-stale-volumes output to utf8 string")
        })?;
        let stale_json: serde_json::Value = serde_json::from_str(stale_output)
            .with_context(|| anyhow!("Failed to parse list-stale-volumes output as JSON"))?;

        let default_v = vec![];
        let stale_volumes = stale_json.as_array().unwrap_or(&default_v);
        if stale_volumes.is_empty() {
            println!("No stale volumes detected");
            return Ok(0);
        }

        if std::io::stdin().is_terminal() {
            println!("Warning: this operation will permanently delete the following volumes:");
            for volume in stale_volumes.iter() {
                println!("    {}", volume.as_str().unwrap_or(""));
            }

            if !Confirm::new()
                .with_prompt("Do you want to continue?")
                .interact()?
            {
                println!("Not deleting volumes");
                return Ok(2);
            }
        }

        let mut res = 0;
        for vol in stale_volumes {
            if let Some(vol_str) = vol.as_str() {
                let args = &["delete-volume", vol_str];
                let output = Command::new(APFS_HELPER)
                    .args(args)
                    .output()
                    .with_context(|| {
                        format!(
                            "Failed to execute apfs_helper cmd: `{} {}`.",
                            APFS_HELPER,
                            shlex::join(args.iter().copied()),
                        )
                    })?;
                if !output.status.success() {
                    res = 1;
                    eprintln!(
                        "Failed to delete volume {} due to {}",
                        vol_str,
                        String::from_utf8_lossy(&output.stderr)
                    );
                } else {
                    println!("Deleted volume {}", vol_str);
                }
            } else {
                eprintln!(
                    "Could not convert serde_json::Value object to string: {}",
                    vol
                );
            }
        }
        Ok(res)
    }
}

#[async_trait]
impl Subcommand for RedirectCmd {
    async fn run(&self) -> Result<ExitCode> {
        match self {
            Self::List { mount, json } => self.list(mount.to_owned(), *json).await,
            Self::Add {
                mount,
                repo_path,
                redir_type,
                force_remount_bind_mounts,
                strict,
            } => {
                self.add(
                    mount.to_owned(),
                    repo_path,
                    redir_type,
                    *force_remount_bind_mounts,
                    *strict,
                )
                .await
            }
            Self::Unmount { mount } => self.unmount(mount.to_owned()).await,
            Self::Del { mount, repo_path } => self.del(mount.to_owned(), repo_path).await,
            Self::Fixup {
                mount,
                force_remount_bind_mounts,
                only_repo_source,
            } => {
                self.fixup(
                    mount.to_owned(),
                    *force_remount_bind_mounts,
                    *only_repo_source,
                )
                .await
            }
            Self::CleanupApfs {} => self.cleanup_apfs().await,
        }
    }

    fn get_mount_path_override(&self) -> Option<PathBuf> {
        match self {
            Self::List { mount, .. }
            | Self::Add { mount, .. }
            | Self::Unmount { mount }
            | Self::Del { mount, .. }
            | Self::Fixup { mount, .. } => mount.to_owned(),
            Self::CleanupApfs {} => None,
        }
    }
}


/*
 * 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.
 */

//! edenfsctl status

use std::time::Duration;

use anyhow::anyhow;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::DaemonHealthy;
use edenfs_client::EdenFsInstance;
use futures::stream::StreamExt;
use tokio::time;
use tracing::event;
use tracing::Level;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Check the health of the EdenFS service")]
pub struct StatusCmd {
    /// Wait up to TIMEOUT seconds for the daemon to respond
    #[clap(long, default_value = "3")]
    timeout: u64,

    #[clap(
        long,
        help = "Print progress of a starting EdenFS process and wait for it to \
successfully start."
    )]
    wait: bool,
}

enum EdenFsRunningStatus {
    Starting,
    Running(i32), // Holds the pid.
}

impl StatusCmd {
    async fn get_status_simple(
        &self,
        instance: &EdenFsInstance,
    ) -> edenfs_error::Result<thrift_types::edenfs::types::DaemonInfo> {
        let timeout = Duration::from_secs(self.timeout);
        let health = instance.get_health(Some(timeout));

        time::timeout(timeout, health)
            .await
            .map_err(edenfs_error::EdenFsError::RequestTimeout)?
    }

    #[cfg(fbcode_build)]
    async fn get_status_blocking_on_startup(
        &self,
        instance: &EdenFsInstance,
    ) -> edenfs_error::Result<thrift_types::edenfs::types::DaemonInfo> {
        let timeout = Duration::from_secs(self.timeout);
        let initial_result_and_stream = instance.get_health_with_startup_updates_included(timeout);
        let waited_health = time::timeout(timeout, initial_result_and_stream)
            .await
            .map_err(edenfs_error::EdenFsError::RequestTimeout)?;
        match waited_health {
            Ok((initial_result, mut startup_stream))
                if initial_result.status
                    == Some(thrift_types::fb303_core::fb303_status::STARTING) =>
            {
                println!("EdenFS is starting ...");
                while let Some(value) = startup_stream.next().await {
                    match value {
                        Ok(message) => {
                            println!("{}", String::from_utf8_lossy(&message));
                        }
                        Err(e) => {
                            println!("Error received from EdenFS while starting: {}", e);
                            break;
                        }
                    }
                }
                Ok(initial_result)
            }
            Ok((initial_result, _)) => Ok(initial_result),
            Err(edenfs_error::EdenFsError::Other(e)) => Err(edenfs_error::EdenFsError::Other(e)),
            Err(e) => Err(e),
        }
    }

    async fn get_status(
        &self,
        instance: &EdenFsInstance,
    ) -> edenfs_error::Result<thrift_types::edenfs::types::DaemonInfo> {
        #[cfg(fbcode_build)]
        if self.wait {
            let waited_status = self.get_status_blocking_on_startup(instance).await;
            if let Err(edenfs_error::EdenFsError::UnknownMethod(_)) = waited_status {
                println!(
                    "The version of EdenFS you are running does not yet support streaming status. Falling back to regular status ..."
                );
                return self.get_status_simple(instance).await;
            } else {
                return waited_status;
            }
        }
        self.get_status_simple(instance).await
    }

    /// @returns the pid of the running EdenFS daemon, and if the daemon is
    /// starting
    fn interpret_status(
        &self,
        instance: &EdenFsInstance,
        health: edenfs_error::Result<thrift_types::edenfs::types::DaemonInfo>,
    ) -> Result<EdenFsRunningStatus, anyhow::Error> {
        match health {
            Ok(health) if health.is_healthy() => {
                return Ok(EdenFsRunningStatus::Running(health.pid));
            }
            Ok(health) => {
                event!(
                    Level::DEBUG,
                    ?health,
                    "Connected to EdenFS daemon but daemon reported unhealthy status"
                );
                if health.status == Some(thrift_types::fb303_core::fb303_status::STARTING) {
                    return Ok(EdenFsRunningStatus::Starting);
                }
                if let Some(status) = health.status {
                    return Err(anyhow!("EdenFS is {}", status));
                }
            }
            Err(e) => {
                event!(
                    Level::DEBUG,
                    ?e,
                    "Error while collecting status information from EdenFS"
                );
            }
        }

        instance
            .status_from_lock()
            .map(EdenFsRunningStatus::Running)
    }

    fn display_simple(&self, status: Result<EdenFsRunningStatus, anyhow::Error>) -> ExitCode {
        match status {
            Ok(EdenFsRunningStatus::Running(pid)) => {
                println!("EdenFS is running normally (pid {})", pid);
                0
            }
            Ok(EdenFsRunningStatus::Starting) => {
                println!(
                    "EdenFS is still starting (hint: run `eden status --wait` to watch its progress)",
                );
                1
            }
            Err(cause) => {
                println!("EdenFS is not healthy: {}", cause);
                1
            }
        }
    }
}

#[async_trait]
impl crate::Subcommand for StatusCmd {
    async fn run(&self) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let status = self.get_status(instance).await;
        event!(Level::TRACE, ?status, "get_health");
        let display_result = self.interpret_status(instance, status);
        match display_result {
            #[cfg(fbcode_build)]
            Ok(EdenFsRunningStatus::Starting) if self.wait => {
                // get_status will have already printed out all the start up
                // status at this point. EdenFS might have crashed or finished
                // successfully. Check the status and display a clearer message
                // if EdenFS is not running.
                let final_status = self.get_status_simple(instance).await;
                let final_display_result = self.interpret_status(instance, final_status);
                if let Ok(EdenFsRunningStatus::Running(_)) = final_display_result {
                    return Ok(0);
                }
                Ok(self.display_simple(final_display_result))
            }
            _ => Ok(self.display_simple(display_result)),
        }
    }
}


/*
 * 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.
 */

//! edenfsctl top

use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::RwLock;
use std::time::Duration;

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use once_cell::sync::Lazy;
use termwiz::caps::Capabilities;
use termwiz::color::ColorAttribute;
use termwiz::input::InputEvent;
use termwiz::input::KeyCode;
use termwiz::input::KeyEvent;
use termwiz::surface::Change;
use termwiz::terminal::buffered::BufferedTerminal;
use termwiz::terminal::new_terminal;
use termwiz::terminal::Terminal;
use termwiz::widgets::layout::ChildOrientation;
use termwiz::widgets::layout::Constraints;
use termwiz::widgets::RenderArgs;
use termwiz::widgets::Ui;
use termwiz::widgets::UpdateArgs;
use termwiz::widgets::Widget;
use termwiz::widgets::WidgetEvent;
use termwiz::Error;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Monitor EdenFS accesses by process.")]
pub struct TopCmd {
    /// Don't accumulate data; refresh the screen every update
    /// cycle.
    #[clap(short, long)]
    ephemeral: bool,

    /// Specify the rate (in seconds) at which eden top updates.
    #[clap(short, long, default_value = "1")]
    refresh_rate: u64,
}

#[derive(PartialEq, Copy, Clone)]
enum Pages {
    HelpPage,
    MainPage,
}

// top has a few different "views" or "pages". This controls which page will be
// rendered in the next rendering cycle. Wigets that belong in each view should
// only declare non zero width and height if their parent page is the active page.
// widgets should only render themselves if they and their parent page declared
// a non zero width and height in the _last_ rendering cycle. This is because
// the size of a widget is determined by its declared size in the last rendering
// cycle, and it will cause errors to render anything to a widget of zero size.
// Disclaimer: this is a hack. Ideally termwiz would manage visable and hidden
// widgets.
static OBSERVED_ACTIVE_PAGE: Lazy<RwLock<Pages>> = Lazy::new(|| RwLock::new(Pages::MainPage));

struct EdenTopHeader {}

impl EdenTopHeader {
    pub fn new() -> Self {
        Self {}
    }
}

impl Widget for EdenTopHeader {
    fn render(&mut self, args: &mut RenderArgs) {
        args.surface
            .add_change(Change::ClearScreen(ColorAttribute::Default));
        args.surface.add_change("eden top");
    }

    fn get_size_constraints(&self) -> Constraints {
        let mut c = Constraints::default();
        c.set_fixed_height(1);
        c.set_pct_width(100);
        c.child_orientation = ChildOrientation::Horizontal;
        c
    }
}

// This indicates whether the help page delcared itself to have non zero width
// and height in the last cycle. The Help page is allowed to render itself if it
// declared itself to have non zero height.
// Disclaimer: this is a super hack. This could be a member variable of
// HelpPage, but we keep the state global to match the main page state,
// this will also allow any widgets nested in the help page to share state,
// should we add any of these.
static HELP_SHOULD_RENDER: AtomicBool = AtomicBool::new(false);

struct HelpPage;

impl HelpPage {
    pub fn new() -> Self {
        Self {}
    }

    fn set_should_render(&self, new_should_render: bool) {
        HELP_SHOULD_RENDER.store(new_should_render, Ordering::SeqCst)
    }

    fn get_should_render(&self) -> bool {
        HELP_SHOULD_RENDER.load(Ordering::SeqCst)
    }

    pub fn is_active(&self) -> bool {
        *OBSERVED_ACTIVE_PAGE.read().unwrap() == Pages::HelpPage
    }
}

impl Widget for HelpPage {
    fn render(&mut self, args: &mut RenderArgs) {
        if self.get_should_render() {
            args.surface
                .add_change(Change::ClearScreen(ColorAttribute::Default));
            args.surface.add_change("eden help");
        }
    }

    fn get_size_constraints(&self) -> Constraints {
        if self.is_active() {
            self.set_should_render(true);
            Constraints::default()
        } else {
            self.set_should_render(false);
            Constraints::with_fixed_width_height(0, 0)
        }
    }
}

// This indicates whether the main page delcared itself to have non zero width
// and height in the last cycle. The Main page and its nested widgets are
// allowed to render themselves if the main page widgets declared themselves to
// have non zero height. They all should either declare 0 size or non zero size.
// There should not be a mix.
// Disclaimer: this is a super hack. We keep this global state to allow
// cordinating between child widgets of the main page.
static MAIN_SHOULD_RENDER: AtomicBool = AtomicBool::new(false);
struct MainPage;

impl MainPage {
    pub fn new() -> Self {
        Self {}
    }

    fn set_should_render(&self, new_should_render: bool) {
        MAIN_SHOULD_RENDER.store(new_should_render, Ordering::SeqCst)
    }

    pub fn is_active(&self) -> bool {
        *OBSERVED_ACTIVE_PAGE.read().unwrap() == Pages::MainPage
    }
}

impl Widget for MainPage {
    fn render(&mut self, _args: &mut RenderArgs) {}

    fn get_size_constraints(&self) -> Constraints {
        if self.is_active() {
            self.set_should_render(true);
            let mut c = Constraints {
                child_orientation: ChildOrientation::Vertical,
                ..Default::default()
            };
            c.set_pct_width(100);
            c.set_pct_height(100);
            c
        } else {
            self.set_should_render(false);
            Constraints::with_fixed_width_height(0, 0)
        }
    }
}

struct TopLevelStatsSection {}

impl TopLevelStatsSection {
    pub fn new() -> Self {
        Self {}
    }

    // the main wiget sets should render for this widget

    fn get_should_render(&self) -> bool {
        MAIN_SHOULD_RENDER.load(Ordering::SeqCst)
    }

    pub fn is_active(&self) -> bool {
        *OBSERVED_ACTIVE_PAGE.read().unwrap() == Pages::MainPage
    }
}

impl Widget for TopLevelStatsSection {
    fn render(&mut self, args: &mut RenderArgs) {
        if self.get_should_render() {
            args.surface
                .add_change(Change::ClearScreen(ColorAttribute::Default));
            args.surface.add_change("top level stats");
        }
    }

    fn get_size_constraints(&self) -> Constraints {
        if self.is_active() {
            let mut c = Constraints {
                child_orientation: ChildOrientation::Vertical,
                ..Default::default()
            };
            c.set_fixed_height(4);
            c.set_pct_width(100);
            c
        } else {
            Constraints::with_fixed_width_height(0, 0)
        }
    }
}

struct ProcessTableSection {}

impl ProcessTableSection {
    pub fn new() -> Self {
        Self {}
    }

    // the main wiget sets should render for this widget
    fn get_should_render(&self) -> bool {
        MAIN_SHOULD_RENDER.load(Ordering::SeqCst)
    }

    pub fn is_active(&self) -> bool {
        *OBSERVED_ACTIVE_PAGE.read().unwrap() == Pages::MainPage
    }
}

impl Widget for ProcessTableSection {
    fn render(&mut self, args: &mut RenderArgs) {
        if self.get_should_render() {
            args.surface
                .add_change(Change::ClearScreen(ColorAttribute::Default));
            args.surface.add_change("process table stats");
        }
    }

    fn get_size_constraints(&self) -> Constraints {
        if self.is_active() {
            let mut c = Constraints {
                child_orientation: ChildOrientation::Vertical,
                ..Default::default()
            };
            c.set_pct_width(100);
            c
        } else {
            Constraints::with_fixed_width_height(0, 0)
        }
    }
}

struct BasePage {
    height: usize,
    width: usize,
}

impl BasePage {
    pub fn new(inital_height: usize, initial_width: usize) -> Self {
        Self {
            height: inital_height,
            width: initial_width,
        }
    }
}

impl Widget for BasePage {
    fn process_event(&mut self, event: &WidgetEvent, _args: &mut UpdateArgs) -> bool {
        match event {
            WidgetEvent::Input(InputEvent::Resized { rows, cols }) => {
                self.height = rows.clone();
                self.width = cols.clone();
            }
            _ => {}
        };
        true
    }

    fn render(&mut self, _args: &mut RenderArgs) {}

    fn get_size_constraints(&self) -> Constraints {
        let mut c = Constraints {
            child_orientation: ChildOrientation::Vertical,
            ..Default::default()
        };
        // we should be doing this, but there is a bug that the width turns
        // this into 1 column so we use a fized width to avoid this for now
        // c.set_pct_width(100);
        // c.set_pct_height(100);
        c.set_fixed_width(self.width as u16);
        c.set_fixed_height(self.height as u16);
        c
    }
}

impl TopCmd {
    fn main_loop(&self) -> Result<(), Error> {
        let caps = Capabilities::new_from_env()?;
        let mut terminal = new_terminal(caps)?;
        // getting the screen size here is a bit of a hack, we should not actually read
        // this value, but do so to get around a bug with percentage based widget size
        let screen_size = terminal.get_screen_size()?;
        let mut buf = BufferedTerminal::new(terminal)?;
        buf.terminal().set_raw_mode()?;

        let mut ui = Ui::new();
        let base_id = ui.set_root(BasePage::new(screen_size.rows, screen_size.cols));
        ui.add_child(base_id, EdenTopHeader::new());
        let main_id = ui.add_child(base_id, MainPage::new());
        let _help_id = ui.add_child(base_id, HelpPage::new());
        ui.add_child(main_id, TopLevelStatsSection::new());
        ui.add_child(main_id, ProcessTableSection::new());

        // allow resize events to go to the base, so that the high level size gets updated
        // with resizing
        ui.set_focus(base_id);

        loop {
            // let the widgets process events
            ui.process_event_queue()?;

            // After updating and processing all of the widgets, compose them
            // and render them to the screen.
            if ui.render_to_screen(&mut buf)? {
                // We have more events to process immediately; don't block waiting
                // for input below, but jump to the top of the loop to re-run the
                // updates.
                continue;
            }
            // Compute an optimized delta to apply to the terminal and display it
            buf.flush()?;

            match buf
                .terminal()
                .poll_input(Some(Duration::from_secs(self.refresh_rate)))
            {
                Ok(Some(InputEvent::Resized { rows, cols })) => {
                    // TODO(kmancini): this is working around a bug where we
                    // don't realize that we should redraw everything on resize
                    // in BufferedTerminal.
                    buf.add_change(Change::ClearScreen(Default::default()));
                    buf.resize(cols, rows);
                    ui.queue_event(WidgetEvent::Input(InputEvent::Resized { rows, cols }));
                }
                Ok(Some(input)) => match input {
                    InputEvent::Key(KeyEvent {
                        key: KeyCode::Char('q'),
                        ..
                    }) => {
                        break;
                    }
                    InputEvent::Key(KeyEvent {
                        key: KeyCode::Char('h'),
                        ..
                    }) => {
                        *OBSERVED_ACTIVE_PAGE.write().unwrap() = Pages::HelpPage;
                    }
                    InputEvent::Key(KeyEvent {
                        key: KeyCode::Escape,
                        ..
                    }) => {
                        *OBSERVED_ACTIVE_PAGE.write().unwrap() = Pages::MainPage;
                    }
                    input => {
                        ui.queue_event(WidgetEvent::Input(input));
                    }
                },
                Ok(None) => {}
                Err(e) => {
                    print!("{:?}\r\n", e);
                    break;
                }
            }
        }
        // clear the sceen before we exit because the terminal
        // prompt will write over the eden top stuff and then
        // the next command output and eden top output gets
        // jumbled together.
        buf.add_change(Change::ClearScreen(Default::default()));
        buf.flush()?;
        Ok(())
    }
}

#[async_trait]
impl crate::Subcommand for TopCmd {
    async fn run(&self) -> Result<ExitCode> {
        match self.main_loop() {
            Ok(_) => Ok(0),
            Err(cause) => {
                println!("Error: {}", cause);
                Ok(1)
            }
        }
    }
}


/*
 * 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.
 */

//! edenfsctl uptime

use std::time::Duration;

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::DaemonHealthy;
use edenfs_client::EdenFsInstance;
use edenfs_utils::humantime::HumanTime;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Determine uptime of running edenfs daemon")]
pub struct UptimeCmd {}

#[async_trait]
impl crate::Subcommand for UptimeCmd {
    async fn run(&self) -> Result<ExitCode> {
        let instance = EdenFsInstance::global();
        let health = instance.get_health(None).await;

        match health {
            Ok(health) => {
                if health.is_healthy() {
                    let uptime = Duration::from_secs_f32(health.uptime.unwrap_or(0.0));
                    println!(
                        "edenfs uptime (pid: {}): {:#}",
                        health.pid,
                        HumanTime::from(uptime)
                    );
                } else {
                    println!("edenfs (pid: {}) is not healthy", health.pid);
                }
                Ok(0)
            }
            Err(cause) => {
                println!("edenfs not healthy: {}", cause);
                Ok(1)
            }
        }
    }
}


/*
 * 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::Path;
use std::path::PathBuf;

use anyhow::Context;
use anyhow::Result;
use hg_util::path::expand_path;

pub mod jsonrpc;

/// Expand the path if the user has supplied anything. Otherwise, use the current working directory instead.
///
/// Usage:
/// ```ignore
/// #[clap(..., parse(try_from_str = expand_path_or_cwd), default_value = "", ...)]
/// ```
pub fn expand_path_or_cwd(input: &str) -> Result<PathBuf> {
    // TODO(T135653638): This function must be updated when expand_path is updated to use OsString
    if input.is_empty() {
        std::env::current_dir().context("Unable to retrieve current working directory")
    } else {
        Ok(expand_path(input))
    }
}

/// Utility function to remove trailing slashes from user provided relative paths. This is required
/// because some EdenFS internals do not allow trailing slashses on relative paths.
pub fn remove_trailing_slash(path: &Path) -> PathBuf {
    // TODO(T135653638): This function must be updated when expand_path is updated to use OsString
    PathBuf::from(
        path.to_string_lossy()
            .trim_end_matches(if cfg!(windows) { r"\" } else { "/" }),
    )
}

/// Traverse up and locate the repository root
pub fn locate_repo_root(path: &Path) -> Option<&Path> {
    path.ancestors()
        .find(|p| p.join(".hg").is_dir() || p.join(".git").is_dir())
}

pub fn locate_eden_config_dir(path: &Path) -> Option<PathBuf> {
    // Check whether we're in an Eden mount. If we are, some parent directory will contain
    // a .eden dir that contains a socket file. This socket file is symlinked to the
    // socket file contained in the config dir we should use for this mount.
    if let Ok(expanded_path) = path.canonicalize() {
        for ancestor in expanded_path.ancestors() {
            let socket = ancestor.join(".eden").join("socket");
            if socket.exists() {
                if let Ok(resolved_socket) = socket.canonicalize() {
                    if let Some(parent) = resolved_socket.parent() {
                        return Some(parent.to_path_buf());
                    }
                }
            }
        }
    }
    None
}


/*
 * 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.
 */

//! edenfsctl config

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::EdenFsInstance;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Clears local caches of objects stored in RocksDB")]
pub struct ClearLocalCachesCmd {}

#[async_trait]
impl crate::Subcommand for ClearLocalCachesCmd {
    async fn run(&self) -> Result<ExitCode> {
        let client = EdenFsInstance::global().connect(None).await?;
        client.debugClearLocalStoreCaches().await?;
        Ok(0)
    }
}


/*
 * 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.
 */

//! edenfsctl config

use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::EdenFsInstance;

use crate::ExitCode;

#[derive(Parser, Debug)]
#[clap(about = "Asks RocksDB to compact its storage")]
pub struct CompactLocalStorageCmd {}

#[async_trait]
impl crate::Subcommand for CompactLocalStorageCmd {
    async fn run(&self) -> Result<ExitCode> {
        let client = EdenFsInstance::global().connect(None).await?;
        client.debugCompactLocalStorage().await?;
        Ok(0)
    }
}


/*
 * 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.
 */

//! edenfsctl debug subscribe

#[cfg(unix)]
use std::ffi::OsStr;
#[cfg(unix)]
use std::os::unix::ffi::OsStringExt;
#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;

use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use async_trait::async_trait;
use clap::Parser;
use edenfs_client::EdenFsInstance;
use futures::StreamExt;
use hg_util::path::expand_path;
use serde::Serialize;
use thrift_types::edenfs as edenfs_thrift;
use tokio::io::AsyncWriteExt;
use tokio::sync::Notify;
use tokio::time;

use crate::util::jsonrpc::ResponseBuilder;
use crate::util::locate_repo_root;
use crate::ExitCode;

// Defines a few helper functions to make the debug format easier to read.
mod fmt {
    use std::fmt;
    use std::fmt::Debug;

    use thrift_types::edenfs as edenfs_thrift;

    /// Courtesy of https://users.rust-lang.org/t/reusing-an-fmt-formatter/8531/4
    ///
    /// This allows us to provide customized format implementation to avoid
    /// using the default one.
    pub struct Fmt<F>(pub F)
    where
        F: Fn(&mut fmt::Formatter) -> fmt::Result;

    impl<F> fmt::Debug for Fmt<F>
    where
        F: Fn(&mut fmt::Formatter) -> fmt::Result,
    {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            (self.0)(f)
        }
    }

    fn debug_hash(hash: &edenfs_thrift::ThriftRootId) -> impl Debug + '_ {
        Fmt(move |f| write!(f, "{}", hex::encode(hash)))
    }

    fn debug_position(position: &edenfs_thrift::JournalPosition) -> impl Debug + '_ {
        Fmt(|f| {
            f.debug_struct("JournalPosition")
                .field("mountGeneration", &position.mountGeneration)
                .field("sequenceNumber", &position.sequenceNumber)
                .field("snapshotHash", &debug_hash(&position.snapshotHash))
                .finish()
        })
    }

    fn debug_path(path: &edenfs_thrift::PathString) -> impl Debug + '_ {
        Fmt(|f| write!(f, "{}", String::from_utf8_lossy(path)))
    }

    pub fn debug_file_delta(delta: &edenfs_thrift::FileDelta) -> impl Debug + '_ {
        Fmt(|f| {
            f.debug_struct("FileDelta")
                .field("fromPosition", &debug_position(&delta.fromPosition))
                .field("toPosition", &debug_position(&delta.toPosition))
                .field(
                    "changedPaths",
                    &Fmt(|f| {
                        f.debug_list()
                            .entries(delta.changedPaths.iter().map(debug_path))
                            .finish()
                    }),
                )
                .field(
                    "createdPaths",
                    &Fmt(|f| {
                        f.debug_list()
                            .entries(delta.createdPaths.iter().map(debug_path))
                            .finish()
                    }),
                )
                .field(
                    "uncleanPaths",
                    &Fmt(|f| {
                        f.debug_list()
                            .entries(delta.uncleanPaths.iter().map(debug_path))
                            .finish()
                    }),
                )
                .field(
                    "snapshotTransitions",
                    &Fmt(|f| {
                        f.debug_list()
                            .entries(delta.uncleanPaths.iter().map(debug_hash))
                            .finish()
                    }),
                )
                .finish()
        })
    }
}

#[derive(Debug, Serialize)]
struct SubscribeResponse {
    mount_generation: i64,
    // Thrift somehow generates i64 for unsigned64 type
    sequence_number: i64,
    snapshot_hash: String,
}

impl From<edenfs_thrift::JournalPosition> for SubscribeResponse {
    fn from(from: edenfs_thrift::JournalPosition) -> Self {
        Self {
            mount_generation: from.mountGeneration,
            sequence_number: from.sequenceNumber,
            snapshot_hash: hex::encode(from.snapshotHash),
        }
    }
}

#[derive(Parser, Debug)]
#[clap(about = "Subscribes to journal changes. Responses are in JSON format")]
pub struct SubscribeCmd {
    #[clap(parse(from_str = expand_path))]
    /// Path to the mount point
    mount_point: Option<PathBuf>,

    #[clap(short, long, default_value = "500")]
    /// [Unit: ms] number of milliseconds to wait between events
    throttle: u64,

    #[clap(short, long, default_value = "15")]
    /// [Unit: seconds] number of seconds to trigger an arbitrary check of
    /// current journal position in case of event missing.
    guard: u64,
}

impl SubscribeCmd {
    fn get_mount_point(&self) -> Result<PathBuf> {
        if let Some(path) = &self.mount_point {
            Ok(path.clone())
        } else {
            locate_repo_root(
                &std::env::current_dir().context("Unable to retrieve current working directory")?,
            )
            .map(|p| p.to_path_buf())
            .ok_or_else(|| anyhow!("Unable to locate repository root"))
        }
    }
}

fn have_non_hg_changes(changes: &[edenfs_thrift::PathString]) -> bool {
    changes.iter().any(|f| !f.starts_with(b".hg"))
}

fn decide_should_notify(changes: edenfs_thrift::FileDelta) -> bool {
    // If the commit hash has changed, report them
    if changes.fromPosition.snapshotHash != changes.toPosition.snapshotHash {
        return true;
    }
    // If we see any non-Mercurial changes, report them
    if have_non_hg_changes(&changes.createdPaths) {
        return true;
    }
    if have_non_hg_changes(&changes.removedPaths) {
        return true;
    }
    if have_non_hg_changes(&changes.uncleanPaths) {
        return true;
    }
    if have_non_hg_changes(&changes.changedPaths) {
        return true;
    }
    // Otherwise, do not notify
    false
}

impl SubscribeCmd {
    async fn _make_notify_event(
        mount_point: &Vec<u8>,
        last_position: &mut Option<edenfs_thrift::JournalPosition>,
    ) -> Option<ResponseBuilder> {
        let instance = EdenFsInstance::global();
        let client = match instance.connect(None).await {
            Ok(client) => client,
            Err(e) => {
                return Some(ResponseBuilder::error(&format!(
                    "error while establishing connection to EdenFS server {e:?}"
                )));
            }
        };

        let journal = match client.getCurrentJournalPosition(mount_point).await {
            Ok(journal) => journal,
            Err(e) => {
                return Some(ResponseBuilder::error(&format!(
                    "error while getting current journal position: {e:?}",
                )));
            }
        };

        let should_notify = if let Some(last_position) = last_position.replace(journal.clone()) {
            if last_position.sequenceNumber == journal.sequenceNumber {
                tracing::trace!(
                    ?journal,
                    ?last_position,
                    "skipping this event since sequence number matches"
                );
                return None;
            }

            let changes = client
                .getFilesChangedSince(mount_point, &last_position)
                .await;

            match changes {
                Ok(changes) => {
                    tracing::debug!(delta = ?fmt::debug_file_delta(&changes));
                    decide_should_notify(changes)
                }
                Err(e) => {
                    return Some(ResponseBuilder::error(&format!(
                        "error while querying changed files {:?}",
                        e
                    )));
                }
            }
        } else {
            false
        };

        if should_notify {
            let result = match serde_json::to_value(SubscribeResponse::from(journal)) {
                Err(e) => ResponseBuilder::error(&format!(
                    "error while serializing subscription response: {e:?}",
                )),
                Ok(serialized) => ResponseBuilder::result(serialized),
            };
            Some(result)
        } else {
            None
        }
    }
}

#[async_trait]
impl crate::Subcommand for SubscribeCmd {
    #[cfg(not(fbcode_build))]
    async fn run(&self) -> Result<ExitCode> {
        eprintln!("not supported in non-fbcode build");
        Ok(1)
    }

    #[cfg(fbcode_build)]
    async fn run(&self) -> Result<ExitCode> {
        let mount_point_path = self.get_mount_point()?;
        #[cfg(unix)]
        let mount_point = <Path as AsRef<OsStr>>::as_ref(&mount_point_path)
            .to_os_string()
            .into_vec();
        // SAFETY: paths on Windows are Unicode
        #[cfg(windows)]
        let mount_point = mount_point_path.to_string_lossy().into_owned().into_bytes();
        let stream_client = EdenFsInstance::global()
            .connect_streaming(None)
            .await
            .with_context(|| anyhow!("unable to establish Thrift connection to EdenFS server"))?;

        let notify = Arc::new(Notify::new());

        tokio::task::spawn({
            let notify = notify.clone();
            let mount_point = mount_point.clone();
            let mount_point_path = mount_point_path.to_path_buf();

            async move {
                let mut stdout = tokio::io::stdout();

                {
                    let response = ResponseBuilder::result(serde_json::json!({
                        "message": format!("subscribed to {}", mount_point_path.display())
                    }))
                    .build();
                    let mut bytes = serde_json::to_vec(&response).unwrap();
                    bytes.push(b'\n');
                    stdout.write_all(&bytes).await.ok();
                }

                let mut last_position = {
                    if let Ok(client) = EdenFsInstance::global().connect(None).await {
                        client.getCurrentJournalPosition(&mount_point).await.ok()
                    } else {
                        None
                    }
                };

                loop {
                    notify.notified().await;
                    let response =
                        match Self::_make_notify_event(&mount_point, &mut last_position).await {
                            None => continue,
                            Some(response) => response.build(),
                        };

                    match serde_json::to_vec(&response) {
                        Ok(mut bytes) => {
                            bytes.push(b'\n');
                            stdout.write_all(&bytes).await.ok();
                        }
                        Err(e) => {
                            tracing::error!(?e, ?response, "unable to seralize response to JSON");
                        }
                    }
                }
            }
        });

        // TODO: feels weird that this method accepts a `&Vec<u8>` instead of a `&[u8]`.
        let mut subscription = stream_client.subscribeStreamTemporary(&mount_point).await?;
        tracing::info!(?mount_point_path, "subscription created");

        let mut last = Instant::now();
        let throttle = Duration::from_millis(self.throttle);
        // Since EdenFS will not be sending us event if no
        // `getCurrentJournalPosition` is called. We have this guard timer to
        // trigger a round of manual check every few seconds (see command line
        // option for exactly how long).
        let mut guard = time::interval(Duration::from_secs(self.guard));

        loop {
            tokio::select! {
                // when we get a notification from EdenFS subscription
                result = subscription.next() => {
                    match result {
                        // if the stream is ended somehow, we terminates as well
                        None => break,
                        // if there is any error happened during the stream, log them
                        Some(Err(e)) => {
                            tracing::error!(?e, "error while processing subscription");
                            continue;
                        },
                        // otherwise, trigger an event if we haven't sent one in the last 500ms (or other configured throttle limit)
                        Some(Ok(_)) => {
                            if last.elapsed() < throttle {
                                continue;
                            }
                        }
                    }
                },
                // if the guard timer triggers, trigger an event if it's not under throttling
                _ = guard.tick() => {
                    if last.elapsed() < throttle {
                        continue;
                    }
                },
                // in all other cases, we terminate
                else => break,
            }

            notify.notify_one();
            last = Instant::now();
        }

        Ok(0)
    }
}


/*
 * 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 serde::Deserialize;
use serde::Serialize;

#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Request {
    jsonrpc: String,
    pub method: String,
    pub params: Option<serde_json::Value>,
    pub id: Option<String>,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct Response {
    jsonrpc: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    result: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    id: Option<String>,
}

impl Default for Response {
    fn default() -> Self {
        Self {
            jsonrpc: "2.0".to_string(),
            result: None,
            error: None,
            id: None,
        }
    }
}

impl From<anyhow::Error> for Response {
    fn from(error: anyhow::Error) -> Self {
        Self {
            error: Some(error.to_string().into()),
            ..Default::default()
        }
    }
}

#[derive(Debug, Default)]
pub struct ResponseBuilder {
    result: Option<serde_json::Value>,
    error: Option<serde_json::Value>,
    id: Option<String>,
}

impl ResponseBuilder {
    pub fn result(result: serde_json::Value) -> Self {
        Self {
            result: Some(result),
            ..Default::default()
        }
    }

    pub fn error(error: &str) -> Self {
        Self {
            error: Some(error.into()),
            ..Default::default()
        }
    }

    #[allow(unused)]
    pub fn with_id(mut self, id: Option<String>) -> Self {
        self.id = id;
        self
    }

    pub fn build(self) -> Response {
        Response {
            result: self.result,
            error: self.error,
            id: self.id,
            ..Default::default()
        }
    }
}


/*
 * 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::Path;

use anyhow::Context;
use anyhow::Result;
use edenfs_error::EdenFsError;
use serde::Deserialize;
use serde::Serialize;
use stack_config::StackConfig;
use tracing::event;
use tracing::trace;
use tracing::Level;

#[derive(Serialize, Deserialize, StackConfig, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Core {
    #[stack(default, merge = "merge_option")]
    eden_directory: Option<String>,
}

#[derive(Serialize, Deserialize, StackConfig, Debug, Default)]
#[serde(rename_all = "kebab-case")]
pub struct PrefetchProfiles {
    #[stack(default, merge = "merge_option")]
    pub prefetching_enabled: Option<bool>,

    #[stack(default, merge = "merge_option")]
    pub predictive_prefetching_enabled: Option<bool>,

    #[stack(merge = "merge_table", default)]
    #[serde(flatten)]
    pub other: toml::value::Table,
}

#[derive(Serialize, Deserialize, StackConfig, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Redirections {
    #[stack(default)]
    pub darwin_redirection_type: String,

    #[stack(merge = "merge_table", default)]
    #[serde(flatten)]
    pub other: toml::value::Table,
}

impl Default for Redirections {
    fn default() -> Self {
        Redirections {
            darwin_redirection_type: "apfs".to_string(),
            other: Default::default(),
        }
    }
}

#[derive(Serialize, Deserialize, StackConfig, Debug)]
pub struct EdenFsConfig {
    #[stack(nested)]
    pub core: Core,

    #[stack(nested)]
    #[serde(rename = "prefetch-profiles")]
    pub prefetch_profiles: PrefetchProfiles,

    #[stack(nested)]
    pub redirections: Redirections,

    #[stack(merge = "merge_table")]
    #[serde(flatten)]
    /// A catch-all field for unused configuration fields. If you need
    /// to use any configurations, define them above instead of reading from
    /// this field.
    pub other: toml::value::Table,
}

fn merge_option<T>(lhs: &mut Option<T>, rhs: Option<T>) {
    if let Some(rhs) = rhs {
        lhs.replace(rhs);
    }
}

fn merge_table(lhs: &mut toml::value::Table, rhs: toml::value::Table) {
    for (key, value) in rhs.into_iter() {
        if let Some(lhs_value) = lhs.get_mut(&key) {
            // Key exists
            if let (Some(lhs_table), true) = (lhs_value.as_table_mut(), value.is_table()) {
                // Both value are table, we merge them
                // SAFETY: unwrap here is guaranteed by `value.is_table()`. This
                // is awkward because try_into will consume the value, making
                // the else-clause not able to use it later.
                merge_table(lhs_table, value.try_into::<toml::value::Table>().unwrap());
            } else {
                // Otherwise, either the values are not table type, or they have
                // different types. In both case we prefer rhs value.
                *lhs_value = value;
            }
        } else {
            // Key does not exist in lhs
            lhs.insert(key, value);
        }
    }
}

fn load_path(loader: &mut EdenFsConfigLoader, path: &Path) -> Result<()> {
    let content = String::from_utf8(std::fs::read(&path)?)?;
    trace!(?content, ?path, "Loading config");
    loader.load(toml::from_str(&content)?);
    Ok(())
}

fn load_system(loader: &mut EdenFsConfigLoader, etc_dir: &Path) -> Result<()> {
    load_path(loader, &etc_dir.join("edenfs.rc"))
}

fn load_system_rcs(loader: &mut EdenFsConfigLoader, etc_dir: &Path) -> Result<()> {
    let rcs_dir = etc_dir.join("config.d");
    let entries = std::fs::read_dir(&rcs_dir)
        .with_context(|| format!("Unable to read configuration from {:?}", rcs_dir))?;

    for rc in entries {
        let rc = match rc {
            Ok(rc) => rc,
            Err(e) => {
                event!(
                    Level::INFO,
                    "Unable to read configuration, skipped: {:?}",
                    e
                );
                continue;
            }
        };
        let name = rc.file_name();
        let name = if let Some(name) = name.to_str() {
            name
        } else {
            continue;
        };

        if name.starts_with('.') || !name.ends_with(".toml") {
            continue;
        }

        if let Err(e) = load_path(loader, &rc.path()) {
            event!(
                Level::DEBUG,
                "Not able to load '{}': {:?}",
                rc.path().display(),
                e
            );
        }
    }

    Ok(())
}

pub fn load_user(loader: &mut EdenFsConfigLoader, home_dir: &Path) -> Result<()> {
    let home_rc = home_dir.join(".edenrc");
    load_path(loader, &home_rc)
}

pub fn load_config(
    etc_eden_dir: &Path,
    home_dir: Option<&Path>,
) -> Result<EdenFsConfig, EdenFsError> {
    let mut loader = EdenFsConfig::loader();

    if let Err(e) = load_system(&mut loader, &etc_eden_dir) {
        event!(
            Level::INFO,
            etc_eden_dir = ?etc_eden_dir,
            "Unable to load system configuration, skipped: {:?}",
            e
        );
    } else {
        event!(Level::DEBUG, "System configuration loaded");
    }

    if let Err(e) = load_system_rcs(&mut loader, &etc_eden_dir) {
        event!(
            Level::INFO,
            etc_eden_dir = ?etc_eden_dir,
            "Unable to load system RC configurations, skipped: {:?}",
            e
        );
    } else {
        event!(Level::DEBUG, "System RC configurations loaded");
    }

    if let Some(home) = home_dir {
        if let Err(e) = load_user(&mut loader, &home) {
            event!(Level::INFO, home = ?home, "Unable to load user configuration, skipped: {:?}", e);
        } else {
            event!(Level::DEBUG, "User configuration loaded");
        }
    } else {
        event!(
            Level::INFO,
            "Unable to find home dir. User configuration is not loaded."
        );
    }

    Ok(loader.build().map_err(EdenFsError::ConfigurationError)?)
}


/*
 * 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.
 */

//! Define common EdenFS errors

use std::path::PathBuf;
use std::result::Result as StdResult;

use thiserror::Error;
use tokio::time::error::Elapsed;

pub type ExitCode = i32;
pub type Result<T, E = EdenFsError> = std::result::Result<T, E>;

#[derive(Error, Debug)]
pub enum EdenFsError {
    #[error("Timed out when connecting to EdenFS daemon: {0:?}")]
    ThriftConnectionTimeout(PathBuf),

    #[error("IO error when connecting to EdenFS daemon: {0:?}")]
    ThriftIoError(#[source] std::io::Error),

    #[error("Error when loading configurations: {0}")]
    ConfigurationError(String),

    #[error("EdenFS did not respond within set timeout: {0}")]
    RequestTimeout(Elapsed),

    #[error("The running version of the EdenFS daemon doesn't know that method.")]
    UnknownMethod(String),

    #[error("{0}")]
    Other(#[from] anyhow::Error),
}

pub trait ResultExt<T> {
    /// Convert any error in a `Result` type into [`EdenFsError`]. Use this when ?-operator can't
    /// automatically infer the type.
    ///
    /// Note: This method will unconditionally convert everything into [`EdenFsError::Other`]
    /// variant even if there is a better match.
    fn from_err(self) -> StdResult<T, EdenFsError>;
}

impl<T, E: std::error::Error + Send + Sync + 'static> ResultExt<T> for StdResult<T, E> {
    fn from_err(self) -> StdResult<T, EdenFsError> {
        self.map_err(|e| EdenFsError::Other(e.into()))
    }
}


/*
 * 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::fmt;
use std::time::Duration;

#[derive(PartialEq, PartialOrd)]
pub enum TimeUnit {
    Days,
    Hours,
    Minutes,
    Seconds,
    Milliseconds,
    Microseconds,
    Nanoseconds,
}

pub struct HumanTime {
    days: u16,
    hours: u16,
    minutes: u16,
    seconds: u16,
    milliseconds: u16,
    microseconds: u16,
    nanoseconds: u16,
}

impl HumanTime {
    /// Returns a string representing the human time like "1d" for 1 day
    /// with the highest time unit that has a value that is > 0
    /// up to the lowest_time_unit given.
    pub fn simple_human_time(&self, lowest_time_unit: TimeUnit) -> String {
        if self.days > 0 && lowest_time_unit >= TimeUnit::Days {
            format!("{}d", self.days)
        } else if self.hours > 0 && lowest_time_unit >= TimeUnit::Hours {
            format!("{}h", self.hours)
        } else if self.minutes > 0 && lowest_time_unit >= TimeUnit::Minutes {
            format!("{}m", self.minutes)
        } else if self.seconds > 0 && lowest_time_unit >= TimeUnit::Seconds {
            format!("{}s", self.seconds)
        } else if self.milliseconds > 0 && lowest_time_unit >= TimeUnit::Milliseconds {
            format!("{}ms", self.milliseconds)
        } else if self.microseconds > 0 && lowest_time_unit >= TimeUnit::Microseconds {
            format!("{}us", self.microseconds)
        } else if self.nanoseconds > 0 && lowest_time_unit >= TimeUnit::Nanoseconds {
            format!("{}ns", self.nanoseconds)
        } else {
            String::from(match lowest_time_unit {
                TimeUnit::Days => "0d",
                TimeUnit::Hours => "0h",
                TimeUnit::Minutes => "0m",
                TimeUnit::Seconds => "0s",
                TimeUnit::Milliseconds => "0ms",
                TimeUnit::Microseconds => "0\u{03BC}s",
                TimeUnit::Nanoseconds => "0ns",
            })
        }
    }
}

impl From<Duration> for HumanTime {
    fn from(duration: Duration) -> HumanTime {
        let ns_in_us = 1000;
        let ns_in_ms = 1000 * ns_in_us;
        let ns_in_seconds = 1000 * ns_in_ms;
        let ns_in_minutes = 60 * ns_in_seconds;
        let ns_in_hours = 60 * ns_in_minutes;
        let ns_in_days = 24 * ns_in_hours;

        let nanoseconds = duration.as_nanos();

        let days = nanoseconds / ns_in_days;
        let nanoseconds = nanoseconds % ns_in_days;

        let hours = nanoseconds / ns_in_hours;
        let nanoseconds = nanoseconds % ns_in_hours;

        let minutes = nanoseconds / ns_in_minutes;
        let nanoseconds = nanoseconds % ns_in_minutes;

        let seconds = nanoseconds / ns_in_seconds;
        let nanoseconds = nanoseconds % ns_in_seconds;

        let milliseconds = nanoseconds / ns_in_ms;
        let nanoseconds = nanoseconds % ns_in_ms;

        let microseconds = nanoseconds / ns_in_us;
        let nanoseconds = nanoseconds % ns_in_us;

        HumanTime {
            days: days.try_into().unwrap(),
            hours: hours.try_into().unwrap(),
            minutes: minutes.try_into().unwrap(),
            seconds: seconds.try_into().unwrap(),
            milliseconds: milliseconds.try_into().unwrap(),
            microseconds: microseconds.try_into().unwrap(),
            nanoseconds: nanoseconds.try_into().unwrap(),
        }
    }
}

impl fmt::Display for HumanTime {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if f.alternate() {
            if self.days > 0 {
                write!(
                    f,
                    "{} day{}, ",
                    self.days,
                    if self.days == 1 { "" } else { "s" }
                )?;
            }
            write!(f, "{}:{:02}:{:02}", self.hours, self.minutes, self.seconds)
        } else {
            write!(
                f,
                "{}d:{:02}h:{:02}m:{:02}s",
                self.days, self.hours, self.minutes, self.seconds
            )
        }
    }
}

#[test]
fn test_simple_human_time_basic() {
    let test_duration = Duration::new(0, 300); // 300 ns
    assert_eq!(
        HumanTime::from(test_duration).simple_human_time(TimeUnit::Nanoseconds),
        "300ns",
    )
}

#[test]
fn test_simple_human_time_smaller_than_lowest_time_unit() {
    let test_duration = Duration::new(0, 300); // 300 ns
    assert_eq!(
        HumanTime::from(test_duration).simple_human_time(TimeUnit::Seconds),
        "0s",
    )
}

#[test]
fn test_simple_human_time_larger_than_lowest_time_unit() {
    let test_duration = Duration::new(5, 0); // 5 seconds
    assert_eq!(
        HumanTime::from(test_duration).simple_human_time(TimeUnit::Nanoseconds),
        "5s",
    )
}


/*
 * 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.
 */

//! EdenFS utils

use std::env::var;
use std::ffi::OsString;
use std::fs::read_to_string;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;

use anyhow::anyhow;
use edenfs_error::EdenFsError;
use edenfs_error::Result;
use edenfs_error::ResultExt;
use glob::glob;
use sysinfo::Pid;
use sysinfo::ProcessExt;
use sysinfo::SystemExt;
use tracing::trace;

pub mod humantime;
pub mod metadata;

#[cfg(windows)]
pub mod winargv;

pub fn path_from_bytes(bytes: &[u8]) -> Result<PathBuf> {
    Ok(PathBuf::from(std::str::from_utf8(bytes).from_err()?))
}

pub fn bytes_from_path(path: PathBuf) -> Result<Vec<u8>> {
    Ok(path
        .into_os_string()
        .into_string()
        .map_err(|e| EdenFsError::Other(anyhow!("invalid checkout path {:?}", e)))?
        .as_bytes()
        .to_vec())
}

const ENV_KEYS_TO_REMOVE: &[&str] = &[
    "DYLD_LIBRARY_PATH",
    "DYLD_INSERT_LIBRARIES",
    "PAR_LAUNCH_TIMESTAMP",
];
// some processes like hg and arc are sensitive about their environments, we
// clear variables that might make problems for their dynamic linking.
// note buck is even more sensitive see buck.run_buck_command
//
// Clean out par related environment so that we don't cause problems
// for our child process
pub fn get_environment_suitable_for_subprocess() -> Vec<(OsString, OsString)> {
    std::env::vars()
        .filter_map(|(k, v)| {
            if ENV_KEYS_TO_REMOVE.contains(&k.as_str())
                || k.starts_with("FB_PAR")
                || k.starts_with("PYTHON")
            {
                None
            } else {
                Some((k.into(), v.into()))
            }
        })
        .collect()
}

/// In the EdenFS buck integration tests we build buck from source
/// in these tests we need to use the source built buck. The path for
/// this will be in the SOURCE_BUILT_BUCK environment variable. Otherwise we use
/// the default buck in our path.
pub fn get_buck_command() -> String {
    if let Ok(buck_cmd) = var("SOURCE_BUILT_BUCK") {
        buck_cmd
    } else {
        "buck".to_string()
    }
}

/// Buck is sensitive to many environment variables, so we need to set them up
/// properly before calling into buck
pub fn get_env_with_buck_version(path: &Path) -> Result<Vec<(OsString, OsString)>> {
    let mut env = get_environment_suitable_for_subprocess();
    // If we are going to use locally built buck we don't need to set a buck
    // version. The locally build buck will only use the locally built
    // version

    if env
        .iter()
        .find(|&(k, _)| k == "SOURCE_BUILT_BUCK")
        .is_none()
    {
        // Using BUCKVERSION=last here to avoid triggering a download of a new
        // version of buck just to kill off buck.  This is specific to Facebook's
        // deployment of buck, and has no impact on the behavior of the opensource
        // buck executable.
        let buck_version = if !cfg!(windows) {
            Ok("last".to_string())
        } else {
            // On Windows, "last" doesn't work, fallback to reading the .buck-java11 file.
            let mut version_cmd = Command::new(get_buck_command());
            version_cmd.arg("--version-fast");
            let canonical_path = path.canonicalize().from_err()?;
            #[cfg(windows)]
            let canonical_path = strip_unc_prefix(canonical_path);
            let output = version_cmd
                .current_dir(canonical_path)
                .output()
                .from_err()?;
            if output.status.success() {
                Ok(std::str::from_utf8(&output.stdout)
                    .from_err()?
                    .trim_end()
                    .to_string())
            } else {
                Err(EdenFsError::Other(anyhow!(
                    "Failed to get buck version, stderr: {}, exit status: {:?}",
                    String::from_utf8_lossy(&output.stderr),
                    output.status,
                )))
            }
        }?;
        env.push((OsString::from("BUCKVERSION"), OsString::from(buck_version)));
    }
    Ok(env)
}

pub fn get_executable(pid: sysinfo::Pid) -> Option<PathBuf> {
    let mut system = sysinfo::System::new();

    if system.refresh_process(pid) {
        if let Some(process) = system.process(pid) {
            let executable = process.exe();
            trace!(%pid, ?executable, "found process executable");

            #[cfg(unix)]
            {
                // We may get a path ends with (deleted) if the executable is deleted on UNIX.
                let path = executable
                    .to_str()
                    .unwrap_or("")
                    .trim_end_matches(" (deleted)");
                return Some(path.into());
            }
            #[cfg(not(unix))]
            {
                return Some(executable.into());
            }
        } else {
            trace!(%pid, "unable to find process");
        }
    } else {
        trace!("unable to load process information");
    }

    None
}

pub fn is_process_running(pid: Pid) -> bool {
    let mut system = sysinfo::System::new();

    if system.refresh_process(pid) {
        system.process(pid).is_some()
    } else {
        false
    }
}

pub fn find_second_level_buck_projects(path: &Path) -> Result<Vec<PathBuf>> {
    /*
     * While repos usually have a top level buckconfig, in some cases projects have
     * their own configuration files one level down.  This glob() finds those directories for us.
     */
    let buck_configs = glob(&format!("{}/*/.buckconfig", path.to_string_lossy())).from_err()?;
    let buck_projects = buck_configs
        .filter_map(|c| match c {
            Ok(project) => {
                if project.is_file() {
                    project.parent().map(|p| p.to_owned())
                } else {
                    None
                }
            }
            Err(_) => None,
        })
        .collect();
    Ok(buck_projects)
}

/// Stop the major buckd instances that are likely to be running for path
pub fn stop_buckd_for_repo(path: &Path) {
    match find_second_level_buck_projects(path) {
        Ok(projects) => {
            for project in projects {
                if is_buckd_running_for_path(&project) {
                    if let Err(e) = stop_buckd_for_path(&project) {
                        eprintln!(
                            "Failed to kill buck. Please manually run `buck kill` in `{}`\n{:?}\n\n",
                            &project.display(),
                            e
                        )
                    }
                }
            }
        }
        Err(_) => {}
    };
}

pub fn is_buckd_running_for_path(path: &Path) -> bool {
    let pid_file = path.join(".buckd").join("pid");
    let file_contents = read_to_string(&pid_file).unwrap_or_default();

    if let Ok(buck_pid) = file_contents.trim().parse::<Pid>() {
        is_process_running(buck_pid)
    } else {
        false
    }
}

/// Buck is sensitive to many environment variables, so we need to set them up
/// properly before calling into buck. Use this function to guarantee environment
/// variables are set up correctly.
pub fn run_buck_command(buck_command: &mut Command, path: &Path) -> Result<Output> {
    let buck_envs = get_env_with_buck_version(path)?;
    buck_command
        .envs(buck_envs)
        .current_dir(path)
        .output()
        .from_err()
}

pub fn stop_buckd_for_path(path: &Path) -> Result<()> {
    println!("Stopping buck in {}...", path.display());
    let mut kill_cmd = Command::new(get_buck_command());
    kill_cmd.arg("kill");
    let canonical_path = path.canonicalize().from_err()?;
    #[cfg(windows)]
    let canonical_path = strip_unc_prefix(canonical_path);
    let out = run_buck_command(&mut kill_cmd, &canonical_path)?;
    if out.status.success() {
        Ok(())
    } else {
        Err(EdenFsError::Other(anyhow!(
            "Failed to kill buck, stderr: {}, exit status: {:?}",
            String::from_utf8_lossy(&out.stderr),
            out.status,
        )))
    }
}

#[cfg(windows)]
pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
    path.to_string_lossy()
        .strip_prefix(r"\\?\")
        .map(From::from)
        .unwrap_or(path)
}

#[cfg(unix)]
/// on Unixy platforms, all symlinks are files and must be removed with std::fs::remove_file
pub fn remove_symlink(path: &Path) -> Result<()> {
    std::fs::remove_file(path).from_err()?;
    Ok(())
}

#[cfg(windows)]
/// on Windows, directory symlinks must be removed with std::fs::remove_dir instead.
pub fn remove_symlink(path: &Path) -> Result<()> {
    std::fs::remove_dir(path).from_err()?;
    Ok(())
}

#[cfg(not(any(windows, unix)))]
/// on other platforms, we don't know how to handle removing symlinks. Panic instead of guessing
pub fn remove_symlink(path: &Path) -> Result<()> {
    panic!("failed to remove symlink, unsupported platform");
}


/*
 * 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::fs::Metadata;
#[cfg(target_os = "linux")]
use std::os::linux::fs::MetadataExt as MetadataLinuxExt;
#[cfg(target_os = "macos")]
use std::os::unix::fs::MetadataExt as MetadataMacosExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt as MetadataWindowsExt;

#[cfg(any(target_os = "macos", target_os = "linux"))]
use nix::sys::stat::Mode;

/// Metadata helper methods that map equivalent methods for the
/// edenfs purposes
pub trait MetadataExt {
    /// Returns the ID of the device containing the file
    fn eden_dev(&self) -> u64;

    /// Returns the file size
    fn eden_file_size(&self) -> u64;

    fn is_setuid_set(&self) -> bool;
}

#[cfg(windows)]
impl MetadataExt for Metadata {
    fn eden_dev(&self) -> u64 {
        // Dummy value
        0
    }

    fn eden_file_size(&self) -> u64 {
        self.file_size()
    }

    fn is_setuid_set(&self) -> bool {
        // This doesn't exist for windows
        false
    }
}

#[cfg(target_os = "linux")]
impl MetadataExt for Metadata {
    fn eden_dev(&self) -> u64 {
        self.st_dev()
    }

    fn eden_file_size(&self) -> u64 {
        // Use st_blocks as this represents the actual amount of
        // disk space allocated by the file, not its apparent
        // size.
        self.st_blocks() * 512
    }

    fn is_setuid_set(&self) -> bool {
        let isuid = Mode::S_ISUID;
        self.st_mode() & isuid.bits() != 0
    }
}

#[cfg(target_os = "macos")]
impl MetadataExt for Metadata {
    fn eden_dev(&self) -> u64 {
        self.dev()
    }

    fn eden_file_size(&self) -> u64 {
        self.blocks() * 512
    }

    fn is_setuid_set(&self) -> bool {
        let isuid = Mode::S_ISUID;
        self.mode() & (isuid.bits() as u32) != 0
    }
}


/*
 * 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.
 */

#![cfg(windows)]

use std::borrow::Cow;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::ops::Deref;
use std::os::windows::ffi::OsStrExt;
use std::os::windows::ffi::OsStringExt;

use anyhow::anyhow;
use anyhow::bail;
use anyhow::Result;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::shellapi::CommandLineToArgvW;
use winapi::um::winbase::LocalFree;

/// Quotes (if necessary) a set of Windows command-line arguments for consumption
/// by CommandLineToArgvW.
///
/// N.B.: This does not perform quoting for cmd.exe.
pub fn argv_to_command_line(argv: &[&OsStr]) -> Result<OsString> {
    let quoted_args = argv
        .iter()
        .enumerate()
        .map(|(i, a)| {
            if i == 0 && has_illegal_cmd_chars(&a) {
                bail!("illegal chars for argv[0] in {:?}", a);
            }
            quote_arg(a)
        })
        .collect::<Result<Vec<_>>>()?;

    let mut result = OsString::new();
    for (i, a) in quoted_args.into_iter().enumerate() {
        if i != 0 {
            result.push(" ");
        }
        result.push(a.deref());
    }
    Ok(result)
}

fn has_illegal_cmd_chars(cmd: &OsStr) -> bool {
    match cmd.to_str() {
        Some(cmd_str) => cmd_str.contains('"') || cmd_str.ends_with('\\'),
        None => true,
    }
}

/// Quotes a single argument based on the algorithm described by Microsoft:
/// https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
///
/// With the modification that we bail out if we're asked to quote control
/// characters, since CommandLineToArgvW can't round-trip them.
fn quote_arg<'a>(arg: &'a OsStr) -> Result<Cow<'a, OsStr>> {
    let arg_str = arg
        .to_str()
        .ok_or(anyhow!("arg invalid as Unicode string"))?;

    for c in arg_str.chars() {
        if c.is_control() {
            bail!("cannot quote control characters");
        }
    }

    const CHARS_TO_QUOTE: [char; 5] = [' ', '\t', '\n', '\u{000B}', '"'];
    if arg_str.find(&CHARS_TO_QUOTE) == None && !arg_str.is_empty() {
        // No quoting needed.
        return Ok(Cow::Borrowed(arg));
    }

    let mut quoted = "\"".to_owned();
    let mut num_backslashes = 0usize;
    for c in arg_str.chars() {
        if c == '\\' {
            num_backslashes += 1;
            continue;
        }
        if c == '"' {
            push_n(&mut quoted, 2 * num_backslashes + 1, '\\');
        } else {
            push_n(&mut quoted, num_backslashes, '\\');
        }
        quoted.push(c);
        num_backslashes = 0;
    }
    push_n(&mut quoted, 2 * num_backslashes, '\\');
    quoted.push('"');

    Ok(Cow::Owned(quoted.into()))
}

fn push_n(s: &mut String, n: usize, c: char) {
    for _ in 0..n {
        s.push(c);
    }
}

/// Parses a Windows command line into an argv vector of program name followed
/// by zero or more command-line arguments.
pub fn command_line_to_argv(command_line: &OsStr) -> Result<Vec<OsString>> {
    if command_line.is_empty() {
        // CommandLineToArgvW assumes the current executable file if passed an
        // empty string, but we don't want that behavior.
        return Ok(vec![]);
    }

    let command_line_w = command_line
        .encode_wide()
        .chain(Some(0))
        .collect::<Vec<_>>();

    let mut argv = Vec::<OsString>::new();

    let mut num_args: i32 = 0;
    let argv_w = LocalPtr(unsafe { CommandLineToArgvW(command_line_w.as_ptr(), &mut num_args) });
    if argv_w.0.is_null() {
        return Err(anyhow!("CommandLineToArgvW failed: {:?}", unsafe {
            GetLastError()
        }));
    }

    for i in 0..num_args {
        let arg_offset: isize = i.try_into()?;
        let arg_w = unsafe { *argv_w.0.offset(arg_offset) } as *const u16;
        let arg_w_slice = unsafe { null_terminated_slice(arg_w)? };
        argv.push(OsString::from_wide(arg_w_slice));
    }
    Ok(argv)
}

/// Owned pointer that needs to be freed with LocalFree.
struct LocalPtr<T>(*mut T);

impl<T> Drop for LocalPtr<T> {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe { LocalFree(self.0 as *mut _) };
        }
    }
}

/// Given a pointer to a null-terminated wide character string, returns a slice
/// of the string, excluding the null wide character.  Behavior is undefined if
/// given a pointer to a non-null-terminated string.
unsafe fn null_terminated_slice<'a>(ptr: *const u16) -> Result<&'a [u16]> {
    let mut i = 0isize;
    loop {
        if *ptr.offset(i) == 0u16 {
            break;
        }
        i += 1;
    }
    if *ptr.offset(i) != 0u16 {
        bail!("No null terminator found");
    }

    let slice_size: usize = i.try_into()?;
    Ok(std::slice::from_raw_parts(ptr, slice_size))
}

#[cfg(test)]
mod tests {
    use std::ffi::OsStr;
    use std::ffi::OsString;
    use std::str::FromStr;

    use anyhow::Result;
    use quickcheck::TestResult;
    use quickcheck_macros::quickcheck;

    use super::argv_to_command_line;
    use super::command_line_to_argv;
    use super::null_terminated_slice;
    use super::quote_arg;

    #[test]
    fn test_quote_arg() -> Result<()> {
        assert_eq!(quote_arg(OsStr::new(""))?, OsString::from_str("\"\"")?);
        assert_eq!(
            quote_arg(OsStr::new("argument1"))?,
            OsString::from_str("argument1")?
        );
        assert_eq!(
            quote_arg(OsStr::new("argument 2"))?,
            OsString::from_str("\"argument 2\"")?
        );
        assert_eq!(
            quote_arg(OsStr::new("\\some\\path with\\spaces"))?,
            OsString::from_str("\"\\some\\path with\\spaces\"")?
        );
        assert_eq!(
            quote_arg(OsStr::new("\\some\\path\\without\\spaces"))?,
            OsString::from_str("\\some\\path\\without\\spaces")?
        );
        assert_eq!(
            quote_arg(OsStr::new("with\"quote"))?,
            OsString::from_str("\"with\\\"quote\"")?
        );

        Ok(())
    }

    fn argv_round_trips(argv: Vec<OsString>) -> Result<bool> {
        let quoted_argv = argv_to_command_line(
            argv.iter()
                .map(OsString::as_os_str)
                .collect::<Vec<_>>()
                .as_slice(),
        )?;

        let parsed_argv = command_line_to_argv(quoted_argv.as_os_str()).unwrap();
        Ok(parsed_argv == argv)
    }

    // Try to gain confidence that our argv_to_command_line round-trips with
    // win32's CommandLineToArgvW, or returns an error if quoting is impossible.
    // The important cases to cover seem to be quoting argv[0] and argv[1..],
    // so we cover those two cases explicitly to allow quickcheck to generate
    // more comprehensive sets of test strings.
    #[quickcheck]
    fn check_argv_to_command_line_round_trips_argv0(argv: OsString) -> TestResult {
        let round_trips_ok = argv_round_trips(vec![argv]);
        if round_trips_ok.is_err() {
            // Discard arguments that quote_arg recognizes as non-quotable.
            return TestResult::discard();
        }
        TestResult::from_bool(round_trips_ok.unwrap())
    }

    #[quickcheck]
    fn check_argv_to_command_line_round_trips_argv1(argv: OsString) -> TestResult {
        let round_trips_ok = argv_round_trips(vec![OsString::from_str("foo.exe").unwrap(), argv]);
        if round_trips_ok.is_err() {
            // Discard arguments that quote_arg recognizes as non-quotable.
            return TestResult::discard();
        }
        TestResult::from_bool(round_trips_ok.unwrap())
    }

    // Specific round trip cases to check.
    #[test]
    fn test_argv_to_command_line_round_trips() -> Result<()> {
        assert!(argv_round_trips(vec![OsString::from("\"")]).is_err());
        assert!(argv_round_trips(vec![OsString::from(" \\")]).is_err());
        assert!(argv_round_trips(vec![OsString::from(" !\\")]).is_err());
        assert!(argv_round_trips(vec![OsString::from(" \\a")])?);

        Ok(())
    }

    #[test]
    fn test_command_line_to_argv() -> Result<()> {
        assert!(command_line_to_argv(OsStr::new(""))?.is_empty());
        assert_eq!(
            command_line_to_argv(OsStr::new("C:\\Windows\\system32\\svchost.exe"))?,
            vec![OsString::from_str("C:\\Windows\\system32\\svchost.exe")?]
        );
        assert_eq!(
            command_line_to_argv(OsStr::new("foo.exe bar.txt"))?,
            vec![
                OsString::from_str("foo.exe")?,
                OsString::from_str("bar.txt")?
            ]
        );

        Ok(())
    }

    #[test]
    fn test_null_terminated_slice() -> Result<()> {
        unsafe {
            assert!(null_terminated_slice(vec![0u16].as_ptr())?.is_empty());
            assert_eq!(
                null_terminated_slice(vec![1u16, 0u16].as_ptr())?,
                vec![1u16]
            );
            assert_eq!(
                null_terminated_slice(vec![1u16, 2u16, 0u16].as_ptr())?,
                vec![1u16, 2u16]
            );
        }

        Ok(())
    }
}