/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::PerfLogEvent;
use common::PerfLogger;
use graphql_watchman::WatchmanFile;
use graphql_watchman::WatchmanFileSourceResult;
use graphql_watchman::WatchmanFileSourceSubscription;
use log::debug;
use log::info;
use log::warn;
pub use watchman_client::prelude::Clock;
use watchman_client::prelude::*;

use super::watchman_query_builder::get_all_roots;
use super::watchman_query_builder::get_watchman_expr;
use super::FileSourceResult;
use crate::compiler_state::CompilerState;
use crate::config::Config;
use crate::errors::Error;
use crate::errors::Result;
use crate::saved_state::SavedStateLoader;

pub struct WatchmanFileSource<'config> {
    client: Client,
    config: &'config Config,
    resolved_root: ResolvedRoot,
}

impl<'config> WatchmanFileSource<'config> {
    pub async fn connect(
        config: &'config Config,
        perf_logger_event: &impl PerfLogEvent,
    ) -> Result<WatchmanFileSource<'config>> {
        let connect_timer = perf_logger_event.start("file_source_connect_time");
        let client = Connector::new().connect().await?;
        let canonical_root = CanonicalPath::canonicalize(&config.root_dir).map_err(|err| {
            Error::CanonicalizeRoot {
                root: config.root_dir.clone(),
                source: err,
            }
        })?;
        let resolved_root = client.resolve_root(canonical_root).await?;
        perf_logger_event.stop(connect_timer);
        debug!(
            "WatchmanFileSource::connect(...) resolved_root = {:?}",
            resolved_root
        );
        Ok(Self {
            client,
            config,
            resolved_root,
        })
    }

    /// Executes a point query (as opposed to a subscription) to find all files
    /// to compile and returns the result.
    pub async fn query(
        &self,
        perf_logger_event: &impl PerfLogEvent,
        perf_logger: &impl PerfLogger,
    ) -> Result<CompilerState> {
        info!("querying files to compile...");
        let query_time = perf_logger_event.start("file_source_query_time");
        // If the saved state flag is passed, load from it or fail.
        if let Some(saved_state_path) = &self.config.load_saved_state_file {
            let mut compiler_state = perf_logger_event.time("deserialize_saved_state", || {
                CompilerState::deserialize_from_file(saved_state_path)
            })?;
            let query_timer = perf_logger_event.start("watchman_query_time");
            let file_source_result = self.query_file_result(compiler_state.clock.clone()).await?;
            perf_logger_event.stop(query_timer);
            compiler_state
                .pending_file_source_changes
                .write()
                .unwrap()
                .push(file_source_result);
            compiler_state.merge_file_source_changes(self.config, perf_logger, true)?;
            perf_logger_event.stop(query_time);
            return Ok(compiler_state);
        }

        // If saved state is configured, try using saved state unless the config
        // forces a full build.
        if let Config {
            compile_everything: false,
            saved_state_config: Some(saved_state_config),
            saved_state_loader: Some(saved_state_loader),
            saved_state_version,
            ..
        } = self.config
        {
            match self
                .try_saved_state(
                    perf_logger,
                    perf_logger_event,
                    saved_state_config.clone(),
                    saved_state_loader.as_ref(),
                    saved_state_version,
                    self.config,
                )
                .await
            {
                Ok(load_result) => {
                    perf_logger_event.stop(query_time);
                    return load_result;
                }
                Err(saved_state_failure) => {
                    warn!(
                        "Unable to load saved state, falling back to full build: {}",
                        saved_state_failure
                    );
                }
            }
        }

        // Finally, do a simple full query.
        let full_query_result = self.full_query(perf_logger_event, perf_logger).await;

        perf_logger_event.stop(query_time);
        full_query_result
    }

    pub async fn full_query(
        &self,
        perf_logger_event: &impl PerfLogEvent,
        perf_logger: &impl PerfLogger,
    ) -> Result<CompilerState> {
        let file_source_result = self.query_file_result(None).await?;
        let compiler_state = perf_logger_event.time("from_file_source_changes", || {
            CompilerState::from_file_source_changes(
                self.config,
                &file_source_result,
                perf_logger_event,
                perf_logger,
            )
        })?;
        Ok(compiler_state)
    }

    /// Starts a subscription sending updates since the given clock.
    pub async fn subscribe(
        self,
        perf_logger_event: &impl PerfLogEvent,
        perf_logger: &impl PerfLogger,
    ) -> Result<(CompilerState, WatchmanFileSourceSubscription)> {
        let timer = perf_logger_event.start("file_source_subscribe_time");
        let compiler_state = self.query(perf_logger_event, perf_logger).await?;

        let expression = get_watchman_expr(self.config);

        let query_timer = perf_logger_event.start("watchman_query_time_before_subscribe");
        let file_source_result = self.query_file_result(compiler_state.clock.clone()).await?;
        perf_logger_event.stop(query_timer);

        let query_timer = perf_logger_event.start("watchman_query_time_subscribe");
        let (subscription, _initial) = self
            .client
            .subscribe::<WatchmanFile>(
                &self.resolved_root,
                SubscribeRequest {
                    expression: Some(expression),
                    since: file_source_result.clock(),
                    defer: vec!["hg.update"],
                    ..Default::default()
                },
            )
            .await?;
        perf_logger_event.stop(query_timer);

        perf_logger_event.stop(timer);

        Ok((
            compiler_state,
            WatchmanFileSourceSubscription::new(self.resolved_root.clone(), subscription),
        ))
    }

    /// Internal method to issue a watchman query, returning a raw
    /// WatchmanFileSourceResult.
    async fn query_file_result(&self, since_clock: Option<Clock>) -> Result<FileSourceResult> {
        let expression = get_watchman_expr(self.config);
        debug!(
            "WatchmanFileSource::query_file_result(...) get_watchman_expr = {:?}",
            &expression
        );

        // If `since` is available, we should not pass the `path` parameter.
        // Watchman ignores `since` parameter if both `path` and `since` are
        // passed as the request params
        let request = if since_clock.is_some() {
            QueryRequestCommon {
                expression: Some(expression),
                since: since_clock,
                ..Default::default()
            }
        } else {
            let query_roots = get_all_roots(self.config)
                .into_iter()
                .map(PathGeneratorElement::RecursivePath)
                .collect();
            QueryRequestCommon {
                expression: Some(expression),
                path: Some(query_roots),
                ..Default::default()
            }
        };
        debug!(
            "WatchmanFileSource::query_file_result(...) request = {:?}",
            &request
        );
        let query_result = self
            .client
            .query::<WatchmanFile>(&self.resolved_root, request)
            .await?;

        // print debug information for this result
        // (file list will include only files with specified extension)
        debug_query_results(&query_result, "graphql");

        let files = query_result.files.ok_or(Error::EmptyQueryResult)?;
        Ok(FileSourceResult::Watchman(WatchmanFileSourceResult {
            files,
            resolved_root: self.resolved_root.clone(),
            clock: query_result.clock,
            saved_state_info: query_result.saved_state_info,
        }))
    }

    /// Tries to load saved state with a watchman query.
    /// The return value is a nested Result:
    /// The outer Result indicates the result of a possible saved state infrastructure failure.
    /// The inner Result is a potential parse error.
    async fn try_saved_state(
        &self,
        perf_logger: &impl PerfLogger,
        perf_logger_event: &impl PerfLogEvent,
        saved_state_config: ScmAwareClockData,
        saved_state_loader: &'_ (dyn SavedStateLoader + Send + Sync),
        saved_state_version: &str,
        config: &Config,
    ) -> std::result::Result<Result<CompilerState>, &'static str> {
        let scm_since = Clock::ScmAware(FatClockData {
            clock: ClockSpec::null(),
            scm: Some(saved_state_config),
        });
        debug!(
            "WatchmanFileSource::try_saved_state(...) scm_since = {:?}",
            &scm_since
        );
        let query_timer = perf_logger_event.start("watchman_query_time_try_saved_state");
        let file_source_result = self
            .query_file_result(Some(scm_since))
            .await
            .map_err(|_| "query failed")?;
        perf_logger_event.stop(query_timer);

        debug!(
            "WatchmanFileSource::try_saved_state(...) file_source_result = {:?}",
            &file_source_result
        );
        let saved_state_info = file_source_result
            .saved_state_info()
            .as_ref()
            .ok_or("no saved state in watchman response")?;
        debug!(
            "WatchmanFileSource::saved_state_info(...) file_source_result = {:?}",
            &saved_state_info
        );
        let saved_state_path = perf_logger_event.time("saved_state_loading_time", || {
            saved_state_loader
                .load(saved_state_info, self.config)
                .ok_or("unable to load")
        })?;
        let mut compiler_state = perf_logger_event
            .time("deserialize_saved_state", || {
                CompilerState::deserialize_from_file(&saved_state_path)
            })
            .map_err(|err| {
                let error_event = perf_logger.create_event("saved_state_loader_error");
                error_event.string("error", format!("Failed to deserialize: {}", err));
                error_event.complete();
                "failed to deserialize"
            })?;
        // For cases, where we want to debug saved state integration, that doesn't include
        // saved_state format changes we may need to disable this by adding this env variable
        if std::env::var("RELAY_COMPILER_IGNORE_SAVED_STATE_VERSION").is_err()
            && compiler_state.saved_state_version != saved_state_version
        {
            return Err("Saved state version doesn't match.");
        }
        compiler_state
            .pending_file_source_changes
            .write()
            .unwrap()
            .push(file_source_result);

        if let Some(update_compiler_state_from_saved_state) =
            &config.update_compiler_state_from_saved_state
        {
            update_compiler_state_from_saved_state(&mut compiler_state, config);
        }

        if let Err(parse_error) = perf_logger_event.time("merge_file_source_changes", || {
            compiler_state.merge_file_source_changes(self.config, perf_logger, true)
        }) {
            Ok(Err(parse_error))
        } else {
            Ok(Ok(compiler_state))
        }
    }
}

