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

use common::Diagnostic;
use common::PerfLogEvent;
use common::PerfLogger;
use common::WithDiagnostics;
use futures::future::join_all;
use graphql_watchman::WatchmanFileSourceSubscriptionNextChange;
use log::debug;
use log::info;
use rayon::prelude::*;
use tokio::sync::Notify;
use tokio::task;
use tokio::task::JoinHandle;

use crate::build_project::build_project;
use crate::build_project::commit_project;
use crate::build_project::BuildProjectFailure;
use crate::compiler_state::ArtifactMapKind;
use crate::compiler_state::CompilerState;
use crate::config::Config;
use crate::errors::Error;
use crate::errors::Result;
use crate::file_source::FileSource;
use crate::file_source::FileSourceSubscriptionNextChange;
use crate::graphql_asts::GraphQLAsts;
use crate::red_to_green::RedToGreen;
use crate::FileSourceResult;

pub struct Compiler<TPerfLogger>
where
    TPerfLogger: PerfLogger + 'static,
{
    config: Arc<Config>,
    perf_logger: Arc<TPerfLogger>,
}

impl<TPerfLogger: PerfLogger> Compiler<TPerfLogger> {
    pub fn new(config: Arc<Config>, perf_logger: Arc<TPerfLogger>) -> Self {
        Self {
            config,
            perf_logger,
        }
    }

    pub async fn compile(&self) -> Result<CompilerState> {
        let setup_event = self.perf_logger.create_event("compiler_setup");
        self.config.status_reporter.build_starts();
        let result: Result<(CompilerState, Vec<Diagnostic>)> = async {
            if let Some(initialize_resources) = &self.config.initialize_resources {
                let timer = setup_event.start("load_resources");
                initialize_resources();
                setup_event.stop(timer);
            }
            let file_source = FileSource::connect(&self.config, &setup_event).await?;
            let mut compiler_state = file_source
                .query(&setup_event, self.perf_logger.as_ref())
                .await?;

            let diagnostics = self
                .build_projects(&mut compiler_state, &setup_event)
                .await?;

            Ok((compiler_state, diagnostics))
        }
        .await;
        setup_event.complete();

        match result {
            Ok((compiler_state, non_fatal_diagnostics)) => {
                self.config
                    .status_reporter
                    .build_completes(&non_fatal_diagnostics);
                Ok(compiler_state)
            }
            Err(error) => {
                self.config.status_reporter.build_errors(&error);
                Err(error)
            }
        }
    }

    pub async fn watch(&self) -> Result<()> {
        'watch: loop {
            let setup_event = self.perf_logger.create_event("compiler_setup");
            self.config.status_reporter.build_starts();
            let result: Result<(CompilerState, Arc<Notify>, JoinHandle<()>)> = async {
                if let Some(initialize_resources) = &self.config.initialize_resources {
                    let timer = setup_event.start("load_resources");
                    initialize_resources();
                    setup_event.stop(timer);
                }
                let file_source = FileSource::connect(&self.config, &setup_event).await?;

                let (compiler_state, mut subscription) = file_source
                    .subscribe(&setup_event, self.perf_logger.as_ref())
                    .await?;

                let pending_file_source_changes =
                    compiler_state.pending_file_source_changes.clone();
                let source_control_update_status =
                    Arc::clone(&compiler_state.source_control_update_status);

                let notify_sender = Arc::new(Notify::new());
                let notify_receiver = notify_sender.clone();

                // First, set up watchman subscription
                let subscription_handle = task::spawn(async move {
                    loop {
                        let next_change = subscription.next_change().await;
                        match next_change {
                            Ok(FileSourceSubscriptionNextChange::Watchman(watchman_next_change)) => {
                                match watchman_next_change {
                                    WatchmanFileSourceSubscriptionNextChange::Result(file_source_changes) => {
                                        pending_file_source_changes
                                            .write()
                                            .unwrap()
                                            .push(FileSourceResult::Watchman(file_source_changes));
                                        notify_sender.notify_one();
                                    }
                                    WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateEnter => {
                                        info!("hg.update started...");
                                        source_control_update_status.mark_as_started();
                                    }
                                    WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateLeave => {
                                        info!("hg.update completed.");
                                        source_control_update_status.set_to_default();
                                    }
                                    WatchmanFileSourceSubscriptionNextChange::SourceControlUpdate => {
                                        info!("hg.update completed. Detected new base revision...");
                                        source_control_update_status.mark_as_completed();
                                        notify_sender.notify_one();
                                        break;
                                    }
                                    WatchmanFileSourceSubscriptionNextChange::None => {}
                                }
                            }
                            Err(err) => {
                                panic!("Watchman subscription error: {}", err);
                            }
                        }
                    }
                });

                Ok((compiler_state, notify_receiver, subscription_handle))

            }
            .await;

            match result {
                Ok((mut compiler_state, notify_receiver, subscription_handle)) => {
                    let mut red_to_green = RedToGreen::new();
                    match self.build_projects(&mut compiler_state, &setup_event).await {
                        Ok(diagnostics) => {
                            self.config.status_reporter.build_completes(&diagnostics);
                        }
                        Err(err) => {
                            red_to_green.log_error();
                            self.config.status_reporter.build_errors(&err);
                        }
                    };

                    setup_event.complete();
                    info!("Watching for new changes...");

                    self.incremental_build_loop(
                        compiler_state,
                        notify_receiver,
                        &subscription_handle,
                    )
                    .await;
                }
                Err(err) => {
                    self.config.status_reporter.build_errors(&err);
                    break 'watch Err(err);
                }
            }
        }
    }

    async fn incremental_build_loop(
        &self,
        mut compiler_state: CompilerState,
        notify_receiver: Arc<Notify>,
        subscription_handle: &JoinHandle<()>,
    ) {
        let mut red_to_green = RedToGreen::new();

        loop {
            notify_receiver.notified().await;

            if compiler_state.source_control_update_status.is_started() {
                continue;
            }

            if compiler_state.source_control_update_status.is_completed() {
                subscription_handle.abort();
                return;
            }

            // Single change to file sometimes produces 2 watchman change events for the same file
            // wait for 50ms in case there is a subsequent request
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;

            if compiler_state.has_pending_file_source_changes() {
                let incremental_build_event =
                    self.perf_logger.create_event("incremental_build_event");
                let incremental_build_time =
                    incremental_build_event.start("incremental_build_time");

                let had_new_changes = match compiler_state.merge_file_source_changes(
                    &self.config,
                    self.perf_logger.as_ref(),
                    false,
                ) {
                    Ok(b) => b,
                    Err(err) => {
                        let error_event = self.perf_logger.create_event("watch_build_error");
                        error_event.string("error", format!("Ignored Compilation Error: {}", err));
                        error_event.complete();
                        return;
                    }
                };

                if had_new_changes {
                    self.config.status_reporter.build_starts();
                    info!("Change detected, start compiling...");

                    match self
                        .build_projects(&mut compiler_state, &incremental_build_event)
                        .await
                    {
                        Ok(diagnostics) => {
                            self.config.status_reporter.build_completes(&diagnostics);
                            red_to_green.clear_error_and_log(self.perf_logger.as_ref());
                        }
                        Err(err) => {
                            red_to_green.log_error();
                            self.config.status_reporter.build_errors(&err);
                        }
                    }

                    incremental_build_event.stop(incremental_build_time);
                    info!("Watching for new changes...");
                } else {
                    debug!("No new changes detected.");
                    incremental_build_event.stop(incremental_build_time);
                }
                incremental_build_event.complete();
            }
        }
    }

    async fn build_projects(
        &self,
        compiler_state: &mut CompilerState,
        setup_event: &impl PerfLogEvent,
    ) -> Result<Vec<Diagnostic>> {
        let build_projects_time = setup_event.start("build_projects_time");
        let result = build_projects(
            Arc::clone(&self.config),
            Arc::clone(&self.perf_logger),
            setup_event,
            compiler_state,
        )
        .await;
        setup_event.stop(build_projects_time);
        setup_event.time("post_build_projects_time", || {
            result.and_then(|diagnostics| {
                compiler_state.complete_compilation();
                self.config.artifact_writer.finalize()?;
                if let Some(post_artifacts_write) = &self.config.post_artifacts_write {
                    post_artifacts_write(&self.config)
                        .map_err(|error| Error::PostArtifactsError { error })?;
                }

                Ok(diagnostics)
            })
        })
    }
}

