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

use common::Diagnostic;
use common::DiagnosticsResult;
use common::NamedItem;
use graphql_ir::Condition;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentSpread;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::Program;
use graphql_ir::Validator;
use schema::Schema;
use schema::Type;
use schema::TypeReference;

use super::ensure_discriminated_union_is_created;
use super::ValidationMessage;
use crate::UPDATABLE_DIRECTIVE;

pub fn validate_updatable_fragment_spread(program: &Program) -> DiagnosticsResult<()> {
    UpdatableFragmentSpread {
        program,
        path: vec![],
    }
    .validate_program(program)
}

/// An enum that keeps track of the path we have taken to get to a given
/// updatable fragment spread. We then iterate this path in reverse and check things like:
/// * is this fragment spread top level?
/// * is there a condition between the fragment spread and a linked field?
/// * are there multiple inline fragments between the fragment spread and the linked field?
///
/// If a single inline fragment is found, that linked field is marked (via setting
/// LinkedFieldPathItem.should_ensure_discriminated_union_is_created to true) as requiring
/// additional validation that ensures that a discriminated union is created.
enum PathItem {
    InlineFragment,
    LinkedField(LinkedFieldPathItem),
    Condition,
}

struct LinkedFieldPathItem {
    type_reference: TypeReference<Type>,
    should_ensure_discriminated_union_is_created: bool,
}

struct UpdatableFragmentSpread<'a> {
    program: &'a Program,
    path: Vec<PathItem>,
}

impl<'a> UpdatableFragmentSpread<'a> {
    /// Validate many conditions for spreads of updatable fragments:
    /// * the fragment spread contains no directives
    /// * there is no @if or @skip between the linked field and the fragment spread
    /// * the fragment spread is not at the top level
    /// * the fragment's type is a superset or equal to the outer type
    ///   * this ensures that if we read this fragment with readUpdatableFragment,
    ///     the result is guaranteed to be valid, i.e. the concrete type is guaranteed
    ///     to be updatable as that fragment type.
    ///   * So, `node { ...updatable_user }` is disallowed, but
    ///     `user { ...updatable_node }` and `node { ...updatable_node }` are allowed.
    ///   * "outer type" means the type of the enclosing linked field **or** the type of
    ///     the one (and only one) inline fragment, which must have a type condition
    ///
    /// If we encountered a type refinement between the enclosing linked field and the
    /// fragment spread:
    /// * ensure that we encountered only one. Later, we check that it refined to a
    ///   concrete type. In other words, it is **currently impossible** to refine from
    ///   an interface type to another interface type and use updatable fragments.
    /// * Later, we validate that the enclosing linked field has an abstract type and
    ///   contains only inline fragments which each refine to a unique concrete type
    ///   and which contain no directives, and each inline fragment contains an
    ///   unaliased __typename selection with no directives.
    ///   * this ensures that a discriminated union is created by the typegen
    /// * updatable fragments/queries must conform to a very similar requirement
    fn validate_fragment_spread_and_mark_enclosing_linked_field(
        &mut self,
        fragment_spread: &FragmentSpread,
        fragment_definition: &FragmentDefinition,
    ) -> DiagnosticsResult<()> {
        let mut errors = vec![];

        if !fragment_spread.directives.is_empty() {
            errors.push(Diagnostic::error(
                ValidationMessage::UpdatableFragmentSpreadNoDirectives,
                fragment_spread.fragment.location,
            ));
        }

        let mut encountered_inline_fragment = false;
        let mut encountered_linked_field = false;
        for mut item in self.path.iter_mut().rev() {
            match &mut item {
                PathItem::InlineFragment => {
                    if encountered_inline_fragment {
                        errors.push(Diagnostic::error(
                            ValidationMessage::UpdatableFragmentSpreadContainingInlineFragmentSingleNesting,
                            fragment_spread.fragment.location
                        ));
                    }
                    encountered_inline_fragment = true;
                }
                PathItem::LinkedField(ref mut linked_field_path_item) => {
                    encountered_linked_field = true;

                    if !encountered_inline_fragment {
                        // The fragment definition's type must be a superset or equal to the linked field's type.
                        // In other words, in `foo { ...Fragment_Y }`, foo must always be a Y.
                        //
                        // We want to prevent developers from writing
                        // node { ...Fragment_user } and modifying a given Node as if
                        // it was a User, regardless of whether its concrete type was "User"
                        //
                        // Note that we are comparing named, inner types. So users { ...Fragment_node }
                        // should not produce an error, for example.
                        //
                        // Note also that if the linked field has a concrete type and the updatable fragment spread
                        // does not match, we wouldn't get here - earlier validations would catch that the fragment
                        // can never occur on that type. Note also that  inline fragments are enforced to refine to a
                        // concrete type. Therefore, this check isn't necessary for inline fragments.
                        if !self.program.schema.is_type_subtype_of(
                            &TypeReference::Named(linked_field_path_item.type_reference.inner()),
                            &TypeReference::Named(fragment_definition.type_condition),
                        ) {
                            errors.push(Diagnostic::error(
                                ValidationMessage::UpdatableFragmentSpreadSubtypeOrEqualLinkedField {
                                    updatable_fragment_type: self
                                        .program
                                        .schema
                                        .get_type_name(fragment_definition.type_condition),
                                    linked_field_inner_type: self
                                        .program
                                        .schema
                                        .get_type_name(linked_field_path_item.type_reference.inner()),
                                    linked_field_type: self
                                        .program
                                        .schema
                                        .get_type_string(&linked_field_path_item.type_reference),
                                },
                                fragment_spread.fragment.location,
                            ));
                        }
                    } else {
                        // Since we've encountered an updatable fragment spread in an inline fragment, this
                        // linked field result in an a discriminated union.
                        //
                        // Mutate the linked_field_path_item. It would be ideal for linked_field_path_item to
                        // contain &'a LinkedField and to do the validation here, but the lifetimes currently
                        // don't line up in the Validator trait. Instead, do the validation afterward in
                        // validate_linked_field.
                        linked_field_path_item.should_ensure_discriminated_union_is_created = true;
                    }

                    break;
                }
                PathItem::Condition => errors.push(Diagnostic::error(
                    ValidationMessage::UpdatableFragmentSpreadNoCondition,
                    fragment_spread.fragment.location,
                )),
            }
        }

        if !encountered_linked_field {
            errors.push(Diagnostic::error(
                ValidationMessage::UpdatableFragmentTopLevel,
                fragment_spread.fragment.location,
            ));
        }

        if !errors.is_empty() {
            Err(errors)
        } else {
            Ok(())
        }
    }
}

impl<'a> Validator for UpdatableFragmentSpread<'a> {
    const NAME: &'static str = "UpdatableFragmentSpread";
    const VALIDATE_ARGUMENTS: bool = false;
    const VALIDATE_DIRECTIVES: bool = false;

