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

use common::Diagnostic;
use common::SourceLocationKey;
use dependency_analyzer::ExecutableDefinitionNameSet;
use dependency_analyzer::ExecutableDefinitionNameVec;
use fnv::FnvHashMap;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::OperationDefinitionName;
use graphql_syntax::ExecutableDefinition;

use crate::compiler_state::GraphQLSources;
use crate::compiler_state::ProjectName;
use crate::errors::Error;
use crate::errors::Result;
use crate::file_source::LocatedGraphQLSource;

#[derive(Debug)]
pub struct GraphQLAsts {
    asts: FnvHashMap<PathBuf, Vec<ExecutableDefinition>>,
    /// Names of fragments and operations that are updated or created
    pub pending_definition_names: ExecutableDefinitionNameSet,
    /// Names of fragments and operations that are deleted
    pub removed_definition_names: ExecutableDefinitionNameVec,
}

impl GraphQLAsts {
    pub fn get_executable_definitions_for_file(
        &self,
        file_path: &Path,
    ) -> Option<&Vec<ExecutableDefinition>> {
        self.asts.get(file_path)
    }

    pub fn get_all_executable_definitions(&self) -> Vec<ExecutableDefinition> {
        self.asts.values().flatten().cloned().collect()
    }

    pub fn from_graphql_sources_map(
        graphql_sources_map: &FnvHashMap<ProjectName, GraphQLSources>,
        dirty_definitions_map: &FnvHashMap<ProjectName, Vec<ExecutableDefinitionName>>,
    ) -> Result<FnvHashMap<ProjectName, GraphQLAsts>> {
        graphql_sources_map
            .iter()
            .map(|(&project_name, sources)| {
                let asts = GraphQLAsts::from_graphql_sources(
                    sources,
                    dirty_definitions_map.get(&project_name),
                )?;
                Ok((project_name, asts))
            })
            .collect::<Result<_>>()
    }