async fn build_projects<TPerfLogger: PerfLogger + 'static>(
    config: Arc<Config>,
    perf_logger: Arc<TPerfLogger>,
    setup_event: &impl PerfLogEvent,
    compiler_state: &mut CompilerState,
) -> Result<Vec<Diagnostic>> {
    let mut graphql_asts = setup_event.time("parse_sources_time", || {
        GraphQLAsts::from_graphql_sources_map(
            &compiler_state.graphql_sources,
            &compiler_state.get_dirty_definitions(&config),
        )
    })?;

    if compiler_state.should_cancel_current_build() {
        debug!("Build is cancelled: new file changes are pending.");
        return Err(Error::Cancelled);
    }

    let build_results: Vec<_> = config
        .par_enabled_projects()
        .filter(|project_config| {
            if let Some(base) = project_config.base {
                if compiler_state.project_has_pending_changes(base) {
                    return true;
                }
            }
            compiler_state.project_has_pending_changes(project_config.name)
        })
        .map(|project_config| {
            build_project(
                &config,
                project_config,
                compiler_state,
                &graphql_asts,
                Arc::clone(&perf_logger),
            )
        })
        .collect();
    let mut results = Vec::new();
    let mut errors = Vec::new();
    for result in build_results {
        match result {
            Ok(result) => results.push(result),
            Err(error) => match error {
                BuildProjectFailure::Error(error) => errors.push(error),
                BuildProjectFailure::Cancelled => {
                    return Err(Error::Cancelled);
                }
            },
        }
    }

    if !errors.is_empty() {
        return Err(Error::BuildProjectsErrors { errors });
    }

    if compiler_state.should_cancel_current_build() {
        debug!("Build is cancelled: updates in source code/or new file changes are pending.");
        return Err(Error::Cancelled);
    }

    let mut handles: Vec<JoinHandle<std::result::Result<_, BuildProjectFailure>>> = Vec::new();
    for WithDiagnostics {
        item: (project_name, schema, programs, artifacts),
        diagnostics,
    } in results
    {
        let config = Arc::clone(&config);
        let perf_logger = Arc::clone(&perf_logger);
        let artifact_map = compiler_state
            .artifacts
            .get(&project_name)
            .cloned()
            .unwrap_or_else(|| Arc::new(ArtifactMapKind::Unconnected(Default::default())));
        let removed_definition_names = graphql_asts
            .remove(&project_name)
            .expect("Expect GraphQLAsts to exist.")
            .removed_definition_names;
        let dirty_artifact_paths = compiler_state
            .dirty_artifact_paths
            .get(&project_name)
            .cloned()
            .unwrap_or_default();

        let source_control_update_status = Arc::clone(&compiler_state.source_control_update_status);
        handles.push(task::spawn(async move {
            let project_config = &config.projects[&project_name];
            Ok((
                (
                    project_name,
                    commit_project(
                        &config,
                        project_config,
                        perf_logger,
                        &schema,
                        programs,
                        artifacts,
                        artifact_map,
                        removed_definition_names,
                        dirty_artifact_paths,
                        source_control_update_status,
                    )
                    .await?,
                    schema,
                ),
                diagnostics,
            ))
        }));
    }

    let mut build_cancelled_during_commit = false;
    let mut all_diagnostics: Vec<Diagnostic> = Vec::new();
    for commit_result in join_all(handles).await {
        let commit_result: std::result::Result<std::result::Result<_, _>, _> = commit_result;
        let mut inner_result = commit_result.map_err(|e| Error::JoinError {
            error: e.to_string(),
        })?;
        match inner_result {
            Ok(((project_name, next_artifact_map, schema), ref mut diagnostics)) => {
                let next_artifact_map = Arc::new(ArtifactMapKind::Mapping(next_artifact_map));
                compiler_state
                    .artifacts
                    .insert(project_name, next_artifact_map);
                compiler_state.schema_cache.insert(project_name, schema);

                all_diagnostics.append(diagnostics);
            }
            Err(BuildProjectFailure::Error(error)) => {
                errors.push(error);
            }
            Err(BuildProjectFailure::Cancelled) => {
                build_cancelled_during_commit = true;
            }
        }
    }

    if !errors.is_empty() {
        return Err(Error::BuildProjectsErrors { errors });
    }

    if build_cancelled_during_commit {
        return Err(Error::Cancelled);
    }

    Ok(all_diagnostics)
}