    fn validate_linked_field(&mut self, linked_field: &LinkedField) -> DiagnosticsResult<()> {
        let mut errors = vec![];
        self.path.push(PathItem::LinkedField(LinkedFieldPathItem {
            type_reference: self
                .program
                .schema
                .field(linked_field.definition.item)
                .type_
                .clone(),
            should_ensure_discriminated_union_is_created: false,
        }));
        match self.default_validate_linked_field(linked_field) {
            Ok(_) => {}
            Err(e) => errors.extend(e.into_iter()),
        }
        let linked_field_item = match self.path.pop().expect("path should not be empty") {
            PathItem::LinkedField(l) => l,
            _ => panic!("Unexpected path item"),
        };

        if linked_field_item.should_ensure_discriminated_union_is_created {
            if let Err(e) = ensure_discriminated_union_is_created(
                &self.program.schema,
                linked_field,
                "an updatable fragment was spread in an inline fragment in this linked field",
            ) {
                errors.extend(e)
            }
        }

        if !errors.is_empty() {
            Err(errors)
        } else {
            Ok(())
        }
    }

    fn validate_inline_fragment(
        &mut self,
        inline_fragment: &InlineFragment,
    ) -> DiagnosticsResult<()> {
        self.path.push(PathItem::InlineFragment);
        let result = self.default_validate_inline_fragment(inline_fragment);
        self.path.pop().expect("path should not be empty");
        result
    }

    fn validate_fragment_spread(
        &mut self,
        fragment_spread: &FragmentSpread,
    ) -> DiagnosticsResult<()> {
        let fragment_definition = self
            .program
            .fragment(fragment_spread.fragment.item)
            .expect("Fragment definition not found");

        if fragment_definition
            .directives
            .named(*UPDATABLE_DIRECTIVE)
            .is_some()
        {
            self.validate_fragment_spread_and_mark_enclosing_linked_field(
                fragment_spread,
                fragment_definition,
            )
        } else {
            Ok(())
        }
    }

    fn validate_condition(&mut self, condition: &Condition) -> DiagnosticsResult<()> {
        self.path.push(PathItem::Condition);
        let result = self.default_validate_condition(condition);
        self.path.pop().expect("path should not be empty");
        result
    }
}


