/*
 * 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::hash::Hash;
use std::hash::Hasher;
use std::sync::Arc;

use common::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::FeatureFlag;
use common::FeatureFlags;
use common::Location;
use common::NamedItem;
use common::WithLocation;
use fnv::FnvBuildHasher;
use fnv::FnvHashMap;
use graphql_ir::associated_data_impl;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentDefinitionNameMap;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::TransformedValue;
use graphql_ir::Transformer;
use graphql_ir::Value;
use indexmap::IndexSet;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::Lookup;
use relay_config::ModuleImportConfig;
use schema::FieldID;
use schema::ScalarID;
use schema::Schema;
use schema::Type;
use schema::TypeReference;

use super::validation_message::ValidationMessage;
use crate::defer_stream::DEFER_STREAM_CONSTANTS;
use crate::inline_data_fragment::INLINE_DIRECTIVE_NAME;
use crate::match_::MATCH_CONSTANTS;
use crate::no_inline::attach_no_inline_directives_to_fragments;
use crate::no_inline::validate_required_no_inline_directive;
use crate::util::get_normalization_operation_name;

/// Transform and validate @match and @module
pub fn transform_match(
    program: &Program,
    feature_flags: &FeatureFlags,
    module_import_config: ModuleImportConfig,
) -> DiagnosticsResult<Program> {
    let mut transformer = MatchTransform::new(program, feature_flags, module_import_config);
    let next_program = transformer.transform_program(program);
    if transformer.errors.is_empty() {
        Ok(next_program.replace_or_else(|| program.clone()))
    } else {
        Err(transformer.errors)
    }
}

#[derive(Eq, Clone, Debug)]
struct Path {
    item: StringKey,
    location: Location,
}
impl PartialEq for Path {
    fn eq(&self, other: &Self) -> bool {
        self.item == other.item
    }
}
impl Hash for Path {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.item.hash(state);
    }
}

struct TypeMatch {
    fragment: WithLocation<FragmentDefinitionName>,
    module_directive_name_argument: StringKey,
}
struct Matches {
    match_directive_key_argument: StringKey,
    types: FnvHashMap<Type, TypeMatch>,
}
type MatchesForPath = FnvHashMap<Vec<Path>, Matches>;

pub struct MatchTransform<'program, 'flag> {
    program: &'program Program,
    parent_type: Type,
    document_name: ExecutableDefinitionName,
    match_directive_key_argument: Option<StringKey>,
    errors: Vec<Diagnostic>,
    path: Vec<Path>,
    matches_for_path: MatchesForPath,
    enable_3d_branch_arg_generation: bool,
    no_inline_flag: &'flag FeatureFlag,
    // Stores the fragments that should use @no_inline and their parent document name
    no_inline_fragments: FragmentDefinitionNameMap<Vec<ExecutableDefinitionName>>,
    module_import_config: ModuleImportConfig,
}

impl<'program, 'flag> MatchTransform<'program, 'flag> {
    fn new(
        program: &'program Program,
        feature_flags: &'flag FeatureFlags,
        module_import_config: ModuleImportConfig,
    ) -> Self {
        Self {
            program,
            // Placeholders to make the types non-optional,
            parent_type: Type::Scalar(ScalarID(0)),
            document_name: ExecutableDefinitionName::OperationDefinitionName(
                OperationDefinitionName("".intern()),
            ),
            match_directive_key_argument: None,
            errors: Vec::new(),
            path: Default::default(),
            matches_for_path: Default::default(),
            enable_3d_branch_arg_generation: feature_flags.enable_3d_branch_arg_generation,
            no_inline_flag: &feature_flags.no_inline,
            no_inline_fragments: Default::default(),
            module_import_config,
        }
    }

    fn push_fragment_spread_with_module_selection_err(
        &mut self,
        selection_location: Location,
        match_location: Location,
    ) {
        self.errors.push(
            Diagnostic::error(
                ValidationMessage::InvalidMatchNotAllSelectionsFragmentSpreadWithModule,
                selection_location,
            )
            .annotate("in @match directive", match_location),
        )
    }

    // Validate that `JSDependency` is a server scalar type in the schema
    fn validate_js_module_type(&self, spread_location: Location) -> Result<(), Diagnostic> {
        match self
            .program
            .schema
            .get_type(MATCH_CONSTANTS.js_field_type.0)
        {
            Some(js_module_type) => match js_module_type {
                Type::Scalar(id) => {
                    if self.program.schema.scalar(id).is_extension {
                        Err(Diagnostic::error(
                            ValidationMessage::MissingServerSchemaDefinition {
                                name: MATCH_CONSTANTS.js_field_name,
                            },
                            spread_location,
                        ))
                    } else {
                        Ok(())
                    }
                }
                _ => Err(Diagnostic::error(
                    ValidationMessage::InvalidModuleNonScalarJSField {
                        js_field_type: MATCH_CONSTANTS.js_field_type,
                    },
                    spread_location,
                )),
            },
            None => Err(Diagnostic::error(
                ValidationMessage::MissingServerSchemaDefinition {
                    name: MATCH_CONSTANTS.js_field_name,
                },
                spread_location,
            )),
        }
    }

    // Get and validate the `js(module: String!, id: String): JSDependency` field in the object
    fn get_js_field_args(
        &self,
        fragment: &FragmentDefinition,
        spread: &FragmentSpread,
    ) -> Result<
        (
            FieldID,
            bool, /* has_js_field_id_arg */
            bool, /* has_js_branch_arg */
        ),
        Diagnostic,
    > {
        match fragment.type_condition {
            Type::Object(id) => {
                let object = self.program.schema.object(id);
                let js_field_id = object.fields.iter().find(|field_id| {
                    let field = self.program.schema.field(**field_id);
                    field.name.item == MATCH_CONSTANTS.js_field_name
                });
                if let Some(js_field_id) = js_field_id {
                    let js_field_id = *js_field_id;
                    let js_field = self.program.schema.field(js_field_id);

                    let js_field_module_arg = js_field
                        .arguments
                        .named(MATCH_CONSTANTS.js_field_module_arg);
                    let is_module_valid = {
                        if let Some(js_field_module_arg) = js_field_module_arg {
                            if let Some(non_list_type) = js_field_module_arg.type_.non_list_type() {
                                self.program.schema.is_string(non_list_type)
                            } else {
                                false
                            }
                        } else {
                            false
                        }
                    };

                    let js_field_id_arg = js_field.arguments.named(MATCH_CONSTANTS.js_field_id_arg);
                    let is_id_valid = {
                        if let Some(js_field_id_arg) = js_field_id_arg {
                            if let Some(id_non_list_type) = js_field_id_arg.type_.non_list_type() {
                                self.program.schema.is_string(id_non_list_type)
                            } else {
                                false
                            }
                        } else {
                            // `id` field is optional
                            true
                        }
                    };

                    let js_field_branch_arg = js_field
                        .arguments
                        .named(MATCH_CONSTANTS.js_field_branch_arg);
                    let is_branch_valid = {
                        if let Some(js_field_branch_arg) = js_field_branch_arg {
                            if let Some(branch_non_list_type) =
                                js_field_branch_arg.type_.non_list_type()
                            {
                                self.program.schema.is_string(branch_non_list_type)
                            } else {
                                false
                            }
                        } else {
                            // `id` field is optional
                            true
                        }
                    };

                    if is_module_valid && is_id_valid && is_branch_valid {
                        return Ok((
                            js_field_id,
                            js_field_id_arg.is_some(),
                            js_field_branch_arg.is_some(),
                        ));
                    }
                }
                Err(Diagnostic::error(
                    ValidationMessage::InvalidModuleInvalidSchemaArguments {
                        spread_name: spread.fragment.item,
                        type_string: self.program.schema.get_type_name(fragment.type_condition),
                        js_field_name: MATCH_CONSTANTS.js_field_name,
                        js_field_module_arg: MATCH_CONSTANTS.js_field_module_arg,
                        js_field_id_arg: MATCH_CONSTANTS.js_field_id_arg,
                        js_field_type: MATCH_CONSTANTS.js_field_type,
                    },
                    spread.fragment.location,
                )
                .annotate("related location", fragment.name.location))
            }
            // @module should only be used on `Object`
            _ => Err(Diagnostic::error(
                ValidationMessage::InvalidModuleNotOnObject {
                    spread_name: spread.fragment.item,
                    type_string: self.program.schema.get_type_name(fragment.type_condition),
                },
                spread.fragment.location,
            )
            .annotate("related location", fragment.name.location)),
        }
    }

    fn validate_transform_fragment_spread(
        &mut self,
        spread: &FragmentSpread,
    ) -> Result<Transformed<Selection>, Diagnostic> {
        let module_directive = spread
            .directives
            .named(MATCH_CONSTANTS.module_directive_name);

        // Only process the fragment spread with @module
        if let Some(module_directive) = module_directive {
            let should_use_no_inline = self.no_inline_flag.is_enabled_for(spread.fragment.item.0);
            // @arguments on the fragment spread is not allowed without @no_inline
            if !should_use_no_inline && !spread.arguments.is_empty() {
                return Err(Diagnostic::error(
                    ValidationMessage::InvalidModuleWithArguments,
                    spread.arguments[0].name.location,
                ));
            }

            // no other directives are allowed
            if spread.directives.len() != 1
                && !(spread.directives.len() == 2
                    && spread
                        .directives
                        .named(DEFER_STREAM_CONSTANTS.defer_name)
                        .is_some())
            {
                // allow @defer and @module in typegen transforms
                return Err(Diagnostic::error(
                    ValidationMessage::InvalidModuleWithAdditionalDirectives {
                        spread_name: spread.fragment.item,
                    },
                    spread.directives[1].name.location,
                ));
            }

            let needs_js_fields = self.module_import_config.dynamic_module_provider.is_none();
            if needs_js_fields {
                self.validate_js_module_type(spread.fragment.location)?;
            }

            let fragment = self.program.fragment(spread.fragment.item).unwrap();

            // Disallow @inline on fragments whose spreads are decorated with @module
            if let Some(inline_data_directive) = fragment.directives.named(*INLINE_DIRECTIVE_NAME) {
                return Err(Diagnostic::error(
                    ValidationMessage::InvalidModuleWithInline,
                    module_directive.name.location,
                )
                .annotate(
                    "@inline directive location",
                    inline_data_directive.name.location,
                ));
            }

            let module_directive_name_argument =
                get_module_directive_name_argument(module_directive, spread.fragment.location)?;

            let parent_name = self.path.last();
            // self.match_directive_key_argument is the value passed to @match(key: "...") that we
            // most recently encountered while traversing the operation, or the document name
            let match_directive_key_argument = self
                .match_directive_key_argument
                .unwrap_or_else(|| self.document_name.into());

            // If this is the first time we are encountering @module at this path, also ensure
            // that we have not previously encountered another @module associated with the same
            // match_directive_key_argument.
            //
            // This ensures that all of the @module's associated with a given @match occur at
            // a single path.
            let matches = match self.matches_for_path.get_mut(&self.path) {
                None => {
                    let existing_match_with_key = self.matches_for_path.values().any(|entry| {
                        entry.match_directive_key_argument == match_directive_key_argument
                    });
                    if existing_match_with_key {
                        let parent_name = parent_name.expect("Cannot have @module selections at multiple paths unless the selections are within fields.");
                        return Err(Diagnostic::error(
                            ValidationMessage::InvalidModuleSelectionWithoutKey {
                                document_name: self.document_name,
                                parent_name: parent_name.item,
                            },
                            parent_name.location,
                        ));
                    }
                    self.matches_for_path.insert(
                        self.path.clone(),
                        Matches {
                            match_directive_key_argument,
                            types: Default::default(),
                        },
                    );
                    self.matches_for_path.get_mut(&self.path).unwrap()
                }
                Some(matches) => matches,
            };

            if match_directive_key_argument != matches.match_directive_key_argument {
                // The user can't override the key locally (per @module),
                // so this is just an internal sanity check
                panic!(
                    "Invalid @module selection: expected all selections at path '{:?} to have the same 'key', got '{}' and '{}'.",
                    &self.path, match_directive_key_argument, matches.match_directive_key_argument
                );
            }

            // For each @module we encounter at this path, we also keep track of the type condition of the
            // spread fragment (i.e. the type from which the fragment makes selections), and ensure that
            // if we have multiple @module directives at the same path and type condition, they are
            // exactly the same: i.e. are on a spread of the same fragment, and have the same
            // value passed to name in @module(name: "...").
            //
            // This is required because, as currently set up, the resulting payload will have fields
            // __typeName, __module_operation_FragmentName and __module_component_FragmentName.
            // If multiple @module directives shared a fragment spread's type condition, but differed:
            // - in which fragment was spread (resulting in a different __module_operation_FragmentName
            //   value in the response), or
            // - in the @module(name: "...") parameter (resulting in a different
            //   __module_component_FragmentName value in the response)
            // The server could nonetheless only return the correct 3D payload for one of the @module's.
            let previous_match_for_type = matches.types.get(&fragment.type_condition);
            if let Some(previous_match_for_type) = previous_match_for_type {
                if previous_match_for_type.fragment.item != spread.fragment.item
                    || previous_match_for_type.module_directive_name_argument
                        != module_directive_name_argument
                {
                    return Err(Diagnostic::error(
                        ValidationMessage::InvalidModuleSelectionMultipleMatches {
                            type_name: self.program.schema.get_type_name(fragment.type_condition),
                            alias_path: self
                                .path
                                .iter()
                                .map(|with_loc| with_loc.item.lookup())
                                .collect::<Vec<&str>>()
                                .join("."),
                        },
                        spread.fragment.location,
                    )
                    .annotate(
                        "related location",
                        previous_match_for_type.fragment.location,
                    ));
                }
            } else {
                matches.types.insert(
                    fragment.type_condition,
                    TypeMatch {
                        fragment: spread.fragment,
                        module_directive_name_argument,
                    },
                );
            }

            // Done validating. Build out the resulting fragment spread.

            let module_id = if self.path.is_empty() {
                self.document_name.into()
            } else {
                let mut str = String::new();
                str.push_str(self.document_name.lookup());
                for path in &self.path {
                    str.push('.');
                    str.push_str(path.item.lookup());
                }
                str.intern()
            };

            let next_spread = Selection::FragmentSpread(Arc::new(FragmentSpread {
                directives: spread
                    .directives
                    .iter()
                    .filter(|directive| {
                        directive.name.item != MATCH_CONSTANTS.module_directive_name
                    })
                    .cloned()
                    .collect(),
                ..spread.clone()
            }));

            if should_use_no_inline {
                self.no_inline_fragments
                    .entry(fragment.name.item)
                    .or_insert_with(std::vec::Vec::new)
                    .push(self.document_name);
            }

            let mut next_selections = vec![next_spread];
            if needs_js_fields {
                let (operation_field, component_field) = self.generate_js_fields(
                    match_directive_key_argument,
                    module_id,
                    module_directive_name_argument,
                    module_directive,
                    fragment,
                    spread,
                )?;
                next_selections.push(operation_field);
                next_selections.push(component_field);
            }

            Ok(Transformed::Replace(Selection::InlineFragment(Arc::new(
                InlineFragment {
                    type_condition: Some(fragment.type_condition),
                    directives: vec![],
                    selections: vec![Selection::InlineFragment(Arc::new(InlineFragment {
                        type_condition: Some(fragment.type_condition),
                        directives: vec![
                            ModuleMetadata {
                                key: match_directive_key_argument,
                                module_id,
                                module_name: module_directive_name_argument,
                                source_document_name: self.document_name,
                                fragment_name: spread.fragment.item,
                                fragment_source_location: self
                                    .program
                                    .fragment(spread.fragment.item)
                                    .unwrap()
                                    .name
                                    .location,
                                location: module_directive.name.location,
                                no_inline: should_use_no_inline,
                            }
                            .into(),
                        ],
                        selections: next_selections,
                        spread_location: Location::generated(),
                    }))],
                    spread_location: Location::generated(),
                },
            ))))
        } else {
            Ok(Transformed::Keep)
        }
    }

    fn generate_js_fields(
        &self,
        match_directive_key_argument: StringKey,
        module_id: StringKey,
        module_directive_name_argument: StringKey,
        module_directive: &Directive,
        fragment: &FragmentDefinition,
        spread: &FragmentSpread,
    ) -> Result<(Selection, Selection), Diagnostic> {
        let (js_field_id, has_js_field_id_arg, has_js_field_branch_arg) =
            self.get_js_field_args(fragment, spread)?;

        let mut component_field_arguments = vec![build_string_literal_argument(
            MATCH_CONSTANTS.js_field_module_arg,
            module_directive_name_argument,
            module_directive.name.location,
        )];

        let mut normalization_name = get_normalization_operation_name(spread.fragment.item.0);
        normalization_name.push_str(".graphql");
        let mut operation_field_arguments = vec![build_string_literal_argument(
            MATCH_CONSTANTS.js_field_module_arg,
            normalization_name.intern(),
            module_directive.name.location,
        )];

        if has_js_field_id_arg {
            let id_arg = build_string_literal_argument(
                MATCH_CONSTANTS.js_field_id_arg,
                module_id,
                module_directive.name.location,
            );
            component_field_arguments.push(id_arg.clone());
            operation_field_arguments.push(id_arg);
        }

        if has_js_field_branch_arg && self.enable_3d_branch_arg_generation {
            let branch_arg = build_string_literal_argument(
                MATCH_CONSTANTS.js_field_branch_arg,
                self.program
                    .schema
                    .as_ref()
                    .get_type_name(fragment.type_condition),
                module_directive.name.location,
            );
            component_field_arguments.push(branch_arg.clone());
            operation_field_arguments.push(branch_arg);
        }

        let component_field = Selection::ScalarField(Arc::new(ScalarField {
            alias: Some(WithLocation::new(
                module_directive.name.location,
                format!("__module_component_{}", match_directive_key_argument).intern(),
            )),
            definition: WithLocation::new(module_directive.name.location, js_field_id),
            arguments: component_field_arguments,
            directives: Default::default(),
        }));

        let operation_field = Selection::ScalarField(Arc::new(ScalarField {
            alias: Some(WithLocation::new(
                module_directive.name.location,
                format!("__module_operation_{}", match_directive_key_argument).intern(),
            )),
            definition: WithLocation::new(module_directive.name.location, js_field_id),
            arguments: operation_field_arguments,
            directives: Default::default(),
        }));
        Ok((operation_field, component_field))
    }

    fn validate_transform_linked_field_with_match_directive(
        &mut self,
        field: &LinkedField,
        match_directive: &Directive,
    ) -> Result<Transformed<Selection>, Diagnostic> {
        // Validate and keep track of the module key
        let field_definition = self.program.schema.field(field.definition.item);
        let key_arg = match_directive.arguments.named(MATCH_CONSTANTS.key_arg);
        if let Some(arg) = key_arg {
            let str = arg.value.item.expect_constant().unwrap_string();

            if str.lookup().starts_with(self.document_name.lookup()) {
                self.match_directive_key_argument = Some(str);
            } else {
                return Err(Diagnostic::error(
                    ValidationMessage::InvalidMatchKeyArgument {
                        document_name: self.document_name,
                    },
                    arg.value.location,
                ));
            }
        }

        let previous_parent_type = self.parent_type;
        self.parent_type = field_definition.type_.inner();
        self.path.push(Path {
            location: field.definition.location,
            item: field.alias_or_name(&self.program.schema),
        });
        let next_selections = self.transform_selections(&field.selections);
        self.path.pop();
        self.parent_type = previous_parent_type;

        // The linked field definition should have: 'supported: [String]'
        let supported_arg_definition = field_definition
            .arguments
            .named(MATCH_CONSTANTS.supported_arg);
        match supported_arg_definition {
            None => {
                if key_arg.is_none() {
                    return Err(Diagnostic::error(
                        ValidationMessage::InvalidMatchWithNoSupportedArgument,
                        match_directive.name.location,
                    ));
                }
                return Ok(if let TransformedValue::Keep = next_selections {
                    Transformed::Keep
                } else {
                    Transformed::Replace(Selection::LinkedField(Arc::new(LinkedField {
                        alias: field.alias,
                        definition: field.definition,
                        arguments: field.arguments.clone(),
                        directives: field.directives.clone(),
                        selections: next_selections.replace_or_else(|| field.selections.clone()),
                    })))
                });
            }
            Some(supported_arg_definition) => {
                let is_supported_string = {
                    if let TypeReference::List(of) = supported_arg_definition.type_.nullable_type()
                    {
                        if let TypeReference::Named(of) = of.nullable_type() {
                            self.program.schema.is_string(*of)
                        } else {
                            false
                        }
                    } else {
                        false
                    }
                };
                if !is_supported_string {
                    return Err(Diagnostic::error(
                        ValidationMessage::InvalidMatchNotOnNonNullListString {
                            field_name: field_definition.name.item,
                        },
                        field.definition.location,
                    ));
                }
            }
        }

        // The linked field should be an abstract type
        if !field_definition.type_.inner().is_abstract_type() {
            return Err(Diagnostic::error(
                ValidationMessage::InvalidMatchNotOnUnionOrInterface {
                    field_name: field_definition.name.item,
                },
                field.definition.location,
            ));
        }

        // The supported arg shouldn't be defined by the user
        let supported_arg = field.arguments.named(MATCH_CONSTANTS.supported_arg);
        if let Some(supported_arg) = supported_arg {
            return Err(Diagnostic::error(
                ValidationMessage::InvalidMatchNoUserSuppliedSupportedArg {
                    supported_arg: MATCH_CONSTANTS.supported_arg,
                },
                supported_arg.name.location,
            ));
        }

        // Track fragment spread types that has @module
        // Validate that there are only `__typename`, and `...spread @module` selections
        let mut seen_types = IndexSet::with_hasher(FnvBuildHasher::default());
        for selection in &field.selections {
            match selection {
                Selection::FragmentSpread(field) => {
                    let has_directive_with_module = field.directives.iter().any(|directive| {
                        directive.name.item == MATCH_CONSTANTS.module_directive_name
                    });
                    if has_directive_with_module {
                        let fragment = self.program.fragment(field.fragment.item).unwrap();
                        seen_types.insert(fragment.type_condition);
                    } else {
                        self.push_fragment_spread_with_module_selection_err(
                            field.fragment.location,
                            match_directive.name.location,
                        );
                    }
                }
                Selection::ScalarField(field) => {
                    if field.definition.item != self.program.schema.typename_field() {
                        self.push_fragment_spread_with_module_selection_err(
                            field.definition.location,
                            match_directive.name.location,
                        );
                    }
                }
                Selection::LinkedField(field) => self
                    .push_fragment_spread_with_module_selection_err(
                        field.definition.location,
                        match_directive.name.location,
                    ),
                // TODO: no location on InlineFragment and Condition yet
                _ => self.push_fragment_spread_with_module_selection_err(
                    field.definition.location,
                    match_directive.name.location,
                ),
            }
        }
        if seen_types.is_empty() {
            return Err(Diagnostic::error(
                ValidationMessage::InvalidMatchNoModuleSelection,
                match_directive.name.location,
            ));
        }

        let mut next_arguments = field.arguments.clone();
        next_arguments.push(Argument {
            name: WithLocation::new(field.definition.location, MATCH_CONSTANTS.supported_arg),
            value: WithLocation::new(
                field.definition.location,
                Value::Constant(ConstantValue::List(
                    seen_types
                        .into_iter()
                        .map(|type_| {
                            ConstantValue::String(self.program.schema.get_type_name(type_))
                        })
                        .collect(),
                )),
            ),
        });
        let mut next_directives = Vec::with_capacity(field.directives.len() - 1);
        for directive in &field.directives {
            if directive.name.item != MATCH_CONSTANTS.match_directive_name {
                next_directives.push(directive.clone());
            }
        }

        Ok(Transformed::Replace(Selection::LinkedField(Arc::new(
            LinkedField {
                alias: field.alias,
                definition: field.definition,
                arguments: next_arguments,
                directives: next_directives,
                selections: next_selections.replace_or_else(|| field.selections.clone()),
            },
        ))))
    }
}