/*
 * 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::hash_map::Entry;
use std::env;
use std::fmt;
use std::fs::File as FsFile;
use std::hash::Hash;
use std::io::BufReader;
use std::io::BufWriter;
use std::path::PathBuf;
use std::slice;
use std::sync::Arc;
use std::sync::RwLock;
use std::vec;

use bincode::Options;
use common::PerfLogEvent;
use common::PerfLogger;
use common::SourceLocationKey;
use dashmap::DashSet;
use fnv::FnvBuildHasher;
use fnv::FnvHashMap;
use fnv::FnvHashSet;
use graphql_ir::ExecutableDefinitionName;
use intern::string_key::StringKey;
use rayon::prelude::*;
use relay_config::SchemaConfig;
use schema::SDLSchema;
use schema_diff::definitions::SchemaChange;
use schema_diff::detect_changes;
use serde::Deserialize;
use serde::Serialize;
use zstd::stream::read::Decoder as ZstdDecoder;
use zstd::stream::write::Encoder as ZstdEncoder;

use crate::artifact_map::ArtifactMap;
use crate::config::Config;
use crate::errors::Error;
use crate::errors::Result;
use crate::file_source::categorize_files;
use crate::file_source::extract_javascript_features_from_file;
use crate::file_source::read_file_to_string;
use crate::file_source::Clock;
use crate::file_source::File;
use crate::file_source::FileGroup;
use crate::file_source::FileSourceResult;
use crate::file_source::LocatedDocblockSource;
use crate::file_source::LocatedGraphQLSource;
use crate::file_source::LocatedJavascriptSourceFeatures;
use crate::file_source::SourceControlUpdateStatus;

/// Name of a compiler project.
pub type ProjectName = StringKey;

/// Set of project names.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(from = "DeserializableProjectSet")]
pub struct ProjectSet(Vec<ProjectName>);

impl ProjectSet {
    pub fn new(names: Vec<ProjectName>) -> Self {
        if names.is_empty() {
            panic!("Expected project set to have at least one project.")
        }
        ProjectSet(names)
    }
    pub fn of(name: ProjectName) -> Self {
        ProjectSet(vec![name])
    }
    /// Inserts a new project name into this set.
    pub fn insert(&mut self, project_name: ProjectName) {
        let existing_names = &mut self.0;
        assert!(!existing_names.contains(&project_name));
        existing_names.push(project_name);
    }

    pub fn iter(&self) -> slice::Iter<'_, StringKey> {
        self.0.iter()
    }

    pub fn has_multiple_projects(&self) -> bool {
        self.0.len() > 1
    }

    pub fn first(&self) -> Option<&ProjectName> {
        self.0.first()
    }
}

impl IntoIterator for ProjectSet {
    type Item = ProjectName;
    type IntoIter = vec::IntoIter<StringKey>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl fmt::Display for ProjectSet {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let names = self
            .0
            .iter()
            .map(|name| name.to_string())
            .collect::<Vec<String>>()
            .join(",");

        write!(f, "{}", names)
    }
}

// Serde has the built in ability for deserializing enums that might be in
// different shapes. In this case a `Vec<StringKey>` or `StringKey`. However, we
// want our actual `ProjectSet` object to be modeled as a single Vec internally.
// So, we provide this enum to use sede's polymorphic deserialization and then
// tell `ProjectSet` to deserialize via this enum using `From`.
#[derive(Deserialize, Debug)]
#[serde(untagged)]
pub enum DeserializableProjectSet {
    ProjectName(ProjectName),
    ProjectNames(Vec<ProjectName>),
}

impl From<DeserializableProjectSet> for ProjectSet {
    fn from(legacy: DeserializableProjectSet) -> Self {
        match legacy {
            DeserializableProjectSet::ProjectName(name) => ProjectSet::of(name),
            DeserializableProjectSet::ProjectNames(names) => ProjectSet::new(names),
        }
    }
}

pub trait Source {
    fn is_empty(&self) -> bool;
}

impl Source for Vec<LocatedGraphQLSource> {
    fn is_empty(&self) -> bool {
        self.is_empty()
    }
}

impl Source for Vec<LocatedDocblockSource> {
    fn is_empty(&self) -> bool {
        self.is_empty()
    }
}

type IncrementalSourceSet<V> = FnvHashMap<PathBuf, V>;

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IncrementalSources<V: Source> {
    pub pending: IncrementalSourceSet<V>,
    pub processed: IncrementalSourceSet<V>,
}

impl<V: Source + Clone> IncrementalSources<V> {
    /// Merges additional pending sources into this states pending sources.
    /// Skip if a pending source is empty and the processed source doesn't exist.
    /// A pending source can be empty when a file doesn't contain any source (GraphQL,
    /// Docblock, .etc). We need to keep the empty source only when there is a
    /// corresponding source in `processed`, and compiler needs to do the work to remove it.
    fn merge_pending_sources(&mut self, additional_pending_sources: &IncrementalSourceSet<V>) {
        for (key, value) in additional_pending_sources.iter() {
            match self.pending.entry(key.to_path_buf()) {
                Entry::Occupied(mut entry) => {
                    entry.insert(value.clone());
                }
                Entry::Vacant(vacant) => {
                    if !value.is_empty() || self.processed.get(key).map_or(false, |v| !v.is_empty())
                    {
                        vacant.insert(value.clone());
                    }
                }
            }
        }
    }

    fn commit_pending_sources(&mut self) {
        for (file_name, pending_graphql_sources) in self.pending.drain() {
            if pending_graphql_sources.is_empty() {
                self.processed.remove(&file_name);
            } else {
                self.processed.insert(file_name, pending_graphql_sources);
            }
        }
    }
    pub fn get_all(&self) -> Vec<(&PathBuf, &V)> {
        let mut sources: Vec<_>;
        if self.pending.is_empty() {
            sources = self.processed.iter().collect();
        } else {
            sources = self.pending.iter().collect();
            for (key, value) in self.processed.iter() {
                if !self.pending.contains_key(key) {
                    sources.push((key, value));
                }
            }
        }
        sources.sort_by_key(|file_content| file_content.0);
        sources
    }
}

impl<V: Source> Default for IncrementalSources<V> {
    fn default() -> Self {
        IncrementalSources {
            pending: FnvHashMap::default(),
            processed: FnvHashMap::default(),
        }
    }
}

type GraphQLSourceSet = IncrementalSourceSet<Vec<LocatedGraphQLSource>>;
type DocblockSourceSet = IncrementalSourceSet<Vec<LocatedDocblockSource>>;
pub type GraphQLSources = IncrementalSources<Vec<LocatedGraphQLSource>>;
pub type SchemaSources = IncrementalSources<String>;
pub type DocblockSources = IncrementalSources<Vec<LocatedDocblockSource>>;

impl Source for String {
    fn is_empty(&self) -> bool {
        self.is_empty()
    }
}

impl SchemaSources {
    pub fn get_sources(&self) -> Vec<&String> {
        let sources = self.get_all();
        sources.iter().map(|file_content| file_content.1).collect()
    }

    pub fn get_sources_with_location(&self) -> Vec<(&String, SourceLocationKey)> {
        let sources = self.get_all();
        sources
            .iter()
            .map(|file_content| {
                (
                    file_content.1,
                    SourceLocationKey::standalone(file_content.0.to_str().unwrap()),
                )
            })
            .collect()
    }

    pub fn get_old_sources(&self) -> Vec<&String> {
        let mut sources: Vec<_> = self.processed.iter().collect();
        sources.sort_by_key(|file_content| file_content.0);
        sources.iter().map(|file_content| file_content.1).collect()
    }
}
#[derive(Serialize, Deserialize, Debug)]
pub enum ArtifactMapKind {
    /// A simple set of paths of generated files. This kind is used when the
    /// compiler starts without a saved state and doesn't know the connection
    /// between generated files and the artifacts that produced them.
    Unconnected(FnvHashSet<PathBuf>),
    /// A known mapping from input documents to generated outputs.
    Mapping(ArtifactMap),
}

#[derive(Serialize, Deserialize, Debug, Default)]
pub struct CompilerState {
    pub graphql_sources: FnvHashMap<ProjectName, GraphQLSources>,
    pub schemas: FnvHashMap<ProjectName, SchemaSources>,
    pub extensions: FnvHashMap<ProjectName, SchemaSources>,
    pub docblocks: FnvHashMap<ProjectName, DocblockSources>,
    pub artifacts: FnvHashMap<ProjectName, Arc<ArtifactMapKind>>,
    #[serde(with = "clock_json_string")]
    pub clock: Option<Clock>,
    pub saved_state_version: String,
    #[serde(skip)]
    pub dirty_artifact_paths: FnvHashMap<ProjectName, DashSet<PathBuf, FnvBuildHasher>>,
    #[serde(skip)]
    pub pending_file_source_changes: Arc<RwLock<Vec<FileSourceResult>>>,
    #[serde(skip)]
    pub schema_cache: FnvHashMap<ProjectName, Arc<SDLSchema>>,
    #[serde(skip)]
    pub source_control_update_status: Arc<SourceControlUpdateStatus>,
}

impl CompilerState {
    pub fn from_file_source_changes(
        config: &Config,
        file_source_changes: &FileSourceResult,
        setup_event: &impl PerfLogEvent,
        perf_logger: &impl PerfLogger,
    ) -> Result<Self> {
        let categorized = setup_event.time("categorize_files_time", || {
            categorize_files(config, file_source_changes)
        });

        let mut result = Self {
            clock: file_source_changes.clock(),
            saved_state_version: config.saved_state_version.clone(),
            ..Default::default()
        };

        for (category, files) in categorized {
            match category {
                FileGroup::Source { project_set } => {
                    let (graphql_sources, docblock_sources) = extract_sources(
                        &project_set,
                        files,
                        file_source_changes,
                        false,
                        perf_logger,
                    )?;

                    for project_name in project_set {
                        result.set_pending_source_set(project_name, &graphql_sources);
                        result.set_pending_docblock_set(project_name, &docblock_sources);
                    }
                }
                FileGroup::Schema { project_set } => {
                    Self::process_schema_change(
                        file_source_changes,
                        files,
                        project_set,
                        &mut result.schemas,
                    )?;
                }
                FileGroup::Extension { project_set } => {
                    Self::process_schema_change(
                        file_source_changes,
                        files,
                        project_set,
                        &mut result.extensions,
                    )?;
                }
                FileGroup::Generated { project_name } => {
                    result.artifacts.insert(
                        project_name,
                        Arc::new(ArtifactMapKind::Unconnected(
                            files.into_iter().map(|file| file.name).collect(),
                        )),
                    );
                }
                FileGroup::Ignore => {}
            }
        }

        Ok(result)
    }

    pub fn project_has_pending_changes(&self, project_name: ProjectName) -> bool {
        self.graphql_sources
            .get(&project_name)
            .map_or(false, |sources| !sources.pending.is_empty())
            || self.project_has_pending_schema_changes(project_name)
            || self.dirty_artifact_paths.contains_key(&project_name)
    }

    pub fn project_has_pending_schema_changes(&self, project_name: ProjectName) -> bool {
        self.schemas
            .get(&project_name)
            .map_or(false, |sources| !sources.pending.is_empty())
            || self
                .extensions
                .get(&project_name)
                .map_or(false, |sources| !sources.pending.is_empty())
            || self
                .docblocks
                .get(&project_name)
                .map_or(false, |sources| !sources.pending.is_empty())
    }

    pub fn has_processed_changes(&self) -> bool {
        self.graphql_sources
            .values()
            .any(|sources| !sources.processed.is_empty())
            || self
                .schemas
                .values()
                .any(|sources| !sources.processed.is_empty())
            || self
                .extensions
                .values()
                .any(|sources| !sources.processed.is_empty())
            || self
                .docblocks
                .values()
                .any(|sources| !sources.processed.is_empty())
    }

    fn is_change_safe(&self, sources: &SchemaSources, schema_config: &SchemaConfig) -> bool {
        let previous = sources
            .get_old_sources()
            .into_iter()
            .map(String::as_str)
            .collect::<Vec<_>>();

        let current = sources
            .get_sources()
            .into_iter()
            .map(String::as_str)
            .collect::<Vec<_>>();

        let schema_change = detect_changes(&current, &previous);

        if schema_change == SchemaChange::None {
            true
        } else {
            let current_sources_with_location = sources
                .get_sources_with_location()
                .into_iter()
                .map(|(schema, location_key)| (schema.as_str(), location_key))
                .collect::<Vec<_>>();

            match relay_schema::build_schema_with_extensions(
                &current_sources_with_location,
                &Vec::<(&str, SourceLocationKey)>::new(),
            ) {
                Ok(schema) => schema_change.is_safe(&schema, schema_config),
                Err(_) => false,
            }
        }
    }

    /// This method will detect any schema changes in the pending sources (for LSP Server, to invalidate schema cache)
    pub fn has_schema_changes(&self) -> bool {
        self.docblocks
            .values()
            .any(|sources| !sources.pending.is_empty())
            || self
                .extensions
                .values()
                .any(|sources| !sources.pending.is_empty())
            || self
                .schemas
                .iter()
                .any(|(_, sources)| !sources.pending.is_empty())
    }

    /// This method is looking at the pending schema changes to see if they may be breaking (removed types, renamed field, etc)
    pub fn has_breaking_schema_change(
        &self,
        project_name: StringKey,
        schema_config: &SchemaConfig,
    ) -> bool {
        if let Some(extension) = self.extensions.get(&project_name) {
            if !extension.pending.is_empty() {
                return true;
            }
        }
        if let Some(docblocks) = self.docblocks.get(&project_name) {
            if !docblocks.pending.is_empty() {
                return true;
            }
        }
        if let Some(schema) = self.schemas.get(&project_name) {
            if !(schema.pending.is_empty() || self.is_change_safe(schema, schema_config)) {
                return true;
            }
        }
        false
    }

    /// Merges pending changes from the file source into the compiler state.
    /// Returns a boolean indicating if any new changes were merged.
    pub fn merge_file_source_changes(
        &mut self,
        config: &Config,
        perf_logger: &impl PerfLogger,
        // When loading from saved state, collect dirty artifacts for recompiling their source definitions
        should_collect_changed_artifacts: bool,
    ) -> Result<bool> {
        let mut has_changed = false;
        for file_source_changes in self.pending_file_source_changes.write().unwrap().drain(..) {
            let log_event = perf_logger.create_event("merge_file_source_changes");
            log_event.number("number_of_changes", file_source_changes.size());
            let categorized = log_event.time("categorize_files_time", || {
                categorize_files(config, &file_source_changes)
            });

            for (category, files) in categorized {
                match category {
                    FileGroup::Source { project_set } => {
                        // TODO: possible optimization to only set this if the
                        // extracted sources actually differ.
                        has_changed = true;

                        let (graphql_sources, docblock_sources) = extract_sources(
                            &project_set,
                            files,
                            &file_source_changes,
                            true,
                            perf_logger,
                        )?;

                        for project_name in project_set {
                            self.graphql_sources
                                .entry(project_name)
                                .or_default()
                                .merge_pending_sources(&graphql_sources);
                            self.docblocks
                                .entry(project_name)
                                .or_default()
                                .merge_pending_sources(&docblock_sources);
                        }
                    }
                    FileGroup::Schema { project_set } => {
                        has_changed = true;
                        Self::process_schema_change(
                            &file_source_changes,
                            files,
                            project_set,
                            &mut self.schemas,
                        )?;
                    }
                    FileGroup::Extension { project_set } => {
                        has_changed = true;
                        Self::process_schema_change(
                            &file_source_changes,
                            files,
                            project_set,
                            &mut self.extensions,
                        )?;
                    }
                    FileGroup::Generated { project_name } => {
                        if should_collect_changed_artifacts {
                            let mut dashset =
                                DashSet::with_capacity_and_hasher(files.len(), Default::default());
                            dashset.extend(files.into_iter().map(|f| f.name));
                            self.dirty_artifact_paths.insert(project_name, dashset);
                        }
                    }
                    FileGroup::Ignore => {}
                }
            }
        }

        Ok(has_changed)
    }

    pub fn complete_compilation(&mut self) {
        for sources in self.graphql_sources.values_mut() {
            sources.commit_pending_sources();
        }
        for sources in self.schemas.values_mut() {
            sources.commit_pending_sources();
        }
        for sources in self.extensions.values_mut() {
            sources.commit_pending_sources();
        }
        for sources in self.docblocks.values_mut() {
            sources.commit_pending_sources();
        }
        self.dirty_artifact_paths.clear();
    }

    /// Calculate dirty definitions from dirty artifacts
    pub fn get_dirty_definitions(
        &self,
        config: &Config,
    ) -> FnvHashMap<ProjectName, Vec<ExecutableDefinitionName>> {
        if self.dirty_artifact_paths.is_empty() {
            return Default::default();
        }
        let mut result = FnvHashMap::default();
        for (project_name, paths) in self.dirty_artifact_paths.iter() {
            if config.projects[project_name].enabled {
                let paths = paths.clone();
                let artifacts = self
                    .artifacts
                    .get(project_name)
                    .expect("Expected the artifacts map to exist.");
                if let ArtifactMapKind::Mapping(artifacts) = &**artifacts {
                    let mut dirty_definitions = vec![];
                    'outer: for entry in artifacts.0.iter() {
                        let (definition_name, artifact_records) = entry.pair();
                        let mut added = false;
                        for artifact_record in artifact_records {
                            if paths.remove(&artifact_record.path).is_some() && !added {
                                dirty_definitions.push(*definition_name);
                                if paths.is_empty() {
                                    break 'outer;
                                }
                                added = true;
                            }
                        }
                    }
                    if !dirty_definitions.is_empty() {
                        result.insert(*project_name, dirty_definitions);
                    }
                } else {
                    panic!("Expected the artifacts map to be populated.")
                }
            }
        }
        result
    }

    pub fn serialize_to_file(&self, path: &PathBuf) -> Result<()> {
        let writer = FsFile::create(path)
            .and_then(|writer| ZstdEncoder::new(writer, 12))
            .map_err(|err| Error::WriteFileError {
                file: path.clone(),
                source: err,
            })?
            .auto_finish();
        let writer =
            BufWriter::with_capacity(ZstdEncoder::<FsFile>::recommended_input_size(), writer);
        bincode::serialize_into(writer, self).map_err(|err| Error::SerializationError {
            file: path.clone(),
            source: err,
        })
    }

    pub fn deserialize_from_file(path: &PathBuf) -> Result<Self> {
        let reader = FsFile::open(path)
            .and_then(ZstdDecoder::new)
            .map_err(|err| Error::ReadFileError {
                file: path.clone(),
                source: err,
            })?;
        let reader = BufReader::with_capacity(
            ZstdDecoder::<BufReader<FsFile>>::recommended_output_size(),
            reader,
        );

        let memory_limit: u64 = env::var("RELAY_SAVED_STATE_MEMORY_LIMIT")
            .map(|limit| {
                limit.parse::<u64>().expect(
                    "Expected RELAY_SAVED_STATE_MEMORY_LIMIT environment variable to be a number.",
                )
            })
            .unwrap_or_else(|_| 10_u64.pow(10) /* 10GB */);

        bincode::DefaultOptions::new()
            .with_fixint_encoding()
            .allow_trailing_bytes()
            .with_limit(memory_limit)
            .deserialize_from(reader)
            .map_err(|err| Error::DeserializationError {
                file: path.clone(),
                source: err,
            })
    }

    pub fn has_pending_file_source_changes(&self) -> bool {
        !self.pending_file_source_changes.read().unwrap().is_empty()
    }

    fn set_pending_source_set(&mut self, project_name: ProjectName, source_set: &GraphQLSourceSet) {
        let entry = &mut self.graphql_sources.entry(project_name).or_default();
        entry.merge_pending_sources(source_set);
    }

    fn set_pending_docblock_set(
        &mut self,
        project_name: ProjectName,
        source_set: &DocblockSourceSet,
    ) {
        let entry = &mut self.docblocks.entry(project_name).or_default();
        entry.merge_pending_sources(source_set);
    }

    fn process_schema_change(
        file_source_changes: &FileSourceResult,
        files: Vec<File>,
        project_set: ProjectSet,
        source_map: &mut FnvHashMap<ProjectName, SchemaSources>,
    ) -> Result<()> {
        let mut removed_sources = vec![];
        let mut added_sources = FnvHashMap::default();
        for file in files {
            let file_name = file.name.clone();
            if file.exists {
                added_sources.insert(file_name, read_file_to_string(file_source_changes, &file)?);
            } else {
                removed_sources.push(file_name);
            }
        }
        for project_name in project_set {
            let entry = source_map.entry(project_name).or_default();
            for source in &removed_sources {
                entry.pending.insert(source.clone(), "".to_string());
            }
            entry.merge_pending_sources(&added_sources);
        }
        Ok(())
    }

    pub fn is_source_control_update_in_progress(&self) -> bool {
        self.source_control_update_status.is_started()
    }

    /// Over the course of the build, we may need to stop current progress
    /// as there maybe incoming file change or source control update in progress
    pub fn should_cancel_current_build(&self) -> bool {
        self.is_source_control_update_in_progress() || self.has_pending_file_source_changes()
    }
}

