/*
SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
*/

//! Message evaluation for parameterized locale strings.
//!
//! This module provides the `MessageEvaluator` trait for evaluating
//! ICU MessageFormat messages at runtime. The trait acts as a seam
//! for future ICU4X migration, allowing different evaluator implementations
//! to be swapped without changing call sites.

/// Arguments passed to message evaluation.
///
/// Contains optional named variables that may be referenced in a message.
/// The engine populates these based on rendering context.
#[derive(Debug, Clone, Default)]
pub struct MessageArgs<'a> {
    /// Numeric count for plural dispatch.
    pub count: Option<u64>,
    /// Named string variable (e.g., a name list or URL).
    pub value: Option<&'a str>,
    /// Gender for select dispatch (MF2 `select` with gender keys).
    pub gender: Option<&'a str>,
    /// Pre-formatted name list string.
    pub names: Option<&'a str>,
    /// Start of a range (e.g., page range start).
    pub start: Option<&'a str>,
    /// End of a range (e.g., page range end).
    pub end: Option<&'a str>,
    /// URL string.
    pub url: Option<&'a str>,
    /// Pre-formatted date string.
    pub date: Option<&'a str>,
    /// Year component of a date (e.g. `"2023"`).
    pub year: Option<&'a str>,
    /// Month component of a date, already inflected for this locale's date
    /// context (e.g. `"January"`, `"urtarrila"`).
    pub month: Option<&'a str>,
    /// Day-of-month component of a date (e.g. `"12"`).
    pub day: Option<&'a str>,
    /// Main contributor list for "et al." patterns.
    pub main_list: Option<&'a str>,
}

/// Evaluates a parameterized message string with runtime arguments.
///
/// # ICU4X swap path
///
/// This trait is the seam for future ICU4X migration. When
/// `icu_message_format` (ICU4X) reaches stable, replace `Mf2MessageEvaluator`
/// with an ICU4X-backed struct implementing this same trait.
/// See bean `csl26-qrpo` and <https://github.com/unicode-org/icu4x/issues/3028>.
///
/// # Implementation notes
///
/// Implementations are expected to be cheap to clone or wrap in `Arc<T>`
/// for concurrent rendering contexts.
pub trait MessageEvaluator: Send + Sync {
    /// Evaluate a message with the provided arguments.
    ///
    /// Returns `Some(result)` on successful evaluation, or `None` if:
    /// - The message body is unparseable as MF2
    /// - A required variable is missing from `args`
    /// - An unrecoverable error occurs
    ///
    /// The caller (engine) provides fallback behavior (e.g., returning a
    /// legacy term or a bare message ID) on `None`.
    fn evaluate(&self, message: &str, args: &MessageArgs<'_>) -> Option<String>;
}

/// MF2 message evaluator for ICU MessageFormat 2 syntax.
///
/// Evaluates MF2 messages with `.match` statements and variable substitution
/// without external dependencies.
#[derive(Debug, Clone)]
pub struct Mf2MessageEvaluator;

impl MessageEvaluator for Mf2MessageEvaluator {
    fn evaluate(&self, message: &str, args: &MessageArgs<'_>) -> Option<String> {
        let trimmed = message.trim();

        if trimmed.starts_with(".match") {
            evaluate_mf2_matcher(trimmed, args)
        } else {
            substitute_mf2_vars(trimmed, args)
        }
    }
}

/// Substitute `{$var}` references in a simple MF2 pattern.
///
/// Variable names resolve directly from `MessageArgs` fields.
/// Returns `None` if a referenced variable is missing.
fn substitute_mf2_vars(pattern: &str, args: &MessageArgs<'_>) -> Option<String> {
    if !pattern.contains('{') {
        return Some(pattern.to_string());
    }

    let mut result = String::new();
    let mut cursor = 0usize;

    while let Some(offset) = pattern.get(cursor..).and_then(|s| s.find('{')) {
        let open = cursor + offset;
        #[allow(
            clippy::string_slice,
            reason = "cursor and open are valid char boundaries"
        )]
        result.push_str(&pattern[cursor..open]);