impl Transformer for MatchTransform<'_, '_> {
    const NAME: &'static str = "MatchTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_program(&mut self, program: &Program) -> TransformedValue<Program> {
        let next_program = self.default_transform_program(program);
        if self.no_inline_fragments.is_empty() {
            next_program
        } else {
            if let Err(errors) =
                validate_required_no_inline_directive(&self.no_inline_fragments, program)
            {
                self.errors.extend(errors);
                return next_program;
            }
            let mut next_program = next_program.replace_or_else(|| program.clone());
            attach_no_inline_directives_to_fragments(
                &mut self.no_inline_fragments,
                &mut next_program,
            );
            TransformedValue::Replace(next_program)
        }
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        self.document_name = fragment.name.item.into();
        self.matches_for_path = Default::default();
        self.match_directive_key_argument = None;
        self.parent_type = fragment.type_condition;
        self.path = Default::default();
        self.default_transform_fragment(fragment)
    }

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        self.document_name = operation.name.item.into();
        self.matches_for_path = Default::default();
        self.match_directive_key_argument = None;
        self.parent_type = operation.type_;
        self.path = Default::default();
        self.default_transform_operation(operation)
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        if let Some(type_) = fragment.type_condition {
            let previous_parent_type = self.parent_type;
            self.parent_type = type_;
            let result = self.default_transform_inline_fragment(fragment);
            self.parent_type = previous_parent_type;
            result
        } else {
            self.default_transform_inline_fragment(fragment)
        }
    }

    // Validate `js` field
    fn transform_scalar_field(&mut self, field: &ScalarField) -> Transformed<Selection> {
        let field_definition = self.program.schema.field(field.definition.item);
        if field_definition.name.item == MATCH_CONSTANTS.js_field_name {
            match self
                .program
                .schema
                .get_type(MATCH_CONSTANTS.js_field_type.0)
            {
                None => self.errors.push(Diagnostic::error(
                    ValidationMessage::MissingServerSchemaDefinition {
                        name: MATCH_CONSTANTS.js_field_name,
                    },
                    field.definition.location,
                )),
                Some(js_module_type) => {
                    if matches!(js_module_type, Type::Scalar(_))
                        && field_definition.type_.inner() == js_module_type
                    {
                        self.errors.push(Diagnostic::error(
                            ValidationMessage::InvalidDirectUseOfJSField {
                                field_name: MATCH_CONSTANTS.js_field_name,
                            },
                            field.definition.location,
                        ));
                    }
                }
            }
        }
        Transformed::Keep
    }

    // Validate and transform `@match`
    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let match_directive = field.directives.named(MATCH_CONSTANTS.match_directive_name);
        let match_directive_key_argument = self.match_directive_key_argument;
        self.match_directive_key_argument = None;

        // Only process fields with @match
        let result = if let Some(match_directive) = match_directive {
            match self.validate_transform_linked_field_with_match_directive(field, match_directive)
            {
                Ok(result) => result,
                Err(error) => {
                    self.errors.push(error);
                    Transformed::Keep
                }
            }
        } else {
            let previous_parent_type = self.parent_type;
            self.parent_type = self
                .program
                .schema
                .field(field.definition.item)
                .type_
                .inner();
            self.path.push(Path {
                location: field.definition.location,
                item: field.alias_or_name(&self.program.schema),
            });
            let result = self.default_transform_linked_field(field);
            self.path.pop();
            self.parent_type = previous_parent_type;
            result
        };
        self.match_directive_key_argument = match_directive_key_argument;
        result
    }

    // validate and transform `@module` into a custom directive for codegen
    fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed<Selection> {
        match self.validate_transform_fragment_spread(spread) {
            Ok(result) => result,
            Err(err) => {
                self.errors.push(err);
                Transformed::Keep
            }
        }
    }
}