fn debug_query_results(query_result: &QueryResult<WatchmanFile>, extension_filter: &str) {
    if let Ok(rust_log) = std::env::var("RUST_LOG") {
        if rust_log == *"debug" {
            debug!(
                "WatchmanFileSource::query_file_result(...) query_result.version = {:?}",
                query_result.version
            );
            debug!(
                "WatchmanFileSource::query_file_result(...) query_result.clock = {:?}",
                query_result.clock
            );
            debug!(
                "WatchmanFileSource::query_file_result(...) query_result.is_fresh_instance = {:?}",
                query_result.is_fresh_instance
            );
            debug!(
                "WatchmanFileSource::query_file_result(...) query_result.saved_state_info = {:?}",
                query_result.saved_state_info
            );
            debug!(
                "WatchmanFileSource::query_file_result(...) query_result.state_metadata = {:?}",
                query_result.state_metadata
            );
            if let Some(files) = &query_result.files {
                debug!(
                    "WatchmanFileSource::query_file_result(...) query_result.files(only=*.{}) = \n{}",
                    extension_filter,
                    files
                        .iter()
                        .filter_map(|file| {
                            if file.name.extension().is_some()
                                && file.name.extension().unwrap() == extension_filter
                            {
                                Some(format!("name: {:?}, exists: {}", *file.name, *file.exists))
                            } else {
                                None
                            }
                        })
                        .collect::<Vec<String>>()
                        .join("\n")
                );
            } else {
                debug!("WatchmanFileSource::query_file_result(...): no files found");
            }
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::path::PathBuf;

use relay_typegen::TypegenLanguage;
use watchman_client::prelude::*;

use crate::config::Config;
use crate::config::SchemaLocation;

pub fn get_watchman_expr(config: &Config) -> Expr {
    let mut sources_conditions = vec![expr_any(
        config
            .sources
            .iter()
            .flat_map(|(path, project_set)| {
                project_set
                    .iter()
                    .map(|name| (path, &config.projects[name]))
                    .collect::<Vec<_>>()
            })
            .map(|(path, project)| {
                Expr::All(vec![
                    // Ending in *.js(x) or *.ts(x) depending on the project language.
                    Expr::Suffix(match &project.typegen_config.language {
                        TypegenLanguage::Flow | TypegenLanguage::JavaScript => {
                            vec![PathBuf::from("js"), PathBuf::from("jsx")]
                        }
                        TypegenLanguage::TypeScript => {
                            vec![
                                PathBuf::from("js"),
                                PathBuf::from("jsx"),
                                PathBuf::from("ts"),
                                PathBuf::from("tsx"),
                            ]
                        }
                    }),
                    // In the related source root.
                    Expr::DirName(DirNameTerm {
                        path: path.clone(),
                        depth: None,
                    }),
                ])
            })
            .collect(),
    )];
    // not excluded by any glob
    if !config.excludes.is_empty() {
        sources_conditions.push(Expr::Not(Box::new(expr_any(
            config
                .excludes
                .iter()
                .map(|item| {
                    Expr::Match(MatchTerm {
                        glob: item.into(),
                        wholename: true,
                        ..Default::default()
                    })
                })
                .collect(),
        ))));
    }
    let sources_expr = Expr::All(sources_conditions);

    let mut expressions = vec![sources_expr];

    let output_dir_paths = get_output_dir_paths(config);
    if !output_dir_paths.is_empty() {
        let output_dir_expr = expr_files_in_dirs(output_dir_paths);
        expressions.push(output_dir_expr);
    }

    let schema_file_paths = get_schema_file_paths(config);
    if !schema_file_paths.is_empty() {
        let schema_file_expr = Expr::Name(NameTerm {
            paths: get_schema_file_paths(config),
            wholename: true,
        });
        expressions.push(schema_file_expr);
    }

    let schema_dir_paths = get_schema_dir_paths(config);
    if !schema_dir_paths.is_empty() {
        let schema_dir_expr = expr_graphql_files_in_dirs(schema_dir_paths);
        expressions.push(schema_dir_expr);
    }

    let extension_roots = get_extension_roots(config);
    if !extension_roots.is_empty() {
        let extensions_expr = expr_graphql_files_in_dirs(extension_roots);
        expressions.push(extensions_expr);
    }

    Expr::All(vec![
        // we generally only care about regular files
        Expr::FileType(FileType::Regular),
        Expr::Any(expressions),
    ])
}

/// Compute all root paths that we need to query Watchman with. All files
/// relevant to the compiler should be in these directories.
pub fn get_all_roots(config: &Config) -> Vec<PathBuf> {
    let source_roots = get_source_roots(config);
    let output_roots = get_output_dir_paths(config);
    let extension_roots = get_extension_roots(config);
    let schema_file_roots = get_schema_file_roots(config);
    let schema_dir_roots = get_schema_dir_paths(config);
    unify_roots(
        source_roots
            .into_iter()
            .chain(output_roots)
            .chain(extension_roots)
            .chain(schema_file_roots)
            .chain(schema_dir_roots)
            .collect(),
    )
}

/// Returns all root directories of JS source files for the config.
fn get_source_roots(config: &Config) -> Vec<PathBuf> {
    config.sources.keys().cloned().collect()
}

/// Returns all root directories of GraphQL schema extension files for the
/// config.
fn get_extension_roots(config: &Config) -> Vec<PathBuf> {
    config
        .projects
        .values()
        .flat_map(|project_config| project_config.schema_extensions.iter().cloned())
        .collect()
}

/// Returns all output and extra artifact output directories for the config.
fn get_output_dir_paths(config: &Config) -> Vec<PathBuf> {
    let output_dirs = config
        .projects
        .values()
        .filter_map(|project_config| project_config.output.clone());

    let extra_artifact_output_dirs = config
        .projects
        .values()
        .filter_map(|project_config| project_config.extra_artifacts_output.clone());

    output_dirs.chain(extra_artifact_output_dirs).collect()
}

/// Returns all paths that contain GraphQL schema files for the config.
fn get_schema_file_paths(config: &Config) -> Vec<PathBuf> {
    config
        .projects
        .values()
        .filter_map(|project_config| match &project_config.schema_location {
            SchemaLocation::File(schema_file) => Some(schema_file.clone()),
            SchemaLocation::Directory(_) => None,
        })
        .collect()
}

/// Returns all GraphQL schema directories for the config.
fn get_schema_dir_paths(config: &Config) -> Vec<PathBuf> {
    config
        .projects
        .values()
        .filter_map(|project_config| match &project_config.schema_location {
            SchemaLocation::File(_) => None,
            SchemaLocation::Directory(schema_dir) => Some(schema_dir.clone()),
        })
        .collect()
}

/// Returns root directories that contain GraphQL schema files.
fn get_schema_file_roots(config: &Config) -> impl Iterator<Item = PathBuf> {
    get_schema_file_paths(config)
        .into_iter()
        .map(|schema_path| {
            schema_path
                .parent()
                .expect("A schema in the project root directory is currently not supported.")
                .to_owned()
        })
}

fn expr_files_in_dirs(roots: Vec<PathBuf>) -> Expr {
    expr_any(
        roots
            .into_iter()
            .map(|path| Expr::DirName(DirNameTerm { path, depth: None }))
            .collect(),
    )
}

fn expr_graphql_files_in_dirs(roots: Vec<PathBuf>) -> Expr {
    Expr::All(vec![
        // ending in *.graphql
        Expr::Suffix(vec!["graphql".into(), "gql".into()]),
        // in one of the extension directories
        expr_files_in_dirs(roots),
    ])
}

/// Helper to create an `anyof` expression if multiple items are passed or just
/// return the expression for a single item input `Vec`.
/// Panics for empty expressions. These are not valid in Watchman. We could
/// return `Expr::false`, but for that case the caller should just skip this
/// expression.
fn expr_any(expressions: Vec<Expr>) -> Expr {
    match expressions.len() {
        0 => panic!("expr_any called with empty expressions, this is an invalid query."),
        1 => expressions.into_iter().next().unwrap(),
        _ => Expr::Any(expressions),
    }
}

/// Finds the roots of a set of paths. This filters any paths
/// that are a subdirectory of other paths in the input.
fn unify_roots(mut paths: Vec<PathBuf>) -> Vec<PathBuf> {
    paths.sort();
    let mut roots = Vec::new();
    for path in paths {
        match roots.last() {
            Some(prev) if path.starts_with(&prev) => {
                // skip
            }
            _ => {
                roots.push(path);
            }
        }
    }
    roots
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_unify_roots() {
        assert_eq!(unify_roots(vec![]).len(), 0);
        assert_eq!(
            unify_roots(vec!["Apps".into(), "Libraries".into()]),
            &[PathBuf::from("Apps"), PathBuf::from("Libraries")]
        );
        assert_eq!(
            unify_roots(vec!["Apps".into(), "Apps/Foo".into()]),
            &[PathBuf::from("Apps")]
        );
        assert_eq!(
            unify_roots(vec!["Apps/Foo".into(), "Apps".into()]),
            &[PathBuf::from("Apps")]
        );
        assert_eq!(
            unify_roots(vec!["Foo".into(), "Foo2".into()]),
            &[PathBuf::from("Foo"), PathBuf::from("Foo2"),]
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::BTreeMap;
use std::fs::File;
use std::io::BufWriter;
use std::io::Write;

use async_trait::async_trait;
use dashmap::DashMap;
use md5::Md5;
use persist_query::PersistError;
use relay_config::LocalPersistAlgorithm;
use relay_config::LocalPersistConfig;
use sha1::Digest;
use sha1::Sha1;
use sha2::Sha256;

use crate::config::ArtifactForPersister;
use crate::OperationPersister;

pub struct LocalPersister {
    config: LocalPersistConfig,
    query_map: DashMap<String, String>,
}

impl LocalPersister {
    pub fn new(config: LocalPersistConfig) -> Self {
        let query_map: DashMap<String, String> = match std::fs::read_to_string(&config.file) {
            Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
            Err(_e) => {
                panic!(
                    "LocalPersister: Expected the {} file to exist.",
                    &config.file.display(),
                )
            }
        };

        Self { config, query_map }
    }

    fn hash_operation(&self, operation_text: String) -> String {
        match self.config.algorithm {
            LocalPersistAlgorithm::MD5 => {
                let mut md5 = Md5::new();
                md5.update(operation_text);
                hex::encode(md5.finalize())
            }
            LocalPersistAlgorithm::SHA1 => {
                let mut hash = Sha1::new();
                hash.update(&operation_text);
                hex::encode(hash.finalize())
            }
            LocalPersistAlgorithm::SHA256 => {
                let mut hash = Sha256::new();
                hash.update(&operation_text);
                hex::encode(hash.finalize())
            }
        }
    }
}

#[async_trait]
impl OperationPersister for LocalPersister {
    async fn persist_artifact(
        &self,
        artifact: ArtifactForPersister,
    ) -> Result<String, PersistError> {
        let operation_hash = self.hash_operation(artifact.text.clone());

        if !self.query_map.contains_key(&operation_hash) {
            self.query_map.insert(operation_hash.clone(), artifact.text);
        }

        Ok(operation_hash)
    }

    fn finalize(&self) -> Result<(), PersistError> {
        let ordered: BTreeMap<_, _> = self
            .query_map
            .iter()
            .map(|x| (x.key().clone(), x.value().clone()))
            .collect();

        let mut writer = BufWriter::new(File::create(&self.config.file)?);
        serde_json::to_writer_pretty(&mut writer, &ordered)?;
        writer.write_all(b"\n")?;
        writer.flush()?;
        Ok(())
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

mod local_persister;
mod remote_persister;

pub use local_persister::LocalPersister;
pub use remote_persister::RemotePersister;


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use async_trait::async_trait;
use persist_query::persist;
use persist_query::PersistError;
use relay_config::RemotePersistConfig;
use tokio::sync::Semaphore;

use crate::config::ArtifactForPersister;
use crate::OperationPersister;

pub struct RemotePersister {
    pub config: RemotePersistConfig,
    semaphore: Option<Semaphore>,
}

impl RemotePersister {
    pub fn new(config: RemotePersistConfig) -> Self {
        let semaphore = config.semaphore_permits.map(Semaphore::new);
        Self { config, semaphore }
    }
}

#[async_trait]
impl OperationPersister for RemotePersister {
    async fn persist_artifact(
        &self,
        artifact: ArtifactForPersister,
    ) -> Result<String, PersistError> {
        let params = &self.config.params;
        let headers = &self.config.headers;

        let url = &self.config.url;
        if let Some(semaphore) = &self.semaphore {
            let permit = (*semaphore).acquire().await.unwrap();
            let result = persist(&artifact.text, url, params, headers).await;
            drop(permit);
            result
        } else {
            persist(&artifact.text, url, params, headers).await
        }
    }
}