fn extract_sources(
    project_set: &ProjectSet,
    files: Vec<File>,
    file_source_changes: &FileSourceResult,
    preserve_empty: bool,
    perf_logger: &impl PerfLogger,
) -> Result<(GraphQLSourceSet, DocblockSourceSet)> {
    let log_event = perf_logger.create_event("categorize");
    log_event.string("source_set_name", project_set.to_string());
    let extract_timer = log_event.start("extract_graphql_strings_from_file_time");

    let source_features = files
        .par_iter()
        .map(|file| {
            if file.exists {
                match extract_javascript_features_from_file(file_source_changes, file) {
                    Ok(features) => Ok((file, features)),
                    Err(err) => Err(err),
                }
            } else {
                Ok((file, LocatedJavascriptSourceFeatures::default()))
            }
        })
        .collect::<Result<Vec<_>>>()?;

    log_event.stop(extract_timer);

    let mut graphql_sources: GraphQLSourceSet = FnvHashMap::default();
    let mut docblock_sources: DocblockSourceSet = FnvHashMap::default();
    for (file, features) in source_features {
        if preserve_empty || !features.graphql_sources.is_empty() {
            graphql_sources.insert(file.name.clone(), features.graphql_sources);
        }
        if preserve_empty || !features.docblock_sources.is_empty() {
            docblock_sources.insert(file.name.clone(), features.docblock_sources);
        }
    }

    Ok((graphql_sources, docblock_sources))
}

/// A module to serialize a watchman Clock value via JSON.
/// The reason is that `Clock` internally uses an untagged enum value
/// which requires "self descriptive" serialization formats and `bincode` does not
/// support those enums.
mod clock_json_string {
    use serde::de::Error as DeserializationError;
    use serde::de::Visitor;
    use serde::ser::Error as SerializationError;
    use serde::Deserializer;
    use serde::Serializer;

    use crate::file_source::Clock;

    pub fn serialize<S>(clock: &Option<Clock>, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let json_string = serde_json::to_string(clock).map_err(|err| {
            SerializationError::custom(format!("Unable to serialize clock value. Error {}", err))
        })?;
        serializer.serialize_str(&json_string)
    }

    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Clock>, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_str(JSONStringVisitor)
    }

    struct JSONStringVisitor;
    impl<'de> Visitor<'de> for JSONStringVisitor {
        type Value = Option<Clock>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            formatter.write_str("a JSON encoded watchman::Clock value")
        }

        fn visit_str<E: DeserializationError>(self, v: &str) -> Result<Option<Clock>, E> {
            serde_json::from_str(v).map_err(|err| {
                DeserializationError::custom(format!(
                    "Unable deserialize clock value. Error {}",
                    err
                ))
            })
        }
    }
}

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    impl Source for Vec<u32> {
        fn is_empty(&self) -> bool {
            self.is_empty()
        }
    }

    #[test]
    fn empty_pending_incremental_source_overwrites_existing_pending_source() {
        let mut incremental_source: IncrementalSources<Vec<u32>> = IncrementalSources::default();

        let a = PathBuf::new();

        let mut initial = FnvHashMap::default();
        initial.insert(a.clone(), vec![1, 2, 3]);

        // Starting with a pending source of a => [1, 2, 3]
        incremental_source.merge_pending_sources(&initial);

        assert_eq!(incremental_source.pending.get(&a), Some(&vec![1, 2, 3]));

        let mut update: FnvHashMap<PathBuf, Vec<u32>> = FnvHashMap::default();
        update.insert(a.clone(), Vec::new());

        // Merge in a pending source of a => []
        incremental_source.merge_pending_sources(&update);

        // Pending for a should now be empty
        assert_eq!(incremental_source.pending.get(&a), Some(&vec![]));
    }

    #[test]
    fn empty_pending_incremental_is_ignored_if_no_processed_source_exists() {
        let mut incremental_source: IncrementalSources<Vec<u32>> = IncrementalSources::default();

        let a = PathBuf::new();

        let mut update: FnvHashMap<PathBuf, Vec<u32>> = FnvHashMap::default();
        update.insert(a.clone(), Vec::new());

        // Merge in a pending source of a => []
        incremental_source.merge_pending_sources(&update);

        // Pending for a should not be populated
        assert_eq!(incremental_source.pending.get(&a), None);
    }

    #[test]
    fn empty_pending_incremental_is_ignored_if_no_processed_source_is_empty() {
        let mut incremental_source: IncrementalSources<Vec<u32>> = IncrementalSources::default();

        let a = PathBuf::new();

        incremental_source.processed.insert(a.clone(), Vec::new());

        let mut update: FnvHashMap<PathBuf, Vec<u32>> = FnvHashMap::default();
        update.insert(a.clone(), Vec::new());

        // Merge in a pending source of a => []
        incremental_source.merge_pending_sources(&update);

        // Pending for a should not be populated
        assert_eq!(incremental_source.pending.get(&a), None);
    }
}