fn get_module_directive_name_argument(
    module_directive: &Directive,
    spread_location: Location,
) -> Result<StringKey, Diagnostic> {
    let name_arg = module_directive
        .arguments
        .named(MATCH_CONSTANTS.name_arg)
        .ok_or_else(|| {
            Diagnostic::error(ValidationMessage::InvalidModuleNoName, spread_location)
        })?;
    name_arg.value.item.get_string_literal().ok_or_else(|| {
        Diagnostic::error(
            ValidationMessage::InvalidModuleNonLiteralName,
            name_arg.name.location,
        )
    })
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ModuleMetadata {
    pub location: Location,
    pub key: StringKey,
    pub module_id: StringKey,
    pub module_name: StringKey,
    pub source_document_name: ExecutableDefinitionName,
    pub fragment_name: FragmentDefinitionName,
    pub fragment_source_location: Location,
    pub no_inline: bool,
}
associated_data_impl!(ModuleMetadata);

fn build_string_literal_argument(
    name: ArgumentName,
    value: StringKey,
    location: Location,
) -> Argument {
    Argument {
        name: WithLocation::new(location, name),
        value: WithLocation::new(location, Value::Constant(ConstantValue::String(value))),
    }
}


/*
 * 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 constants;
mod hash_supported_argument;
mod match_transform;
mod split_module_import;
mod split_operation_metadata;
mod subscription_transform;
mod validation_message;

pub use constants::MATCH_CONSTANTS;
pub use hash_supported_argument::hash_supported_argument;
pub use match_transform::transform_match;
pub use match_transform::ModuleMetadata;
pub use split_module_import::split_module_import;
pub use split_operation_metadata::RawResponseGenerationMode;
pub use split_operation_metadata::SplitOperationMetadata;
pub use split_operation_metadata::DIRECTIVE_SPLIT_OPERATION;
pub use subscription_transform::transform_subscriptions;


/*
 * 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::WithLocation;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::InlineFragment;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::TransformedValue;
use graphql_ir::Transformer;
use graphql_syntax::OperationKind;
use intern::string_key::Intern;
use intern::string_key::StringKeyMap;
use schema::Schema;

use super::SplitOperationMetadata;
use super::MATCH_CONSTANTS;
use crate::util::get_normalization_operation_name;
use crate::ModuleMetadata;

pub fn split_module_import(
    program: &Program,
    base_fragment_names: &FragmentDefinitionNameSet,
) -> Program {
    let mut transform = SplitModuleImportTransform::new(program, base_fragment_names);
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

pub struct SplitModuleImportTransform<'program, 'base_fragment_names> {
    program: &'program Program,
    split_operations: StringKeyMap<(SplitOperationMetadata, OperationDefinition)>,
    base_fragment_names: &'base_fragment_names FragmentDefinitionNameSet,
}

impl<'program, 'base_fragment_names> SplitModuleImportTransform<'program, 'base_fragment_names> {
    fn new(
        program: &'program Program,
        base_fragment_names: &'base_fragment_names FragmentDefinitionNameSet,
    ) -> Self {
        Self {
            program,
            split_operations: Default::default(),
            base_fragment_names,
        }
    }

    fn inline_module_metadata<'a>(
        &self,
        fragment: &'a InlineFragment,
    ) -> Option<&'a ModuleMetadata> {
        if fragment.directives.len() == 1 {
            if let Some(module_metadata) = ModuleMetadata::find(&fragment.directives) {
                if !module_metadata.no_inline {
                    return Some(module_metadata);
                }
            }
        }
        None
    }
}

impl Transformer for SplitModuleImportTransform<'_, '_> {
    const NAME: &'static str = "SplitModuleImportTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_program(&mut self, program: &Program) -> TransformedValue<Program> {
        for operation in program.operations() {
            self.transform_operation(operation);
        }
        for fragment in program.fragments() {
            self.transform_fragment(fragment);
        }

        if self.split_operations.is_empty() {
            TransformedValue::Keep
        } else {
            let mut next_program = program.clone();
            for (_, (metadata, mut operation)) in self.split_operations.drain() {
                operation.directives.push(metadata.into());
                next_program.insert_operation(Arc::new(operation))
            }
            TransformedValue::Replace(next_program)
        }
    }

    fn transform_inline_fragment(&mut self, fragment: &InlineFragment) -> Transformed<Selection> {
        if let Some(module_metadata) = self.inline_module_metadata(fragment) {
            let parent_type = fragment
                .type_condition
                .expect("Expect the module import inline fragment to have a type");

            // We do not need to to write normalization files for base fragments.
            // This is because when we process the base project, the normalization fragment will
            // be written, and we do not want to emit multiple normalization fragments with
            // the same name. If we did, Haste would complain about a duplicate module definition.
            if self
                .base_fragment_names
                .contains(&module_metadata.fragment_name)
            {
                return self.default_transform_inline_fragment(fragment);
            }

            let normalization_name =
                get_normalization_operation_name(module_metadata.fragment_name.0).intern();
            let schema = &self.program.schema;
            let created_split_operation = self
                .split_operations
                .entry(normalization_name)
                .or_insert_with(|| {
                    // Exclude `__module_operation/__module_component: js` field selections from `SplitOperation`
                    let next_selections = fragment
                        .selections
                        .iter()
                        .filter(|selection| match selection {
                            Selection::ScalarField(field) => {
                                field.alias.is_none()
                                    || schema.field(field.definition.item).name.item
                                        != MATCH_CONSTANTS.js_field_name
                            }
                            _ => true,
                        })
                        .cloned()
                        .collect();
                    (
                        SplitOperationMetadata {
                            derived_from: Some(module_metadata.fragment_name),
                            location: module_metadata.fragment_source_location,
                            parent_documents: Default::default(),
                            raw_response_type_generation_mode: None,
                        },
                        OperationDefinition {
                            name: WithLocation::new(
                                module_metadata.location,
                                OperationDefinitionName(normalization_name),
                            ),
                            type_: parent_type,
                            variable_definitions: vec![],
                            directives: vec![],
                            selections: next_selections,
                            kind: OperationKind::Query,
                        },
                    )
                });
            created_split_operation
                .0
                .parent_documents
                .insert(module_metadata.source_document_name);
        }
        self.default_transform_inline_fragment(fragment)
    }
}


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

use common::ArgumentName;
use common::DirectiveName;
use common::Location;
use graphql_ir::associated_data_impl;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinitionName;
use intern::string_key::Intern;
use lazy_static::lazy_static;
use rustc_hash::FxHashSet;

lazy_static! {
    pub static ref DIRECTIVE_SPLIT_OPERATION: DirectiveName =
        SplitOperationMetadata::directive_name();
    static ref ARG_DERIVED_FROM: ArgumentName = ArgumentName("derivedFrom".intern());
    static ref ARG_PARENT_DOCUMENTS: ArgumentName = ArgumentName("parentDocuments".intern());
    static ref ARG_RAW_RESPONSE_TYPE: ArgumentName = ArgumentName("rawResponseType".intern());
    static ref ARG_RAW_RESPONSE_TYPE_STRICT: ArgumentName =
        ArgumentName("rawResponseTypeStrict".intern());
}

/// The split operation metadata directive indicates that an operation was split
/// out by the compiler from a parent normalization file.
///
/// In the following GraphQL code, we would generate a `F$normalization.graphql.js`
/// file. For that `SplitOperation`:
/// - `derived_from` is `F`.
/// - `parent_documents` are `Q1` and `F2`.
///
/// ```graphql
/// fragment F on Query {
///   # ...
/// }
/// query Q1 {
///   ...F @module
/// }
/// query Q2 {
///   ...F
/// }
/// fragment F2 on Query {
///   ...F @module
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SplitOperationMetadata {
    /// Name of the fragment that this split operation represents. This is used
    /// to determine the name of the generated artifact.
    pub derived_from: Option<FragmentDefinitionName>,

    /// Location of the source file for this split operation
    pub location: Location,

    /// The names of the fragments and operations that included this fragment.
    /// They are the reason this split operation exists. If they are all removed,
    /// this file also needs to be removed.
    pub parent_documents: FxHashSet<ExecutableDefinitionName>,

    /// Should a @raw_response_type style type be generated.
    pub raw_response_type_generation_mode: Option<RawResponseGenerationMode>,
}

// associated_data_impl requires Hash, and we cannot derive Hash since HashSet iteration
// order is unstable.
impl Hash for SplitOperationMetadata {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        // We should only have a single SplitOperationMetadata per unique derived_from or
        // location. In addition, Hash implementations only requires this property.
        //   k1 == k2 -> hash(k1) == hash(k2)
        self.derived_from.hash(state);
        self.location.hash(state);
    }
}

associated_data_impl!(SplitOperationMetadata);

/// For split operations. This will define the mode for the type generation of RawResponse type
/// With Relay resolvers we may need to require all keys to be presented in the response shape.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RawResponseGenerationMode {
    /// All keys are optional
    AllFieldsOptional,
    /// All keys in the raw response type are required
    /// (values can still be optional, based on the schema type)
    AllFieldsRequired,
}


/*
 * 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 common::Location;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use graphql_ir::Value;
use graphql_syntax::OperationKind;
use intern::string_key::Intern;
use intern::Lookup;
use schema::FieldID;
use schema::Schema;
use schema::Type;
use schema::TypeReference;

use crate::match_::MATCH_CONSTANTS;
use crate::util::get_normalization_operation_name;
use crate::ModuleMetadata;

pub fn transform_subscriptions(program: &Program) -> DiagnosticsResult<Program> {
    let mut transformer = SubscriptionTransform::new(program);
    let next_program = transformer.transform_program(program);
    Ok(next_program.replace_or_else(|| program.clone()))
}

pub struct SubscriptionTransform<'program> {
    program: &'program Program,
}

impl<'program> SubscriptionTransform<'program> {
    fn new(program: &'program Program) -> Self {
        Self { program }
    }

    /// Validate that the given operation meets the following conditions:
    /// - Is a subscription, which
    /// - has a single selection, which
    /// - is a linked field, whose type
    /// - is an object with a js: JSDependency field; and
    /// - the linked field has a single selection, which is a fragment spread
    /// These restrictions may be loosened over time.
    ///
    /// Return Some(Vec<ValidFieldResult>) if the operation is valid
    /// (i.e. if each field is valid), or None otherwise.
    fn validate_operation<'operation>(
        &self,
        operation: &'operation OperationDefinition,
    ) -> Option<Vec<ValidFieldResult<'operation>>> {
        if matches!(operation.kind, OperationKind::Subscription) && operation.selections.len() == 1
        {
            operation
                .selections
                .iter()
                .map(|selection| self.validate_selection(selection))
                .collect::<Option<Vec<_>>>()
        } else {
            None
        }
    }

    fn validate_selection<'operation>(
        &self,
        selection: &'operation Selection,
    ) -> Option<ValidFieldResult<'operation>> {
        match selection {
            Selection::LinkedField(linked_field) => {
                let field_id = linked_field.definition.item;
                let field = self.program.schema.field(field_id);
                let type_ = field.type_.inner();

                let fragment_spread = self.validate_linked_field(linked_field)?;

                match type_ {
                    Type::Object(object_id) => {
                        let object = self.program.schema.object(object_id);
                        for object_field_id in object.fields.iter() {
                            let object_field = self.program.schema.field(*object_field_id);
                            if object_field.name.item == MATCH_CONSTANTS.js_field_name {
                                // if we find a js field, it must be valid
                                return self.is_valid_js_dependency(&object_field.type_).then(
                                    || ValidFieldResult {
                                        linked_field,
                                        js_field_id: *object_field_id,
                                        fragment_spread,
                                    },
                                );
                            }
                        }
                        None
                    }
                    _ => None,
                }
            }
            _ => None,
        }
    }

    /// The linked field must contain only a single fragment spread.
    fn validate_linked_field<'operation>(
        &self,
        linked_field: &'operation Arc<LinkedField>,
    ) -> Option<&'operation FragmentSpread> {
        if linked_field.selections.len() != 1 {
            return None;
        }
        let first_item = linked_field.selections.get(0).unwrap();
        match first_item {
            Selection::FragmentSpread(fragment_spread) => Some(fragment_spread),
            _ => None,
        }
    }

    fn is_valid_js_dependency(&self, type_: &TypeReference<Type>) -> bool {
        match type_ {
            TypeReference::Named(Type::Scalar(scalar_id)) => {
                let scalar = self.program.schema.scalar(*scalar_id);
                scalar.name.item == MATCH_CONSTANTS.js_field_type && !scalar.is_extension
            }
            _ => false,
        }
    }

    fn get_replacement_selection<'operation>(
        &self,
        operation: &OperationDefinition,
        valid_result: ValidFieldResult<'operation>,
    ) -> Selection {
        let ValidFieldResult {
            linked_field,
            js_field_id,
            fragment_spread,
        } = valid_result;
        let location = linked_field.definition.location;
        let operation_name_with_suffix =
            format!("{}__subscription", operation.name.item.0.lookup());
        let normalization_operation_name = format!(
            "{}.graphql",
            get_normalization_operation_name(fragment_spread.fragment.item.0)
        )
        .intern();

        let mut selections = linked_field.selections.clone();
        selections.push(Selection::ScalarField(Arc::new(ScalarField {
            alias: Some(WithLocation::new(
                location,
                format!("__module_operation_{}", operation_name_with_suffix).intern(),
            )),
            definition: WithLocation::new(location, js_field_id),
            arguments: vec![Argument {
                name: WithLocation::new(location, MATCH_CONSTANTS.js_field_module_arg),
                value: WithLocation::new(
                    location,
                    Value::Constant(ConstantValue::String(normalization_operation_name)),
                ),
            }],
            directives: vec![],
        })));

        let type_condition = Some(
            (&self.program.schema)
                .field(linked_field.definition.item)
                .type_
                .inner(),
        );
        let name_location = linked_field.alias_or_name_location();

        let selections = vec![Selection::InlineFragment(Arc::new(InlineFragment {
            type_condition,
            directives: vec![],
            selections: vec![Selection::InlineFragment(Arc::new(InlineFragment {
                type_condition,
                directives: vec![
                    ModuleMetadata {
                        key: operation_name_with_suffix.intern(),
                        module_id: format!(
                            "{}.{}",
                            operation.name.item.0,
                            linked_field.alias_or_name(&self.program.schema).lookup()
                        )
                        .intern(),
                        module_name: normalization_operation_name,
                        source_document_name: operation.name.item.into(),
                        fragment_name: fragment_spread.fragment.item,
                        fragment_source_location: self
                            .program
                            .fragment(fragment_spread.fragment.item)
                            .unwrap()
                            .name
                            .location,
                        location: name_location,
                        no_inline: false,
                    }
                    .into(),
                ],
                selections,
                spread_location: Location::generated(),
            }))],
            spread_location: Location::generated(),
        }))];

        Selection::LinkedField(Arc::new(LinkedField {
            alias: linked_field.alias,
            definition: linked_field.definition,
            arguments: linked_field.arguments.clone(),
            directives: linked_field.directives.clone(),
            selections,
        }))
    }
}

/// If an operation is valid, each field will be a LinkedField pointing to an
/// Object. The successful result of validating an operation will be a vector
/// where this information is extracted from each field, in the form of a
/// ValidFieldResult.
struct ValidFieldResult<'operation> {
    /// a reference to the valid linked field
    linked_field: &'operation Arc<LinkedField>,
    /// the FieldID of the js: JSDependency field in the schema
    js_field_id: FieldID,
    /// a reference to the contained fragment spread
    fragment_spread: &'operation FragmentSpread,
}

impl Transformer for SubscriptionTransform<'_> {
    const NAME: &'static str = "SubscriptionTransform";
    const VISIT_ARGUMENTS: bool = false;
    const VISIT_DIRECTIVES: bool = false;

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        if let Some(valid_results) = self.validate_operation(operation) {
            let selections = valid_results
                .into_iter()
                .map(|valid_result| self.get_replacement_selection(operation, valid_result))
                .collect::<Vec<_>>();

            Transformed::Replace(OperationDefinition {
                selections,
                ..operation.clone()
            })
        } else {
            Transformed::Keep
        }
    }

    fn transform_fragment(
        &mut self,
        _fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        Transformed::Keep
    }
}


/*
 * 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::ArgumentName;
use common::ScalarName;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinitionName;
use intern::string_key::StringKey;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ValidationMessage {
    #[error("Invalid @match selection: all selections should be fragment spreads with @module.")]
    InvalidMatchNotAllSelectionsFragmentSpreadWithModule,

    #[error("'{name}' should be defined on the server schema.")]
    MissingServerSchemaDefinition { name: StringKey },

    #[error("Using @module requires the schema to define a scalar '{js_field_type}' type.")]
    InvalidModuleNonScalarJSField { js_field_type: ScalarName },

    #[error(
        "@module used on invalid fragment spread '...{spread_name}'. @module requires the fragment type '{type_string}' to have a '{js_field_name}({js_field_module_arg}: String!, {js_field_id_arg}: String): {js_field_type}' field (your schema may choose to omit the 'id'  argument but if present it must accept a 'String')."
    )]
    InvalidModuleInvalidSchemaArguments {
        spread_name: FragmentDefinitionName,
        type_string: StringKey,
        js_field_name: StringKey,
        js_field_module_arg: ArgumentName,
        js_field_id_arg: ArgumentName,
        js_field_type: ScalarName,
    },

    #[error(
        "@module used on invalid fragment spread '...{spread_name}'. @module may only be used with fragments on a concrete (object) type, but the fragment has abstract type '{type_string}'."
    )]
    InvalidModuleNotOnObject {
        spread_name: FragmentDefinitionName,
        type_string: StringKey,
    },

    #[error("@module does not support @arguments.")]
    InvalidModuleWithArguments,

    #[error(
        "@module used on invalid fragment spread '...{spread_name}'. @module may not have additional directives."
    )]
    InvalidModuleWithAdditionalDirectives { spread_name: FragmentDefinitionName },

    #[error("@module does not support @inline fragments.")]
    InvalidModuleWithInline,

    #[error("Expected the 'name' argument of @module to be a literal string.")]
    InvalidModuleNonLiteralName,

    #[error("Expected the 'name' argument to be defined.")]
    InvalidModuleNoName,

    #[error(
        "Invalid @module selection: documents with multiple fields containing 3D selections must specify a unique 'key' value for each field: use '{parent_name} @match(key: \"{document_name}_<localName>\")'."
    )]
    InvalidModuleSelectionWithoutKey {
        document_name: ExecutableDefinitionName,
        parent_name: StringKey,
    },

    #[error(
        "Invalid @module selection: concrete type '{type_name}' was matched multiple times at path '{alias_path}' but with a different fragment or module name."
    )]
    InvalidModuleSelectionMultipleMatches {
        type_name: StringKey,
        alias_path: String,
    },

    #[error("Direct use of the '{field_name}' field is not allowed, use '@match/@module instead.")]
    InvalidDirectUseOfJSField { field_name: StringKey },

    #[error(
        "Expected the 'key' argument of @match to be a literal string starting with the document name, e.g. '{document_name}_<localName>'."
    )]
    InvalidMatchKeyArgument {
        document_name: ExecutableDefinitionName,
    },

    #[error(
        "@match used on incompatible field '{field_name}'. @match may only be used with fields that accept a 'supported: [String]' argument."
    )]
    InvalidMatchNotOnNonNullListString { field_name: StringKey },

    #[error(
        "@match used on incompatible field '{field_name}'. @match may only be used with fields that return a union or interface."
    )]
    InvalidMatchNotOnUnionOrInterface { field_name: StringKey },

    #[error(
        "Invalid @match selection: the '{supported_arg}' argument is automatically added and cannot be supplied explicitly.'"
    )]
    InvalidMatchNoUserSuppliedSupportedArg { supported_arg: ArgumentName },

    #[error(
        "Invalid @match selection: expected at least one @module selection. Remove @match or add a '...Fragment @module()' selection."
    )]
    InvalidMatchNoModuleSelection,

    #[error(
        "@match on a field without the `supported` argument is a no-op, please remove the `@match`."
    )]
    InvalidMatchWithNoSupportedArgument,
}


/*
 * 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::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinitionNameMap;
use graphql_ir::FragmentSpread;
use graphql_ir::Program;
use graphql_ir::Validator;
use graphql_ir::Value;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;

use crate::ValidationMessage;
use crate::MATCH_CONSTANTS;

lazy_static! {
    pub static ref NO_INLINE_DIRECTIVE_NAME: DirectiveName = DirectiveName("no_inline".intern());
    pub static ref PARENT_DOCUMENTS_ARG: ArgumentName = ArgumentName("__parentDocuments".intern());
    // Note: this is used as both an ArgumentName and as a DirectiveName
    pub static ref RAW_RESPONSE_TYPE_NAME: StringKey = "raw_response_type".intern();
}

pub fn attach_no_inline_directives_to_fragments(
    no_inline_fragments: &mut FragmentDefinitionNameMap<Vec<ExecutableDefinitionName>>,
    program: &mut Program,
) {
    for (fragment_name, parent_sources) in no_inline_fragments.drain() {
        let fragment = Arc::make_mut(program.fragment_mut(fragment_name).unwrap());

        let no_inline_directive = fragment
            .directives
            .iter_mut()
            .find(|d| d.name.item == *NO_INLINE_DIRECTIVE_NAME);
        if let Some(no_inline_directive) = no_inline_directive {
            let parent_documents_arg = no_inline_directive
                .arguments
                .iter_mut()
                .find(|arg| arg.name.item == *PARENT_DOCUMENTS_ARG);
            if let Some(parent_documents_arg) = parent_documents_arg {
                if let Value::Constant(ConstantValue::List(parent_documents)) =
                    &mut parent_documents_arg.value.item
                {
                    parent_documents.extend(
                        parent_sources
                            .into_iter()
                            .map(|name| ConstantValue::String(name.into())),
                    );
                } else {
                    panic!("Expected parent arguments to be a constant list of String");
                }
            } else {
                no_inline_directive
                    .arguments
                    .push(create_parent_documents_arg(parent_sources));
            }
        } else {
            fragment.directives.push(Directive {
                name: WithLocation::new(fragment.name.location, *NO_INLINE_DIRECTIVE_NAME),
                arguments: vec![create_parent_documents_arg(parent_sources)],
                data: None,
            })
        }
    }
}

pub fn is_raw_response_type_enabled(directive: &Directive) -> bool {
    if let Some(Value::Constant(ConstantValue::Boolean(val))) = directive
        .arguments
        .named(ArgumentName(*RAW_RESPONSE_TYPE_NAME))
        .map(|arg| &arg.value.item)
    {
        *val
    } else {
        false
    }
}

/// If `@no_inline` is added to a fragment by @module or @relay_client_component
/// transform, and the fragment is also used without these directives, manually
/// adding `@no_inline` is required. Because in watch mode, if the path with @module
/// or @relay_client_component isn't changed, `@no_inline` won't get added.
pub fn validate_required_no_inline_directive(
    no_inline_fragments: &FragmentDefinitionNameMap<Vec<ExecutableDefinitionName>>,
    program: &Program,
) -> DiagnosticsResult<()> {
    let mut validator = RequiredNoInlineValidator::new(no_inline_fragments, program);
    validator.validate_program(program)
}

pub(crate) fn create_parent_documents_arg(
    parent_sources: Vec<ExecutableDefinitionName>,
) -> Argument {
    Argument {
        name: WithLocation::generated(*PARENT_DOCUMENTS_ARG),
        value: WithLocation::generated(Value::Constant(ConstantValue::List(
            parent_sources
                .into_iter()
                .map(|executable_definition_name| {
                    ConstantValue::String(executable_definition_name.into())
                })
                .collect(),
        ))),
    }
}

struct RequiredNoInlineValidator<'f, 'p> {
    no_inline_fragments: &'f FragmentDefinitionNameMap<Vec<ExecutableDefinitionName>>,
    program: &'p Program,
}

impl<'f, 'p> RequiredNoInlineValidator<'f, 'p> {
    fn new(
        no_inline_fragments: &'f FragmentDefinitionNameMap<Vec<ExecutableDefinitionName>>,
        program: &'p Program,
    ) -> Self {
        Self {
            no_inline_fragments,
            program,
        }
    }
}

impl<'f, 'p> Validator for RequiredNoInlineValidator<'f, 'p> {
    const NAME: &'static str = "RequiredNoInlineValidator";
    const VALIDATE_ARGUMENTS: bool = false;
    const VALIDATE_DIRECTIVES: bool = false;

    fn validate_fragment_spread(&mut self, spread: &FragmentSpread) -> DiagnosticsResult<()> {
        if !self.no_inline_fragments.contains_key(&spread.fragment.item) {
            return Ok(());
        }
        let fragment = self.program.fragment(spread.fragment.item).unwrap();
        let has_no_inline = fragment
            .directives
            .named(*NO_INLINE_DIRECTIVE_NAME)
            .is_some();
        if has_no_inline {
            return Ok(());
        }
        // If the fragment spread isn't used for @module or @relay_client_component
        // then explicit @no_inline is required.
        if spread.directives.is_empty()
            || !spread
                .directives
                .iter()
                .any(|directive| directive.name.item == MATCH_CONSTANTS.module_directive_name)
        {
            Err(vec![
                Diagnostic::error(
                    ValidationMessage::RequiredExplicitNoInlineDirective {
                        fragment_name: spread.fragment.item,
                    },
                    spread.fragment.location,
                )
                .annotate("fragment definition", fragment.name.location),
            ])
        } else {
            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::sync::Arc;

use common::Diagnostic;
use common::DiagnosticsResult;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::FragmentDefinition;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinitionName;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Value;
use graphql_ir::Variable;
use graphql_ir::VariableDefinition;
use graphql_ir::VariableName;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use relay_config::SchemaConfig;
use schema::Argument as ArgumentDef;
use schema::FieldID;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use super::build_fragment_metadata_as_directive;
use super::build_fragment_spread;
use super::build_operation_variable_definitions;
use super::build_used_global_variables;
use super::validation_message::ValidationMessage;
use super::QueryGenerator;
use super::RefetchRoot;
use super::RefetchableIdentifierInfo;
use super::RefetchableMetadata;
use super::CONSTANTS;
use crate::root_variables::VariableMap;

fn build_refetch_operation(
    schema: &SDLSchema,
    schema_config: &SchemaConfig,
    fragment: &Arc<FragmentDefinition>,
    query_name: OperationDefinitionName,
    variables_map: &VariableMap,
) -> DiagnosticsResult<Option<RefetchRoot>> {
    let id_name = schema_config.node_interface_id_field;

    if let Some(identifier_field_name) = get_fetchable_field_name(fragment, schema)? {
        let identifier_field_id = get_identifier_field_id(fragment, schema, identifier_field_name)?;

        let query_type = schema.query_type().unwrap();
        let fetch_field_name =
            format!("fetch__{}", schema.get_type_name(fragment.type_condition)).intern();
        let (fetch_field_id, id_arg) =
            get_fetch_field_id_and_id_arg(fragment, schema, query_type, fetch_field_name)?;

        let fragment = Arc::new(FragmentDefinition {
            name: fragment.name,
            variable_definitions: fragment.variable_definitions.clone(),
            used_global_variables: build_used_global_variables(
                variables_map,
                &fragment.variable_definitions,
            )?,
            type_condition: fragment.type_condition,
            directives: build_fragment_metadata_as_directive(
                fragment,
                RefetchableMetadata {
                    operation_name: query_name,
                    path: vec![fetch_field_name],
                    identifier_info: Some(RefetchableIdentifierInfo {
                        identifier_field: identifier_field_name,
                        identifier_query_variable_name: schema_config
                            .node_interface_id_variable_name,
                    }),
                },
            ),
            selections: enforce_selections_with_id_field(
                fragment,
                identifier_field_id,
                schema.fetch_token_field(),
            ),
        });
        let mut variable_definitions = build_operation_variable_definitions(&fragment);
        if let Some(id_argument) = variable_definitions.named(VariableName(id_name)) {
            return Err(vec![Diagnostic::error(
                ValidationMessage::RefetchableFragmentOnNodeWithExistingID {
                    fragment_name: fragment.name.item,
                },
                id_argument.name.location,
            )]);
        }
        variable_definitions.push(VariableDefinition {
            name: WithLocation::new(fragment.name.location, VariableName(id_name)),
            type_: id_arg.type_.non_null(),
            default_value: None,
            directives: vec![],
        });

        Ok(Some(RefetchRoot {
            variable_definitions,
            selections: vec![Selection::LinkedField(Arc::new(LinkedField {
                alias: None,
                definition: WithLocation::new(fragment.name.location, fetch_field_id),
                arguments: vec![Argument {
                    name: WithLocation::new(fragment.name.location, id_arg.name),
                    value: WithLocation::new(
                        fragment.name.location,
                        Value::Variable(Variable {
                            name: WithLocation::new(fragment.name.location, VariableName(id_name)),
                            type_: id_arg.type_.non_null(),
                        }),
                    ),
                }],
                directives: vec![],
                selections: vec![build_fragment_spread(&fragment)],
            }))],
            fragment,
        }))
    } else {
        Ok(None)
    }
}

fn get_fetchable_field_name(
    fragment: &FragmentDefinition,
    schema: &SDLSchema,
) -> DiagnosticsResult<Option<StringKey>> {
    let fetchable_directive = match fragment.type_condition {
        Type::Interface(interface_id) => {
            let interface = schema.interface(interface_id);
            interface.directives.named(CONSTANTS.fetchable)
        }
        Type::Object(object_id) => {
            let object = schema.object(object_id);
            object.directives.named(CONSTANTS.fetchable)
        }
        _ => None,
    };

    if let Some(fetchable) = fetchable_directive {
        let field_name_arg = fetchable.arguments.named(CONSTANTS.field_name);
        if let Some(field_name_arg) = field_name_arg {
            if let Some(value) = field_name_arg.value.get_string_literal() {
                return Ok(Some(value));
            }
        }
        return Err(vec![Diagnostic::error(
            ValidationMessage::InvalidRefetchDirectiveDefinition {
                fragment_name: fragment.name.item,
            },
            fragment.name.location,
        )]);
    }

    Ok(None)
}

fn get_identifier_field_id(
    fragment: &FragmentDefinition,
    schema: &SDLSchema,
    identifier_field_name: StringKey,
) -> DiagnosticsResult<FieldID> {
    let identifier_field_id = schema.named_field(fragment.type_condition, identifier_field_name);
    if let Some(identifier_field_id) = identifier_field_id {
        let identifier_field = schema.field(identifier_field_id);
        if schema.is_id(identifier_field.type_.inner()) {
            return Ok(identifier_field_id);
        }
    }
    Err(vec![Diagnostic::error(
        ValidationMessage::InvalidRefetchIdentifyingField {
            fragment_name: fragment.name.item,
            identifier_field_name,
            type_name: schema.get_type_name(fragment.type_condition),
        },
        fragment.name.location,
    )])
}

fn get_fetch_field_id_and_id_arg<'s>(
    fragment: &FragmentDefinition,
    schema: &'s SDLSchema,
    query_type: Type,
    fetch_field_name: StringKey,
) -> DiagnosticsResult<(FieldID, &'s ArgumentDef)> {
    let fetch_field_id = schema.named_field(query_type, fetch_field_name);
    if let Some(fetch_field_id) = fetch_field_id {
        let fetch_field = schema.field(fetch_field_id);
        if let Some(inner_type) = fetch_field.type_.non_list_type() {
            if inner_type == fragment.type_condition {
                let mut arg_iter = fetch_field.arguments.iter();
                if let Some(id_arg) = arg_iter.next() {
                    if !id_arg.type_.is_list() && schema.is_id(id_arg.type_.inner()) {
                        return Ok((fetch_field_id, id_arg));
                    }
                }
            }
        }
    }
    Err(vec![Diagnostic::error(
        ValidationMessage::InvalidRefetchFetchField {
            fetch_field_name,
            fragment_name: fragment.name.item,
            type_name: schema.get_type_name(fragment.type_condition),
        },
        fragment.name.location,
    )])
}

fn has_field(selections: &[Selection], field_id: FieldID) -> bool {
    selections.iter().any(|sel| match sel {
        Selection::ScalarField(field) => field.definition.item == field_id,
        _ => false,
    })
}

fn enforce_selections_with_id_field(
    fragment: &FragmentDefinition,
    identifier_field_id: FieldID,
    fetch_token_field_id: FieldID,
) -> Vec<Selection> {
    let mut next_selections = fragment.selections.clone();
    if !has_field(&next_selections, identifier_field_id) {
        next_selections.push(Selection::ScalarField(Arc::new(ScalarField {
            alias: None,
            definition: WithLocation::new(fragment.name.location, identifier_field_id),
            arguments: vec![],
            directives: vec![],
        })));
    }
    if !has_field(&next_selections, fetch_token_field_id) {
        next_selections.push(Selection::ScalarField(Arc::new(ScalarField {
            alias: None,
            definition: WithLocation::generated(fetch_token_field_id),
            arguments: vec![],
            directives: vec![],
        })));
    }
    next_selections
}

pub const FETCHABLE_QUERY_GENERATOR: QueryGenerator = QueryGenerator {
    // T138625502 we should support interfaces and maybe unions
    description: "server objects and interfaces with the @fetchable directive",
    build_refetch_operation,
};


/*
 * 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 fetchable_query_generator;
mod node_query_generator;
mod query_query_generator;
mod refetchable_directive;
mod utils;
mod validation_message;
mod viewer_query_generator;

use std::fmt::Write;
use std::sync::Arc;

use ::errors::validate_map;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::NamedItem;
use common::WithLocation;
use fetchable_query_generator::FETCHABLE_QUERY_GENERATOR;
use graphql_ir::Directive;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::OperationDefinition;
use graphql_ir::OperationDefinitionName;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::VariableDefinition;
use graphql_syntax::OperationKind;
use intern::string_key::StringKeyMap;
use node_query_generator::NODE_QUERY_GENERATOR;
use query_query_generator::QUERY_QUERY_GENERATOR;
use relay_config::SchemaConfig;
use schema::SDLSchema;
use schema::Schema;
pub use utils::RefetchableDerivedFromMetadata;
pub use utils::RefetchableMetadata;
pub use utils::CONSTANTS;
use utils::*;
use viewer_query_generator::VIEWER_QUERY_GENERATOR;

use self::refetchable_directive::RefetchableDirective;
pub use self::refetchable_directive::REFETCHABLE_NAME;
use self::validation_message::ValidationMessage;
use crate::connections::extract_connection_metadata_from_directive;
use crate::connections::ConnectionConstants;
use crate::relay_directive::PLURAL_ARG_NAME;
use crate::relay_directive::RELAY_DIRECTIVE_NAME;
use crate::root_variables::InferVariablesVisitor;
use crate::root_variables::VariableMap;

/// This transform synthesizes "refetch" queries for fragments that
/// are trivially refetchable. This is comprised of three main stages:
///
/// 1. Validating that fragments marked with @refetchable qualify for
///    refetch query generation; mainly this means that the fragment
///    type is able to be refetched in some canonical way.
/// 2. Determining the variable definitions to use for each generated
///    query. GraphQL does not have a notion of fragment-local variables
///    at all, and although Relay adds this concept developers are still
///    allowed to reference global variables. This necessitates a
///    visiting all reachable fragments for each @refetchable fragment,
///    and finding the union of all global variables expected to be defined.
/// 3. Building the refetch queries, a straightforward copying transform from
///    Fragment to Root IR nodes.
pub fn transform_refetchable_fragment(
    program: &Program,
    schema_config: &SchemaConfig,
    base_fragment_names: &'_ FragmentDefinitionNameSet,
    for_typegen: bool,
) -> DiagnosticsResult<Program> {
    let mut next_program = Program::new(Arc::clone(&program.schema));

    let mut transformer = RefetchableFragment::new(program, schema_config, for_typegen);

    for operation in program.operations() {
        next_program.insert_operation(Arc::clone(operation));
    }

    validate_map(program.fragments(), |fragment| {
        let operation_result = transformer.transform_refetch_fragment(fragment)?;
        if let Some((refetchable_directive, operation_result)) = operation_result {
            next_program.insert_fragment(operation_result.fragment);
            if !base_fragment_names.contains(&fragment.name.item) {
                let mut directives = refetchable_directive.directives;
                directives.push(RefetchableDerivedFromMetadata(fragment.name.item).into());

                next_program.insert_operation(Arc::new(OperationDefinition {
                    kind: OperationKind::Query,
                    name: WithLocation::new(
                        fragment.name.location,
                        refetchable_directive.query_name.item,
                    ),
                    type_: program.schema.query_type().unwrap(),
                    variable_definitions: operation_result.variable_definitions,
                    directives,
                    selections: operation_result.selections,
                }));
            }
        } else {
            next_program.insert_fragment(Arc::clone(fragment));
        }
        Ok(())
    })?;

    Ok(next_program)
}

type ExistingRefetchOperations = StringKeyMap<WithLocation<FragmentDefinitionName>>;

pub struct RefetchableFragment<'program, 'sc> {
    connection_constants: ConnectionConstants,
    existing_refetch_operations: ExistingRefetchOperations,
    for_typegen: bool,
    program: &'program Program,
    schema_config: &'sc SchemaConfig,
}

impl<'program, 'sc> RefetchableFragment<'program, 'sc> {
    pub fn new(
        program: &'program Program,
        schema_config: &'sc SchemaConfig,
        for_typegen: bool,
    ) -> Self {
        RefetchableFragment {
            connection_constants: Default::default(),
            existing_refetch_operations: Default::default(),
            for_typegen,
            program,
            schema_config,
        }
    }

    fn transform_refetch_fragment(
        &mut self,
        fragment: &Arc<FragmentDefinition>,
    ) -> DiagnosticsResult<Option<(RefetchableDirective, RefetchRoot)>> {
        let refetchable_directive = fragment.directives.named(*REFETCHABLE_NAME);
        if refetchable_directive.is_some() && self.program.schema.query_type().is_none() {
            return Err(vec![Diagnostic::error(
                "Unable to use @refetchable directive. The `Query` type is not defined on the schema.",
                refetchable_directive.unwrap().name.location,
            )]);
        }

        refetchable_directive
            .map(|refetchable_directive| {
                self.transform_refetch_fragment_with_refetchable_directive(
                    fragment,
                    refetchable_directive,
                )
            })
            .transpose()
    }

    pub fn transform_refetch_fragment_with_refetchable_directive(
        &mut self,
        fragment: &Arc<FragmentDefinition>,
        directive: &Directive,
    ) -> DiagnosticsResult<(RefetchableDirective, RefetchRoot)> {
        let refetchable_directive =
            RefetchableDirective::from_directive(&self.program.schema, directive)?;
        self.validate_sibling_directives(fragment)?;
        self.validate_refetch_name(fragment, &refetchable_directive)?;
        let variables_map =
            InferVariablesVisitor::new(self.program).infer_fragment_variables(fragment);

        for generator in GENERATORS.iter() {
            if let Some(refetch_root) = (generator.build_refetch_operation)(
                &self.program.schema,
                self.schema_config,
                fragment,
                refetchable_directive.query_name.item,
                &variables_map,
            )? {
                if !self.for_typegen {
                    self.validate_connection_metadata(refetch_root.fragment.as_ref())?;
                }
                return Ok((refetchable_directive, refetch_root));
            }
        }
        let mut descriptions = String::new();
        for generator in GENERATORS.iter() {
            writeln!(descriptions, " - {}", generator.description).unwrap();
        }
        descriptions.pop();
        Err(vec![Diagnostic::error(
            ValidationMessage::UnsupportedRefetchableFragment {
                fragment_name: fragment.name.item,
                descriptions,
            },
            fragment.name.location,
        )])
    }

    fn validate_sibling_directives(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> DiagnosticsResult<()> {
        let relay_directive = fragment.directives.named(*RELAY_DIRECTIVE_NAME);
        let plural_directive = relay_directive
            .filter(|directive| directive.arguments.named(*PLURAL_ARG_NAME).is_some());
        if let Some(directive) = plural_directive {
            Err(vec![Diagnostic::error(
                ValidationMessage::InvalidRefetchableFragmentWithRelayPlural {
                    fragment_name: fragment.name.item,
                },
                directive.name.location,
            )])
        } else {
            Ok(())
        }
    }

    fn validate_refetch_name(
        &mut self,
        fragment: &FragmentDefinition,
        refetchable_directive: &RefetchableDirective,
    ) -> DiagnosticsResult<()> {
        let fragment_name = fragment.name;

        // check for conflict with other @refetchable names
        if let Some(previous_fragment) = self
            .existing_refetch_operations
            .insert(refetchable_directive.query_name.item.0, fragment_name)
        {
            let (first_fragment, second_fragment) = if fragment.name.item > previous_fragment.item {
                (previous_fragment, fragment_name)
            } else {
                (fragment_name, previous_fragment)
            };
            return Err(vec![
                Diagnostic::error(
                    ValidationMessage::DuplicateRefetchableOperation {
                        query_name: refetchable_directive.query_name.item,
                        first_fragment_name: first_fragment.item,
                        second_fragment_name: second_fragment.item,
                    },
                    first_fragment.location,
                )
                .annotate("also defined here", second_fragment.location),
            ]);
        }

        // check for conflict with operations
        if let Some(existing_query) = self
            .program
            .operation(refetchable_directive.query_name.item)
        {
            return Err(vec![
                Diagnostic::error(
                    ValidationMessage::RefetchableQueryConflictWithQuery {
                        query_name: refetchable_directive.query_name.item,
                    },
                    refetchable_directive.query_name.location,
                )
                .annotate(
                    "an operation with that name is already defined here",
                    existing_query.name.location,
                ),
            ]);
        }

        Ok(())
    }

    /// Validate that any @connection usage is valid for refetching:
    /// - Variables are used for both the "count" and "cursor" arguments
    ///   (after/first or before/last)
    /// - Exactly one connection
    /// - Has a stable path to the connection data
    ///
    /// Connection metadata is extracted in `transform_connection`
    fn validate_connection_metadata(&self, fragment: &FragmentDefinition) -> DiagnosticsResult<()> {
        if let Some(metadatas) = extract_connection_metadata_from_directive(&fragment.directives) {
            // TODO: path or connection field locations in the error messages
            if metadatas.len() > 1 {
                return Err(vec![Diagnostic::error(
                    ValidationMessage::RefetchableWithMultipleConnections {
                        fragment_name: fragment.name.item,
                    },
                    fragment.name.location,
                )]);
            } else if metadatas.len() == 1 {
                let metadata = &metadatas[0];
                if metadata.path.is_none() {
                    return Err(vec![Diagnostic::error(
                        ValidationMessage::RefetchableWithConnectionInPlural {
                            fragment_name: fragment.name.item,
                        },
                        fragment.name.location,
                    )]);
                }
                if (metadata.after.is_none() || metadata.first.is_none())
                    && metadata.direction != self.connection_constants.direction_backward
                {
                    return Err(vec![Diagnostic::error(
                        ValidationMessage::RefetchableWithConstConnectionArguments {
                            fragment_name: fragment.name.item,
                            arguments: "after and first",
                        },
                        fragment.name.location,
                    )]);
                } else if (metadata.before.is_none() || metadata.last.is_none())
                    && metadata.direction != self.connection_constants.direction_forward
                {
                    return Err(vec![Diagnostic::error(
                        ValidationMessage::RefetchableWithConstConnectionArguments {
                            fragment_name: fragment.name.item,
                            arguments: "before and last",
                        },
                        fragment.name.location,
                    )]);
                }
            }
        }
        Ok(())
    }
}

type BuildRefetchOperationFn = fn(
    schema: &SDLSchema,
    schema_config: &SchemaConfig,
    fragment: &Arc<FragmentDefinition>,
    query_name: OperationDefinitionName,
    variables_map: &VariableMap,
) -> DiagnosticsResult<Option<RefetchRoot>>;
/// A strategy to generate queries for a given fragment. Multiple strategies
/// can be tried, such as generating a `node(id: ID)` query or a query directly
/// on the root query type.
pub struct QueryGenerator {
    /// Used to describe what fragments this QueryGenerator applies to, used in
    /// error messages.
    pub description: &'static str,

    /// Returns RefetchRoot or null if not applicable. Might throw a user error
    /// for an invalid schema or other problems.
    pub build_refetch_operation: BuildRefetchOperationFn,
}

const GENERATORS: [QueryGenerator; 4] = [
    VIEWER_QUERY_GENERATOR,
    QUERY_QUERY_GENERATOR,
    NODE_QUERY_GENERATOR,
    FETCHABLE_QUERY_GENERATOR,
];

pub struct RefetchRoot {
    pub fragment: Arc<FragmentDefinition>,
    pub selections: Vec<Selection>,
    pub variable_definitions: Vec<VariableDefinition>,
}