//! Enumerate the concrete table identifiers a templated table path can refer to.

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

use crate::err::TemplateSubstitutionError;
use crate::tree::ast::clause::TemplatedTablePath;
use crate::tree::ast::dataset_identifier::DatasetIdentifier;
use crate::tree::ast::identifier::SimpleIdentifier;

use super::{
    cartesian_template_values, materialize_templated_table_path, ordered_params_in_templated_path,
    MAX_TABLE_PATH_EXPANSIONS,
};

/// Return all concrete [`DatasetIdentifier`]s a templated table path can refer to (the Cartesian product
/// of each parameter's `IdentifierFragment` domain), bounded by [`MAX_TABLE_PATH_EXPANSIONS`].
///
/// `lookup_domain` resolves a parameter name to its enum domain. It is the caller's job to surface
/// "not declared" / "primitive parameter used in a path" failures as
/// [`TemplateSubstitutionError`]; this function does not assume how those declarations are stored
/// (typecheck context vs. raw spec map vs. anything else), it only knows how to walk the path.
pub fn expand_templated_table_path<F>(
    path: &TemplatedTablePath,
    lookup_domain: F,
) -> Result<Vec<DatasetIdentifier>, TemplateSubstitutionError>
where
    F: Fn(&SimpleIdentifier) -> Result<Arc<[String]>, TemplateSubstitutionError>,
{
    let param_order = ordered_params_in_templated_path(path)?;

    let mut domain_lists: Vec<Arc<[String]>> = Vec::with_capacity(param_order.len());
    for p in &param_order {
        let domain = lookup_domain(p)?;
        if domain.is_empty() {
            return Err(TemplateSubstitutionError::EmptyIdentifierFragmentDomain(
                p.to_string(),
            ));
        }
        domain_lists.push(domain);
    }

    let mut product: usize = 1;
    for d in &domain_lists {
        product = product.saturating_mul(d.len());
        if product > MAX_TABLE_PATH_EXPANSIONS {
            return Err(TemplateSubstitutionError::TooManyExpansions(
                MAX_TABLE_PATH_EXPANSIONS,
            ));
        }
    }

    let combos = cartesian_template_values(&domain_lists);
    let mut out = Vec::with_capacity(combos.len());
    for combo in combos {
        let mut subst_map: HashMap<SimpleIdentifier, String> = HashMap::new();
        for (p, v) in param_order.iter().zip(combo.iter()) {
            subst_map.insert(p.clone(), v.clone());
        }
        out.push(materialize_templated_table_path(path, &subst_map)?);
    }
    Ok(out)
}