        let close = find_matching_brace(pattern, open)?;
        let inner = pattern.get(open + 1..close)?.trim();

        if !inner.starts_with('$') {
            return None;
        }

        #[allow(clippy::string_slice, reason = "inner starts with '$' (1-byte ASCII)")]
        let var_name = &inner[1..];
        let var_value = resolve_var(var_name, args)?;
        result.push_str(var_value);
        cursor = close + 1;
    }

    #[allow(clippy::string_slice, reason = "cursor is a valid char boundary")]
    result.push_str(&pattern[cursor..]);
    Some(result)
}

/// Resolve a variable name to its value in `MessageArgs`.
fn resolve_var<'a>(var_name: &str, args: &'a MessageArgs<'a>) -> Option<&'a str> {
    match var_name {
        "value" => args.value,
        "gender" => args.gender,
        "names" => args.names,
        "start" => args.start,
        "end" => args.end,
        "url" => args.url,
        "date" => args.date,
        "year" => args.year,
        "month" => args.month,
        "day" => args.day,
        "main_list" => args.main_list,
        _ => None,
    }
}

/// Evaluate a `.match` statement with selectors and variants.
fn evaluate_mf2_matcher(message: &str, args: &MessageArgs<'_>) -> Option<String> {
    let trimmed = message.trim();
    if !trimmed.starts_with(".match") {
        return None;
    }

    let (selectors, variants_start) = parse_mf2_selectors(trimmed)?;
    let match_keys = selectors
        .iter()
        .map(|(var_name, function)| determine_match_key(var_name, *function, args))
        .collect::<Option<Vec<_>>>()?;
    let matched_pattern = find_mf2_variant(variants_start, &match_keys)?;

    substitute_mf2_vars(&matched_pattern, args)
}

/// Parse selector expressions after `.match`.
fn parse_mf2_selectors(message: &str) -> Option<(Vec<(&str, Option<&str>)>, &str)> {
    #[allow(clippy::string_slice, reason = "'.match' is 1-byte ASCII")]
    let mut rest = message[".match".len()..].trim_start();
    let mut selectors = Vec::new();

    while rest.starts_with('{') {
        let close_brace = find_matching_brace(rest, 0)?;
        let selector_text = rest.get(1..close_brace)?.trim();
        selectors.push(parse_mf2_selector(selector_text)?);
        rest = rest.get(close_brace + 1..)?.trim_start();
    }

    if selectors.is_empty() {
        return None;
    }

    Some((selectors, rest))
}

/// Parse an MF2 selector like `$count :plural` or `$gender :select`.
///
/// Returns `(variable_name, optional_function)`.
fn parse_mf2_selector(selector: &str) -> Option<(&str, Option<&str>)> {
    let parts: Vec<&str> = selector.split_whitespace().collect();
    let var_name = parts.first()?.strip_prefix('$')?;

    let function = parts
        .get(1)
        .and_then(|func_part| func_part.strip_prefix(':'));

    Some((var_name, function))
}

/// Determine the match key based on variable value and function.
fn determine_match_key(
    var_name: &str,
    function: Option<&str>,
    args: &MessageArgs<'_>,
) -> Option<String> {
    match function {
        Some("plural") => {
            // :plural dispatch is only valid for $count
            if var_name != "count" {
                return None;
            }
            let count = args.count?;
            if count == 1 {
                Some("one".to_string())
            } else {
                Some("*".to_string())
            }
        }
        Some("select") | None => {
            let value = match var_name {
                "count" => args.count.map(|c| c.to_string()),
                "value" => args.value.map(|s| s.to_string()),
                "gender" => args.gender.map(|s| s.to_string()),
                "names" => args.names.map(|s| s.to_string()),
                "start" => args.start.map(|s| s.to_string()),
                "end" => args.end.map(|s| s.to_string()),
                "url" => args.url.map(|s| s.to_string()),
                "date" => args.date.map(|s| s.to_string()),
                "year" => args.year.map(|s| s.to_string()),
                "month" => args.month.map(|s| s.to_string()),
                "day" => args.day.map(|s| s.to_string()),
                "main_list" => args.main_list.map(|s| s.to_string()),
                _ => None,
            }?;
            Some(value)
        }
        _ => None,
    }
}