    /// Parses all source files for all projects into ASTs and builds up a Sources map that can
    /// be used to print errors with source code listing.
    /// Additionally collects the set of definition names that updated, given the compiler state
    pub fn from_graphql_sources(
        graphql_sources: &GraphQLSources,
        dirty_definitions: Option<&Vec<ExecutableDefinitionName>>,
    ) -> Result<Self> {
        let mut syntax_errors = Vec::new();

        let mut asts: FnvHashMap<PathBuf, Vec<ExecutableDefinition>> = Default::default();
        let mut pending_definition_names: ExecutableDefinitionNameSet = HashSet::default();
        let mut removed_definition_names = Vec::new();

        if let Some(dirty_definitions) = dirty_definitions {
            pending_definition_names.extend(dirty_definitions);
        }

        // Iterate over all pending sources, and parse each graphql string.
        // Prefer the entry from the pending source set, which contains the
        // latest values for the graphql strings in the file.
        for (file_name, pending_graphql_sources) in graphql_sources.pending.iter() {
            let mut definitions_for_file = Vec::new();
            for LocatedGraphQLSource {
                index,
                graphql_source,
            } in pending_graphql_sources.iter()
            {
                let source_location =
                    SourceLocationKey::embedded(&file_name.to_string_lossy(), *index);
                match graphql_syntax::parse_executable(
                    &graphql_source.text_source().text,
                    source_location,
                ) {
                    Ok(document) => {
                        for def in &document.definitions {
                            if let Some(name) = def.name() {
                                match def {
                                    ExecutableDefinition::Operation(_) => pending_definition_names
                                        .insert(OperationDefinitionName(name).into()),
                                    ExecutableDefinition::Fragment(_) => pending_definition_names
                                        .insert(FragmentDefinitionName(name).into()),
                                };
                            } else {
                                syntax_errors.push(Diagnostic::error(
                                    "Expected operation to have a name (e.g. 'query <Name>')",
                                    def.location(),
                                ))
                            }
                        }
                        definitions_for_file.extend(document.definitions);
                    }
                    Err(errors) => syntax_errors.extend(errors),
                }
            }
            // Parse the processed source to get all processed definition names
            // and collect definition names that are removed from that file.
            // (A definition moved to another file is considered as a deletion and a new source)
            if let Some(processed_graphql_sources) = graphql_sources.processed.get(file_name) {
                for LocatedGraphQLSource {
                    index,
                    graphql_source,
                } in processed_graphql_sources.iter()
                {
                    // TODO: parse name instead of the whole graphql text
                    let source_location =
                        SourceLocationKey::embedded(&file_name.to_string_lossy(), *index);
                    if let Ok(document) = graphql_syntax::parse_executable(
                        &graphql_source.text_source().text,
                        source_location,
                    ) {
                        for def in document.definitions {
                            let name = def.name();
                            if let Some(def_name) = name {
                                if !definitions_for_file.iter().any(|def| def.name() == name) {
                                    match def {
                                        ExecutableDefinition::Operation(_) => {
                                            removed_definition_names
                                                .push(OperationDefinitionName(def_name).into())
                                        }
                                        ExecutableDefinition::Fragment(_) => {
                                            removed_definition_names
                                                .push(FragmentDefinitionName(def_name).into())
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
            asts.insert(file_name.clone(), definitions_for_file);
        }

        // Iterate over processed sources that aren't in the pending source set,
        // and parse each graphql string.
        for (file_name, processed_graphql_sources) in &graphql_sources.processed {
            // Only parse the file if it isn't in the pending sources
            if graphql_sources.pending.contains_key(file_name) {
                continue;
            }

            let mut definitions_for_file = Vec::new();
            for LocatedGraphQLSource {
                index,
                graphql_source,
            } in processed_graphql_sources.iter()
            {
                let source_location =
                    SourceLocationKey::embedded(&file_name.to_string_lossy(), *index);
                match graphql_syntax::parse_executable(
                    &graphql_source.text_source().text,
                    source_location,
                ) {
                    Ok(document) => {
                        definitions_for_file.extend(document.definitions);
                    }
                    Err(errors) => syntax_errors.extend(errors),
                }
            }
            match asts.entry(file_name.clone()) {
                Entry::Vacant(entry) => {
                    entry.insert(definitions_for_file);
                }
                Entry::Occupied(mut entry) => {
                    let definitions = entry.get_mut();
                    definitions.extend(definitions_for_file)
                }
            }
        }

        if syntax_errors.is_empty() {
            Ok(Self {
                asts,
                pending_definition_names,
                removed_definition_names,
            })
        } else {
            Err(Error::DiagnosticsError {
                errors: syntax_errors,
            })
        }
    }
}


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

#![deny(warnings)]
#![deny(rust_2018_idioms)]
#![deny(clippy::all)]

pub mod artifact_content;
mod artifact_map;
pub mod build_project;
pub mod compiler;
pub mod compiler_state;
pub mod config;
mod docblocks;
pub mod errors;
mod file_source;
mod graphql_asts;
mod operation_persister;
mod red_to_green;
pub mod saved_state;
pub mod status_reporter;

pub use build_project::add_to_mercurial;
pub use build_project::artifact_writer::ArtifactDifferenceShardedWriter;
pub use build_project::artifact_writer::ArtifactDifferenceWriter;
pub use build_project::artifact_writer::ArtifactFileWriter;
pub use build_project::artifact_writer::ArtifactValidationWriter;
pub use build_project::artifact_writer::ArtifactWriter;
pub use build_project::artifact_writer::NoopArtifactWriter;
pub use build_project::build_programs;
pub use build_project::build_raw_program;
pub use build_project::build_schema;
pub use build_project::find_duplicates;
pub use build_project::generate_artifacts;
pub use build_project::generate_extra_artifacts::GenerateExtraArtifactsFn;
pub use build_project::transform_program;
pub use build_project::validate;
pub use build_project::validate_program;
pub use build_project::AdditionalValidations;
pub use build_project::Artifact;
pub use build_project::ArtifactContent;
pub use build_project::ArtifactGeneratedTypes;
pub use build_project::BuildProjectFailure;
pub use build_project::SourceHashes;
pub use config::ConfigFileProject;
pub use config::FileSourceKind;
pub use config::LocalPersistConfig;
pub use config::OperationPersister;
pub use config::PersistConfig;
pub use config::ProjectConfig;
pub use config::RemotePersistConfig;
pub use config::SchemaLocation;
pub use file_source::source_for_location;
pub use file_source::ExternalFileSourceResult;
pub use file_source::File;
pub use file_source::FileCategorizer;
pub use file_source::FileGroup;
pub use file_source::FileSource;
pub use file_source::FileSourceResult;
pub use file_source::FileSourceSubscription;
pub use file_source::FileSourceSubscriptionNextChange;
pub use file_source::FsSourceReader;
pub use file_source::SourceControlUpdateStatus;
pub use file_source::SourceReader;
pub use graphql_asts::GraphQLAsts;
pub use operation_persister::LocalPersister;
pub use operation_persister::RemotePersister;


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

use std::time::Instant;

use common::PerfLogEvent;
use common::PerfLogger;

/// A struct to keep track of the state of how many build,
///  and how long it takes for errors to be resolved
pub struct RedToGreen {
    timer: Option<Instant>,
    error_count: usize,
}

impl RedToGreen {
    pub fn new() -> Self {
        Self {
            timer: None,
            error_count: 0,
        }
    }

    pub fn log_error(&mut self) {
        self.error_count += 1;
        if self.timer.is_none() {
            self.timer = Some(Instant::now());
        }
    }

    pub fn clear_error_and_log(&mut self, logger: &impl PerfLogger) {
        if let Some(timer) = self.timer {
            let event = logger.create_event("red_to_green");
            event.number("time_to_resolve", timer.elapsed().as_millis() as usize);
            event.number("try_count", self.error_count);
            event.complete();
            self.error_count = 0;
            self.timer = 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::path::PathBuf;

use serde_bser::value::Value;

use crate::config::Config;

pub trait SavedStateLoader {
    fn load(&self, saved_state_info: &Value, config: &Config) -> Option<PathBuf>;
}


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

use std::path::PathBuf;

use common::Diagnostic;
use common::DiagnosticSeverity;
use graphql_cli::DiagnosticPrinter;
use log::error;
use log::info;
use log::warn;

use crate::errors::BuildProjectError;
use crate::errors::Error;
use crate::source_for_location;
use crate::FsSourceReader;
use crate::SourceReader;

pub trait StatusReporter {
    fn build_starts(&self);
    fn build_completes(&self, diagnostics: &[Diagnostic]);
    fn build_errors(&self, error: &Error);
}

pub struct ConsoleStatusReporter {
    source_reader: Box<dyn SourceReader + Send + Sync>,
    root_dir: PathBuf,
    is_multi_project: bool,
}

impl ConsoleStatusReporter {
    pub fn new(root_dir: PathBuf, is_multi_project: bool) -> Self {
        Self {
            root_dir,
            source_reader: Box::new(FsSourceReader),
            is_multi_project,
        }
    }
}

impl ConsoleStatusReporter {
    fn print_error(&self, error: &Error) {
        match error {
            Error::DiagnosticsError { errors } => {
                self.print_diagnostics_by_severity(errors);
            }
            Error::BuildProjectsErrors { errors } => {
                for error in errors {
                    self.print_project_error(error);
                }
            }
            Error::Cancelled => {
                info!("Compilation cancelled due to new changes.");
            }
            error => {
                error!("{}", error);
            }
        }
    }

    fn print_diagnostics_by_severity(&self, diagnostics: &[Diagnostic]) {
        diagnostics
            .iter()
            .map(|diagnostic| (diagnostic.severity(), self.print_diagnostic(diagnostic)))
            .for_each(|(severity, output)| self.print_by_severity(severity, output));
    }

    fn print_by_severity(&self, severity: DiagnosticSeverity, output: String) {
        match severity {
            DiagnosticSeverity::ERROR => error!("{}", output),
            DiagnosticSeverity::WARNING => warn!("{}", output),
            _ => info!("{}", output),
        }
    }

    fn print_project_error(&self, error: &BuildProjectError) {
        match error {
            BuildProjectError::ValidationErrors {
                errors,
                project_name,
            } => {
                errors
                    .iter()
                    .map(|diagnostic| {
                        let output = self.print_diagnostic(diagnostic);
                        let formatted_output = match diagnostic.severity() {
                            DiagnosticSeverity::ERROR => {
                                if self.is_multi_project {
                                    format!("Error in the project `{}`: {}", project_name, output)
                                } else {
                                    format!("Error: {}", output)
                                }
                            }
                            _ => {
                                if self.is_multi_project {
                                    format!("In the project `{}`: {}", project_name, output)
                                } else {
                                    output
                                }
                            }
                        };

                        (diagnostic.severity(), formatted_output)
                    })
                    .for_each(|(severity, output)| self.print_by_severity(severity, output));
            }
            BuildProjectError::PersistErrors {
                errors,
                project_name,
            } => {
                for error in errors {
                    if self.is_multi_project {
                        error!("Error in the project `{}`: {}", project_name, error);
                    } else {
                        error!("Error: {}", error);
                    }
                }
            }
            _ => {
                error!("{}", error);
            }
        }
    }

    fn print_diagnostic(&self, diagnostic: &Diagnostic) -> String {
        let printer = DiagnosticPrinter::new(|source_location| {
            source_for_location(&self.root_dir, source_location, self.source_reader.as_ref())
                .map(|source| source.to_text_source())
        });
        printer.diagnostic_to_string(diagnostic)
    }
}

impl StatusReporter for ConsoleStatusReporter {
    fn build_starts(&self) {}

    fn build_completes(&self, diagnostics: &[Diagnostic]) {
        self.print_diagnostics_by_severity(diagnostics);
        info!("Compilation completed.");
    }

    fn build_errors(&self, error: &Error) {
        self.print_error(error);

        if !matches!(error, Error::Cancelled) {
            error!("Compilation failed.");
        }
    }
}


/*
 * 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::fmt::Error as FmtError;
use std::fmt::Result as FmtResult;
use std::fmt::Write;
use std::sync::Arc;

use common::NamedItem;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::OperationDefinition;
use intern::Lookup;
use relay_codegen::build_request_params;
use relay_codegen::Printer;
use relay_codegen::QueryID;
use relay_codegen::TopLevelStatement;
use relay_codegen::CODEGEN_CONSTANTS;
use relay_transforms::is_operation_preloadable;
use relay_transforms::RelayDataDrivenDependencyMetadata;
use relay_transforms::ASSIGNABLE_DIRECTIVE;
use relay_typegen::generate_fragment_type_exports_section;
use relay_typegen::generate_named_validator_export;
use relay_typegen::generate_operation_type_exports_section;
use relay_typegen::generate_split_operation_type_exports_section;
use relay_typegen::FragmentLocations;
use relay_typegen::TypegenConfig;
use relay_typegen::TypegenLanguage;
use schema::SDLSchema;
use signedsource::SIGNING_TOKEN;

use super::super::ArtifactGeneratedTypes;
use super::content_section::CommentAnnotationsSection;
use super::content_section::ContentSection;
use super::content_section::ContentSections;
use super::content_section::DocblockSection;
use super::content_section::GenericSection;
use crate::config::Config;
use crate::config::ProjectConfig;

#[allow(clippy::too_many_arguments)]
pub fn generate_updatable_query(
    config: &Config,
    project_config: &ProjectConfig,
    printer: &mut Printer<'_>,
    schema: &SDLSchema,
    reader_operation: &OperationDefinition,
    typegen_operation: &OperationDefinition,
    source_hash: String,
    skip_types: bool,
    fragment_locations: &FragmentLocations,
) -> Result<Vec<u8>, FmtError> {
    let operation_fragment = FragmentDefinition {
        name: reader_operation.name.map(|x| FragmentDefinitionName(x.0)),
        variable_definitions: reader_operation.variable_definitions.clone(),
        selections: reader_operation.selections.clone(),
        used_global_variables: Default::default(),
        directives: reader_operation.directives.clone(),
        type_condition: reader_operation.type_,
    };

    let mut content_sections = ContentSections::default();

    // -- Begin Docblock Section --
    content_sections.push(ContentSection::Docblock(generate_docblock_section(
        config,
        project_config,
        vec![],
    )?));
    // -- End Docblock Section --

    // -- Begin Disable Lint Section --
    content_sections.push(ContentSection::Generic(generate_disable_lint_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Disable Lint Section --

    // -- Begin Use Strict Section --
    content_sections.push(ContentSection::Generic(generate_use_strict_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Use Strict Section --

    // -- Begin Types Section --
    let mut section = GenericSection::default();
    let generated_types =
        ArtifactGeneratedTypes::from_updatable_query(typegen_operation, skip_types);

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "/*::")?;
    }

    write_import_type_from(
        project_config,
        &mut section,
        generated_types.imported_types,
        "relay-runtime",
    )?;

    if !skip_types {
        write!(
            section,
            "{}",
            generate_operation_type_exports_section(
                typegen_operation,
                reader_operation,
                schema,
                project_config,
                fragment_locations,
            )
        )?;
    }

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "*/")?;
    }

    content_sections.push(ContentSection::Generic(section));
    // -- End Types Section --

    // -- Begin Query Node Section --
    let mut section = GenericSection::default();
    let request = printer.print_updatable_query(schema, &operation_fragment);
    write_variable_value_with_type(
        &project_config.typegen_config.language,
        &mut section,
        "node",
        generated_types.ast_type,
        &request,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Query Node Section --

    // -- Begin Query Node Hash Section --
    let mut section = GenericSection::default();
    write_source_hash(
        config,
        &project_config.typegen_config.language,
        &mut section,
        &source_hash,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Query Node Hash Section --

    // -- Begin Export Query Node Section --
    let mut section = GenericSection::default();
    write_export_generated_node(
        &project_config.typegen_config,
        &mut section,
        "node",
        generated_types.exported_type,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Export Query Node Section --

    content_sections.into_signed_bytes()
}

#[allow(clippy::too_many_arguments)]
pub fn generate_operation(
    config: &Config,
    project_config: &ProjectConfig,
    printer: &mut Printer<'_>,
    schema: &SDLSchema,
    normalization_operation: &OperationDefinition,
    reader_operation: &OperationDefinition,
    typegen_operation: &OperationDefinition,
    source_hash: String,
    text: &Option<String>,
    id_and_text_hash: &Option<QueryID>,
    skip_types: bool,
    fragment_locations: &FragmentLocations,
) -> Result<Vec<u8>, FmtError> {
    let mut request_parameters = build_request_params(normalization_operation);

    if id_and_text_hash.is_some() {
        request_parameters.id = id_and_text_hash;
        if project_config
            .persist
            .as_ref()
            .map_or(false, |config| config.include_query_text())
        {
            request_parameters.text = text.clone();
        }
    } else {
        request_parameters.text = text.clone();
    }

    let operation_fragment = FragmentDefinition {
        name: reader_operation.name.map(|x| FragmentDefinitionName(x.0)),
        variable_definitions: reader_operation.variable_definitions.clone(),
        selections: reader_operation.selections.clone(),
        used_global_variables: Default::default(),
        directives: reader_operation.directives.clone(),
        type_condition: reader_operation.type_,
    };

    let mut content_sections = ContentSections::default();

    // -- Begin Docblock Section --
    let v = match id_and_text_hash {
        Some(QueryID::Persisted { text_hash, .. }) => vec![format!("@relayHash {}", text_hash)],
        _ => vec![],
    };
    content_sections.push(ContentSection::Docblock(generate_docblock_section(
        config,
        project_config,
        v,
    )?));
    // -- End Docblock Section --

    // -- Begin Disable Lint Section --
    content_sections.push(ContentSection::Generic(generate_disable_lint_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Disable Lint Section --

    // -- Begin Use Strict Section --
    content_sections.push(ContentSection::Generic(generate_use_strict_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Use Strict Section --

    // -- Begin Metadata Annotations Section --
    let mut section = CommentAnnotationsSection::default();
    if let Some(QueryID::Persisted { id, .. }) = &request_parameters.id {
        writeln!(section, "@relayRequestID {}", id)?;
    }
    if project_config.variable_names_comment {
        write!(section, "@relayVariables")?;
        for variable_definition in &normalization_operation.variable_definitions {
            write!(section, " {}", variable_definition.name.item)?;
        }
        writeln!(section)?;
    }
    let data_driven_dependency_metadata =
        RelayDataDrivenDependencyMetadata::find(&operation_fragment.directives);
    if let Some(data_driven_dependency_metadata) = data_driven_dependency_metadata {
        write_data_driven_dependency_annotation(&mut section, data_driven_dependency_metadata)?;
    }
    content_sections.push(ContentSection::CommentAnnotations(section));
    // -- End Metadata Annotations Section --

    // -- Begin Types Section --
    let mut section = GenericSection::default();
    let generated_types = ArtifactGeneratedTypes::from_operation(
        typegen_operation,
        skip_types,
        request_parameters.is_client_request(),
    );

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "/*::")?;
    }

    write_import_type_from(
        project_config,
        &mut section,
        generated_types.imported_types,
        "relay-runtime",
    )?;

    if !skip_types {
        write!(
            section,
            "{}",
            generate_operation_type_exports_section(
                typegen_operation,
                normalization_operation,
                schema,
                project_config,
                fragment_locations,
            )
        )?;
    }

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "*/")?;
    }
    content_sections.push(ContentSection::Generic(section));
    // -- End Types Section --

    // -- Begin Top Level Statements Section --
    let mut section = GenericSection::default();
    let mut top_level_statements = Default::default();
    if let Some(provided_variables) =
        printer.print_provided_variables(schema, normalization_operation, &mut top_level_statements)
    {
        let mut provided_variable_text = String::new();
        write_variable_value_with_type(
            &project_config.typegen_config.language,
            &mut provided_variable_text,
            CODEGEN_CONSTANTS.provided_variables_definition.lookup(),
            relay_typegen::PROVIDED_VARIABLE_TYPE,
            &provided_variables,
        )
        .unwrap();
        top_level_statements.insert(
            CODEGEN_CONSTANTS.provided_variables_definition.to_string(),
            TopLevelStatement::VariableDefinition(provided_variable_text),
        );
    }

    let request = printer.print_request(
        schema,
        normalization_operation,
        &operation_fragment,
        request_parameters,
        &mut top_level_statements,
    );

    write!(section, "{}", &top_level_statements)?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Top Level Statements Section --

    // -- Begin Query Node Section --
    let mut section = GenericSection::default();
    write_variable_value_with_type(
        &project_config.typegen_config.language,
        &mut section,
        "node",
        generated_types.ast_type,
        &request,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Query Node Section --

    // -- Begin Query Node Hash Section --
    let mut section = GenericSection::default();
    write_source_hash(
        config,
        &project_config.typegen_config.language,
        &mut section,
        &source_hash,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Query Node Hash Section --

    // -- Begin PreloadableQueryRegistry Section --
    let mut section = GenericSection::default();
    if is_operation_preloadable(normalization_operation) && id_and_text_hash.is_some() {
        match project_config.typegen_config.language {
            TypegenLanguage::Flow => {
                writeln!(
                    section,
                    "require('relay-runtime').PreloadableQueryRegistry.set((node.params/*: any*/).id, node);",
                )?;
            }
            TypegenLanguage::JavaScript => {
                writeln!(
                    section,
                    "require('relay-runtime').PreloadableQueryRegistry.set(node.params.id, node);",
                )?;
            }
            TypegenLanguage::TypeScript => {
                writeln!(
                    section,
                    "import {{ PreloadableQueryRegistry }} from 'relay-runtime';",
                )?;
                writeln!(
                    section,
                    "PreloadableQueryRegistry.set(node.params.id, node);",
                )?;
            }
        }
    }
    content_sections.push(ContentSection::Generic(section));
    // -- End PreloadableQueryRegistry Section --

    // -- Begin Export Section --
    let mut section = GenericSection::default();
    write_export_generated_node(
        &project_config.typegen_config,
        &mut section,
        "node",
        generated_types.exported_type,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Export Section --

    content_sections.into_signed_bytes()
}

#[allow(clippy::too_many_arguments)]
pub fn generate_split_operation(
    config: &Config,
    project_config: &ProjectConfig,
    printer: &mut Printer<'_>,
    schema: &SDLSchema,
    normalization_operation: &OperationDefinition,
    typegen_operation: &Option<Arc<OperationDefinition>>,
    source_hash: Option<&String>,
    fragment_locations: &FragmentLocations,
    no_optional_fields_in_raw_response_type: bool,
) -> Result<Vec<u8>, FmtError> {
    let mut content_sections = ContentSections::default();

    // -- Begin Docblock Section --
    content_sections.push(ContentSection::Docblock(generate_docblock_section(
        config,
        project_config,
        vec![],
    )?));
    // -- End Docblock Section --

    // -- Begin Disable Lint Section --
    content_sections.push(ContentSection::Generic(generate_disable_lint_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Disable Lint Section --

    // -- Begin Use Strict Section --
    content_sections.push(ContentSection::Generic(generate_use_strict_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Use Strict Section --

    // -- Begin Types Section --
    let mut section = GenericSection::default();
    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "/*::")?;
    }
    write_import_type_from(
        project_config,
        &mut section,
        "NormalizationSplitOperation",
        "relay-runtime",
    )?;
    writeln!(section)?;

    if let Some(typegen_operation) = typegen_operation {
        writeln!(
            section,
            "{}",
            generate_split_operation_type_exports_section(
                typegen_operation,
                normalization_operation,
                schema,
                project_config,
                fragment_locations,
                no_optional_fields_in_raw_response_type
            )
        )?;
    }

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "*/")?;
    }
    content_sections.push(ContentSection::Generic(section));
    // -- End Types Section --

    // -- Begin Top Level Statements Section --
    let mut section = GenericSection::default();
    let mut top_level_statements = Default::default();
    let operation =
        printer.print_operation(schema, normalization_operation, &mut top_level_statements);

    write!(section, "{}", &top_level_statements)?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Top Level Statements Section --

    // -- Begin Operation Node Section --
    let mut section = GenericSection::default();
    write_variable_value_with_type(
        &project_config.typegen_config.language,
        &mut section,
        "node",
        "NormalizationSplitOperation",
        &operation,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Operation Node Section --

    // -- Begin Operation Node Hash Section --
    let mut section = GenericSection::default();
    if let Some(source_hash) = source_hash {
        write_source_hash(
            config,
            &project_config.typegen_config.language,
            &mut section,
            source_hash,
        )?;
    }
    content_sections.push(ContentSection::Generic(section));
    // -- End Operation Node Hash Section --

    // -- Begin Export Section --
    let mut section = GenericSection::default();
    write_export_generated_node(&project_config.typegen_config, &mut section, "node", None)?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Export Section --

    content_sections.into_signed_bytes()
}

#[allow(clippy::too_many_arguments)]
pub fn generate_fragment(
    config: &Config,
    project_config: &ProjectConfig,
    printer: &mut Printer<'_>,
    schema: &SDLSchema,
    reader_fragment: &FragmentDefinition,
    typegen_fragment: &FragmentDefinition,
    source_hash: Option<&String>,
    skip_types: bool,
    fragment_locations: &FragmentLocations,
) -> Result<Vec<u8>, FmtError> {
    let is_assignable_fragment = typegen_fragment
        .directives
        .named(*ASSIGNABLE_DIRECTIVE)
        .is_some();
    if is_assignable_fragment {
        generate_assignable_fragment(
            config,
            project_config,
            schema,
            typegen_fragment,
            skip_types,
            fragment_locations,
        )
    } else {
        generate_read_only_fragment(
            config,
            project_config,
            printer,
            schema,
            reader_fragment,
            typegen_fragment,
            source_hash,
            skip_types,
            fragment_locations,
        )
    }
}

#[allow(clippy::too_many_arguments)]
fn generate_read_only_fragment(
    config: &Config,
    project_config: &ProjectConfig,
    printer: &mut Printer<'_>,
    schema: &SDLSchema,
    reader_fragment: &FragmentDefinition,
    typegen_fragment: &FragmentDefinition,
    source_hash: Option<&String>,
    skip_types: bool,
    fragment_locations: &FragmentLocations,
) -> Result<Vec<u8>, FmtError> {
    let mut content_sections = ContentSections::default();

    // -- Begin Docblock Section --
    content_sections.push(ContentSection::Docblock(generate_docblock_section(
        config,
        project_config,
        vec![],
    )?));
    // -- End Docblock Section --

    // -- Begin Disable Lint Section --
    content_sections.push(ContentSection::Generic(generate_disable_lint_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Disable Lint Section --

    // -- Begin Use Strict Section --
    content_sections.push(ContentSection::Generic(generate_use_strict_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Use Strict Section --

    // -- Begin Metadata Annotations Section --
    let mut section = CommentAnnotationsSection::default();
    if let Some(data_driven_dependency_metadata) =
        RelayDataDrivenDependencyMetadata::find(&reader_fragment.directives)
    {
        write_data_driven_dependency_annotation(&mut section, data_driven_dependency_metadata)?;
    }
    content_sections.push(ContentSection::CommentAnnotations(section));
    // -- End Metadata Annotations Section --

    // -- Begin Types Section --
    let mut section = GenericSection::default();
    let generated_types = ArtifactGeneratedTypes::from_fragment(typegen_fragment, skip_types);

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "/*::")?;
    }

    write_import_type_from(
        project_config,
        &mut section,
        generated_types.imported_types,
        "relay-runtime",
    )?;

    if !skip_types {
        write!(
            section,
            "{}",
            generate_fragment_type_exports_section(
                typegen_fragment,
                schema,
                project_config,
                fragment_locations
            )
        )?;
    }

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "*/")?;
    }
    content_sections.push(ContentSection::Generic(section));
    // -- End Types Section --

    // -- Begin Top Level Statements Section --
    let mut section = GenericSection::default();
    let mut top_level_statements = Default::default();
    let fragment = printer.print_fragment(schema, reader_fragment, &mut top_level_statements);

    write!(section, "{}", &top_level_statements)?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Top Level Statements Section --

    // -- Begin Fragment Node Section --
    let mut section = GenericSection::default();
    write_variable_value_with_type(
        &project_config.typegen_config.language,
        &mut section,
        "node",
        generated_types.ast_type,
        &fragment,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Fragment Node Section --

    // -- Begin Fragment Node Hash Section --
    if let Some(source_hash) = source_hash {
        let mut section = GenericSection::default();
        write_source_hash(
            config,
            &project_config.typegen_config.language,
            &mut section,
            source_hash,
        )?;
        content_sections.push(ContentSection::Generic(section));
    }
    // -- End Fragment Node Hash Section --

    // -- Begin Fragment Node Export Section --
    let mut section = GenericSection::default();
    write_export_generated_node(
        &project_config.typegen_config,
        &mut section,
        "node",
        generated_types.exported_type,
    )?;
    content_sections.push(ContentSection::Generic(section));
    // -- End Fragment Node Export Section --

    content_sections.into_signed_bytes()
}

fn generate_assignable_fragment(
    config: &Config,
    project_config: &ProjectConfig,
    schema: &SDLSchema,
    typegen_fragment: &FragmentDefinition,
    skip_types: bool,
    fragment_locations: &FragmentLocations,
) -> Result<Vec<u8>, FmtError> {
    let mut content_sections = ContentSections::default();

    // -- Begin Docblock Section --
    content_sections.push(ContentSection::Docblock(generate_docblock_section(
        config,
        project_config,
        vec![],
    )?));
    // -- End Docblock Section --

    // -- Begin Disable Lint Section --
    content_sections.push(ContentSection::Generic(generate_disable_lint_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Disable Lint Section --

    // -- Begin Use Strict Section --
    content_sections.push(ContentSection::Generic(generate_use_strict_section(
        &project_config.typegen_config.language,
    )?));
    // -- End Use Strict Section --

    // -- Begin Types Section --
    let mut section = GenericSection::default();
    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "/*::")?;
    }

    if !skip_types {
        write!(
            section,
            "{}",
            generate_fragment_type_exports_section(
                typegen_fragment,
                schema,
                project_config,
                fragment_locations
            )
        )?;
    }

    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "*/")?;
    }
    content_sections.push(ContentSection::Generic(section));
    // -- End Types Section --

    // -- Begin Export Section --
    let mut section = GenericSection::default();
    // Assignable fragments should never be passed to useFragment, and thus, we
    // don't need to emit a reader fragment.
    // Instead, we only need a named validator export, i.e.
    // module.exports.validator = ...
    let named_validator_export = generate_named_validator_export(
        typegen_fragment,
        schema,
        project_config,
        fragment_locations,
    );
    writeln!(section, "{}", named_validator_export).unwrap();
    content_sections.push(ContentSection::Generic(section));
    // -- End Export Section --

    content_sections.into_signed_bytes()
}

fn write_variable_value_with_type(
    language: &TypegenLanguage,
    section: &mut dyn Write,
    variable_name: &str,
    type_: &str,
    value: &str,
) -> FmtResult {
    match language {
        TypegenLanguage::JavaScript => writeln!(section, "var {} = {};", variable_name, value),
        TypegenLanguage::Flow => {
            writeln!(section, "var {}/*: {}*/ = {};", variable_name, type_, value)
        }
        TypegenLanguage::TypeScript => {
            writeln!(section, "const {}: {} = {};", variable_name, type_, value)
        }
    }
}

fn generate_disable_lint_section(language: &TypegenLanguage) -> Result<GenericSection, FmtError> {
    let mut section = GenericSection::default();
    match language {
        TypegenLanguage::TypeScript => {
            writeln!(section, "/* tslint:disable */")?;
            writeln!(section, "/* eslint-disable */")?;
            writeln!(section, "// @ts-nocheck")?;
        }
        TypegenLanguage::Flow | TypegenLanguage::JavaScript => {
            writeln!(section, "/* eslint-disable */")?;
        }
    }
    Ok(section)
}

fn generate_use_strict_section(language: &TypegenLanguage) -> Result<GenericSection, FmtError> {
    let mut section = GenericSection::default();
    match language {
        TypegenLanguage::TypeScript => {}
        TypegenLanguage::Flow | TypegenLanguage::JavaScript => {
            writeln!(section, "'use strict';")?;
        }
    }
    Ok(section)
}

fn write_import_type_from(
    project_config: &ProjectConfig,
    section: &mut dyn Write,
    type_: &str,
    from: &str,
) -> FmtResult {
    let language = &project_config.typegen_config.language;
    match language {
        TypegenLanguage::JavaScript => Ok(()),
        TypegenLanguage::Flow => writeln!(section, "import type {{ {} }} from '{}';", type_, from),
        TypegenLanguage::TypeScript => writeln!(
            section,
            "import {}{{ {} }} from '{}';",
            if project_config.typegen_config.use_import_type_syntax {
                "type "
            } else {
                ""
            },
            type_,
            from
        ),
    }
}

fn write_export_generated_node(
    typegen_config: &TypegenConfig,
    section: &mut dyn Write,
    variable_node: &str,
    forced_type: Option<String>,
) -> FmtResult {
    let export_value = match (typegen_config.language, forced_type) {
        (TypegenLanguage::Flow, None) | (TypegenLanguage::JavaScript, _) => {
            variable_node.to_string()
        }
        (TypegenLanguage::TypeScript, _) => {
            // TODO: Support force_type for TypeScript
            variable_node.to_string()
        }
        (TypegenLanguage::Flow, Some(forced_type)) => {
            format!("(({}/*: any*/)/*: {}*/)", variable_node, forced_type)
        }
    };
    if typegen_config.eager_es_modules || typegen_config.language == TypegenLanguage::TypeScript {
        writeln!(section, "export default {};", export_value)
    } else {
        writeln!(section, "module.exports = {};", export_value)
    }
}

fn generate_docblock_section(
    config: &Config,
    project_config: &ProjectConfig,
    extra_annotations: Vec<String>,
) -> Result<DocblockSection, FmtError> {
    let mut section = DocblockSection::default();
    if !config.header.is_empty() {
        for header_line in &config.header {
            writeln!(section, "{}", header_line)?;
        }
        writeln!(section)?;
    }
    writeln!(section, "{}", SIGNING_TOKEN)?;
    for annotation in extra_annotations {
        writeln!(section, "{}", annotation)?;
    }
    if project_config.typegen_config.language == TypegenLanguage::Flow {
        writeln!(section, "@flow")?;
    }
    writeln!(section, "@lightSyntaxTransform")?;
    writeln!(section, "@nogrep")?;
    if let Some(codegen_command) = &config.codegen_command {
        writeln!(section, "@codegen-command: {}", codegen_command)?;
    }
    Ok(section)
}

fn write_source_hash(
    config: &Config,
    language: &TypegenLanguage,
    section: &mut dyn Write,
    source_hash: &str,
) -> FmtResult {
    if let Some(is_dev_variable_name) = &config.is_dev_variable_name {
        writeln!(section, "if ({}) {{", is_dev_variable_name)?;
        match language {
            TypegenLanguage::Flow => {
                writeln!(section, "  (node/*: any*/).hash = \"{}\";", source_hash)?
            }
            TypegenLanguage::JavaScript => writeln!(section, "  node.hash = \"{}\";", source_hash)?,
            TypegenLanguage::TypeScript => {
                writeln!(section, "  (node as any).hash = \"{}\";", source_hash)?
            }
        };
        writeln!(section, "}}")?;
    } else {
        match language {
            TypegenLanguage::Flow => {
                writeln!(section, "(node/*: any*/).hash = \"{}\";", source_hash)?
            }
            TypegenLanguage::JavaScript => writeln!(section, "node.hash = \"{}\";", source_hash)?,
            TypegenLanguage::TypeScript => {
                writeln!(section, "(node as any).hash = \"{}\";", source_hash)?
            }
        };
    }

    Ok(())
}

fn write_data_driven_dependency_annotation(
    section: &mut CommentAnnotationsSection,
    data_driven_dependency_metadata: &RelayDataDrivenDependencyMetadata,
) -> FmtResult {
    for (key, value) in data_driven_dependency_metadata
        .direct_dependencies
        .iter()
        .flatten()
    {
        writeln!(section, "@dataDrivenDependency {} {}", key, value)?;
    }
    for (key, value) in data_driven_dependency_metadata
        .indirect_dependencies
        .iter()
        .flatten()
    {
        writeln!(section, "@indirectDataDrivenDependency {} {}", key, value)?;
    }
    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::fmt::Error as FmtError;
use std::fmt::Result as FmtResult;
use std::fmt::Write;

use signedsource::sign_file;

pub enum ContentSection {
    CommentAnnotations(CommentAnnotationsSection),
    Docblock(DocblockSection),
    Generic(GenericSection),
}

impl std::fmt::Display for ContentSection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> FmtResult {
        match self {
            ContentSection::CommentAnnotations(section) => write!(f, "{}", section),
            ContentSection::Docblock(section) => write!(f, "{}", section),
            ContentSection::Generic(section) => write!(f, "{}", section),
        }
    }
}

impl ContentSection {
    fn is_empty(&self) -> bool {
        match self {
            ContentSection::CommentAnnotations(CommentAnnotationsSection(s))
            | ContentSection::Docblock(DocblockSection(s))
            | ContentSection::Generic(GenericSection(s)) => s.is_empty(),
        }
    }
}

impl FromIterator<ContentSection> for Vec<String> {
    fn from_iter<I: IntoIterator<Item = ContentSection>>(iter: I) -> Self {
        iter.into_iter()
            .map(|section| section.to_string())
            .collect()
    }
}

#[derive(Default)]
pub struct CommentAnnotationsSection(String);

impl Write for CommentAnnotationsSection {
    fn write_str(&mut self, str: &str) -> FmtResult {
        write!(self.0, "{}", str)
    }
}

impl std::fmt::Display for CommentAnnotationsSection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> FmtResult {
        for line in self.0.lines() {
            if line.is_empty() {
                writeln!(f)?;
            } else {
                writeln!(f, "// {}", line)?;
            }
        }
        Ok(())
    }
}

#[derive(Default)]
pub struct DocblockSection(String);

impl Write for DocblockSection {
    fn write_str(&mut self, str: &str) -> FmtResult {
        write!(self.0, "{}", str)
    }
}

impl std::fmt::Display for DocblockSection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> FmtResult {
        if !self.0.is_empty() {
            writeln!(f, "/**")?;
            for line in self.0.lines() {
                if line.is_empty() {
                    writeln!(f, " *")?;
                } else {
                    writeln!(f, " * {}", line)?;
                }
            }
            writeln!(f, " */")
        } else {
            Ok(())
        }
    }
}

#[derive(Default)]
pub struct GenericSection(String);

impl Write for GenericSection {
    fn write_str(&mut self, str: &str) -> FmtResult {
        write!(self.0, "{}", str)
    }
}

impl std::fmt::Display for GenericSection {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> FmtResult {
        write!(f, "{}", self.0)
    }
}

#[derive(Default)]
pub struct ContentSections(Vec<ContentSection>);

impl ContentSections {
    pub fn push(&mut self, section: ContentSection) {
        if !section.is_empty() {
            self.0.push(section);
        }
    }

    pub fn into_signed_bytes(self) -> Result<Vec<u8>, FmtError> {
        Ok(sign_file(
            self.0
                .into_iter()
                .collect::<Vec<String>>()
                .join("\n")
                .as_str(),
        )
        .into_bytes())
    }
}


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

mod content;
mod content_section;

use std::sync::Arc;

use common::SourceLocationKey;
use content::generate_fragment;
use content::generate_operation;
use content::generate_split_operation;
use content::generate_updatable_query;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use relay_codegen::Printer;
use relay_codegen::QueryID;
use relay_typegen::FragmentLocations;
use schema::SDLSchema;

use crate::config::Config;
use crate::config::ProjectConfig;

#[derive(Clone)]
pub enum ArtifactContent {
    Operation {
        normalization_operation: Arc<OperationDefinition>,
        reader_operation: Arc<OperationDefinition>,
        typegen_operation: Arc<OperationDefinition>,
        source_hash: String,
        text: Option<String>,
        id_and_text_hash: Option<QueryID>,
    },
    UpdatableQuery {
        reader_operation: Arc<OperationDefinition>,
        typegen_operation: Arc<OperationDefinition>,
        source_hash: String,
    },
    Fragment {
        reader_fragment: Arc<FragmentDefinition>,
        typegen_fragment: Arc<FragmentDefinition>,
        source_hash: Option<String>,
    },
    SplitOperation {
        normalization_operation: Arc<OperationDefinition>,
        typegen_operation: Option<Arc<OperationDefinition>>,
        source_hash: Option<String>,
        no_optional_fields_in_raw_response_type: bool,
    },
    Generic {
        content: Vec<u8>,
    },
}

impl ArtifactContent {
    pub fn as_bytes(
        &self,
        config: &Config,
        project_config: &ProjectConfig,
        printer: &mut Printer<'_>,
        schema: &SDLSchema,
        source_file: SourceLocationKey,
        fragment_locations: &FragmentLocations,
    ) -> Vec<u8> {
        let skip_types = project_config
            .skip_types_for_artifact
            .as_ref()
            .map_or(false, |skip_types_fn| skip_types_fn(source_file));
        match self {
            ArtifactContent::Operation {
                normalization_operation,
                reader_operation,
                typegen_operation,
                source_hash,
                text,
                id_and_text_hash,
            } => generate_operation(
                config,
                project_config,
                printer,
                schema,
                normalization_operation,
                reader_operation,
                typegen_operation,
                source_hash.into(),
                text,
                id_and_text_hash,
                skip_types,
                fragment_locations,
            )
            .unwrap(),
            ArtifactContent::UpdatableQuery {
                reader_operation,
                typegen_operation,
                source_hash,
            } => generate_updatable_query(
                config,
                project_config,
                printer,
                schema,
                reader_operation,
                typegen_operation,
                source_hash.into(),
                skip_types,
                fragment_locations,
            )
            .unwrap(),
            ArtifactContent::SplitOperation {
                normalization_operation,
                typegen_operation,
                no_optional_fields_in_raw_response_type,
                source_hash,
            } => generate_split_operation(
                config,
                project_config,
                printer,
                schema,
                normalization_operation,
                typegen_operation,
                source_hash.as_ref(),
                fragment_locations,
                *no_optional_fields_in_raw_response_type,
            )
            .unwrap(),
            ArtifactContent::Fragment {
                reader_fragment,
                typegen_fragment,
                source_hash,
            } => generate_fragment(
                config,
                project_config,
                printer,
                schema,
                reader_fragment,
                typegen_fragment,
                source_hash.as_ref(),
                skip_types,
                fragment_locations,
            )
            .unwrap(),
            ArtifactContent::Generic { content } => content.clone(),
        }
    }
}


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

use common::NamedItem;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_syntax::OperationKind;
use relay_transforms::RefetchableMetadata;
use relay_transforms::INLINE_DIRECTIVE_NAME;
use relay_transforms::UPDATABLE_DIRECTIVE;
use relay_typegen::has_raw_response_type_directive;

/// Specifies the imported and exported generated types of an
/// artifact.
pub struct ArtifactGeneratedTypes {
    pub imported_types: &'static str,
    pub ast_type: &'static str,
    pub exported_type: Option<String>,
}

impl ArtifactGeneratedTypes {
    pub fn from_operation(
        operation: &OperationDefinition,
        skip_types: bool,
        is_client_only: bool,
    ) -> Self {
        if skip_types {
            Self {
                imported_types: "ConcreteRequest",
                ast_type: "ConcreteRequest",
                exported_type: None,
            }
        } else {
            let (kind, imported_types) = match operation.kind {
                OperationKind::Query => {
                    if is_client_only {
                        ("ClientQuery", "ClientRequest, ClientQuery")
                    } else {
                        ("Query", "ConcreteRequest, Query")
                    }
                }
                OperationKind::Mutation => ("Mutation", "ConcreteRequest, Mutation"),
                OperationKind::Subscription => (
                    "GraphQLSubscription",
                    "ConcreteRequest, GraphQLSubscription",
                ),
            };
            let exported_type = if has_raw_response_type_directive(operation) {
                format!(
                    "{kind}<\n  {name}$variables,\n  {name}$data,\n  {name}$rawResponse,\n>",
                    kind = kind,
                    name = operation.name.item.0
                )
            } else {
                format!(
                    "{kind}<\n  {name}$variables,\n  {name}$data,\n>",
                    kind = kind,
                    name = operation.name.item.0
                )
            };
            Self {
                imported_types,
                ast_type: if is_client_only {
                    "ClientRequest"
                } else {
                    "ConcreteRequest"
                },
                exported_type: Some(exported_type),
            }
        }
    }

    pub fn from_updatable_query(typegen_operation: &OperationDefinition, skip_types: bool) -> Self {
        if skip_types {
            Self {
                imported_types: "ConcreteUpdatableQuery",
                ast_type: "ConcreteUpdatableQuery",
                exported_type: None,
            }
        } else {
            let exported_type = format!(
                "UpdatableQuery<\n  {name}$variables,\n  {name}$data,\n>",
                name = typegen_operation.name.item.0,
            );
            Self {
                imported_types: "UpdatableQuery, ConcreteUpdatableQuery",
                ast_type: "ConcreteUpdatableQuery",
                exported_type: Some(exported_type),
            }
        }
    }

    pub fn from_fragment(fragment: &FragmentDefinition, skip_types: bool) -> Self {
        let is_inline_data_fragment = fragment.directives.named(*INLINE_DIRECTIVE_NAME).is_some();
        let is_updatable_fragment = fragment.directives.named(*UPDATABLE_DIRECTIVE).is_some();

        if skip_types {
            if is_inline_data_fragment {
                Self {
                    imported_types: "ReaderInlineDataFragment",
                    ast_type: "ReaderInlineDataFragment",
                    exported_type: None,
                }
            } else {
                Self {
                    imported_types: "ReaderFragment",
                    ast_type: "ReaderFragment",
                    exported_type: None,
                }
            }
        } else if is_inline_data_fragment {
            Self {
                imported_types: "InlineFragment, ReaderInlineDataFragment",
                ast_type: "ReaderInlineDataFragment",
                exported_type: Some(format!(
                    "InlineFragment<\n  {name}$fragmentType,\n  {name}$data,\n>",
                    name = fragment.name.item
                )),
            }
        } else if let Some(refetchable_metadata) = RefetchableMetadata::find(&fragment.directives) {
            Self {
                imported_types: "ReaderFragment, RefetchableFragment",
                ast_type: "ReaderFragment",
                exported_type: Some(format!(
                    "RefetchableFragment<\n  {name}$fragmentType,\n  {name}$data,\n  {refetchable_name}$variables,\n>",
                    name = fragment.name.item,
                    refetchable_name = refetchable_metadata.operation_name
                )),
            }
        } else if is_updatable_fragment {
            Self {
                imported_types: "UpdatableFragment, ReaderFragment",
                ast_type: "ReaderFragment",
                exported_type: Some(format!(
                    "UpdatableFragment<\n  {name}$fragmentType,\n  {name}$data,\n>",
                    name = fragment.name.item
                )),
            }
        } else {
            Self {
                imported_types: "Fragment, ReaderFragment",
                ast_type: "ReaderFragment",
                exported_type: Some(format!(
                    "Fragment<\n  {name}$fragmentType,\n  {name}$data,\n>",
                    name = fragment.name.item
                )),
            }
        }
    }
}


/*
 * 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::fmt::Write as _;
use std::fs::create_dir_all;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::path::PathBuf;
use std::sync::atomic::AtomicUsize;
use std::sync::Mutex;

use common::sync::Ordering::Acquire;
use dashmap::DashSet;
use log::info;
use serde::Serialize;
use serde::Serializer;

use crate::errors::BuildProjectError;
use crate::errors::Error;

type BuildProjectResult = Result<(), BuildProjectError>;

pub trait ArtifactWriter {
    fn should_write(&self, path: &PathBuf, content: &[u8]) -> Result<bool, BuildProjectError>;
    fn write(&self, path: PathBuf, content: Vec<u8>) -> BuildProjectResult;
    fn remove(&self, path: PathBuf) -> BuildProjectResult;
    fn finalize(&self) -> crate::errors::Result<()>;
}

type SourceControlFn =
    fn(&PathBuf, &Mutex<Vec<PathBuf>>, &Mutex<Vec<PathBuf>>) -> crate::errors::Result<()>;
pub struct ArtifactFileWriter {
    added: Mutex<Vec<PathBuf>>,
    removed: Mutex<Vec<PathBuf>>,
    source_control_fn: Option<SourceControlFn>,
    root_dir: PathBuf,
}

impl Default for ArtifactFileWriter {
    fn default() -> Self {
        Self {
            added: Default::default(),
            removed: Default::default(),
            source_control_fn: None,
            root_dir: Default::default(),
        }
    }
}

impl ArtifactFileWriter {
    pub fn new(source_control_fn: Option<SourceControlFn>, root_dir: PathBuf) -> Self {
        Self {
            added: Default::default(),
            removed: Default::default(),
            source_control_fn,
            root_dir,
        }
    }

    fn write_file(&self, path: &PathBuf, content: &[u8]) -> io::Result<()> {
        let mut should_add = false;
        if !path.exists() {
            should_add = true;
            ensure_file_directory_exists(path)?;
        }

        let mut file = File::create(path)?;
        file.write_all(content)?;
        if should_add {
            self.added.lock().unwrap().push(path.clone());
        }
        Ok(())
    }
}
impl ArtifactWriter for ArtifactFileWriter {
    fn should_write(&self, path: &PathBuf, content: &[u8]) -> Result<bool, BuildProjectError> {
        content_is_different(path, content).map_err(|error| BuildProjectError::WriteFileError {
            file: path.clone(),
            source: error,
        })
    }

    fn write(&self, path: PathBuf, content: Vec<u8>) -> BuildProjectResult {
        self.write_file(&path, &content)
            .map_err(|error| BuildProjectError::WriteFileError {
                file: path,
                source: error,
            })
    }

    fn remove(&self, path: PathBuf) -> BuildProjectResult {
        match std::fs::remove_file(&path) {
            Ok(_) => {
                self.removed.lock().unwrap().push(path);
            }
            _ => info!("tried to delete already deleted file: {:?}", path),
        }
        Ok(())
    }

    fn finalize(&self) -> crate::errors::Result<()> {
        if let Some(source_control_fn) = self.source_control_fn {
            (source_control_fn)(&self.root_dir, &self.added, &self.removed)
        } else {
            Ok(())
        }
    }
}

#[derive(Serialize)]
struct CodegenRecords {
    pub removed: Vec<ArtifactDeletionRecord>,
    pub changed: Vec<ArtifactUpdateRecord>,
}

#[derive(Serialize)]
struct ArtifactDeletionRecord {
    pub path: PathBuf,
}

#[derive(Serialize)]
struct ArtifactUpdateRecord {
    pub path: PathBuf,
    #[serde(serialize_with = "from_utf8")]
    pub data: Vec<u8>,
}

fn from_utf8<S>(slice: &[u8], s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    s.serialize_str(std::str::from_utf8(slice).unwrap())
}

pub struct ArtifactDifferenceWriter {
    codegen_records: Mutex<CodegenRecords>,
    codegen_filepath: PathBuf,
    verify_changes_against_filesystem: bool,
}

impl ArtifactDifferenceWriter {
    pub fn new(
        codegen_filepath: PathBuf,
        verify_changes_against_filesystem: bool,
    ) -> ArtifactDifferenceWriter {
        ArtifactDifferenceWriter {
            codegen_filepath,
            codegen_records: Mutex::new(CodegenRecords {
                changed: Vec::new(),
                removed: Vec::new(),
            }),
            verify_changes_against_filesystem,
        }
    }
}

impl ArtifactWriter for ArtifactDifferenceWriter {
    fn should_write(&self, path: &PathBuf, content: &[u8]) -> Result<bool, BuildProjectError> {
        Ok(!self.verify_changes_against_filesystem
            || content_is_different(path, content).map_err(|error| {
                BuildProjectError::WriteFileError {
                    file: path.clone(),
                    source: error,
                }
            })?)
    }

    fn write(&self, path: PathBuf, content: Vec<u8>) -> BuildProjectResult {
        self.codegen_records
            .lock()
            .unwrap()
            .changed
            .push(ArtifactUpdateRecord {
                path,
                data: content,
            });
        Ok(())
    }

    fn remove(&self, path: PathBuf) -> BuildProjectResult {
        if path.exists() {
            self.codegen_records
                .lock()
                .unwrap()
                .removed
                .push(ArtifactDeletionRecord { path });
        }
        Ok(())
    }

    fn finalize(&self) -> crate::errors::Result<()> {
        (|| {
            let mut file = File::create(&self.codegen_filepath)?;
            file.write_all(serde_json::to_string(&self.codegen_records)?.as_bytes())
        })()
        .map_err(|error| crate::errors::Error::WriteFileError {
            file: self.codegen_filepath.clone(),
            source: error,
        })
    }
}

#[derive(Serialize)]
struct ArtifactUpdateShardedRecord {
    pub path: PathBuf,
    pub index: usize,
}

#[derive(Serialize)]
struct CodegenShardedRecords {
    pub removed: Vec<ArtifactDeletionRecord>,
    pub changed: Vec<ArtifactUpdateShardedRecord>,
}
pub struct ArtifactDifferenceShardedWriter {
    codegen_records: Mutex<CodegenShardedRecords>,
    codegen_filepath: PathBuf,
    codegen_shard_directory: PathBuf,
    verify_changes_against_filesystem: bool,
    codegen_index: AtomicUsize,
}

impl ArtifactDifferenceShardedWriter {
    pub fn new(
        codegen_filepath: PathBuf,
        codegen_shard_directory: PathBuf,
        verify_changes_against_filesystem: bool,
    ) -> Self {
        Self {
            codegen_filepath,
            codegen_records: Mutex::new(CodegenShardedRecords {
                changed: Vec::new(),
                removed: Vec::new(),
            }),
            codegen_shard_directory,
            verify_changes_against_filesystem,
            codegen_index: AtomicUsize::new(0),
        }
    }
}

impl ArtifactWriter for ArtifactDifferenceShardedWriter {
    fn should_write(&self, path: &PathBuf, content: &[u8]) -> Result<bool, BuildProjectError> {
        Ok(!self.verify_changes_against_filesystem
            || content_is_different(path, content).map_err(|error| {
                BuildProjectError::WriteFileError {
                    file: path.clone(),
                    source: error,
                }
            })?)
    }

    fn write(&self, path: PathBuf, content: Vec<u8>) -> BuildProjectResult {
        let index = self.codegen_index.fetch_add(1, Acquire);
        (|| {
            let mut file = File::create(self.codegen_shard_directory.join(index.to_string()))?;
            file.write_all(&content)
        })()
        .map_err(|error| BuildProjectError::WriteFileError {
            file: path.clone(),
            source: error,
        })?;
        self.codegen_records
            .lock()
            .unwrap()
            .changed
            .push(ArtifactUpdateShardedRecord { path, index });
        Ok(())
    }

    fn remove(&self, path: PathBuf) -> BuildProjectResult {
        if path.exists() {
            self.codegen_records
                .lock()
                .unwrap()
                .removed
                .push(ArtifactDeletionRecord { path });
        }
        Ok(())
    }

    fn finalize(&self) -> crate::errors::Result<()> {
        (|| {
            let mut file = File::create(&self.codegen_filepath)?;
            file.write_all(serde_json::to_string(&self.codegen_records)?.as_bytes())
        })()
        .map_err(|error| crate::errors::Error::WriteFileError {
            file: self.codegen_filepath.clone(),
            source: error,
        })
    }
}

fn ensure_file_directory_exists(file_path: &PathBuf) -> io::Result<()> {
    if let Some(file_directory) = file_path.parent() {
        if !file_directory.exists() {
            create_dir_all(file_directory)?;
        }
    }

    Ok(())
}

fn content_is_different(path: &PathBuf, content: &[u8]) -> io::Result<bool> {
    if path.exists() {
        let existing_content = std::fs::read(path)?;
        Ok(existing_content != content)
    } else {
        Ok(true)
    }
}

pub struct NoopArtifactWriter;
impl ArtifactWriter for NoopArtifactWriter {
    fn should_write(&self, _: &PathBuf, _: &[u8]) -> Result<bool, BuildProjectError> {
        Ok(false)
    }

    fn write(&self, _: PathBuf, _: Vec<u8>) -> Result<(), BuildProjectError> {
        Ok(())
    }
    fn remove(&self, _: PathBuf) -> Result<(), BuildProjectError> {
        Ok(())
    }
    fn finalize(&self) -> crate::errors::Result<()> {
        Ok(())
    }
}

#[derive(Default)]
pub struct ArtifactValidationWriter {
    added: DashSet<PathBuf>,
    updated: DashSet<PathBuf>,
    removed: DashSet<PathBuf>,
}

impl ArtifactWriter for ArtifactValidationWriter {
    fn should_write(&self, path: &PathBuf, content: &[u8]) -> Result<bool, BuildProjectError> {
        content_is_different(path, content).map_err(|error| BuildProjectError::WriteFileError {
            file: path.clone(),
            source: error,
        })
    }

    fn write(&self, path: PathBuf, _: Vec<u8>) -> BuildProjectResult {
        if path.exists() {
            self.updated.insert(path);
        } else {
            self.added.insert(path);
        }
        Ok(())
    }

    fn remove(&self, path: PathBuf) -> BuildProjectResult {
        if path.exists() {
            self.removed.insert(path);
        }
        Ok(())
    }

    fn finalize(&self) -> crate::errors::Result<()> {
        let mut output = String::new();
        write_outdated_artifacts(&mut output, "\nOut of date:", &self.updated);
        write_outdated_artifacts(&mut output, "\nMissing:", &self.added);
        write_outdated_artifacts(&mut output, "\nExtra:", &self.removed);

        if output.is_empty() {
            Ok(())
        } else {
            Err(Error::ArtifactsValidationError { error: output })
        }
    }
}

fn write_outdated_artifacts(output: &mut String, title: &str, artifacts: &DashSet<PathBuf>) {
    if !artifacts.is_empty() {
        writeln!(output, "{}", title).unwrap();
        artifacts.iter().for_each(|artifact_path| {
            writeln!(output, " - {:#?}", artifact_path.as_path()).unwrap()
        });
    }
}


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

use common::Diagnostic;
use dependency_analyzer::get_reachable_ir;
use fnv::FnvHashMap;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::OperationDefinitionName;
use graphql_syntax::ExecutableDefinition;
use graphql_text_printer::print_executable_definition_ast;
use md5::Digest;
use md5::Md5;
use schema::SDLSchema;

use super::ProjectAsts;
use crate::config::ProjectConfig;

pub struct BuildIRResult {
    pub ir: Vec<graphql_ir::ExecutableDefinition>,
    pub source_hashes: SourceHashes,
}

/// Map fragments and queries definition names to the md5 of they printed source
pub struct SourceHashes(FnvHashMap<ExecutableDefinitionName, String>);

impl SourceHashes {
    pub fn from_definitions(definitions: &[ExecutableDefinition]) -> Self {
        let mut source_hashes = FnvHashMap::default();
        for ast in definitions {
            if let Some(name) = ast.name() {
                let key = match ast {
                    ExecutableDefinition::Operation(_) => OperationDefinitionName(name).into(),
                    ExecutableDefinition::Fragment(_) => FragmentDefinitionName(name).into(),
                };
                source_hashes.insert(key, md5(&print_executable_definition_ast(ast)));
            }
        }
        Self(source_hashes)
    }

    pub fn get(&self, k: &ExecutableDefinitionName) -> Option<&String> {
        self.0.get(k)
    }
}

pub fn build_ir(
    _project_config: &ProjectConfig,
    project_asts: ProjectAsts,
    schema: &SDLSchema,
    is_incremental_build: bool,
) -> Result<BuildIRResult, Vec<Diagnostic>> {
    let asts = project_asts.definitions;
    let source_hashes = SourceHashes::from_definitions(&asts);
    let ir = graphql_ir::build_ir_in_relay_mode(schema, &asts)?;
    if is_incremental_build {
        let affected_ir = get_reachable_ir(
            ir,
            project_asts.base_definition_names,
            project_asts.changed_names,
            schema,
        );
        Ok(BuildIRResult {
            ir: affected_ir,
            source_hashes,
        })
    } else {
        Ok(BuildIRResult { ir, source_hashes })
    }
}

fn md5(data: &str) -> String {
    let mut md5 = Md5::new();
    md5.update(data);
    hex::encode(md5.finalize())
}


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

use common::DiagnosticsResult;
use docblock_syntax::DocblockAST;
use fnv::FnvHashMap;
use graphql_syntax::ExecutableDefinition;
use relay_docblock::extend_schema_with_resolver_type_system_definition;
use schema::SDLSchema;

use crate::compiler_state::CompilerState;
use crate::compiler_state::ProjectName;
use crate::config::ProjectConfig;
use crate::docblocks::build_schema_documents_from_docblocks;
use crate::docblocks::parse_docblock_asts_from_sources;
use crate::GraphQLAsts;

pub(crate) fn extend_schema_with_resolvers(
    schema: &mut SDLSchema,
    compiler_state: &CompilerState,
    project_config: &ProjectConfig,
    graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
) -> DiagnosticsResult<()> {
    let ResolverSchemaDocuments {
        type_asts,
        field_asts_and_definitions,
    } = extract_schema_documents_for_resolvers(compiler_state, project_config, graphql_asts_map)?;

    extend_schema_with_types(schema, project_config, type_asts)?;
    extend_schema_with_fields(schema, project_config, field_asts_and_definitions)?;

    Ok(())
}

fn extend_schema_with_types(
    schema: &mut SDLSchema,
    project_config: &ProjectConfig,
    type_asts: TypeAsts,
) -> DiagnosticsResult<()> {
    let type_definitions =
        build_schema_documents_from_docblocks(&type_asts.0, project_config, schema, None)?;

    for schema_document in type_definitions {
        for definition in schema_document.definitions {
            extend_schema_with_resolver_type_system_definition(
                definition,
                schema,
                schema_document.location,
            )?;
        }
    }

    Ok(())
}

fn extend_schema_with_fields<'a>(
    schema: &mut SDLSchema,
    project_config: &ProjectConfig,
    field_asts_and_definitions: FieldAstsAndDefinitions<'a>,
) -> DiagnosticsResult<()> {
    let mut field_definitions = vec![];
    for (asts, definitions) in field_asts_and_definitions.0 {
        field_definitions.extend(build_schema_documents_from_docblocks(
            &asts,
            project_config,
            schema,
            definitions,
        )?);
    }

    for schema_document in field_definitions {
        for definition in schema_document.definitions {
            extend_schema_with_resolver_type_system_definition(
                definition,
                schema,
                schema_document.location,
            )?;
        }
    }

    Ok(())
}

struct ResolverSchemaDocuments<'a> {
    type_asts: TypeAsts,
    field_asts_and_definitions: FieldAstsAndDefinitions<'a>,
}
struct TypeAsts(Vec<DocblockAST>);
struct FieldAstsAndDefinitions<'a>(Vec<(Vec<DocblockAST>, Option<&'a Vec<ExecutableDefinition>>)>);

fn extract_schema_documents_for_resolvers<'a>(
    compiler_state: &'a CompilerState,
    project_config: &'a ProjectConfig,
    graphql_asts_map: &'a FnvHashMap<ProjectName, GraphQLAsts>,
) -> DiagnosticsResult<ResolverSchemaDocuments<'a>> {
    let mut projects = vec![project_config.name];
    projects.extend(project_config.base);

    let docblock_ast_sources = projects.iter().map(|project_name| {
        (
            compiler_state.docblocks.get(project_name),
            graphql_asts_map.get(project_name),
        )
    });

    let mut errors = vec![];
    let mut type_asts = vec![];
    let mut field_asts_and_definitions = vec![];

    for docblock_ast in docblock_ast_sources {
        if let (Some(docblocks), Some(graphql_asts)) = docblock_ast {
            for (file_path, docblock_sources) in &docblocks.get_all() {
                match parse_docblock_asts_from_sources(file_path, docblock_sources) {
                    Ok(result) => {
                        // Type resolvers should not rely on any fragments
                        // @rootFragment is not supported for them, so
                        // we don't need to extract any fragments from the `file_path`
                        type_asts.extend(result.types);

                        // But for fields, we may need to validate the correctness
                        // of the @rootFragment.
                        // And here we're reading GraphQL asts for the file,
                        // and keeping them together with Docblock ASTs
                        if !result.fields.is_empty() {
                            field_asts_and_definitions.push((
                                result.fields,
                                graphql_asts.get_executable_definitions_for_file(file_path),
                            ));
                        }
                    }
                    Err(err) => errors.extend(err),
                }
            }
        } else {
            panic!("Expected to have access to AST and docblock sources.");
        }
    }
    if errors.is_empty() {
        Ok(ResolverSchemaDocuments {
            type_asts: TypeAsts(type_asts),
            field_asts_and_definitions: FieldAstsAndDefinitions(field_asts_and_definitions),
        })
    } else {
        Err(errors)
    }
}


/*
 * 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::DiagnosticsResult;
use fnv::FnvHashMap;
use schema::SDLSchema;

use super::build_resolvers_schema::extend_schema_with_resolvers;
use crate::compiler_state::CompilerState;
use crate::compiler_state::ProjectName;
use crate::config::ProjectConfig;
use crate::GraphQLAsts;

pub fn build_schema(
    compiler_state: &CompilerState,
    project_config: &ProjectConfig,
    graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
) -> DiagnosticsResult<Arc<SDLSchema>> {
    let schema = compiler_state.schema_cache.get(&project_config.name);
    match schema {
        Some(schema) if !compiler_state.project_has_pending_schema_changes(project_config.name) => {
            Ok(schema.clone())
        }
        _ => {
            let mut extensions = vec![];
            if let Some(project_extensions) = compiler_state.extensions.get(&project_config.name) {
                extensions.extend(project_extensions.get_sources_with_location());
            }
            if let Some(base_project_name) = project_config.base {
                if let Some(base_project_extensions) =
                    compiler_state.extensions.get(&base_project_name)
                {
                    extensions.extend(base_project_extensions.get_sources_with_location());
                }
            }
            let mut schema_sources = Vec::new();
            schema_sources.extend(
                compiler_state.schemas[&project_config.name]
                    .get_sources_with_location()
                    .into_iter()
                    .map(|(schema, location_key)| (schema.as_str(), location_key)),
            );
            let mut schema =
                relay_schema::build_schema_with_extensions(&schema_sources, &extensions)?;

            if project_config.feature_flags.enable_relay_resolver_transform {
                extend_schema_with_resolvers(
                    &mut schema,
                    compiler_state,
                    project_config,
                    graphql_asts_map,
                )?;
            }

            Ok(Arc::new(schema))
        }
    }
}


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

use std::path::PathBuf;
use std::sync::Arc;

use common::NamedItem;
use common::SourceLocationKey;
use fnv::FnvHashMap;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinition;
use graphql_ir::OperationDefinition;
use graphql_text_printer::OperationPrinter;
use graphql_text_printer::PrinterOptions;
use intern::string_key::StringKey;
use intern::Lookup;
use relay_transforms::ClientEdgeGeneratedQueryMetadataDirective;
use relay_transforms::Programs;
use relay_transforms::RawResponseGenerationMode;
use relay_transforms::RefetchableDerivedFromMetadata;
use relay_transforms::SplitOperationMetadata;
use relay_transforms::UPDATABLE_DIRECTIVE;

pub use super::artifact_content::ArtifactContent;
use super::build_ir::SourceHashes;
use crate::config::Config;
use crate::config::ProjectConfig;

/// Represents a generated output artifact.
pub struct Artifact {
    pub source_definition_names: Vec<ExecutableDefinitionName>,
    pub path: PathBuf,
    pub content: ArtifactContent,
    /// The source file responsible for generating this file.
    /// For example: `my_project/Component.react.js`
    pub source_file: SourceLocationKey,
}

pub fn generate_artifacts(
    _config: &Config,
    project_config: &ProjectConfig,
    programs: &Programs,
    source_hashes: Arc<SourceHashes>,
) -> Vec<Artifact> {
    let printer_options = PrinterOptions {
        compact: project_config
            .feature_flags
            .compact_query_text
            .is_fully_enabled(),
        ..Default::default()
    };
    let mut operation_printer = OperationPrinter::new(&programs.operation_text, printer_options);
    return group_operations(programs).into_values().map(|operations| {
            if let Some(normalization) = operations.normalization {
                // We have a normalization AST... so we'll move forward with that
                if let Some(metadata) = SplitOperationMetadata::find(&normalization.directives)
                {
                    // Generate normalization file for SplitOperation
                    let source_file = metadata.location.source_location();
                    let source_hash = metadata.derived_from.and_then(|derived_from| source_hashes.get(&derived_from.into()).cloned());
                    let typegen_operation = if metadata.raw_response_type_generation_mode.is_some() {
                        Some(Arc::clone(normalization))
                    } else {
                        None
                    };

                    return Artifact {
                        source_definition_names: metadata.parent_documents.iter().copied().collect(),
                        path: project_config
                            .path_for_artifact(source_file, normalization.name.item.0),
                        content: ArtifactContent::SplitOperation {
                            normalization_operation: Arc::clone(normalization),
                            typegen_operation,
                            no_optional_fields_in_raw_response_type: matches!(metadata.raw_response_type_generation_mode, Some(RawResponseGenerationMode::AllFieldsRequired)),
                            source_hash,
                        },
                        source_file,
                    }
                } else if let Some(derived_from_metadata) =
                    RefetchableDerivedFromMetadata::find(&normalization.directives)
                {
                    let source_name = derived_from_metadata.0;
                    let source_fragment = programs
                        .source
                        .fragment(source_name)
                        .expect("Expected the source document for the SplitOperation to exist.");
                    let source_hash = source_hashes.get(&source_name.into()).cloned().unwrap();

                    return generate_normalization_artifact(
                        &mut operation_printer,
                        source_name.into(),
                        project_config,
                        &operations,
                        source_hash,
                        source_fragment.name.location.source_location(),
                    )
                } else if let Some(client_edges_directive) =
                    ClientEdgeGeneratedQueryMetadataDirective::find(&normalization.directives)
                {
                    let source_name = client_edges_directive.source_name.item;
                    let source_file = client_edges_directive
                        .source_name
                        .location
                        .source_location();
                    let source_hash = source_hashes.get(&source_name).cloned().unwrap();
                    return generate_normalization_artifact(
                        &mut operation_printer,
                        source_name,
                        project_config,
                        &operations,
                        source_hash,
                        source_file,
                    )
                } else {
                    let source_hash = source_hashes
                        .get(&normalization.name.item.into())
                        .cloned()
                        .unwrap();
                    return generate_normalization_artifact(
                        &mut operation_printer,
                        normalization.name.item.into(),
                        project_config,
                        &operations,
                        source_hash,
                        normalization.name.location.source_location(),
                    )
                }
            } else if let Some(reader) = operations.reader {
                // We don't have a normalization AST, but we do have a reader.
                // Therefore this must be an updatable query in order to continue.
                if reader
                    .directives
                    .named(*UPDATABLE_DIRECTIVE)
                    .is_some()
                {
                    let source_hash = source_hashes
                        .get(&reader.name.item.into())
                        .cloned()
                        .unwrap();
                    return generate_updatable_query_artifact(
                        reader.name.item.into(),
                        project_config,
                        &operations,
                        source_hash,
                        reader.name.location.source_location(),
                    )
                }
            }
            panic!("Expected at least one of an @updatable reader AST, or normalization AST to be present");
        })
        .chain(programs.reader.fragments().map(|reader_fragment| {
            let source_name = if let Some(client_edges_directive) =
                ClientEdgeGeneratedQueryMetadataDirective::find(&reader_fragment.directives)
            {
                client_edges_directive.source_name.item
            } else {
                reader_fragment.name.item.into()
            };

            let source_hash = source_hashes.get(&source_name).cloned();
            let source_definition_names = vec![source_name];
            generate_reader_artifact(
                project_config,
                programs,
                reader_fragment,
                source_hash,
                source_definition_names,
            )
        }))
        .collect();
}

fn generate_normalization_artifact(
    operation_printer: &mut OperationPrinter<'_>,
    source_definition_name: ExecutableDefinitionName,
    project_config: &ProjectConfig,
    operations: &OperationGroup<'_>,
    source_hash: String,
    source_file: SourceLocationKey,
) -> Artifact {
    let text = operations
        .operation_text
        .map(|operation| operation_printer.print(operation));

    let normalization = operations
        .normalization
        .expect("Operations must have a normalization entry.");

    Artifact {
        source_definition_names: vec![source_definition_name],
        path: project_config.path_for_artifact(source_file, normalization.name.item.0),
        content: ArtifactContent::Operation {
            normalization_operation: Arc::clone(normalization),
            reader_operation: operations.expect_reader(),
            typegen_operation: operations.expect_typegen(),
            source_hash,
            text,
            id_and_text_hash: None,
        },
        source_file: normalization.name.location.source_location(),
    }
}

fn generate_updatable_query_artifact(
    source_definition_name: ExecutableDefinitionName,
    project_config: &ProjectConfig,
    operations: &OperationGroup<'_>,
    source_hash: String,
    source_file: SourceLocationKey,
) -> Artifact {
    let reader = operations
        .reader
        .expect("Updatable operations must have a reader entry.");

    Artifact {
        source_definition_names: vec![source_definition_name],
        path: project_config.path_for_artifact(source_file, reader.name.item.0),
        content: ArtifactContent::UpdatableQuery {
            reader_operation: operations.expect_reader(),
            typegen_operation: operations.expect_typegen(),
            source_hash,
        },
        source_file: reader.name.location.source_location(),
    }
}

fn generate_reader_artifact(
    project_config: &ProjectConfig,
    programs: &Programs,
    reader_fragment: &Arc<FragmentDefinition>,
    source_hash: Option<String>,
    source_definition_names: Vec<ExecutableDefinitionName>,
) -> Artifact {
    let name = reader_fragment.name.item;
    let typegen_fragment = programs
        .typegen
        .fragment(name)
        .expect("a type fragment should be generated for this fragment");
    Artifact {
        source_definition_names,
        path: project_config
            .path_for_artifact(reader_fragment.name.location.source_location(), name.0),
        content: ArtifactContent::Fragment {
            reader_fragment: Arc::clone(reader_fragment),
            typegen_fragment: Arc::clone(typegen_fragment),
            source_hash,
        },
        source_file: reader_fragment.name.location.source_location(),
    }
}

/// Operation with the same name from different `Program`s.
struct OperationGroup<'a> {
    normalization: Option<&'a Arc<OperationDefinition>>,
    operation_text: Option<&'a OperationDefinition>,
    reader: Option<&'a Arc<OperationDefinition>>,
    typegen: Option<&'a Arc<OperationDefinition>>,
}

impl<'a> OperationGroup<'a> {
    fn new() -> Self {
        OperationGroup {
            normalization: None,
            operation_text: None,
            reader: None,
            typegen: None,
        }
    }

    fn expect_reader(&self) -> Arc<OperationDefinition> {
        let normal_name = self
            .normalization
            .map_or("MISSING_ENTRY", |n| n.name.item.0.lookup());

        Arc::clone(
            self.reader.unwrap_or_else(|| {
                panic!("Expected to have a reader operation for `{}`", normal_name)
            }),
        )
    }

    fn expect_typegen(&self) -> Arc<OperationDefinition> {
        let normal_name = self
            .normalization
            .map_or("MISSING_ENTRY", |n| n.name.item.0.lookup());

        Arc::clone(self.typegen.unwrap_or_else(|| {
            panic!("Expected to have a typegen operation for `{}`", normal_name)
        }))
    }
}

/// Groups operations from the given programs by name for efficient access.
/// `Programs::operation(name)` does a linear search, so it's more efficient to
/// group in a batch.
fn group_operations(programs: &Programs) -> FnvHashMap<StringKey, OperationGroup<'_>> {
    let mut grouped_operations: FnvHashMap<StringKey, OperationGroup<'_>> = programs
        .normalization
        .operations
        .iter()
        .map(|normalization_operation| {
            (
                normalization_operation.name.item.0,
                OperationGroup {
                    normalization: Some(normalization_operation),
                    operation_text: None,
                    reader: None,
                    typegen: None,
                },
            )
        })
        .collect();

    for operation in programs.operation_text.operations() {
        grouped_operations
            .entry(operation.name.item.0)
            .or_insert_with(OperationGroup::new)
            .operation_text = Some(operation);
    }
    for operation in programs.reader.operations() {
        grouped_operations
            .entry(operation.name.item.0)
            .or_insert_with(OperationGroup::new)
            .reader = Some(operation);
    }
    for operation in programs.typegen.operations() {
        grouped_operations
            .entry(operation.name.item.0)
            .or_insert_with(OperationGroup::new)
            .typegen = Some(operation);
    }

    grouped_operations
}


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

use super::Artifact;
use super::Programs;
use super::ProjectConfig;

pub type GenerateExtraArtifactsFn =
    Box<dyn Fn(&ProjectConfig, &SDLSchema, &Programs, &[Artifact]) -> Vec<Artifact> + Send + Sync>;