/*
 * 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::env::current_dir;
use std::ffi::OsStr;
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::vec;

use async_trait::async_trait;
use common::FeatureFlags;
use common::Rollout;
use common::ScalarName;
use dunce::canonicalize;
use fnv::FnvBuildHasher;
use fnv::FnvHashSet;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use indexmap::IndexMap;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use js_config_loader::LoaderSource;
use log::warn;
use persist_query::PersistError;
use rayon::prelude::*;
use regex::Regex;
use relay_config::CustomScalarType;
use relay_config::DiagnosticReportConfig;
use relay_config::FlowTypegenConfig;
use relay_config::JsModuleFormat;
pub use relay_config::LocalPersistConfig;
use relay_config::ModuleImportConfig;
pub use relay_config::PersistConfig;
pub use relay_config::ProjectConfig;
pub use relay_config::RemotePersistConfig;
use relay_config::SchemaConfig;
pub use relay_config::SchemaLocation;
use relay_config::TypegenConfig;
pub use relay_config::TypegenLanguage;
use relay_transforms::CustomTransformsConfig;
use serde::de::Error as DeError;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_json::Value;
use sha1::Digest;
use sha1::Sha1;
use watchman_client::pdu::ScmAwareClockData;

use crate::build_project::artifact_writer::ArtifactFileWriter;
use crate::build_project::artifact_writer::ArtifactWriter;
use crate::build_project::generate_extra_artifacts::GenerateExtraArtifactsFn;
use crate::build_project::AdditionalValidations;
use crate::compiler_state::CompilerState;
use crate::compiler_state::ProjectName;
use crate::compiler_state::ProjectSet;
use crate::errors::ConfigValidationError;
use crate::errors::Error;
use crate::errors::Result;
use crate::saved_state::SavedStateLoader;
use crate::status_reporter::ConsoleStatusReporter;
use crate::status_reporter::StatusReporter;

type FnvIndexMap<K, V> = IndexMap<K, V, FnvBuildHasher>;

type PostArtifactsWriter = Box<
    dyn Fn(&Config) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>
        + Send
        + Sync,
>;

type OperationPersisterCreator =
    Box<dyn Fn(&ProjectConfig) -> Option<Box<dyn OperationPersister + Send + Sync>> + Send + Sync>;

type UpdateCompilerStateFromSavedState =
    Option<Box<dyn Fn(&mut CompilerState, &Config) + Send + Sync>>;

/// The full compiler config. This is a combination of:
/// - the configuration file
/// - the absolute path to the root of the compiled projects
/// - command line options
/// - TODO: injected code to produce additional files
pub struct Config {
    /// Optional name for this config. This might be used by compiler extension
    /// code like logging or extra artifact generation.
    pub name: Option<String>,
    /// Root directory of all projects to compile. Any other paths in the
    /// compiler should be relative to this root unless otherwise noted.
    pub root_dir: PathBuf,
    pub sources: FnvIndexMap<PathBuf, ProjectSet>,
    pub excludes: Vec<String>,
    pub projects: FnvIndexMap<ProjectName, ProjectConfig>,
    pub header: Vec<String>,
    pub codegen_command: Option<String>,
    /// If set, tries to initialize the compiler from the saved state file.
    pub load_saved_state_file: Option<PathBuf>,
    /// Function to generate extra
    pub generate_extra_artifacts: Option<GenerateExtraArtifactsFn>,
    pub generate_virtual_id_file_name: Option<
        Box<
            dyn Fn(&ProjectConfig, &OperationDefinition, &Program) -> Option<StringKey>
                + Send
                + Sync,
        >,
    >,

    /// Path to which to write the output of the compilation
    pub artifact_writer: Box<dyn ArtifactWriter + Send + Sync>,

    /// Compile all files. Persist ids are still re-used unless
    /// `Config::repersist_operations` is also set.
    pub compile_everything: bool,

    /// Do not reuse persist ids from artifacts even if the text hash matches.
    pub repersist_operations: bool,

    pub saved_state_config: Option<ScmAwareClockData>,
    pub saved_state_loader: Option<Box<dyn SavedStateLoader + Send + Sync>>,
    pub saved_state_version: String,

    /// Function that creates a function that is
    /// called to save operation text (e.g. to a database) and to generate an id.
    pub create_operation_persister: Option<OperationPersisterCreator>,

    pub post_artifacts_write: Option<PostArtifactsWriter>,

    /// Validations that can be added to the config that will be called in addition to default
    /// validation rules.
    pub additional_validations: Option<AdditionalValidations>,

    pub status_reporter: Box<dyn StatusReporter + Send + Sync>,

    /// We may generate some content in the artifacts that's stripped in production if __DEV__ variable is set
    /// This config option is here to define the name of that special variable
    pub is_dev_variable_name: Option<String>,

    /// Type of file source to use in the Compiler
    pub file_source_config: FileSourceKind,

    /// A set of custom transform functions, that can be applied before,
    /// and after each major transformation step (common, operations, etc)
    /// in the `apply_transforms(...)`.
    pub custom_transforms: Option<CustomTransformsConfig>,

    pub export_persisted_query_ids_to_file: Option<PathBuf>,

    /// The async function is called before the compiler connects to the file
    /// source.
    pub initialize_resources: Option<Box<dyn Fn() + Send + Sync>>,

    /// Runs in `try_saved_state` when the compiler state is initialized from saved state.
    pub update_compiler_state_from_saved_state: UpdateCompilerStateFromSavedState,
}

pub enum FileSourceKind {
    Watchman,
    /// List with changed files in format "file_path,exists".
    /// This can be used to replace watchman queries
    External(PathBuf),
    WalkDir,
}

fn normalize_path_from_config(
    current_dir: PathBuf,
    common_path: PathBuf,
    path_from_config: PathBuf,
) -> PathBuf {
    let mut src = current_dir.join(path_from_config.clone());

    src = canonicalize(src.clone())
        .unwrap_or_else(|err| panic!("Unable to canonicalize file {:?}. Error: {:?}", &src, err));

    src.strip_prefix(common_path.clone())
        .unwrap_or_else(|_| {
            panic!(
                "Expect to be able to strip common_path from {:?} {:?}",
                &src,
                &common_path.clone(),
            );
        })
        .to_path_buf()
}

impl From<SingleProjectConfigFile> for Config {
    fn from(config: SingleProjectConfigFile) -> Self {
        Self::from_struct(
            "/virtual/path".into(),
            ConfigFile::SingleProject(config),
            false,
        )
        .unwrap()
    }
}

impl Config {
    pub fn search(start_dir: &Path) -> Result<Self> {
        Self::load_config(
            start_dir,
            &[
                LoaderSource::PackageJson("relay".to_string()),
                LoaderSource::Json("relay.config.json".to_string()),
                LoaderSource::Js("relay.config.js".to_string()),
            ],
        )
    }

    pub fn load(config_path: PathBuf) -> Result<Self> {
        let loader = if config_path.extension() == Some(OsStr::new("js")) {
            LoaderSource::Js(config_path.display().to_string())
        } else if config_path.extension() == Some(OsStr::new("json")) {
            LoaderSource::Json(config_path.display().to_string())
        } else {
            return Err(Error::ConfigError {
                details: format!(
                    "Invalid file extension. Expected `.js` or `.json`. Provided file \"{}\".",
                    config_path.display()
                ),
            });
        };
        Self::load_config(
            &current_dir().expect("Unable to get current working directory."),
            &[loader],
        )
    }

    fn load_config(start_dir: &Path, loaders_sources: &[LoaderSource]) -> Result<Self> {
        match js_config_loader::load(start_dir, loaders_sources) {
            Ok(Some(config)) => Self::from_struct(config.path, config.value, true),
            Ok(None) => Err(Error::ConfigError {
                details: format!(
                    r#"
Configuration for Relay compiler not found.

Please make sure that the configuration file is created in {}.

You can also pass the path to the configuration file as `relay-compiler ./path-to-config/relay.json`.

Example file:
{{
  "src": "./src",
  "schema": "./path-to/schema.graphql",
  "language": "javascript"
}}
"#,
                    match loaders_sources.len() {
                        1 => loaders_sources[0].to_string(),
                        2 => format!("{} or {}", loaders_sources[0], loaders_sources[1]),
                        _ => {
                            let mut loaders_str = loaders_sources
                                .iter()
                                .map(|loader| loader.to_string())
                                .collect::<Vec<_>>();
                            let last_option = loaders_str.pop().unwrap();
                            format!("{}, or {}", loaders_str.join(", "), last_option)
                        }
                    }
                ),
            }),
            Err(error) => Err(Error::ConfigError {
                details: format!("Error searching config: {}", error),
            }),
        }
    }

    /// Loads a config file without validation for use in tests.
    #[cfg(test)]
    pub fn from_string_for_test(config_string: &str) -> Result<Self> {
        let path = PathBuf::from("/virtual/root/virtual_config.json");
        let config_file: ConfigFile =
            serde_json::from_str(config_string).map_err(|err| Error::ConfigError {
                details: format!("Failed to parse config file `{}`: {}", path.display(), err,),
            })?;
        Self::from_struct(path, config_file, false)
    }

    /// `validate_fs` disables all filesystem checks for existence of files
    fn from_struct(
        config_path: PathBuf,
        config_file: ConfigFile,
        validate_fs: bool,
    ) -> Result<Self> {
        let mut hash = Sha1::new();
        serde_json::to_writer(&mut hash, &config_file).unwrap();

        let is_multi_project = match config_file {
            ConfigFile::MultiProject(_) => true,
            ConfigFile::SingleProject(_) => false,
        };

        let config_file = match config_file {
            ConfigFile::MultiProject(config) => *config,
            ConfigFile::SingleProject(config) => {
                config.create_multi_project_config(&config_path)?
            }
        };

        let MultiProjectConfigFile {
            feature_flags: config_file_feature_flags,
            projects,
            ..
        } = config_file;
        let projects = projects
            .into_iter()
            .map(|(project_name, config_file_project)| {
                let schema_location =
                    match (config_file_project.schema, config_file_project.schema_dir) {
                        (Some(schema_file), None) => Ok(SchemaLocation::File(schema_file)),
                        (None, Some(schema_dir)) => Ok(SchemaLocation::Directory(schema_dir)),
                        _ => Err(Error::ConfigFileValidation {
                            config_path: config_path.clone(),
                            validation_errors: vec![
                                ConfigValidationError::ProjectNeedsSchemaXorSchemaDir {
                                    project_name,
                                },
                            ],
                        }),
                    }?;

                let shard_strip_regex = config_file_project
                    .shard_strip_regex
                    .map(|s| Regex::new(&s))
                    .transpose()
                    .map_err(|error| Error::ConfigFileValidation {
                        config_path: config_path.clone(),
                        validation_errors: vec![ConfigValidationError::InvalidRegex {
                            key: "shardStripRegex",
                            project_name,
                            error,
                        }],
                    })?;

                let test_path_regex = config_file_project
                    .test_path_regex
                    .map(|s| Regex::new(&s))
                    .transpose()
                    .map_err(|error| Error::ConfigFileValidation {
                        config_path: config_path.clone(),
                        validation_errors: vec![ConfigValidationError::InvalidRegex {
                            key: "testDirectoryRegex",
                            project_name,
                            error,
                        }],
                    })?;

                let project_config = ProjectConfig {
                    name: project_name,
                    base: config_file_project.base,
                    enabled: true,
                    schema_extensions: config_file_project.schema_extensions,
                    output: config_file_project.output,
                    extra_artifacts_output: config_file_project.extra_artifacts_output,
                    shard_output: config_file_project.shard_output,
                    shard_strip_regex,
                    schema_location,
                    schema_config: config_file_project.schema_config,
                    typegen_config: config_file_project.typegen_config,
                    persist: config_file_project.persist,
                    variable_names_comment: config_file_project.variable_names_comment,
                    extra: config_file_project.extra,
                    test_path_regex,
                    feature_flags: Arc::new(
                        config_file_project
                            .feature_flags
                            .unwrap_or_else(|| config_file_feature_flags.clone()),
                    ),
                    filename_for_artifact: None,
                    skip_types_for_artifact: None,
                    rollout: config_file_project.rollout,
                    js_module_format: config_file_project.js_module_format,
                    module_import_config: config_file_project.module_import_config,
                    diagnostic_report_config: config_file_project.diagnostic_report_config,
                };
                Ok((project_name, project_config))
            })
            .collect::<Result<FnvIndexMap<_, _>>>()?;

        let config_file_dir = config_path.parent().unwrap();

        let root_dir = if let Some(config_root) = config_file.root {
            canonicalize(config_file_dir.join(config_root)).unwrap()
        } else {
            config_file_dir.to_owned()
        };

        let config = Self {
            name: config_file.name,
            artifact_writer: Box::new(ArtifactFileWriter::new(None, root_dir.clone())),
            status_reporter: Box::new(ConsoleStatusReporter::new(
                root_dir.clone(),
                is_multi_project,
            )),
            root_dir,
            sources: config_file.sources,
            excludes: config_file.excludes,
            projects,
            header: config_file.header,
            codegen_command: config_file.codegen_command,
            load_saved_state_file: None,
            generate_extra_artifacts: None,
            generate_virtual_id_file_name: None,
            saved_state_config: config_file.saved_state_config,
            saved_state_loader: None,
            saved_state_version: hex::encode(hash.finalize()),
            create_operation_persister: None,
            compile_everything: false,
            repersist_operations: false,
            post_artifacts_write: None,
            additional_validations: None,
            is_dev_variable_name: config_file.is_dev_variable_name,
            file_source_config: FileSourceKind::Watchman,
            custom_transforms: None,
            export_persisted_query_ids_to_file: None,
            initialize_resources: None,
            update_compiler_state_from_saved_state: None,
        };

        let mut validation_errors = Vec::new();
        config.validate_consistency(&mut validation_errors);
        if validate_fs {
            config.validate_paths(&mut validation_errors);
        }
        if validation_errors.is_empty() {
            Ok(config)
        } else {
            Err(Error::ConfigFileValidation {
                config_path,
                validation_errors,
            })
        }
    }

    /// Iterator over projects that are enabled.
    pub fn enabled_projects(&self) -> impl Iterator<Item = &ProjectConfig> {
        self.projects
            .values()
            .filter(|project_config| project_config.enabled)
    }

    /// Rayon parallel iterator over projects that are enabled.
    pub fn par_enabled_projects(&self) -> impl ParallelIterator<Item = &ProjectConfig> {
        self.projects
            .par_iter()
            .map(|(_project_name, project_config)| project_config)
            .filter(|project_config| project_config.enabled)
    }

    /// Validated internal consistency of the config.
    fn validate_consistency(&self, errors: &mut Vec<ConfigValidationError>) {
        let mut project_names = FnvHashSet::default();
        for (source_dir, project_set) in self.sources.iter() {
            for name in project_set.iter() {
                if self.projects.get(name).is_none() {
                    errors.push(ConfigValidationError::ProjectDefinitionMissing {
                        source_dir: source_dir.clone(),
                        project_name: *name,
                    });
                }

                project_names.insert(*name);
            }
        }

        for (&project_name, project_config) in &self.projects {
            // there should be a source for each project matching the project name
            if !project_names.contains(&project_name) {
                errors.push(ConfigValidationError::ProjectSourceMissing { project_name });
            }

            // If a base of the project is set, it should exist
            if let Some(base_name) = project_config.base {
                if self.projects.get(&base_name).is_none() {
                    errors.push(ConfigValidationError::ProjectBaseMissing {
                        project_name,
                        base_project_name: base_name,
                    })
                }
            }
        }
    }

    /// Validates that all paths actually exist on disk.
    fn validate_paths(&self, errors: &mut Vec<ConfigValidationError>) {
        if !self.root_dir.is_dir() {
            errors.push(ConfigValidationError::RootNotDirectory {
                root_dir: self.root_dir.clone(),
            });
            // early return, no point in continuing validation
            return;
        }

        // each source should point to an existing directory
        for source_dir in self.sources.keys() {
            let abs_source_dir = self.root_dir.join(source_dir);
            if !abs_source_dir.exists() {
                errors.push(ConfigValidationError::SourceNotExistent {
                    source_dir: abs_source_dir.clone(),
                });
            } else if !abs_source_dir.is_dir() {
                errors.push(ConfigValidationError::SourceNotDirectory {
                    source_dir: abs_source_dir.clone(),
                });
            }
        }

        for (&project_name, project) in &self.projects {
            match &project.schema_location {
                SchemaLocation::File(schema_file) => {
                    let abs_schema_file = self.root_dir.join(schema_file);
                    if !abs_schema_file.exists() {
                        errors.push(ConfigValidationError::SchemaFileNotExistent {
                            project_name,
                            schema_file: abs_schema_file.clone(),
                        });
                    } else if !abs_schema_file.is_file() {
                        errors.push(ConfigValidationError::SchemaFileNotFile {
                            project_name,
                            schema_file: abs_schema_file.clone(),
                        });
                    }
                }
                SchemaLocation::Directory(schema_dir) => {
                    let abs_schema_dir = self.root_dir.join(schema_dir);
                    if !abs_schema_dir.exists() {
                        errors.push(ConfigValidationError::SchemaDirNotExistent {
                            project_name,
                            schema_dir: abs_schema_dir.clone(),
                        });
                    } else if !abs_schema_dir.is_dir() {
                        errors.push(ConfigValidationError::SchemaDirNotDirectory {
                            project_name,
                            schema_dir: abs_schema_dir.clone(),
                        });
                    }
                }
            }
        }
    }
}

impl fmt::Debug for Config {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Config {
            name,
            artifact_writer: _,
            root_dir,
            sources,
            excludes,
            compile_everything,
            repersist_operations,
            projects,
            header,
            codegen_command,
            load_saved_state_file,
            generate_extra_artifacts,
            saved_state_config,
            saved_state_loader,
            saved_state_version,
            create_operation_persister,
            post_artifacts_write,
            ..
        } = self;

        fn option_fn_to_string<T>(option: &Option<T>) -> &'static str {
            if option.is_some() { "Some(Fn)" } else { "None" }
        }

        f.debug_struct("Config")
            .field("name", name)
            .field("root_dir", root_dir)
            .field("sources", sources)
            .field("excludes", excludes)
            .field("compile_everything", compile_everything)
            .field("repersist_operations", repersist_operations)
            .field("projects", projects)
            .field("header", header)
            .field("codegen_command", codegen_command)
            .field("load_saved_state_file", load_saved_state_file)
            .field("saved_state_config", saved_state_config)
            .field(
                "create_operation_persister",
                &option_fn_to_string(create_operation_persister),
            )
            .field(
                "generate_extra_artifacts",
                &option_fn_to_string(generate_extra_artifacts),
            )
            .field(
                "saved_state_loader",
                &option_fn_to_string(saved_state_loader),
            )
            .field("saved_state_version", saved_state_version)
            .field(
                "post_artifacts_write",
                &option_fn_to_string(post_artifacts_write),
            )
            .finish()
    }
}

fn get_default_excludes() -> Vec<String> {
    vec![
        "**/node_modules/**".to_string(),
        "**/__mocks__/**".to_string(),
        "**/__generated__/**".to_string(),
    ]
}

/// Schema of the compiler configuration JSON file.
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
struct MultiProjectConfigFile {
    /// Optional name for this config, might be used for logging or custom extra
    /// artifact generator code.
    #[serde(default)]
    name: Option<String>,

    /// Root directory relative to the config file. Defaults to the directory
    /// where the config is located.
    #[serde(default)]
    root: Option<PathBuf>,

    #[serde(default)]
    header: Vec<String>,

    #[serde(default)]
    codegen_command: Option<String>,

    /// A mapping from directory paths (relative to the root) to a source set.
    /// If a path is a subdirectory of another path, the more specific path
    /// wins.
    sources: IndexMap<PathBuf, ProjectSet, fnv::FnvBuildHasher>,

    /// Glob patterns that should not be part of the sources even if they are
    /// in the source set directories.
    #[serde(default = "get_default_excludes")]
    excludes: Vec<String>,

    /// Configuration of projects to compile.
    projects: FnvIndexMap<ProjectName, ConfigFileProject>,

    #[serde(default)]
    feature_flags: FeatureFlags,

    /// Watchman saved state config.
    saved_state_config: Option<ScmAwareClockData>,

    /// Then name of the global __DEV__ variable to use in generated artifacts
    is_dev_variable_name: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase", default)]
pub struct SingleProjectConfigFile {
    #[serde(skip)]
    pub project_name: StringKey,

    /// Path to schema.graphql
    pub schema: PathBuf,

    /// Root directory of application code
    pub src: PathBuf,

    /// A specific directory to output all artifacts to. When enabling this '
    /// the babel plugin needs `artifactDirectory` set as well.
    pub artifact_directory: Option<PathBuf>,

    /// [DEPRECATED] This is deprecated field, we're not using it in the V13.
    /// Adding to the config, to show the warning, and not a parse error.
    pub include: Vec<String>,

    /// [DEPRECATED] This is deprecated field, we're not using it in the V13.
    /// Adding to the config, to show the warning, and not a parse error.
    pub extensions: Vec<String>,

    /// Directories to ignore under src
    /// default: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**'],
    #[serde(alias = "exclude")]
    pub excludes: Vec<String>,

    /// List of directories with schema extensions.
    pub schema_extensions: Vec<PathBuf>,

    /// This option controls whether or not a catch-all entry is added to enum type definitions
    /// for values that may be added in the future. Enabling this means you will have to update
    /// your application whenever the GraphQL server schema adds new enum values to prevent it
    /// from breaking.
    pub no_future_proof_enums: bool,

    /// The name of the language plugin (?) used for input files and artifacts
    pub language: Option<TypegenLanguage>,

    /// Mappings from custom scalars in your schema to built-in GraphQL
    /// types, for type emission purposes.
    pub custom_scalars: FnvIndexMap<ScalarName, CustomScalarType>,

    /// This option enables emitting es modules artifacts.
    pub eager_es_modules: bool,

    /// Query Persist Configuration
    /// It contains URL and addition parameters that will be included
    /// with the request (think API_KEY, APP_ID, etc...)
    pub persist_config: Option<PersistConfig>,

    /// We may generate some content in the artifacts that's stripped in production if __DEV__ variable is set
    /// This config option is here to define the name of that special variable
    pub is_dev_variable_name: Option<String>,

    /// Name of the command that runs the relay compiler
    pub codegen_command: Option<String>,

    /// Formatting style for generated files.
    pub js_module_format: JsModuleFormat,

    /// Extra configuration for the schema itself.
    pub schema_config: SchemaConfig,

    /// Configuration for @module
    #[serde(default)]
    pub module_import_config: ModuleImportConfig,

    /// Added in 13.1.1 to customize Final/Compat mode in the single project config file
    /// Removed in 14.0.0
    #[serde(default)]
    pub typegen_phase: Option<Value>,

    #[serde(default)]
    pub feature_flags: Option<FeatureFlags>,
}

impl Default for SingleProjectConfigFile {
    fn default() -> Self {
        Self {
            project_name: "default".intern(),
            schema: Default::default(),
            src: Default::default(),
            artifact_directory: Default::default(),
            include: vec![],
            extensions: vec![],
            excludes: get_default_excludes(),
            schema_extensions: vec![],
            no_future_proof_enums: false,
            language: None,
            custom_scalars: Default::default(),
            schema_config: Default::default(),
            eager_es_modules: false,
            persist_config: None,
            is_dev_variable_name: None,
            codegen_command: None,
            js_module_format: JsModuleFormat::CommonJS,
            typegen_phase: None,
            feature_flags: None,
            module_import_config: Default::default(),
        }
    }
}

impl SingleProjectConfigFile {
    fn get_common_root(
        &self,
        root_dir: PathBuf,
    ) -> std::result::Result<PathBuf, ConfigValidationError> {
        let mut paths = vec![];
        if let Some(artifact_directory_path) = self.artifact_directory.clone() {
            paths.push(
                canonicalize(root_dir.join(artifact_directory_path.clone())).map_err(|_| {
                    ConfigValidationError::ArtifactDirectoryNotExistent {
                        path: artifact_directory_path,
                    }
                })?,
            );
        }
        paths.push(canonicalize(root_dir.join(self.src.clone())).map_err(|_| {
            ConfigValidationError::SourceNotExistent {
                source_dir: self.src.clone(),
            }
        })?);
        paths.push(
            canonicalize(root_dir.join(self.schema.clone())).map_err(|_| {
                ConfigValidationError::SchemaFileNotExistent {
                    project_name: self.project_name,
                    schema_file: self.schema.clone(),
                }
            })?,
        );
        for extension_dir in self.schema_extensions.iter() {
            paths.push(
                canonicalize(root_dir.join(extension_dir.clone())).map_err(|_| {
                    ConfigValidationError::ExtensionDirNotExistent {
                        project_name: self.project_name,
                        extension_dir: extension_dir.clone(),
                    }
                })?,
            );
        }
        common_path::common_path_all(paths.iter().map(|path| path.as_path()))
            .ok_or(ConfigValidationError::CommonPathNotFound)
    }

    fn create_multi_project_config(self, config_path: &Path) -> Result<MultiProjectConfigFile> {
        if !self.include.is_empty() {
            warn!(
                r#"The configuration contains `include: {:#?}` section. This configuration option is no longer supported. Consider removing it."#,
                &self.include
            );
        }
        if !self.extensions.is_empty() {
            warn!(
                r#"The configuration contains `extensions: {:#?}` section. This configuration option is no longer supported. Consider removing it."#,
                &self.extensions
            );
        }

        if self.typegen_phase.is_some() {
            return Err(Error::ConfigFileValidation {
                config_path: config_path.into(),
                validation_errors: vec![ConfigValidationError::RemovedConfigField {
                    name: "typegenPhase",
                    action: "Please remove the option and update type imports from generated files to new names.",
                }],
            });
        }

        let current_dir = std::env::current_dir().unwrap();
        let common_root_dir = self.get_common_root(current_dir.clone()).map_err(|err| {
            Error::ConfigFileValidation {
                config_path: config_path.to_path_buf(),
                validation_errors: vec![err],
            }
        })?;

        let language = self.language.ok_or_else(|| {
            let mut variants = vec![];
            for lang in TypegenLanguage::get_variants_as_string() {
                variants.push(format!(r#"  "language": "{}""#, lang));
            }

            Error::ConfigError {
                    details: format!("The `language` option is missing in the Relay configuration file. Please, specify one of the following options:\n{}", variants.join("\n")),
                }
            }
        )?;

        let project_config = ConfigFileProject {
            output: self.artifact_directory.map(|dir| {
                normalize_path_from_config(current_dir.clone(), common_root_dir.clone(), dir)
            }),
            schema: Some(normalize_path_from_config(
                current_dir.clone(),
                common_root_dir.clone(),
                self.schema,
            )),
            schema_config: self.schema_config,
            schema_extensions: self
                .schema_extensions
                .iter()
                .map(|dir| {
                    normalize_path_from_config(
                        current_dir.clone(),
                        common_root_dir.clone(),
                        dir.clone(),
                    )
                })
                .collect(),
            persist: self.persist_config,
            typegen_config: TypegenConfig {
                language,
                custom_scalar_types: self.custom_scalars.clone(),
                eager_es_modules: self.eager_es_modules,
                flow_typegen: FlowTypegenConfig {
                    no_future_proof_enums: self.no_future_proof_enums,
                    ..Default::default()
                },
                ..Default::default()
            },
            js_module_format: self.js_module_format,
            feature_flags: self.feature_flags,
            module_import_config: self.module_import_config,
            ..Default::default()
        };

        let mut projects = FnvIndexMap::default();
        projects.insert(self.project_name, project_config);

        let mut sources = FnvIndexMap::default();
        let src = normalize_path_from_config(current_dir, common_root_dir.clone(), self.src);

        sources.insert(src, ProjectSet::of(self.project_name));

        Ok(MultiProjectConfigFile {
            root: Some(common_root_dir),
            projects,
            sources,
            excludes: self.excludes,
            is_dev_variable_name: self.is_dev_variable_name,
            codegen_command: self.codegen_command,
            ..Default::default()
        })
    }
}

#[derive(Serialize)]
#[serde(untagged)]
enum ConfigFile {
    /// Base case configuration (mostly of OSS) where the project
    /// have single schema, and single source directory
    SingleProject(SingleProjectConfigFile),
    /// Relay can support multiple projects with multiple schemas
    /// and different options (output, typegen, etc...).
    /// This MultiProjectConfigFile is responsible for configuring
    /// these type of projects (complex)
    MultiProject(Box<MultiProjectConfigFile>),
}

impl<'de> Deserialize<'de> for ConfigFile {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
        let value = Value::deserialize(deserializer)?;
        match MultiProjectConfigFile::deserialize(value.clone()) {
            Ok(config) => Ok(ConfigFile::MultiProject(Box::new(config))),
            Err(multi_project_error) => match SingleProjectConfigFile::deserialize(value) {
                Ok(single_project_config) => Ok(ConfigFile::SingleProject(single_project_config)),
                Err(single_project_error) => {
                    let error_message = format!(
                        r#"The config file cannot be parsed as a multi-project config file due to:
- {:?}.

It also cannot be a single project config file due to:
- {:?}."#,
                        multi_project_error, single_project_error
                    );

                    Err(DeError::custom(error_message))
                }
            },
        }
    }
}

#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct ConfigFileProject {
    /// If a base project is set, the documents of that project can be
    /// referenced, but won't produce output artifacts.
    /// Extensions from the base project will be added as well and the schema
    /// of the base project should be a subset of the schema of this project.
    #[serde(default)]
    base: Option<ProjectName>,

    /// A project without an output directory will put the generated files in
    /// a __generated__ directory next to the input file.
    /// All files in these directories should be generated by the Relay
    /// compiler, so that the compiler can cleanup extra files.
    #[serde(default)]
    output: Option<PathBuf>,

    /// Some projects may need to generate extra artifacts. For those, we may
    /// need to provide an additional directory to put them.
    /// By default the will use `output` *if available
    extra_artifacts_output: Option<PathBuf>,

    /// If `output` is provided and `shard_output` is `true`, shard the files
    /// by putting them under `{output_dir}/{source_relative_path}`
    #[serde(default)]
    shard_output: bool,

    /// Regex to match and strip parts of the `source_relative_path`
    #[serde(default)]
    shard_strip_regex: Option<String>,

    /// Directory containing *.graphql files with schema extensions.
    #[serde(default)]
    schema_extensions: Vec<PathBuf>,

    /// Path to the schema.graphql or a directory containing a schema broken up
    /// in multiple *.graphql files.
    /// Exactly 1 of these options needs to be defined.
    schema: Option<PathBuf>,
    schema_dir: Option<PathBuf>,

    /// If this option is set, the compiler will persist queries using this
    /// config.
    persist: Option<PersistConfig>,

    #[serde(flatten)]
    pub typegen_config: TypegenConfig,

    /// Optional regex to restrict @relay_test_operation to directories matching
    /// this regex. Defaults to no limitations.
    #[serde(default)]
    test_path_regex: Option<String>,

    /// Generates a `// @relayVariables name1 name2` header in generated operation files
    #[serde(default)]
    variable_names_comment: bool,

    /// A placeholder for allowing extra information in the config file
    #[serde(default)]
    extra: serde_json::Value,

    #[serde(default)]
    pub feature_flags: Option<FeatureFlags>,

    /// A generic rollout state for larger codegen changes. The default is to
    /// pass, otherwise it should be a number between 0 and 100 as a percentage.
    #[serde(default)]
    pub rollout: Rollout,

    #[serde(default)]
    pub js_module_format: JsModuleFormat,

    #[serde(default)]
    pub schema_config: SchemaConfig,

    #[serde(default)]
    pub module_import_config: ModuleImportConfig,

    #[serde(default)]
    pub diagnostic_report_config: DiagnosticReportConfig,
}

pub type PersistId = String;

pub type PersistResult<T> = std::result::Result<T, PersistError>;

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ArtifactForPersister {
    pub text: String,
    pub relative_path: PathBuf,
}

#[async_trait]
pub trait OperationPersister {
    async fn persist_artifact(&self, artifact: ArtifactForPersister) -> PersistResult<PersistId>;

    fn finalize(&self) -> PersistResult<()> {
        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.
 */

