//! Phase 170-D: Loop Condition Scope Analysis Box
//!
//! Analyzes which variables appear in loop conditions (header, break, continue)
//! and classifies them by their scope:
//! - LoopParam: The loop parameter itself (e.g., 'i' in loop(i < 10))
//! - OuterLocal: Variables from outer scope (pre-existing before loop)
//! - LoopBodyLocal: Variables defined inside the loop body
//!
//! This Box enables Pattern2/4 to determine if they can handle a given loop's
//! condition expressions, providing clear Fail-Fast when conditions reference
//! unsupported loop-body variables.

use crate::ast::ASTNode;
use crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape;
use std::collections::HashSet;

/// Scope classification for a condition variable
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CondVarScope {
    /// The loop parameter itself (e.g., 'i' in loop(i < 10))
    LoopParam,
    /// A variable from outer scope, defined before the loop
    OuterLocal,
    /// A variable defined inside the loop body
    LoopBodyLocal,
}

/// Information about a single condition variable
#[derive(Debug, Clone)]
pub struct CondVarInfo {
    pub name: String,
    pub scope: CondVarScope,
}

/// Analysis result for all variables in loop conditions
#[derive(Debug, Clone)]
pub struct LoopConditionScope {
    pub vars: Vec<CondVarInfo>,
}

impl LoopConditionScope {
    /// Create a new empty condition scope
    pub fn new() -> Self {
        LoopConditionScope { vars: Vec::new() }
    }

    /// Check if this scope contains any loop-body-local variables
    pub fn has_loop_body_local(&self) -> bool {
        self.vars
            .iter()
            .any(|v| v.scope == CondVarScope::LoopBodyLocal)
    }

    /// Check if all variables in this scope are in the allowed set
    pub fn all_in(&self, allowed: &[CondVarScope]) -> bool {
        self.vars.iter().all(|v| allowed.contains(&v.scope))
    }

    /// Get all variable names as a set
    pub fn var_names(&self) -> HashSet<String> {
        self.vars.iter().map(|v| v.name.clone()).collect()
    }

    /// Add a variable to this scope (avoiding duplicates by name)
    ///
    /// # Phase 170-ultrathink: Scope Priority
    ///
    /// If a variable already exists with a different scope, keep the more restrictive one:
    /// - LoopParam > OuterLocal > LoopBodyLocal (in terms of priority)
    ///
    /// This ensures that if a variable is classified in multiple ways due to
    /// different analysis passes or ambiguous AST structures, we prefer the
    /// most specific/restrictive classification.
    ///
    /// Example: If 'i' is detected as both LoopParam and OuterLocal,
    /// we keep LoopParam (more specific).
    pub fn add_var(&mut self, name: String, scope: CondVarScope) {
        // Check if variable already exists
        if let Some(existing) = self.vars.iter_mut().find(|v| v.name == name) {
            // Phase 170-ultrathink: Keep more restrictive scope
            // Priority: LoopParam (highest) > OuterLocal > LoopBodyLocal (lowest)
            let new_is_more_restrictive = match (existing.scope, scope) {
                // Same scope → no change
                (CondVarScope::LoopParam, CondVarScope::LoopParam) => false,
                (CondVarScope::OuterLocal, CondVarScope::OuterLocal) => false,
                (CondVarScope::LoopBodyLocal, CondVarScope::LoopBodyLocal) => false,
                // LoopParam is always most restrictive
                (_, CondVarScope::LoopParam) => true,
                (CondVarScope::LoopParam, _) => false,
                // OuterLocal is more restrictive than LoopBodyLocal
                (CondVarScope::LoopBodyLocal, CondVarScope::OuterLocal) => true,
                (CondVarScope::OuterLocal, CondVarScope::LoopBodyLocal) => false,
            };

            if new_is_more_restrictive {
                existing.scope = scope;
            }
        } else {
            // Variable doesn't exist yet → add it
            self.vars.push(CondVarInfo { name, scope });
        }
    }
}

impl Default for LoopConditionScope {
    fn default() -> Self {
        Self::new()
    }
}

/// Phase 170-D: Loop Condition Scope Analysis Box
///
/// This is the main analyzer that determines variable scopes for condition expressions.
pub struct LoopConditionScopeBox;

impl LoopConditionScopeBox {
    /// Analyze condition variable scopes for a loop
    ///
    /// # Arguments
    ///
    /// * `loop_param_name` - Name of the loop parameter (e.g., "i" in loop(i < 10))
    /// * `condition_nodes` - Array of AST nodes containing conditions to analyze
    /// * `scope` - LoopScopeShape with information about variable definitions
    ///
    /// # Returns
    ///
    /// LoopConditionScope with classified variables
    ///
    /// # Algorithm
    ///
    /// 1. Extract all variables from condition AST nodes using condition_var_analyzer
    /// 2. Classify each variable:
    ///    - If matches loop_param_name → LoopParam
    ///    - Else if in outer scope (via condition_var_analyzer) → OuterLocal
    ///    - Else → LoopBodyLocal (conservative default)
    pub(crate) fn analyze(
        loop_param_name: &str,
        condition_nodes: &[&ASTNode],
        scope: Option<&LoopScopeShape>,
    ) -> LoopConditionScope {
        let mut result = LoopConditionScope::new();
        let mut found_vars = HashSet::new();

        // Phase 170-D-impl-2: Use condition_var_analyzer for extraction
        for node in condition_nodes {
            let vars = super::condition_var_analyzer::extract_all_variables(node);
            found_vars.extend(vars);
        }

        // Classify each variable
        for var_name in found_vars {
            let var_scope = if var_name == loop_param_name {
                CondVarScope::LoopParam
            } else if super::condition_var_analyzer::is_outer_scope_variable(&var_name, scope) {
                CondVarScope::OuterLocal
            } else {
                // Default: assume it's loop-body-local if not identified as outer
                CondVarScope::LoopBodyLocal
            };

            result.add_var(var_name, var_scope);
        }

        result
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_condition_scope_new() {
        let scope = LoopConditionScope::new();
        assert!(scope.vars.is_empty());
        assert!(!scope.has_loop_body_local());
    }

    #[test]
    fn test_add_var() {
        let mut scope = LoopConditionScope::new();
        scope.add_var("i".to_string(), CondVarScope::LoopParam);
        scope.add_var("end".to_string(), CondVarScope::OuterLocal);

        assert_eq!(scope.vars.len(), 2);
        assert!(!scope.has_loop_body_local());
    }

    #[test]
    fn test_has_loop_body_local() {
        let mut scope = LoopConditionScope::new();
        scope.add_var("i".to_string(), CondVarScope::LoopParam);
        scope.add_var("ch".to_string(), CondVarScope::LoopBodyLocal);

        assert!(scope.has_loop_body_local());
    }

    #[test]
    fn test_all_in() {
        let mut scope = LoopConditionScope::new();
        scope.add_var("i".to_string(), CondVarScope::LoopParam);
        scope.add_var("end".to_string(), CondVarScope::OuterLocal);

        assert!(scope.all_in(&[CondVarScope::LoopParam, CondVarScope::OuterLocal]));
        assert!(!scope.all_in(&[CondVarScope::LoopParam]));
    }

    #[test]
    fn test_var_names() {
        let mut scope = LoopConditionScope::new();
        scope.add_var("i".to_string(), CondVarScope::LoopParam);
        scope.add_var("end".to_string(), CondVarScope::OuterLocal);

        let names = scope.var_names();
        assert!(names.contains("i"));
        assert!(names.contains("end"));
        assert_eq!(names.len(), 2);
    }
}
