#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LashlangHostCatalog {
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    module_instances: BTreeMap<String, ModuleInstanceCatalog>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    resource_types: BTreeMap<String, ResourceTypeCatalog>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    named_data_types: BTreeMap<String, NamedDataType>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    value_constructors: BTreeMap<String, ValueConstructorBinding>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    trigger_sources: BTreeMap<String, TriggerSourceBinding>,
}

impl LashlangHostCatalog {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn tool_default(operations: impl IntoIterator<Item = impl Into<String>>) -> Self {
        let mut catalog = Self::new();
        for operation in operations {
            let operation = operation.into();
            catalog.add_module_operation(
                ["tools"],
                "Tools",
                operation.clone(),
                operation,
                TypeExpr::Any,
                TypeExpr::Any,
            );
        }
        catalog
    }

    pub fn add_module_instance(
        &mut self,
        module_path: impl IntoIterator<Item = impl Into<String>>,
        resource_type: impl Into<String>,
    ) -> Result<(), LashlangHostCatalogError> {
        let path = module_path.into_iter().map(Into::into).collect::<Vec<_>>();
        assert!(!path.is_empty(), "module path must not be empty");
        let resource_type = resource_type.into();
        let key = module_path_key(&path);
        if let Some(existing) = self.module_instances.get(&key) {
            if existing.resource_type != resource_type {
                return Err(LashlangHostCatalogError::ConflictingModuleInstance {
                    alias: key,
                    existing: existing.resource_type.clone(),
                    incoming: resource_type,
                });
            }
            self.ensure_resource_type(resource_type);
            return Ok(());
        }
        self.module_instances.insert(
            key.clone(),
            ModuleInstanceCatalog {
                path,
                resource_type: resource_type.clone(),
                alias: key,
                operations: BTreeMap::new(),
            },
        );
        self.ensure_resource_type(resource_type);
        Ok(())
    }

    pub fn ensure_resource_type(&mut self, resource_type: impl Into<String>) {
        self.resource_types.entry(resource_type.into()).or_default();
    }

    pub fn add_operation(
        &mut self,
        resource_type: impl Into<String>,
        operation: impl Into<String>,
        input_ty: TypeExpr,
        output_ty: TypeExpr,
    ) {
        self.resource_types
            .entry(resource_type.into())
            .or_default()
            .operations
            .insert(
                operation.into(),
                ResourceOperationBinding {
                    input_ty,
                    output_ty,
                },
            );
    }

    pub fn add_module_operation(
        &mut self,
        module_path: impl IntoIterator<Item = impl Into<String>>,
        resource_type: impl Into<String>,
        operation: impl Into<String>,
        host_operation: impl Into<String>,
        input_ty: TypeExpr,
        output_ty: TypeExpr,
    ) {
        let path = module_path.into_iter().map(Into::into).collect::<Vec<_>>();
        assert!(!path.is_empty(), "module path must not be empty");
        let resource_type = resource_type.into();
        let operation = operation.into();
        self.add_module_instance(path.iter().map(String::as_str), resource_type.clone())
            .expect("module operation resource type cannot conflict with existing module alias");
        self.add_operation(resource_type, operation.clone(), input_ty, output_ty);
        let key = module_path_key(&path);
        self.module_instances
            .get_mut(&key)
            .expect("module instance was just inserted")
            .operations
            .insert(
                operation,
                ModuleOperationBinding {
                    host_operation: host_operation.into(),
                },
            );
    }

    pub fn add_value_constructor(
        &mut self,
        path: impl IntoIterator<Item = impl Into<String>>,
        input_ty: TypeExpr,
        output_ty: TypeExpr,
    ) {
        let path = path.into_iter().map(Into::into).collect::<Vec<_>>();
        assert!(!path.is_empty(), "constructor path must not be empty");
        let key = module_path_key(&path);
        self.value_constructors.insert(
            key.clone(),
            ValueConstructorBinding {
                path,
                type_name: format_type_expr(&output_ty),
                input_ty,
                output_ty,
            },
        );
    }

    pub fn add_named_data_type(
        &mut self,
        data_type: NamedDataType,
    ) -> Result<(), LashlangHostCatalogError> {
        self.merge_named_data_type(data_type)
    }

    pub fn add_trigger_source_constructor(
        &mut self,
        path: impl IntoIterator<Item = impl Into<String>>,
        input_ty: TypeExpr,
        event_ty: NamedDataType,
    ) -> Result<(), LashlangHostCatalogError> {
        let path = path.into_iter().map(Into::into).collect::<Vec<_>>();
        assert!(!path.is_empty(), "constructor path must not be empty");
        let source_type = module_path_key(&path);
        self.check_named_data_type(&event_ty)?;
        if let Some(existing) = self.trigger_sources.get(source_type.as_str())
            && existing.event_type() != &event_ty
        {
            return Err(LashlangHostCatalogError::ConflictingTriggerSource {
                source_type,
                existing: existing.event_type().name().to_string(),
                incoming: event_ty.name().to_string(),
            });
        }
        self.add_value_constructor(path, input_ty, TypeExpr::Ref(source_type.clone().into()));
        self.add_trigger_source_type(source_type, event_ty)?;
        Ok(())
    }