use std::path::Path;

use common::DiagnosticsResult;
use common::SourceLocationKey;
use docblock_syntax::parse_docblock;
use docblock_syntax::DocblockAST;
use errors::try_all;
use graphql_syntax::ExecutableDefinition;
use graphql_syntax::SchemaDocument;
use relay_config::ProjectConfig;
use relay_docblock::parse_docblock_ast;
use relay_docblock::resolver_maybe_defining_type;
use relay_docblock::ParseOptions;
use schema::SDLSchema;

use crate::file_source::LocatedDocblockSource;
pub struct DocblockASTs {
    pub types: Vec<DocblockAST>,
    pub fields: Vec<DocblockAST>,
}

pub fn parse_docblock_asts_from_sources(
    file_path: &Path,
    docblock_sources: &[LocatedDocblockSource],
) -> DiagnosticsResult<DocblockASTs> {
    let (types, fields) = try_all(
        docblock_sources
            .iter()
            .map(|docblock_source| parse_source_to_ast(file_path, docblock_source)),
    )?
    .into_iter()
    .partition(resolver_maybe_defining_type);

    Ok(DocblockASTs { types, fields })
}

pub fn build_schema_documents_from_docblocks(
    docblocks: &[DocblockAST],
    project_config: &ProjectConfig,
    schema: &SDLSchema,
    definitions: Option<&Vec<ExecutableDefinition>>,
) -> DiagnosticsResult<Vec<SchemaDocument>> {
    try_all(docblocks.iter().filter_map(|ast: &DocblockAST| {
        parse_source(ast, project_config, schema, definitions).transpose()
    }))
}

