use indexmap::IndexSet;
use tracing::trace;

use crate::error::CompositionError;
use crate::error::FederationError;
use crate::merger::merge::Merger;
use crate::schema::position::InterfaceTypeDefinitionPosition;
use crate::schema::position::ObjectOrInterfaceTypeDefinitionPosition;
use crate::schema::position::ObjectTypeDefinitionPosition;
use crate::schema::position::TypeDefinitionPosition;
use crate::utils::human_readable::human_readable_types;

impl Merger {
    // Returns whether the interface has a key (even a non-resolvable one) in any subgraph.
    fn validate_interface_keys(
        &mut self,
        dest: &InterfaceTypeDefinitionPosition,
    ) -> Result<bool, FederationError> {
        trace!("Validating interface keys");
        // Remark: it might be ok to filter @inaccessible types in `supergraphImplementations`, but this requires
        // some more thinking (and I'm not even sure it makes a practical difference given the rules for validity
        // of @inaccessible) and it will be backward compatible to filter them later, while the reverse wouldn't
        // technically be, so we stay on the safe side.
        let supergraph_implementations = self.merged.possible_runtime_types(dest.clone().into())?;

        // Validate that if a source defines a (resolvable) @key on an interface, then that subgraph defines
        // all the implementations of that interface in the supergraph.
        let mut has_key = false;
        for subgraph in self.subgraphs.iter() {
            if !subgraph.schema().is_interface(&dest.type_name) {
                continue;
            }

            let Some(key_directive_name) = subgraph.key_directive_name()? else {
                continue;
            };
            let interface_pos: TypeDefinitionPosition = dest.clone().into();
            let keys = interface_pos.get_applied_directives(subgraph.schema(), &key_directive_name);
            has_key = has_key || !keys.is_empty();
            let federation_spec_definition = subgraph.metadata().federation_spec_definition();
            let Some(resolvable_key) = keys.iter().find(|key| {
                federation_spec_definition
                    .key_directive_arguments(key)
                    .map(|args| args.resolvable)
                    .unwrap_or(true) // @key(resolvable:) defaults to true in its definition
            }) else {
                continue;
            };
            let implementations_in_subgraph = subgraph
                .schema()
                .possible_runtime_types(dest.clone().into())?;
            let missing_implementations: IndexSet<_> = supergraph_implementations
                .difference(&implementations_in_subgraph)
                .collect();
            let subgraph_name = &subgraph.name;
            if !missing_implementations.is_empty() {
                self.error_reporter.add_error(CompositionError::InterfaceKeyMissingImplementationType {
                            message: format!("[{subgraph_name}] Interface type \"{dest}\" has a resolvable key ({}) in subgraph \"{subgraph_name}\" but that subgraph is missing some of the supergraph implementation types of \"{dest}\". Subgraph \"{subgraph_name}\" should define {} (and have {} implement \"{dest}\").",
                                resolvable_key.serialize(),
                                human_readable_types(missing_implementations.iter().map(|impl_type| &impl_type.type_name)),
                                if missing_implementations.len() > 1 { "them" } else { "it" },
                            )
                        });
            }
        }
        Ok(has_key)
    }

    fn validate_interface_objects(
        &mut self,
        dest: &InterfaceTypeDefinitionPosition,
    ) -> Result<(), FederationError> {
        trace!("Validating interface objects");
        let supergraph_implementations = self.merged.possible_runtime_types(dest.clone().into())?;

        // Validates that if a source defines the interface as an @interfaceObject, then it doesn't define any
        // of the implementations. We can discuss if there is ways to lift that limitation later, but an
        // @interfaceObject already "provides" fields for all the underlying impelmentations, so also defining
        // one those implementation would require additional care for shareability and more. This also feel
        // like this can get easily be done by mistake and gets rather confusing, so it's worth some additional
        // consideration before allowing.
        for subgraph in self.subgraphs.iter() {
            // If `dest` has an interface object in this subgraph, it will be an object type (not
            // an interface). So we have to check for the object equivalent of `dest` instead of
            // checking `dest` directly.
            let ty_as_obj = ObjectTypeDefinitionPosition {
                type_name: dest.type_name.clone(),
            };
            if !subgraph.is_interface_object_type(&ty_as_obj.into()) {
                continue;
            }

            let subgraph_name = &subgraph.name;
            let defined_implementations: IndexSet<_> = supergraph_implementations
                .iter()
                .filter(|implementation| implementation.get(subgraph.schema().schema()).is_ok())
                .collect::<IndexSet<_>>();
            if !defined_implementations.is_empty() {
                self.error_reporter.add_error(CompositionError::InterfaceObjectUsageError {
                    message: format!("[{subgraph_name}] Interface type \"{dest}\" is defined as an @interfaceObject in subgraph \"{subgraph_name}\" so that subgraph should not define any of the implementation types of \"{dest}\", but it defines {}",
                        human_readable_types(defined_implementations.iter().map(|impl_type| &impl_type.type_name)),
                    )
                });
            }
        }

        Ok(())
    }

    pub(crate) fn merge_interface(
        &mut self,
        itf: InterfaceTypeDefinitionPosition,
    ) -> Result<(), FederationError> {
        let has_key = self.validate_interface_keys(&itf)?;
        self.validate_interface_objects(&itf)?;

        let added = self.add_fields_shallow(itf.clone())?;

        for (dest_field, subgraph_fields) in added {
            if !has_key {
                let subgraph_types = self
                    .subgraphs
                    .iter()
                    .enumerate()
                    .map(|(idx, subgraph)| {
                        let maybe_ty: Option<ObjectOrInterfaceTypeDefinitionPosition> = subgraph
                            .schema()
                            .get_type(itf.type_name.clone())
                            .ok()
                            .and_then(|ty| ty.try_into().ok());
                        (idx, maybe_ty)
                    })
                    .collect();
                self.hint_on_inconsistent_value_type_field(
                    &subgraph_types,
                    &ObjectOrInterfaceTypeDefinitionPosition::Interface(itf.clone()),
                    &dest_field,
                )?;
            }
            let merge_context = self.validate_override(&subgraph_fields, &dest_field)?;
            trace!("Merging interface field {}", dest_field.field_name());
            self.merge_field(&subgraph_fields, &dest_field, &merge_context)?;
        }
        Ok(())
    }
}
