use std::ops::Deref;
use std::sync::Arc;

use ahash::HashMap;
use ahash::HashMapExt;
use apollo_compiler::Name;
use apollo_compiler::Schema;
use apollo_compiler::ast::InputValueDefinition;
use apollo_compiler::schema::ExtendedType;
use apollo_compiler::validation::Valid;
use apollo_federation::link::cost_spec_definition::CostDirective;
use apollo_federation::link::cost_spec_definition::CostSpecDefinition;
use apollo_federation::link::cost_spec_definition::ListSizeDirective;
use apollo_federation::schema::ValidFederationSchema;

use crate::plugins::demand_control::DemandControlError;
use crate::plugins::demand_control::cost_calculator::directives::RequiresDirective;
use crate::plugins::demand_control::cost_calculator::directives::SizedFields;

pub(in crate::plugins::demand_control) struct InputDefinition {
    name: Name,
    ty: ExtendedType,
    cost_directive: Option<CostDirective>,
}

impl InputDefinition {
    fn new(
        schema: &ValidFederationSchema,
        field_definition: &InputValueDefinition,
    ) -> Result<Self, DemandControlError> {
        let field_type = schema
            .schema()
            .types
            .get(field_definition.ty.inner_named_type())
            .ok_or_else(|| {
                DemandControlError::QueryParseFailure(format!(
                    "Field {} was found in query, but its type is missing from the schema.",
                    field_definition.name
                ))
            })?;
        let processed_inputs = InputDefinition {
            name: field_definition.name.clone(),
            ty: field_type.clone(),
            cost_directive: CostSpecDefinition::cost_directive_from_argument(
                schema,
                field_definition,
                field_type,
            )?,
        };

        Ok(processed_inputs)
    }

    pub(in crate::plugins::demand_control) fn name(&self) -> &Name {
        &self.name
    }

    pub(in crate::plugins::demand_control) fn ty(&self) -> &ExtendedType {
        &self.ty
    }

    pub(in crate::plugins::demand_control) fn cost_directive(&self) -> Option<&CostDirective> {
        self.cost_directive.as_ref()
    }
}

/// One `@listSize` directive and its pre-parsed sizedFields (for demand control cost calculation).
/// `parsed_sized_fields` is `None` when the directive has no `sizedFields` argument.
pub(in crate::plugins::demand_control) struct ListSizeDirectiveEntry {
    pub(in crate::plugins::demand_control) directive: ListSizeDirective,
    pub(in crate::plugins::demand_control) parsed_sized_fields: Option<Arc<SizedFields>>,
}

pub(in crate::plugins::demand_control) struct FieldDefinition {
    ty: ExtendedType,
    cost_directive: Option<CostDirective>,
    /// All `@listSize` directives on this field, each with its parsed sizedFields (if any).
    list_size_directive_entries: Vec<ListSizeDirectiveEntry>,
    requires_directive: Option<RequiresDirective>,
    arguments: HashMap<Name, InputDefinition>,
}

impl FieldDefinition {
    fn new(
        schema: &ValidFederationSchema,
        parent_type_name: &Name,
        field_definition: &apollo_compiler::ast::FieldDefinition,
    ) -> Result<Self, DemandControlError> {
        let field_type = schema
            .schema()
            .types
            .get(field_definition.ty.inner_named_type())
            .ok_or_else(|| {
                DemandControlError::QueryParseFailure(format!(
                    "Field {} was found in query, but its type is missing from the schema.",
                    field_definition.name,
                ))
            })?;
        let cost_directive =
            CostSpecDefinition::cost_directive_from_field(schema, field_definition, field_type)?;
        let directives = CostSpecDefinition::list_size_directives_from_field_definition(
            schema,
            field_definition,
        )?;
        let mut list_size_directive_entries = Vec::with_capacity(directives.len());
        for directive in directives {
            let parsed_sized_fields = match &directive.sized_fields {
                Some(field_names) => Some(Arc::new(SizedFields::from_strings(
                    schema.schema(),
                    field_definition.ty.inner_named_type(),
                    field_names,
                )?)),
                None => None,
            };
            list_size_directive_entries.push(ListSizeDirectiveEntry {
                directive,
                parsed_sized_fields,
            });
        }
        let requires_directive = RequiresDirective::from_field_definition(
            field_definition,
            parent_type_name,
            schema.schema(),
        )?;

        let mut arguments = HashMap::new();
        for argument in &field_definition.arguments {
            arguments.insert(
                argument.name.clone(),
                InputDefinition::new(schema, argument)?,
            );
        }

        Ok(Self {
            ty: field_type.clone(),
            cost_directive,
            list_size_directive_entries,
            requires_directive,
            arguments,
        })
    }