fn parse_source_to_ast(
    file_path: &Path,
    docblock_source: &LocatedDocblockSource,
) -> DiagnosticsResult<DocblockAST> {
    let source_location =
        SourceLocationKey::embedded(file_path.to_str().unwrap(), docblock_source.index);

    parse_docblock(
        &docblock_source.docblock_source.text_source().text,
        source_location,
    )
}

fn parse_source(
    ast: &DocblockAST,
    project_config: &ProjectConfig,
    schema: &SDLSchema,
    definitions: Option<&Vec<ExecutableDefinition>>,
) -> DiagnosticsResult<Option<SchemaDocument>> {
    let maybe_ir = parse_docblock_ast(
        &ast,
        definitions,
        ParseOptions {
            enable_output_type: &project_config
                .feature_flags
                .relay_resolver_enable_output_type,
        },
    )?;
    maybe_ir
        .map(|ir| ir.to_graphql_schema_ast(schema, &project_config.schema_config))
        .transpose()
}


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

use common::Diagnostic;
use glob::PatternError;
use persist_query::PersistError;
use thiserror::Error;

use crate::compiler_state::ProjectName;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Unable to initialize relay compiler configuration. Error details: \n{details}")]
    ConfigError { details: String },

    #[error(
        "Config `{config_path}` is invalid:{}",
        validation_errors
            .iter()
            .map(|err| format!("\n - {}", err))
            .collect::<Vec<_>>()
            .join("")
    )]
    ConfigFileValidation {
        config_path: PathBuf,
        validation_errors: Vec<ConfigValidationError>,
    },

    #[error(
        "Diagnostics Error:{}",
        errors
            .iter()
            .map(|err| format!("\n - {}", err.print_without_source()))
            .collect::<Vec<_>>()
            .join("")
    )]
    DiagnosticsError { errors: Vec<Diagnostic> },

    #[error(
        "Failed to build:{}",
        errors
            .iter()
            .map(|err| format!("\n - {}", err))
            .collect::<Vec<_>>()
            .join("")
    )]
    BuildProjectsErrors { errors: Vec<BuildProjectError> },

    #[error("Failed to read file `{file}`: {source}")]
    ReadFileError { file: PathBuf, source: io::Error },

    #[error("Failed to write file `{file}`: {source}")]
    WriteFileError { file: PathBuf, source: io::Error },

    #[error("Unable to serialize state to file: `{file}`, because of `{source}`.")]
    SerializationError {
        file: PathBuf,
        source: Box<bincode::ErrorKind>,
    },

    #[error("Unable to deserialize state from file: `{file}`, because of `{source}`.")]
    DeserializationError {
        file: PathBuf,
        source: Box<bincode::ErrorKind>,
    },

    #[error("Failed to canonicalize root: `{root}`.")]
    CanonicalizeRoot {
        root: PathBuf,
        source: std::io::Error,
    },

    #[error("Watchman error: {source}")]
    Watchman {
        #[from]
        source: watchman_client::Error,
    },

    #[error("Watchman query returned no results.")]
    EmptyQueryResult,

    #[error("Failed to read file: `{file}`.")]
    FileRead {
        file: PathBuf,
        source: std::io::Error,
    },

    #[error("A thread that the Relay compiler spun up did not shut down gracefully: {error}")]
    JoinError { error: String },

    #[error("Artifacts validation failed: {error}")]
    ArtifactsValidationError { error: String },

    #[error("Error in post artifact writer: {error}")]
    PostArtifactsError {
        error: Box<dyn std::error::Error + Sync + Send>,
    },

    #[error("Compilation cancelled due to new changes")]
    Cancelled,

    #[error("IO error {0}")]
    IOError(std::io::Error),

    #[error("Unable to parse changed files list. {reason}")]
    ExternalSourceParseError { reason: String },

    #[error("JSON parse error in `{file}`: {source}")]
    SerdeError {
        file: PathBuf,
        source: serde_json::Error,
    },

    #[error("glob pattern error: {0}")]
    PatternError(PatternError),

    #[error(
        "Saved state versions mismatch. Saved state: {saved_state_version}, config: {config_version}."
    )]
    SavedStateVersionMismatch {
        saved_state_version: String,
        config_version: String,
    },
}