    pub(crate) fn add_trigger_source_type(
        &mut self,
        source_ty: impl Into<String>,
        event_ty: NamedDataType,
    ) -> Result<(), LashlangHostCatalogError> {
        self.merge_named_data_type(event_ty.clone())?;
        self.trigger_sources
            .insert(source_ty.into(), TriggerSourceBinding::new(event_ty));
        Ok(())
    }

    pub fn extend(&mut self, other: Self) {
        self.try_extend(other)
            .expect("conflicting host catalog entries");
    }

    pub fn try_extend(&mut self, other: Self) -> Result<(), LashlangHostCatalogError> {
        for (resource_type, incoming) in other.resource_types {
            let entry = self.resource_types.entry(resource_type).or_default();
            entry.operations.extend(incoming.operations);
        }
        for (alias, incoming) in other.module_instances {
            match self.module_instances.get_mut(&alias) {
                Some(existing)
                    if existing.path == incoming.path
                        && existing.resource_type == incoming.resource_type
                        && existing.alias == incoming.alias =>
                {
                    existing.operations.extend(incoming.operations);
                }
                Some(existing) => {
                    return Err(LashlangHostCatalogError::ConflictingModuleInstance {
                        alias,
                        existing: existing.resource_type.clone(),
                        incoming: incoming.resource_type,
                    });
                }
                None => {
                    self.module_instances.insert(alias, incoming);
                }
            }
        }
        for data_type in other.named_data_types.into_values() {
            self.merge_named_data_type(data_type)?;
        }
        self.value_constructors.extend(other.value_constructors);
        self.trigger_sources.extend(other.trigger_sources);
        Ok(())
    }

    pub fn union(mut self, other: Self) -> Self {
        self.extend(other);
        self
    }

    pub fn satisfies(&self, required: &Self) -> bool {
        for (path, required_module) in &required.module_instances {
            let Some(module) = self.module_instances.get(path) else {
                return false;
            };
            if module.path != required_module.path
                || module.resource_type != required_module.resource_type
                || module.alias != required_module.alias
            {
                return false;
            }
            for (operation, required_binding) in &required_module.operations {
                if module.operations.get(operation) != Some(required_binding) {
                    return false;
                }
            }
        }
        for (resource_type, required_catalog) in &required.resource_types {
            let Some(catalog) = self.resource_types.get(resource_type) else {
                return false;
            };
            for (operation, required_binding) in &required_catalog.operations {
                if catalog.operations.get(operation) != Some(required_binding) {
                    return false;
                }
            }
        }
        for (path, required_constructor) in &required.value_constructors {
            if self.value_constructors.get(path) != Some(required_constructor) {
                return false;
            }
        }
        for (name, required_data_type) in &required.named_data_types {
            if self.named_data_types.get(name) != Some(required_data_type) {
                return false;
            }
        }
        for (source_type, required_binding) in &required.trigger_sources {
            if self.trigger_sources.get(source_type) != Some(required_binding) {
                return false;
            }
        }
        true
    }

    pub fn has_resource_type(&self, resource_type: &str) -> bool {
        self.resource_types.contains_key(resource_type)
    }

    pub fn has_named_data_type(&self, name: &str) -> bool {
        self.named_data_types.contains_key(name)
    }

    pub fn is_known_opaque_value_type(&self, name: &str) -> bool {
        self.trigger_sources.contains_key(name)
            || self.value_constructors.values().any(|constructor| {
                matches!(&constructor.output_ty, TypeExpr::Ref(type_name) if type_name == name)
            })
    }

    pub fn decode_host_descriptor_as<T: serde::de::DeserializeOwned>(
        &self,
        source_type: &str,
        value: serde_json::Value,
    ) -> Result<T, crate::HostDescriptorError> {
        if !self.is_known_opaque_value_type(source_type) {
            return Err(crate::HostDescriptorError::UnknownSourceType {
                source_type: source_type.to_string(),
            });
        }
        serde_json::from_value(value).map_err(|err| crate::HostDescriptorError::MalformedPayload {
            source_type: source_type.to_string(),
            message: err.to_string(),
        })
    }

    pub fn module_instances(&self) -> impl Iterator<Item = (&str, &ModuleInstanceCatalog)> {
        self.module_instances
            .iter()
            .map(|(path, module)| (path.as_str(), module))
    }

    pub fn resource_types(&self) -> impl Iterator<Item = (&str, &ResourceTypeCatalog)> {
        self.resource_types
            .iter()
            .map(|(resource_type, catalog)| (resource_type.as_str(), catalog))
    }