    pub(in crate::plugins::demand_control) fn ty(&self) -> &ExtendedType {
        &self.ty
    }

    pub(in crate::plugins::demand_control) fn cost_directive(&self) -> Option<&CostDirective> {
        self.cost_directive.as_ref()
    }

    /// All `@listSize` directives on this field, each with its parsed sizedFields (if any).
    pub(in crate::plugins::demand_control) fn list_size_directive_entries(
        &self,
    ) -> &[ListSizeDirectiveEntry] {
        &self.list_size_directive_entries
    }

    pub(in crate::plugins::demand_control) fn requires_directive(
        &self,
    ) -> Option<&RequiresDirective> {
        self.requires_directive.as_ref()
    }

    pub(in crate::plugins::demand_control) fn argument_by_name(
        &self,
        argument_name: &str,
    ) -> Option<&InputDefinition> {
        self.arguments.get(argument_name)
    }
}

pub(crate) struct DemandControlledSchema {
    inner: ValidFederationSchema,
    input_field_definitions: HashMap<Name, HashMap<Name, InputDefinition>>,
    output_field_definitions: HashMap<Name, HashMap<Name, FieldDefinition>>,
}

impl DemandControlledSchema {
    pub(crate) fn new(schema: Arc<Valid<Schema>>) -> Result<Self, DemandControlError> {
        let fed_schema = ValidFederationSchema::new((*schema).clone())?;
        let mut input_field_definitions: HashMap<Name, HashMap<Name, InputDefinition>> =
            HashMap::with_capacity(schema.types.len());
        let mut output_field_definitions: HashMap<Name, HashMap<Name, FieldDefinition>> =
            HashMap::with_capacity(schema.types.len());

        for (type_name, type_) in &schema.types {
            match type_ {
                ExtendedType::Interface(ty) => {
                    let type_fields = output_field_definitions
                        .entry(type_name.clone())
                        .or_insert_with(|| HashMap::with_capacity(ty.fields.len()));
                    for (field_name, field_definition) in &ty.fields {
                        type_fields.insert(
                            field_name.clone(),
                            FieldDefinition::new(&fed_schema, type_name, field_definition)?,
                        );
                    }
                }
                ExtendedType::Object(ty) => {
                    let type_fields = output_field_definitions
                        .entry(type_name.clone())
                        .or_insert_with(|| HashMap::with_capacity(ty.fields.len()));
                    for (field_name, field_definition) in &ty.fields {
                        type_fields.insert(
                            field_name.clone(),
                            FieldDefinition::new(&fed_schema, type_name, field_definition)?,
                        );
                    }
                }
                ExtendedType::InputObject(ty) => {
                    let type_fields = input_field_definitions
                        .entry(type_name.clone())
                        .or_insert_with(|| HashMap::with_capacity(ty.fields.len()));
                    for (field_name, field_definition) in &ty.fields {
                        type_fields.insert(
                            field_name.clone(),
                            InputDefinition::new(&fed_schema, field_definition)?,
                        );
                    }
                }
                _ => {
                    // Other types don't have fields
                }
            }
        }

        input_field_definitions.shrink_to_fit();
        output_field_definitions.shrink_to_fit();

        Ok(Self {
            inner: fed_schema,
            input_field_definitions,
            output_field_definitions,
        })
    }

    pub(crate) fn empty(schema: Arc<Valid<Schema>>) -> Result<Self, DemandControlError> {
        let fed_schema = ValidFederationSchema::new((*schema).clone())?;
        Ok(Self {
            inner: fed_schema,
            input_field_definitions: Default::default(),
            output_field_definitions: Default::default(),
        })
    }

    pub(in crate::plugins::demand_control) fn input_field_definition(
        &self,
        type_name: &str,
        field_name: &str,
    ) -> Option<&InputDefinition> {
        self.input_field_definitions.get(type_name)?.get(field_name)
    }

    pub(in crate::plugins::demand_control) fn output_field_definition(
        &self,
        type_name: &str,
        field_name: &str,
    ) -> Option<&FieldDefinition> {
        self.output_field_definitions
            .get(type_name)?
            .get(field_name)
    }
}

impl AsRef<Valid<Schema>> for DemandControlledSchema {
    fn as_ref(&self) -> &Valid<Schema> {
        self.inner.schema()
    }
}

impl Deref for DemandControlledSchema {
    type Target = Schema;

    fn deref(&self) -> &Self::Target {
        self.inner.schema()
    }
}