#[derive(Debug, Error)]
pub enum ConfigValidationError {
    #[error("Root `{root_dir}` is not a directory.")]
    RootNotDirectory { root_dir: PathBuf },

    #[error("Source `{source_dir}` does not exist.")]
    SourceNotExistent { source_dir: PathBuf },

    #[error("Source `{source_dir}` is not a directory.")]
    SourceNotDirectory { source_dir: PathBuf },

    #[error(
        "There is no source for the project `{project_name}`, the `sources` map should contain at least one path mapping to this project name."
    )]
    ProjectSourceMissing { project_name: ProjectName },

    #[error(
        "Source dir `{source_dir}` tries to use project `{project_name}`, but no such project exists."
    )]
    ProjectDefinitionMissing {
        source_dir: PathBuf,
        project_name: ProjectName,
    },

    #[error(
        "The project `{project_name}` defines the base project `{base_project_name}`, but no such project exists."
    )]
    ProjectBaseMissing {
        project_name: ProjectName,
        base_project_name: ProjectName,
    },

    #[error("Project `{project_name}` needs to define exactly one of `schema` or `schema_dir`.")]
    ProjectNeedsSchemaXorSchemaDir { project_name: ProjectName },

    #[error(
        "The `schema` configured for project `{project_name}` does not exist at `{schema_file}`."
    )]
    SchemaFileNotExistent {
        project_name: ProjectName,
        schema_file: PathBuf,
    },

    #[error(
        "The `schema` configured for project `{project_name}` to be `{schema_file}` is not a file."
    )]
    SchemaFileNotFile {
        project_name: ProjectName,
        schema_file: PathBuf,
    },

    #[error(
        "The `schema_dir` configured for project `{project_name}` does not exist at `{schema_dir}`."
    )]
    SchemaDirNotExistent {
        project_name: ProjectName,
        schema_dir: PathBuf,
    },

    #[error(
        "The `schemaExtensions` configured for project `{project_name}` does not exist at `{extension_dir}`."
    )]
    ExtensionDirNotExistent {
        project_name: ProjectName,
        extension_dir: PathBuf,
    },

    #[error(
        "The `schema_dir` configured for project `{project_name}` to be `{schema_dir}` is not a directory."
    )]
    SchemaDirNotDirectory {
        project_name: ProjectName,
        schema_dir: PathBuf,
    },

    #[error("The regex in `{key}` for project `{project_name}` is invalid.\n {error}.")]
    InvalidRegex {
        key: &'static str,
        project_name: ProjectName,
        error: regex::Error,
    },

    #[error("The `artifactDirectory` does not exist at `{path}`.")]
    ArtifactDirectoryNotExistent { path: PathBuf },

    #[error("Unable to find common path for directories in the config file.")]
    CommonPathNotFound,

    #[error("The config option `{name}` is no longer supported. {action}")]
    RemovedConfigField {
        name: &'static str,
        action: &'static str,
    },
}

#[derive(Debug, Error)]
pub enum BuildProjectError {
    #[error(
        "Validation errors:{}",
        errors
            .iter()
            .map(|err| format!("\n - {}", err.print_without_source()))
            .collect::<Vec<_>>()
            .join("")
    )]
    ValidationErrors {
        errors: Vec<Diagnostic>,
        project_name: ProjectName,
    },

    #[error("Persisting operation(s) failed:{0}",
        errors
            .iter()
            .map(|err| format!("\n - {}", err))
            .collect::<Vec<_>>()
            .join("")
    )]
    PersistErrors {
        errors: Vec<PersistError>,
        project_name: ProjectName,
    },

    #[error("Failed to write file `{file}`: {source}")]
    WriteFileError { file: PathBuf, source: io::Error },
}