    pub fn named_data_types(&self) -> impl Iterator<Item = (&str, &NamedDataType)> {
        self.named_data_types
            .iter()
            .map(|(name, data_type)| (name.as_str(), data_type))
    }

    pub fn value_constructors(&self) -> impl Iterator<Item = (&str, &ValueConstructorBinding)> {
        self.value_constructors
            .iter()
            .map(|(path, constructor)| (path.as_str(), constructor))
    }

    pub fn trigger_sources(&self) -> impl Iterator<Item = (&str, &TriggerSourceBinding)> {
        self.trigger_sources
            .iter()
            .map(|(source_type, binding)| (source_type.as_str(), binding))
    }

    pub fn resolve_named_data_type(&self, name: &str) -> Option<&NamedDataType> {
        self.named_data_types.get(name)
    }

    pub fn resolve_trigger_source(&self, source_ty: &str) -> Option<&TriggerSourceBinding> {
        self.trigger_sources.get(source_ty)
    }

    pub fn resolve_module_path(&self, path: &[impl AsRef<str>]) -> Option<ResourceRefExpr> {
        let key = module_path_key(path);
        let module = self.module_instances.get(&key)?;
        Some(ResourceRefExpr::resolved(
            module
                .path
                .iter()
                .map(|segment| segment.as_str().into())
                .collect(),
            module.resource_type.clone(),
            module.alias.clone(),
        ))
    }

    pub fn resolve_alias(&self, resource: &ResourceRefExpr) -> Option<&ResourceTypeCatalog> {
        if !resource.resource_type.is_empty() {
            return self.resource_types.get(resource.resource_type.as_str());
        }
        let resolved = self.resolve_module_path(&resource.path)?;
        self.resource_types.get(resolved.resource_type.as_str())
    }

    pub fn resolve_operation(
        &self,
        resource_type: &str,
        operation: &str,
    ) -> Option<&ResourceOperationBinding> {
        self.resource_types
            .get(resource_type)?
            .operations
            .get(operation)
    }

    pub fn has_operations(&self) -> bool {
        self.resource_types
            .values()
            .any(|resource_type| !resource_type.operations.is_empty())
    }

    pub fn resolve_module_operation(
        &self,
        resource_type: &str,
        alias: &str,
        operation: &str,
    ) -> Option<&ModuleOperationBinding> {
        let module = self.module_instances.get(alias)?;
        (module.resource_type == resource_type).then_some(())?;
        module.operations.get(operation)
    }

    pub fn resolve_value_constructor(
        &self,
        path: &[impl AsRef<str>],
    ) -> Option<&ValueConstructorBinding> {
        self.value_constructors.get(&module_path_key(path))
    }

    pub fn trigger_source_event(&self, source_ty: &TypeExpr) -> Option<TypeExpr> {
        let TypeExpr::Ref(name) = source_ty else {
            return None;
        };
        self.trigger_sources
            .get(name.as_str())
            .map(|binding| binding.event_type().to_ref_ty())
    }

    pub fn operation_suggestions_for_host(&self, host_operation: &str) -> Vec<String> {
        let mut suggestions = Vec::new();
        for module in self.module_instances.values() {
            for (operation, binding) in &module.operations {
                if binding.host_operation == host_operation {
                    suggestions.push(format!("{}.{}", module.alias, operation));
                }
            }
        }
        suggestions.sort();
        suggestions.dedup();
        suggestions
    }

    pub fn operation_suggestions_for_prefix(
        &self,
        prefix: &[impl AsRef<str>],
        operation: &str,
    ) -> Vec<String> {
        let prefix = module_path_key(prefix);
        let mut suggestions = Vec::new();
        for module in self.module_instances.values() {
            if module.alias == prefix || !module.alias.starts_with(&format!("{prefix}.")) {
                continue;
            }
            if self
                .resolve_operation(&module.resource_type, operation)
                .is_some()
            {
                suggestions.push(format!("{}.{}", module.alias, operation));
            }
        }
        suggestions.sort();
        suggestions.dedup();
        suggestions
    }

    fn check_named_data_type(
        &self,
        data_type: &NamedDataType,
    ) -> Result<(), LashlangHostCatalogError> {
        if let Some(existing) = self.named_data_types.get(data_type.name())
            && existing != data_type
        {
            return Err(LashlangHostCatalogError::ConflictingNamedDataType {
                name: data_type.name().to_string(),
            });
        }
        Ok(())
    }

    fn merge_named_data_type(
        &mut self,
        data_type: NamedDataType,
    ) -> Result<(), LashlangHostCatalogError> {
        self.check_named_data_type(&data_type)?;
        self.named_data_types
            .entry(data_type.name().to_string())
            .or_insert(data_type);
        Ok(())
    }
}