/*
 * 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::DirectiveName;
use intern::string_key::Intern;
use intern::string_key::StringKey;

#[derive(Debug, Copy, Clone)]
pub struct ConnectionConstants {
    pub connection_directive_name: DirectiveName,
    pub stream_connection_directive_name: DirectiveName,

    pub direction_forward: StringKey,
    pub direction_backward: StringKey,
    pub direction_bidirectional: StringKey,

    pub after_arg_name: ArgumentName,
    pub before_arg_name: ArgumentName,
    pub first_arg_name: ArgumentName,
    pub last_arg_name: ArgumentName,
    pub find_arg_name: ArgumentName,
    pub surrounds_arg_name: ArgumentName,
}

impl ConnectionConstants {
    pub fn is_connection_argument(&self, arg: ArgumentName) -> bool {
        arg == self.after_arg_name
            || arg == self.before_arg_name
            || arg == self.first_arg_name
            || arg == self.last_arg_name
            || arg == self.find_arg_name
            || arg == self.surrounds_arg_name
    }
}

impl Default for ConnectionConstants {
    fn default() -> Self {
        Self {
            connection_directive_name: DirectiveName("connection".intern()),
            stream_connection_directive_name: DirectiveName("stream_connection".intern()),

            direction_forward: "forward".intern(),
            direction_backward: "backward".intern(),
            direction_bidirectional: "bidirectional".intern(),

            after_arg_name: ArgumentName("after".intern()),
            before_arg_name: ArgumentName("before".intern()),
            first_arg_name: ArgumentName("first".intern()),
            last_arg_name: ArgumentName("last".intern()),
            find_arg_name: ArgumentName("find".intern()),
            surrounds_arg_name: ArgumentName("surrounds".intern()),
        }
    }
}


/*
 * 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::Location;
use common::NamedItem;
use common::WithLocation;
use graphql_ir::associated_data_impl;
use graphql_ir::Directive;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use intern::string_key::StringKey;
use schema::SDLSchema;
use schema::Schema;
use schema::Type;

use crate::connections::ConnectionConstants;
use crate::connections::ConnectionInterface;
use crate::util::extract_variable_name;

/// Helper to assert and extract the expected selections for a connection
/// field. This function will panic if the expected selections aren't present,
/// with the assumption that the connection field has already been validated.
pub fn assert_connection_selections<'s>(
    schema: &'s SDLSchema,
    selections: &'s [Selection],
    connection_interface: &ConnectionInterface,
) -> ((usize, &'s LinkedField), Option<(usize, &'s LinkedField)>) {
    let mut edges_selection = None;
    let mut page_info_selection = None;
    for (ix, selection) in selections.iter().enumerate() {
        if let Selection::LinkedField(field) = selection {
            let field_name = schema.field(field.definition.item).name.item;
            if field_name == connection_interface.edges {
                if edges_selection.is_some() {
                    unreachable!("Unexpected duplicate selection for edges")
                }
                edges_selection = Some((ix, field.as_ref()));
            }
            if field_name == connection_interface.page_info {
                if page_info_selection.is_some() {
                    unreachable!("Unexpected duplicate selection for page_info")
                }
                page_info_selection = Some((ix, field.as_ref()));
            }
        }
    }

    if let Some(edges_selection) = edges_selection {
        (edges_selection, page_info_selection)
    } else {
        unreachable!("Expected selection for edges to have been previously validated.")
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ConnectionMetadataDirective(pub Vec<ConnectionMetadata>);

associated_data_impl!(ConnectionMetadataDirective);

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ConnectionMetadata {
    pub path: Option<Vec<StringKey>>,
    pub direction: StringKey,
    pub first: Option<StringKey>,
    pub last: Option<StringKey>,
    pub before: Option<StringKey>,
    pub after: Option<StringKey>,
    pub is_stream_connection: bool,
}

/// Builds the connection metadata that will be attached
/// to the document root (fragment or operation)
pub fn build_connection_metadata(
    connection_field: &LinkedField,
    connection_constants: ConnectionConstants,
    path: &Option<Vec<StringKey>>,
    is_stream_connection: bool,
) -> ConnectionMetadata {
    let first_arg = connection_field
        .arguments
        .named(connection_constants.first_arg_name);
    let last_arg = connection_field
        .arguments
        .named(connection_constants.last_arg_name);

    let direction = match (first_arg, last_arg) {
        (Some(_), Some(_)) => connection_constants.direction_bidirectional,
        (Some(_), None) => connection_constants.direction_forward,
        (None, Some(_)) => connection_constants.direction_backward,
        (None, None) => unreachable!(
            "Expected presence of first or last args on connection to have been previously validated."
        ),
    };

    ConnectionMetadata {
        first: extract_variable_name(first_arg),
        last: extract_variable_name(last_arg),
        after: first_arg.and_then(|_| {
            extract_variable_name(
                connection_field
                    .arguments
                    .named(connection_constants.after_arg_name),
            )
        }),
        before: last_arg.and_then(|_| {
            extract_variable_name(
                connection_field
                    .arguments
                    .named(connection_constants.before_arg_name),
            )
        }),
        direction,
        path: path.clone(),
        is_stream_connection,
    }
}

pub fn extract_connection_metadata_from_directive(
    directives: &[Directive],
) -> Option<&[ConnectionMetadata]> {
    ConnectionMetadataDirective::find(directives)
        .map(|connection_metadatas| connection_metadatas.0.as_slice())
}

/// Builds the selections that will be added to the edges selection
/// by the connections transform
pub fn build_edge_selections(
    schema: &SDLSchema,
    edge_type: Type,
    connection_interface: &ConnectionInterface,
) -> Selection {
    let cursor_field_id = schema
        .named_field(edge_type, connection_interface.cursor)
        .expect("Expected presence of cursor field to have been previously validated.");
    let node_field_id = schema
        .named_field(edge_type, connection_interface.node)
        .expect("Expected presence of node field to have been previously validated.");
    let typename_field_id = schema.typename_field();

    Selection::InlineFragment(From::from(InlineFragment {
        type_condition: Some(edge_type),
        directives: Vec::new(),
        selections: vec![
            Selection::ScalarField(From::from(ScalarField {
                alias: None,
                definition: WithLocation::generated(cursor_field_id),
                arguments: Vec::new(),
                directives: Vec::new(),
            })),
            Selection::LinkedField(From::from(LinkedField {
                alias: None,
                definition: WithLocation::generated(node_field_id),
                arguments: Vec::new(),
                directives: Vec::new(),
                selections: vec![
                    // We rely on generate_id_transform to add "id"
                    Selection::ScalarField(From::from(ScalarField {
                        alias: None,
                        definition: WithLocation::generated(typename_field_id),
                        arguments: Vec::new(),
                        directives: Vec::new(),
                    })),
                ],
            })),
        ],
        spread_location: Location::generated(),
    }))
}

/// Builds the selections that will be added to the page_info selection
/// by the connections transform
pub fn build_page_info_selections(
    schema: &SDLSchema,
    page_info_type: Type,
    connection_metadata: &ConnectionMetadata,
    connection_constants: ConnectionConstants,
    connection_interface: &ConnectionInterface,
) -> Selection {
    let end_cursor_field_id = schema
        .named_field(page_info_type, connection_interface.end_cursor)
        .expect("Expected presence of end_cursor field to have been previously validated.");
    let has_next_page_field_id = schema
        .named_field(page_info_type, connection_interface.has_next_page)
        .expect("Expected presence of has_next_page field to have been previously validated.");
    let has_prev_page_field_id = schema
        .named_field(page_info_type, connection_interface.has_previous_page)
        .expect("Expected presence of has_previous_page field to have been previously validated.");
    let start_cursor_field_id = schema
        .named_field(page_info_type, connection_interface.start_cursor)
        .expect("Expected presence of start_cursor field to have been previously validated.");

    if connection_metadata.direction == connection_constants.direction_forward {
        Selection::InlineFragment(From::from(InlineFragment {
            type_condition: Some(page_info_type),
            directives: Vec::new(),
            selections: vec![
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(end_cursor_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(has_next_page_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
            ],
            spread_location: Location::generated(),
        }))
    } else if connection_metadata.direction == connection_constants.direction_backward {
        Selection::InlineFragment(From::from(InlineFragment {
            type_condition: Some(page_info_type),
            directives: Vec::new(),
            selections: vec![
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(has_prev_page_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(start_cursor_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
            ],
            spread_location: Location::generated(),
        }))
    } else if connection_metadata.direction == connection_constants.direction_bidirectional {
        Selection::InlineFragment(From::from(InlineFragment {
            type_condition: Some(page_info_type),
            directives: Vec::new(),
            selections: vec![
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(end_cursor_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(has_next_page_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(has_prev_page_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
                Selection::ScalarField(From::from(ScalarField {
                    alias: None,
                    definition: WithLocation::generated(start_cursor_field_id),
                    arguments: Vec::new(),
                    directives: Vec::new(),
                })),
            ],
            spread_location: Location::generated(),
        }))
    } else {
        unreachable!()
    }
}

/// Helper to extract the connection directive if present in the given list of
/// of directives
pub fn extract_connection_directive(
    directives: &[Directive],
    connection_constants: ConnectionConstants,
) -> Option<&Directive> {
    directives.iter().find(|directive| {
        directive.name.item == connection_constants.connection_directive_name
            || directive.name.item == connection_constants.stream_connection_directive_name
    })
}

/// Helper to get the default set of filters to be used for an @connection handle
/// field when no filters are explicitly specified in the input graphql.
/// By default, we will use all arguments that don't belong to the
/// connection spec as part of the filters.
pub fn get_default_filters(
    connection_field: &LinkedField,
    connection_constants: ConnectionConstants,
) -> Option<Vec<StringKey>> {
    let filtered_args = connection_field
        .arguments
        .iter()
        .filter_map(|arg| {
            if connection_constants.is_connection_argument(arg.name.item) {
                None
            } else {
                Some(arg.name.item.0)
            }
        })
        .collect::<Vec<_>>();
    if filtered_args.is_empty() {
        return None;
    }
    Some(filtered_args)
}


/*
 * 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 connection_constants;
mod connection_util;
pub use connection_constants::ConnectionConstants;
pub use connection_util::assert_connection_selections;
pub use connection_util::build_connection_metadata;
pub use connection_util::build_edge_selections;
pub use connection_util::build_page_info_selections;
pub use connection_util::extract_connection_directive;
pub use connection_util::extract_connection_metadata_from_directive;
pub use connection_util::get_default_filters;
pub use connection_util::ConnectionMetadata;
pub use connection_util::ConnectionMetadataDirective;
pub use relay_config::ConnectionInterface;


/*
 * 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 graphql_ir::Argument;
use graphql_ir::Directive;

use super::DEFER_STREAM_CONSTANTS;

/// Utility to access the arguments of the @defer directive.
pub struct DeferDirective<'a> {
    pub if_arg: Option<&'a Argument>,
    pub label_arg: Option<&'a Argument>,
}

impl<'a> DeferDirective<'a> {
    /// Extracts the arguments from the given directive assumed to be a @defer
    /// directive.
    /// Panics on any unexpected arguments.
    pub fn from(directive: &'a Directive) -> Self {
        let mut if_arg = None;
        let mut label_arg = None;
        for arg in &directive.arguments {
            if arg.name.item == DEFER_STREAM_CONSTANTS.if_arg {
                if_arg = Some(arg);
            } else if arg.name.item == DEFER_STREAM_CONSTANTS.label_arg {
                label_arg = Some(arg);
            } else {
                panic!("Unexpected argument to @defer: {}", arg.name.item);
            }
        }
        Self { if_arg, label_arg }
    }
}

/// Utility to access the arguments of the @stream directive.
pub struct StreamDirective<'a> {
    pub if_arg: Option<&'a Argument>,
    pub label_arg: Option<&'a Argument>,
    pub initial_count_arg: Option<&'a Argument>,
    pub use_customized_batch_arg: Option<&'a Argument>,
}

impl<'a> StreamDirective<'a> {
    /// Extracts the arguments from the given directive assumed to be a @stream
    /// directive.
    /// Panics on any unexpected arguments.
    pub fn from(directive: &'a Directive) -> Self {
        let mut if_arg = None;
        let mut label_arg = None;
        let mut initial_count_arg = None;
        let mut use_customized_batch_arg = None;
        for arg in &directive.arguments {
            if arg.name.item == DEFER_STREAM_CONSTANTS.if_arg {
                if_arg = Some(arg);
            } else if arg.name.item == DEFER_STREAM_CONSTANTS.label_arg {
                label_arg = Some(arg);
            } else if arg.name.item == DEFER_STREAM_CONSTANTS.initial_count_arg {
                initial_count_arg = Some(arg);
            } else if arg.name.item == DEFER_STREAM_CONSTANTS.use_customized_batch_arg {
                use_customized_batch_arg = Some(arg);
            } else {
                panic!("Unexpected argument to @stream: {}", arg.name.item);
            }
        }
        Self {
            if_arg,
            label_arg,
            initial_count_arg,
            use_customized_batch_arg,
        }
    }
}


/*
 * 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 directives;

use std::collections::HashMap;
use std::sync::Arc;

use common::ArgumentName;
use common::Diagnostic;
use common::DiagnosticsResult;
use common::DirectiveName;
use common::Location;
use common::NamedItem;
use common::WithLocation;
pub use directives::DeferDirective;
pub use directives::StreamDirective;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::Field;
use graphql_ir::FragmentDefinition;
use graphql_ir::FragmentDefinitionName;
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 intern::string_key::Intern;
use intern::string_key::StringKey;
use intern::Lookup;
use lazy_static::lazy_static;
use schema::Schema;
use thiserror::Error;

use super::get_applied_fragment_name;
use crate::util::remove_directive;
use crate::util::replace_directive;

pub struct DeferStreamConstants {
    pub defer_name: DirectiveName,
    pub stream_name: DirectiveName,
    pub if_arg: ArgumentName,
    pub label_arg: ArgumentName,
    pub initial_count_arg: ArgumentName,
    pub use_customized_batch_arg: ArgumentName,
}

impl Default for DeferStreamConstants {
    fn default() -> Self {
        Self {
            defer_name: DirectiveName("defer".intern()),
            stream_name: DirectiveName("stream".intern()),
            if_arg: ArgumentName("if".intern()),
            label_arg: ArgumentName("label".intern()),
            initial_count_arg: ArgumentName("initial_count".intern()),
            use_customized_batch_arg: ArgumentName("use_customized_batch".intern()),
        }
    }
}

lazy_static! {
    pub static ref DEFER_STREAM_CONSTANTS: DeferStreamConstants = Default::default();
}

pub fn transform_defer_stream(program: &Program) -> DiagnosticsResult<Program> {
    let mut transformer = DeferStreamTransform {
        program,
        current_document_name: None,
        labels: Default::default(),
        errors: Default::default(),
    };
    let next_program = transformer.transform_program(program);

    if transformer.errors.is_empty() {
        Ok(next_program.replace_or_else(|| program.clone()))
    } else {
        Err(transformer.errors)
    }
}

struct DeferStreamTransform<'s> {
    program: &'s Program,
    current_document_name: Option<StringKey>,
    labels: HashMap<StringKey, Directive>,
    errors: Vec<Diagnostic>,
}

impl DeferStreamTransform<'_> {
    fn set_current_document_name(&mut self, document_name: StringKey) {
        self.current_document_name = Some(document_name)
    }

    fn record_label(&mut self, label: StringKey, directive: &Directive) {
        let prev_directive = self.labels.get(&label);
        match prev_directive {
            Some(prev) => {
                self.errors.push(
                    Diagnostic::error(
                        ValidationMessage::LabelNotUniqueForDeferStream {
                            directive_name: DEFER_STREAM_CONSTANTS.defer_name,
                        },
                        prev.name.location,
                    )
                    .annotate("related location", directive.name.location),
                );
            }
            None => {
                self.labels.insert(label, directive.to_owned());
            }
        };
    }

    fn transform_defer(
        &mut self,
        spread: &FragmentSpread,
        defer: &Directive,
    ) -> Result<Transformed<Selection>, Diagnostic> {
        let DeferDirective { if_arg, label_arg } = DeferDirective::from(defer);

        if is_literal_false_arg(if_arg) {
            return Ok(Transformed::Replace(Selection::FragmentSpread(Arc::new(
                FragmentSpread {
                    directives: remove_directive(&spread.directives, defer.name.item),
                    ..spread.clone()
                },
            ))));
        }

        let label_value = get_literal_string_argument(defer, label_arg)?;
        let label = label_value.unwrap_or_else(|| {
            get_applied_fragment_name(spread.fragment.item, &spread.arguments).0
        });
        let transformed_label = transform_label(
            self.current_document_name
                .expect("We expect the parent name to be defined here."),
            DEFER_STREAM_CONSTANTS.defer_name,
            label,
        );
        self.record_label(transformed_label, defer);
        let next_label_value = Value::Constant(ConstantValue::String(transformed_label));
        let next_label_arg = Argument {
            name: WithLocation {
                item: DEFER_STREAM_CONSTANTS.label_arg,
                location: label_arg.map_or(defer.name.location, |arg| arg.name.location),
            },
            value: WithLocation {
                item: next_label_value,
                location: label_arg.map_or(defer.name.location, |arg| arg.value.location),
            },
        };

        let mut next_arguments = Vec::with_capacity(2);
        next_arguments.push(next_label_arg);
        if let Some(if_arg) = if_arg {
            next_arguments.push(if_arg.clone());
        }

        let next_defer = Directive {
            name: defer.name,
            arguments: next_arguments,
            data: None,
        };

        Ok(Transformed::Replace(Selection::InlineFragment(Arc::new(
            InlineFragment {
                type_condition: None,
                directives: vec![next_defer],
                selections: vec![Selection::FragmentSpread(Arc::new(FragmentSpread {
                    directives: remove_directive(&spread.directives, defer.name.item),
                    ..spread.clone()
                }))],
                spread_location: Location::generated(),
            },
        ))))
    }

    fn transform_stream(
        &mut self,
        linked_field: &LinkedField,
        stream: &Directive,
    ) -> Result<Transformed<Selection>, Diagnostic> {
        let schema_field = self.program.schema.field(linked_field.definition.item);
        if !schema_field.type_.is_list() {
            return Err(Diagnostic::error(
                ValidationMessage::StreamFieldIsNotAList {
                    field_name: schema_field.name.item,
                },
                stream.name.location,
            ));
        }

        let StreamDirective {
            if_arg,
            label_arg,
            initial_count_arg,
            use_customized_batch_arg,
        } = StreamDirective::from(stream);

        let transformed_linked_field = self.default_transform_linked_field(linked_field);
        let get_next_selection = |directives| match transformed_linked_field {
            Transformed::Replace(mut selection) => {
                selection.set_directives(directives);
                Transformed::Replace(selection)
            }
            Transformed::Keep => {
                Transformed::Replace(Selection::LinkedField(Arc::new(LinkedField {
                    directives,
                    ..linked_field.clone()
                })))
            }
            Transformed::Delete => Transformed::Delete,
        };
        if is_literal_false_arg(if_arg) {
            return Ok(get_next_selection(remove_directive(
                &linked_field.directives,
                stream.name.item,
            )));
        }

        if initial_count_arg.is_none() {
            return Err(Diagnostic::error(
                ValidationMessage::StreamInitialCountRequired,
                stream.name.location,
            ));
        }

        let label_value = get_literal_string_argument(stream, label_arg)?;
        let label = label_value.unwrap_or_else(|| {
            get_applied_fragment_name(
                FragmentDefinitionName(linked_field.alias_or_name(&self.program.schema)),
                &linked_field.arguments,
            )
            .0
        });
        let transformed_label = transform_label(
            self.current_document_name
                .expect("We expect the parent name to be defined here."),
            DEFER_STREAM_CONSTANTS.stream_name,
            label,
        );
        self.record_label(transformed_label, stream);
        let next_label_value = Value::Constant(ConstantValue::String(transformed_label));
        let next_label_arg = Argument {
            name: WithLocation {
                item: DEFER_STREAM_CONSTANTS.label_arg,
                location: label_arg.map_or(stream.name.location, |arg| arg.name.location),
            },
            value: WithLocation {
                item: next_label_value,
                location: label_arg.map_or(stream.name.location, |arg| arg.value.location),
            },
        };

        let mut next_arguments = Vec::with_capacity(4);
        next_arguments.push(next_label_arg);
        if let Some(if_arg) = if_arg {
            next_arguments.push(if_arg.clone());
        }
        if let Some(initial_count_arg) = initial_count_arg {
            next_arguments.push(initial_count_arg.clone());
        }
        if let Some(use_customized_batch_arg) = use_customized_batch_arg {
            next_arguments.push(use_customized_batch_arg.clone());
        }

        let next_stream = Directive {
            name: stream.name,
            arguments: next_arguments,
            data: None,
        };

        Ok(get_next_selection(replace_directive(
            &linked_field.directives,
            next_stream,
        )))
    }
}

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

    fn transform_operation(
        &mut self,
        operation: &OperationDefinition,
    ) -> Transformed<OperationDefinition> {
        self.set_current_document_name(operation.name.item.0);
        self.default_transform_operation(operation)
    }

    fn transform_fragment(
        &mut self,
        fragment: &FragmentDefinition,
    ) -> Transformed<FragmentDefinition> {
        self.set_current_document_name(fragment.name.item.0);
        self.default_transform_fragment(fragment)
    }

    /// Validates @defer is not allowed on inline fragments.
    fn transform_inline_fragment(
        &mut self,
        inline_fragment: &InlineFragment,
    ) -> Transformed<Selection> {
        let defer_directive = inline_fragment
            .directives
            .named(DEFER_STREAM_CONSTANTS.defer_name);
        if let Some(directive) = defer_directive {
            // Special case for @defer generated by transform_connection
            if let Some(label) = directive.arguments.named(DEFER_STREAM_CONSTANTS.label_arg) {
                if let Some(label) = label.value.item.get_string_literal() {
                    if label.lookup().contains("$defer$") {
                        return self.default_transform_inline_fragment(inline_fragment);
                    }
                }
            }
            self.errors.push(Diagnostic::error(
                ValidationMessage::InvalidDeferOnInlineFragment,
                directive.name.location,
            ));
        }

        self.default_transform_inline_fragment(inline_fragment)
    }

    /// Transform of fragment spread with @defer is delegated to `transform_defer`.
    fn transform_fragment_spread(&mut self, spread: &FragmentSpread) -> Transformed<Selection> {
        let defer_directive = spread.directives.named(DEFER_STREAM_CONSTANTS.defer_name);
        if let Some(defer) = defer_directive {
            match self.transform_defer(spread, defer) {
                Ok(transformed) => transformed,
                Err(err) => {
                    self.errors.push(err);
                    self.default_transform_fragment_spread(spread)
                }
            }
        } else {
            self.default_transform_fragment_spread(spread)
        }
    }

    /// Validates @stream is not allowed on scalar fields.
    fn transform_scalar_field(&mut self, scalar_field: &ScalarField) -> Transformed<Selection> {
        let stream_directive = &scalar_field
            .directives
            .named(DEFER_STREAM_CONSTANTS.stream_name);
        if let Some(directive) = stream_directive {
            self.errors.push(Diagnostic::error(
                ValidationMessage::InvalidStreamOnScalarField {
                    field_name: scalar_field.alias_or_name(&self.program.schema),
                },
                directive.name.location,
            ));
        }
        self.default_transform_scalar_field(scalar_field)
    }

    /// Transform of linked field with @stream is delegated to `transform_stream`.
    fn transform_linked_field(&mut self, linked_field: &LinkedField) -> Transformed<Selection> {
        let stream_directive = linked_field
            .directives
            .named(DEFER_STREAM_CONSTANTS.stream_name);
        if let Some(stream) = stream_directive {
            match self.transform_stream(linked_field, stream) {
                Ok(transformed) => transformed,
                Err(err) => {
                    self.errors.push(err);
                    self.default_transform_linked_field(linked_field)
                }
            }
        } else {
            self.default_transform_linked_field(linked_field)
        }
    }
}

fn is_literal_false_arg(arg: Option<&Argument>) -> bool {
    if let Some(arg) = arg {
        matches!(
            arg.value.item,
            Value::Constant(ConstantValue::Boolean(false))
        )
    } else {
        false
    }
}

fn transform_label(
    parent_name: StringKey,
    directive_name: DirectiveName,
    label: StringKey,
) -> StringKey {
    format!("{}${}${}", parent_name, directive_name, label).intern()
}

fn get_literal_string_argument(
    directive: &Directive,
    argument: Option<&Argument>,
) -> Result<Option<StringKey>, Diagnostic> {
    if let Some(arg) = argument {
        if let Some(val) = arg.value.item.get_string_literal() {
            Ok(Some(val))
        } else {
            Err(Diagnostic::error(
                ValidationMessage::LiteralStringArgumentExpectedForDirective {
                    arg_name: arg.name.item,
                    directive_name: directive.name.item,
                },
                directive.name.location,
            ))
        }
    } else {
        Ok(None)
    }
}

#[derive(Debug, Error)]
enum ValidationMessage {
    #[error(
        "Invalid use of @{directive_name}, the provided label is not unique. Specify a unique 'label' as a literal string."
    )]
    LabelNotUniqueForDeferStream { directive_name: DirectiveName },

    #[error("Field '{field_name}' is not of list type, therefore cannot use @stream directive.")]
    StreamFieldIsNotAList { field_name: StringKey },

    #[error("Invalid use of @stream, the 'initial_count' argument is required.")]
    StreamInitialCountRequired,

    #[error(
        "Invalid use of @defer on an inline fragment. Relay only supports @defer on fragment spreads."
    )]
    InvalidDeferOnInlineFragment,

    #[error("Invalid use of @stream on scalar field '{field_name}'")]
    InvalidStreamOnScalarField { field_name: StringKey },

    #[error(
        "Expected the '{arg_name}' value to @{directive_name} to be a string literal if provided."
    )]
    LiteralStringArgumentExpectedForDirective {
        arg_name: ArgumentName,
        directive_name: DirectiveName,
    },
}


/*
 * 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::Location;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::Directive;
use graphql_ir::LinkedField;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use intern::string_key::Intern;

use super::extract_values_from_handle_field_directive;
use crate::extract_handle_field_directives;

/// This transform applies field argument updates for client handle fields:
/// - If filters is not set, existing arguments are removed.
/// - If filters is set, only the arguments in the filter array are kept.
/// - If dynamicKey is set, a new __dynamicKey argument is added.
pub fn handle_field_transform(program: &Program) -> Program {
    let mut transform = HandleFieldTransform;
    transform
        .transform_program(program)
        .replace_or_else(|| program.clone())
}

struct HandleFieldTransform;

impl<'s> HandleFieldTransform {
    fn update_arguments(
        &self,
        arguments: &mut Vec<Argument>,
        handle_directive: &Directive,
        location: Location,
    ) {
        let handle_values = extract_values_from_handle_field_directive(handle_directive);
        if let Some(filters) = handle_values.filters {
            arguments.retain(|arg| filters.iter().any(|f| *f == arg.name.item.0));
        } else {
            arguments.clear();
        };
        if let Some(dynamic_key) = handle_values.dynamic_key {
            arguments.push(Argument {
                name: WithLocation::new(location, ArgumentName("__dynamicKey".intern())),
                value: WithLocation::new(location, dynamic_key),
            });
        }
    }
}

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

    fn transform_scalar_field(&mut self, field: &ScalarField) -> Transformed<Selection> {
        let transformed_field = self.default_transform_scalar_field(field);
        let mut handle_field_directives = extract_handle_field_directives(&field.directives);
        if let Some(handle_directive) = handle_field_directives.next() {
            if let Some(other_handle_directive) = handle_field_directives.next() {
                panic!(
                    "Expected at most one handle directive, got `{:?}` and `{:?}`.",
                    handle_directive, other_handle_directive
                );
            }
            let mut transformed_field = match transformed_field {
                Transformed::Replace(Selection::ScalarField(scalar_field)) => {
                    (*scalar_field).clone()
                }
                Transformed::Keep => field.clone(),
                _ => panic!(
                    "HandleFieldTransform got unexpected transform result: `{:?}`.",
                    transformed_field
                ),
            };
            self.update_arguments(
                &mut transformed_field.arguments,
                handle_directive,
                transformed_field.definition.location,
            );
            Transformed::Replace(Selection::ScalarField(Arc::new(transformed_field)))
        } else {
            transformed_field
        }
    }

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let transformed_field = self.default_transform_linked_field(field);
        let mut handle_field_directives = extract_handle_field_directives(&field.directives);
        if let Some(handle_directive) = handle_field_directives.next() {
            if let Some(other_handle_directive) = handle_field_directives.next() {
                panic!(
                    "Expected at most one handle directive, got `{:?}` and `{:?}`.",
                    handle_directive, other_handle_directive
                );
            }
            let mut transformed_field = match transformed_field {
                Transformed::Replace(Selection::LinkedField(linked_field)) => {
                    (*linked_field).clone()
                }
                Transformed::Keep => field.clone(),
                _ => panic!(
                    "HandleFieldTransform got unexpected transform result: `{:?}`.",
                    transformed_field
                ),
            };
            self.update_arguments(
                &mut transformed_field.arguments,
                handle_directive,
                transformed_field.definition.location,
            );
            Transformed::Replace(Selection::LinkedField(Arc::new(transformed_field)))
        } else {
            transformed_field
        }
    }
}


/*
 * 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::DirectiveName;
use common::WithLocation;
use graphql_ir::Argument;
use graphql_ir::ConstantValue;
use graphql_ir::Directive;
use graphql_ir::Value;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;

lazy_static! {
    pub static ref CONNECTION_HANDLER_ARG_NAME: ArgumentName = ArgumentName("handler".intern());
    pub static ref HANDLE_FIELD_DIRECTIVE_NAME: DirectiveName =
        DirectiveName("__clientField".intern());
    pub static ref HANDLER_ARG_NAME: ArgumentName = ArgumentName("handle".intern());
    pub static ref FILTERS_ARG_NAME: ArgumentName = ArgumentName("filters".intern());
    pub static ref KEY_ARG_NAME: ArgumentName = ArgumentName("key".intern());
    pub static ref DYNAMIC_KEY_ARG_NAME: ArgumentName =
        ArgumentName("dynamicKey_UNSTABLE".intern());
    static ref HANLDE_ARGS_NAME: ArgumentName = ArgumentName("handleArgs".intern());
}

pub struct HandleFieldDirectiveArgs<'s> {
    pub handler_arg: Option<(&'s Argument, &'s ConstantValue)>,
    pub key_arg: Option<(&'s Argument, &'s ConstantValue)>,
    pub filters_arg: Option<(&'s Argument, &'s ConstantValue)>,
    pub dynamic_key_arg: Option<(&'s Argument, &'s Value)>,
    pub handle_args_arg: Option<&'s Argument>,
}

pub struct HandleFieldDirectiveValues {
    pub handle: StringKey,
    pub key: StringKey,
    pub filters: Option<Vec<StringKey>>,
    pub dynamic_key: Option<Value>,
    pub handle_args: Option<Vec<Argument>>,
}

/// We have two handler keys, "handler" in connection, and "handle" in everywhere else
/// Speicific helpers are created separately for connection

/// Helper to extract handle field arguments that are present
/// on the directive, without any validation or assumption of
/// correctness of values.
pub fn extract_handle_field_directive_args_for_connection(
    handle_field_directive: &Directive,
) -> HandleFieldDirectiveArgs<'_> {
    extract_handle_field_directive_args_helper(handle_field_directive, *CONNECTION_HANDLER_ARG_NAME)
}

/// Helper to extract the values for handle field arguments that are present
/// on the input handle field directive (e.g. a @__clientField or @connection).
/// This function will panic if the expected argument values aren't
/// present on the directive, with the assumption that the directive
/// has already been validated.
pub fn extract_values_from_handle_field_directive(
    handle_field_directive: &Directive,
) -> HandleFieldDirectiveValues {
    extract_values_from_handle_field_directive_helper(
        handle_field_directive,
        *HANDLER_ARG_NAME,
        None,
        None,
    )
}

/// Helper to build an internal, custom handle field directive (@__clientField)
/// based an input handle field directive that has already been validated (e.g. a @__clientField or @connection).
/// This directive will be used to store the appropriate metadata for the handle,
/// to be later used in codegen.
/// This function will panic if the expected arguments aren't present on the input directive,
/// with the assumption that the input directive has already been validated.
pub fn build_handle_field_directive_from_connection_directive(
    handle_field_directive: &Directive,
    default_handler: Option<StringKey>,
    default_filters: Option<Vec<StringKey>>,
) -> Directive {
    let values = extract_values_from_handle_field_directive_helper(
        handle_field_directive,
        *CONNECTION_HANDLER_ARG_NAME,
        default_handler,
        default_filters,
    );
    build_handle_field_directive(values)
}

pub fn build_handle_field_directive(values: HandleFieldDirectiveValues) -> Directive {
    let HandleFieldDirectiveValues {
        handle,
        key,
        filters,
        dynamic_key,
        handle_args,
    } = values;
    let mut directive_arguments = vec![
        Argument {
            name: WithLocation::generated(*KEY_ARG_NAME),
            value: WithLocation::generated(Value::Constant(ConstantValue::String(key))),
        },
        Argument {
            name: WithLocation::generated(*HANDLER_ARG_NAME),
            value: WithLocation::generated(Value::Constant(ConstantValue::String(handle))),
        },
        Argument {
            name: WithLocation::generated(*FILTERS_ARG_NAME),
            value: WithLocation::generated(Value::Constant(match filters {
                Some(filters) => ConstantValue::List(
                    filters
                        .iter()
                        .map(|filter| ConstantValue::String(*filter))
                        .collect(),
                ),
                None => ConstantValue::Null(),
            })),
        },
        Argument {
            name: WithLocation::generated(*DYNAMIC_KEY_ARG_NAME),
            value: WithLocation::generated(
                dynamic_key.unwrap_or(Value::Constant(ConstantValue::Null())),
            ),
        },
    ];

    if let Some(handle_args) = handle_args {
        directive_arguments.push(Argument {
            name: WithLocation::generated(*HANLDE_ARGS_NAME),
            value: WithLocation::generated(Value::Object(handle_args)),
        });
    }

    Directive {
        name: WithLocation::generated(*HANDLE_FIELD_DIRECTIVE_NAME),
        arguments: directive_arguments,
        data: None,
    }
}

/// Helper to extract the handle field directive if present in the given list of
/// of directives
pub fn extract_handle_field_directives(
    directives: &[Directive],
) -> impl Iterator<Item = &Directive> {
    directives
        .iter()
        .filter(move |directive| directive.name.item == *HANDLE_FIELD_DIRECTIVE_NAME)
}

fn extract_handle_field_directive_args_helper(
    handle_field_directive: &Directive,
    handler_arg_name: ArgumentName,
) -> HandleFieldDirectiveArgs<'_> {
    let mut handler_arg = None;
    let mut key_arg = None;
    let mut filters_arg = None;
    let mut dynamic_key_arg = None;
    let mut handle_args_arg: Option<&Argument> = None;

    for arg in handle_field_directive.arguments.iter() {
        if arg.name.item == handler_arg_name {
            if let Value::Constant(constant_val) = &arg.value.item {
                handler_arg = Some((arg, constant_val));
            }
        } else if arg.name.item == *KEY_ARG_NAME {
            if let Value::Constant(constant_val) = &arg.value.item {
                key_arg = Some((arg, constant_val));
            }
        } else if arg.name.item == *FILTERS_ARG_NAME {
            if let Value::Constant(constant_val) = &arg.value.item {
                filters_arg = Some((arg, constant_val));
            }
        } else if arg.name.item == *DYNAMIC_KEY_ARG_NAME {
            if let Value::Variable(_) = arg.value.item {
                dynamic_key_arg = Some((arg, &arg.value.item));
            }
        } else if arg.name.item == *HANLDE_ARGS_NAME {
            handle_args_arg = Some(arg)
        }
    }

    HandleFieldDirectiveArgs {
        handler_arg,
        key_arg,
        filters_arg,
        dynamic_key_arg,
        handle_args_arg,
    }
}

fn extract_values_from_handle_field_directive_helper(
    handle_field_directive: &Directive,
    hanlder_arg_name: ArgumentName,
    default_handler: Option<StringKey>,
    default_filters: Option<Vec<StringKey>>,
) -> HandleFieldDirectiveValues {
    let HandleFieldDirectiveArgs {
        handler_arg,
        filters_arg,
        key_arg,
        dynamic_key_arg,
        handle_args_arg,
    } = extract_handle_field_directive_args_helper(handle_field_directive, hanlder_arg_name);

    // We expect these values to be available since they should've been
    // validated first as part of validate_connections validation step.
    let key = match key_arg {
        Some((_, value)) => value
            .get_string_literal()
            .expect("Expected key_arg to have been previously validated."),
        None => "".intern(),
    };
    let handle= match handler_arg {
         Some((_, value)) => value.get_string_literal().expect("Expected handler_arg to have been previously validated."),
         None => default_handler.expect("Expected handler_arg to have been previously validated or a default to have been provided."),
     };
    let filters = match filters_arg {
        Some((_, value)) => match value {
            ConstantValue::List(list_val) => Some(
                list_val
                    .iter()
                    .map(|val| {
                        val.get_string_literal()
                            .expect("Expected filters_arg to have been previously validated.")
                    })
                    .collect::<Vec<_>>(),
            ),
            ConstantValue::Null() => None,
            _ => unreachable!("Expected filters_arg to have been previously validated.",),
        },
        None => default_filters,
    };
    let dynamic_key = match dynamic_key_arg {
        Some((_, value)) => match value {
            Value::Variable(_) => Some(value.clone()),
            _ => unreachable!("Expected dynamic_key_arg to have been previously validated."),
        },
        None => None,
    };
    let handle_args = handle_args_arg.map(|arg| {
        if let Value::Object(args) = &arg.value.item {
            args.clone()
        } else {
            unreachable!("Expected handle_args to be 'Value::Object'.")
        }
    });

    HandleFieldDirectiveValues {
        handle,
        key,
        filters,
        dynamic_key,
        handle_args,
    }
}


/*
 * 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 handle_field_transform;
mod handle_field_util;

pub use handle_field_transform::handle_field_transform;
pub use handle_field_util::build_handle_field_directive;
pub use handle_field_util::build_handle_field_directive_from_connection_directive;
pub use handle_field_util::extract_handle_field_directive_args_for_connection;
pub use handle_field_util::extract_handle_field_directives;
pub use handle_field_util::extract_values_from_handle_field_directive;
pub use handle_field_util::HandleFieldDirectiveValues;
pub use handle_field_util::CONNECTION_HANDLER_ARG_NAME;
pub use handle_field_util::DYNAMIC_KEY_ARG_NAME;
pub use handle_field_util::FILTERS_ARG_NAME;
pub use handle_field_util::HANDLER_ARG_NAME;
pub use handle_field_util::HANDLE_FIELD_DIRECTIVE_NAME;
pub use handle_field_util::KEY_ARG_NAME;


/*
 * 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::DirectiveName;
use common::ScalarName;
use intern::string_key::Intern;
use intern::string_key::StringKey;
use lazy_static::lazy_static;

pub struct MatchConstants {
    pub js_field_type: ScalarName,
    pub js_field_id_arg: ArgumentName,
    pub js_field_branch_arg: ArgumentName,
    pub js_field_module_arg: ArgumentName,
    pub js_field_name: StringKey,
    pub key_arg: ArgumentName,
    pub module_directive_name: DirectiveName,
    pub match_directive_name: DirectiveName,
    pub supported_arg: ArgumentName,
    pub name_arg: ArgumentName,
}

lazy_static! {
    pub static ref MATCH_CONSTANTS: MatchConstants = MatchConstants {
        js_field_type: ScalarName("JSDependency".intern()),
        js_field_id_arg: ArgumentName("id".intern()),
        js_field_branch_arg: ArgumentName("branch".intern()),
        js_field_module_arg: ArgumentName("module".intern()),
        js_field_name: "js".intern(),
        key_arg: ArgumentName("key".intern()),
        match_directive_name: DirectiveName("match".intern()),
        module_directive_name: DirectiveName("module".intern()),
        supported_arg: ArgumentName("supported".intern()),
        name_arg: ArgumentName("name".intern()),
    };
}


/*
 * 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 graphql_ir::ConstantValue;
use graphql_ir::LinkedField;
use graphql_ir::Program;
use graphql_ir::Selection;
use graphql_ir::Transformed;
use graphql_ir::Transformer;
use graphql_ir::Value;
use intern::string_key::Intern;
use intern::Lookup;
use schema::SDLSchema;
use schema::Schema;
use schema::TypeReference;
use thiserror::Error;

use super::MATCH_CONSTANTS;
use crate::murmurhash::murmurhash;

pub fn hash_supported_argument(program: &Program) -> DiagnosticsResult<Program> {
    let mut transformer = HashSupportedArgumentTransform {
        schema: &program.schema,
        errors: Default::default(),
    };
    let next_program = transformer.transform_program(program);
    if transformer.errors.is_empty() {
        Ok(next_program.replace_or_else(|| program.clone()))
    } else {
        Err(transformer.errors)
    }
}

struct HashSupportedArgumentTransform<'a> {
    schema: &'a SDLSchema,
    errors: Vec<Diagnostic>,
}

impl<'a> Transformer for HashSupportedArgumentTransform<'a> {
    const NAME: &'static str = "HashSupportedArgumentTransform";

    const VISIT_ARGUMENTS: bool = false;

    const VISIT_DIRECTIVES: bool = false;

    fn transform_linked_field(&mut self, field: &LinkedField) -> Transformed<Selection> {
        let transformed_field = self.default_transform_linked_field(field);

        if !self.has_match_supported_arg(field) {
            return transformed_field;
        }

        let mut new_field = match transformed_field {
            Transformed::Keep => Arc::new(field.clone()),
            Transformed::Replace(Selection::LinkedField(linked_field)) => linked_field,
            Transformed::Delete | Transformed::Replace(_) => {
                panic!(
                    "unexpected transformed_field in HashSupportedArgumentTransform: {:?}",
                    transformed_field
                )
            }
        };

        let supported_arg = Arc::make_mut(&mut new_field)
            .arguments
            .iter_mut()
            .find(|arg| arg.name.item == MATCH_CONSTANTS.supported_arg)
            .expect("expected to find a supported argument as checked before");

        let mut input = String::new();
        match &supported_arg.value.item {
            Value::Constant(ConstantValue::List(items)) => {
                for item in items {
                    if let ConstantValue::String(name) = item {
                        input.push('\0');
                        input.push_str(name.lookup());
                    } else {
                        panic!("expected all supported arguments to be strings, as verified above");
                    }
                }
            }
            Value::Constant(ConstantValue::String(name)) => {
                // Single item lists can be written without the list wrapper per GraphQL spec
                input.push('\0');
                input.push_str(name.lookup());
            }
            _ => {
                self.errors.push(Diagnostic::error(
                    HashSupportedArgumentError::NonStaticSupportedArg,
                    supported_arg.value.location,
                ));
                return Transformed::Keep;
            }
        };

        let hash = murmurhash(&input);

        supported_arg.value.item = Value::Constant(ConstantValue::String(hash.intern()));

        Transformed::Replace(Selection::LinkedField(new_field))
    }
}

impl<'a> HashSupportedArgumentTransform<'a> {
    /// Returns true iff the field is supplied with a `supported` arg and that
    /// arg has a type of `[string]` (potentially non-nullable somewhere).
    fn has_match_supported_arg(&self, field: &LinkedField) -> bool {
        if field
            .arguments
            .named(MATCH_CONSTANTS.supported_arg)
            .is_none()
        {
            return false;
        }
        let supported_arg_def = self
            .schema
            .field(field.definition.item)
            .arguments
            .named(MATCH_CONSTANTS.supported_arg)
            .expect("field has supported arg, but missing from the schema");

        if let TypeReference::List(item_type) = supported_arg_def.type_.nullable_type() {
            if let TypeReference::Named(item_type_name) = item_type.nullable_type() {
                return self.schema.is_string(*item_type_name);
            }
        }
        false
    }
}

#[derive(Debug, Error)]
pub enum HashSupportedArgumentError {
    #[error(
        "Variables cannot be passed to the `supported` argument for data driven dependency fields, please use literal values like `\"ExampleValue\"`."
    )]
    NonStaticSupportedArg,
}