/// Find the matched variant in MF2 when-blocks and return its pattern.
///
/// Scans for `when <keys...> { pattern }` lines. Returns the most specific
/// wildcard-compatible variant.
fn find_mf2_variant(variants_text: &str, match_keys: &[String]) -> Option<String> {
    let mut best_match: Option<(usize, String)> = None;
    let mut rest = variants_text;

    loop {
        let trimmed = rest.trim_start();
        if trimmed.is_empty() {
            break;
        }

        if !trimmed.starts_with("when") {
            break;
        }

        #[allow(clippy::string_slice, reason = "'when' is 1-byte ASCII")]
        let after_when = trimmed["when".len()..].trim_start();
        let brace_pos = after_when.find('{')?;
        #[allow(clippy::string_slice, reason = "brace_pos is found via find('{')")]
        let key_str = after_when[..brace_pos].trim();
        let variant_keys: Vec<&str> = key_str.split_whitespace().collect();

        let open_brace_index = rest.len() - after_when.len() + brace_pos;
        let close_brace_index = find_matching_brace(rest, open_brace_index)?;
        let pattern = rest
            .get(open_brace_index + 1..close_brace_index)?
            .to_string();

        if let Some(score) = variant_match_score(&variant_keys, match_keys)
            && match best_match.as_ref() {
                Some((best_score, _)) => score > *best_score,
                None => true,
            }
        {
            best_match = Some((score, pattern));
        }

        rest = rest.get(close_brace_index + 1..)?;
    }

    best_match.map(|(_, pattern)| pattern)
}

/// Score a variant key tuple against the resolved selector keys.
fn variant_match_score(variant_keys: &[&str], match_keys: &[String]) -> Option<usize> {
    if variant_keys.len() != match_keys.len() {
        return None;
    }

    let mut score = 0usize;
    for (variant_key, match_key) in variant_keys.iter().zip(match_keys) {
        if *variant_key == "*" {
            continue;
        }
        if *variant_key != match_key {
            return None;
        }
        score += 1;
    }

    Some(score)
}

/// Find the matching closing brace for an opening brace at a given index.
fn find_matching_brace(input: &str, open_index: usize) -> Option<usize> {
    let mut depth = 0usize;

    for (index, ch) in input
        .char_indices()
        .skip_while(|(index, _)| *index < open_index)
    {
        match ch {
            '{' => depth += 1,
            '}' => {
                depth = depth.checked_sub(1)?;
                if depth == 0 {
                    return Some(index);
                }
            }
            _ => {}
        }
    }

    None
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::indexing_slicing,
    clippy::todo,
    clippy::unimplemented,
    clippy::unreachable,
    clippy::get_unwrap,
    reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
    use super::*;

    #[test]
    fn test_static_message() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs::default();
        let result = evaluator.evaluate("and", &args);
        assert_eq!(result, Some("and".to_string()));
    }

    #[test]
    fn test_simple_variable() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            value: Some("Smith"),
            ..Default::default()
        };
        let result = evaluator.evaluate("retrieved from {$url}", &args);
        // Should return None because url is not set
        assert_eq!(result, None);

        let args = MessageArgs {
            url: Some("https://example.com"),
            ..Default::default()
        };
        let result = evaluator.evaluate("retrieved from {$url}", &args);
        assert_eq!(
            result,
            Some("retrieved from https://example.com".to_string())
        );
    }

    #[test]
    fn test_plural_one() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            count: Some(1),
            ..Default::default()
        };
        let message = ".match {$count :plural}\nwhen one {p.}\nwhen * {pp.}";
        assert_eq!(evaluator.evaluate(message, &args), Some("p.".to_string()));
    }

    #[test]
    fn test_plural_other() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            count: Some(5),
            ..Default::default()
        };
        let message = ".match {$count :plural}\nwhen one {p.}\nwhen * {pp.}";
        assert_eq!(evaluator.evaluate(message, &args), Some("pp.".to_string()));
    }

    #[test]
    fn test_select() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            gender: Some("masc"),
            ..Default::default()
        };
        let message = ".match {$gender :select}\nwhen masc {él}\nwhen fem {ella}\nwhen * {elle}";
        assert_eq!(evaluator.evaluate(message, &args), Some("él".to_string()));
    }

    #[test]
    fn test_select_fallback_wildcard() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            gender: Some("neuter"),
            ..Default::default()
        };
        let message = ".match {$gender :select}\nwhen masc {él}\nwhen fem {ella}\nwhen * {elle}";
        assert_eq!(evaluator.evaluate(message, &args), Some("elle".to_string()));
    }

    #[test]
    fn test_mixed_text_and_variable() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            url: Some("https://example.com"),
            ..Default::default()
        };
        let result = evaluator.evaluate("retrieved from {$url}", &args);
        assert_eq!(
            result,
            Some("retrieved from https://example.com".to_string())
        );
    }

    #[test]
    fn test_date_component_substitution() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            year: Some("2023"),
            month: Some("urtarrila"),
            day: Some("12"),
            ..Default::default()
        };
        assert_eq!(
            evaluator.evaluate("{$year}ko {$month}ren {$day}a", &args),
            Some("2023ko urtarrilaren 12a".to_string())
        );
        assert_eq!(
            evaluator.evaluate("{$month} {$day}", &args),
            Some("urtarrila 12".to_string())
        );
    }

    #[test]
    fn test_date_component_missing_returns_none() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            year: Some("2023"),
            month: Some("urtarrila"),
            ..Default::default()
        };
        // day is required but absent
        assert_eq!(
            evaluator.evaluate("{$year}ko {$month}ren {$day}a", &args),
            None
        );
    }

    #[test]
    fn test_missing_variable_plural() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs::default();
        let message = ".match {$count :plural}\nwhen one {p.}\nwhen * {pp.}";
        let result = evaluator.evaluate(message, &args);
        // Should return None when count is missing
        assert_eq!(result, None);
    }

    #[test]
    fn test_multi_selector_exact_match() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            count: Some(1),
            gender: Some("feminine"),
            ..Default::default()
        };
        let message = ".match {$gender :select} {$count :plural}\nwhen feminine one {editora}\nwhen feminine * {editoras}\nwhen * * {equipo editorial}";

        assert_eq!(
            evaluator.evaluate(message, &args),
            Some("editora".to_string())
        );
    }

    #[test]
    fn test_multi_selector_partial_wildcard_match() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            count: Some(3),
            gender: Some("feminine"),
            ..Default::default()
        };
        let message = ".match {$gender :select} {$count :plural}\nwhen feminine one {editora}\nwhen feminine * {editoras}\nwhen * * {equipo editorial}";

        assert_eq!(
            evaluator.evaluate(message, &args),
            Some("editoras".to_string())
        );
    }

    #[test]
    fn test_multi_selector_full_wildcard_match() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            count: Some(2),
            gender: Some("common"),
            ..Default::default()
        };
        let message = ".match {$gender :select} {$count :plural}\nwhen feminine one {editora}\nwhen feminine * {editoras}\nwhen * * {equipo editorial}";

        assert_eq!(
            evaluator.evaluate(message, &args),
            Some("equipo editorial".to_string())
        );
    }

    #[test]
    fn test_multi_selector_missing_gender() {
        let evaluator = Mf2MessageEvaluator;
        let args = MessageArgs {
            count: Some(1),
            ..Default::default()
        };
        let message = ".match {$gender :select} {$count :plural}\nwhen feminine one {editora}\nwhen * * {equipo editorial}";

        assert_eq!(evaluator.evaluate(message, &args), None);
    }
}
