//! Carrier variable metadata for JoinIR loop lowering
//!
//! This module defines metadata structures for tracking carrier variables
//! in loop lowering. This enables dynamic generation of exit bindings
//! without hardcoded variable names or ValueIds.
//!
//! Phase 193-2: Enhanced builder methods for flexible construction
//!
//! # Phase 183-2: Primary CarrierInfo Construction
//!
//! This module is the single source of truth for CarrierInfo initialization.
//! Both MIR and JoinIR contexts use `CarrierInfo::from_variable_map()` as the
//! primary construction method.
//!
//! - MIR context: `common_init.rs` delegates to this module
//! - JoinIR context: Uses `from_variable_map()` directly
//!
//! # Phase 76: BindingId-Based Promotion Tracking
//!
//! Replaces name-based promotion hacks (`"digit_pos"` → `"is_digit_pos"`) with
//! type-safe BindingId mapping. This eliminates fragile string matching while
//! maintaining backward compatibility through dual-path lookup.

use crate::mir::ValueId;
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism

#[cfg(feature = "normalized_dev")]
use crate::mir::BindingId; // Phase 76+78: BindingId for promoted carriers

/// Phase 227: CarrierRole - Distinguishes loop state carriers from condition-only carriers
///
/// When LoopBodyLocal variables are promoted to carriers, we need to know whether
/// they carry loop state (need exit PHI) or are only used in conditions (no exit PHI).
///
/// # Example
///
/// ```ignore
/// // LoopState carrier: sum needs exit PHI (value persists after loop)
/// loop(i < n) {
///     sum = sum + i;  // sum updated in loop body
/// }
/// print(sum);  // sum used after loop
///
/// // ConditionOnly carrier: is_digit_pos does NOT need exit PHI
/// loop(p < s.length()) {
///     local digit_pos = digits.indexOf(s.substring(p, p+1));
///     if digit_pos < 0 { break; }  // Only used in condition
///     num_str = num_str + ch;
///     p = p + 1;
/// }
/// // digit_pos not used after loop
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierRole {
    /// Value needed after loop (sum, result, count, p, num_str)
    /// - Participates in header PHI (loop iteration)
    /// - Participates in exit PHI (final value after loop)
    LoopState,

    /// Only used for loop condition (is_digit_pos, is_whitespace)
    /// - Participates in header PHI (loop iteration)
    /// - Does NOT participate in exit PHI (not needed after loop)
    ConditionOnly,
}

/// Phase 228: Initialization policy for carrier variables
///
/// When carriers participate in header PHI, they need an initial value.
/// Most carriers use their host_id value (FromHost), but promoted LoopBodyLocal
/// carriers need explicit bool initialization (BoolConst).
///
/// # Example
///
/// ```ignore
/// // Regular carrier (sum): Use host_id value
/// CarrierVar { name: "sum", host_id: ValueId(10), init: FromHost, .. }
///
/// // ConditionOnly carrier (is_digit_pos): Initialize with false
/// CarrierVar { name: "is_digit_pos", host_id: ValueId(15), init: BoolConst(false), .. }
///
/// // Loop-local derived carrier (digit_value): Initialize with local zero (no host slot)
/// CarrierVar { name: "digit_value", host_id: ValueId(0), init: LoopLocalZero, .. }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierInit {
    /// No explicit initialization (use host_id value)
    FromHost,
    /// Initialize with bool constant (for ConditionOnly carriers)
    BoolConst(bool),
    /// Initialize with loop-local zero (no host slot; used for derived carriers like digit_value)
    LoopLocalZero,
}

// Phase 229: ConditionAlias removed - redundant with promoted_loopbodylocals
// The naming convention (old_name → "is_<old_name>" or "is_<old_name>_match")
// is sufficient to resolve promoted variables dynamically.

/// Information about a single carrier variable
#[derive(Debug, Clone)]
pub struct CarrierVar {
    /// Variable name (e.g., "sum", "printed", "is_digit_pos")
    pub name: String,
    /// Host ValueId for this variable (MIR側)
    pub host_id: ValueId,
    /// Phase 177-STRUCT: JoinIR側でこのキャリアを表すValueId
    ///
    /// ヘッダPHIのdstや、exitで使う値を記録する。
    /// これにより、index ベースのマッチングを名前ベースに置き換えられる。
    ///
    /// - `Some(vid)`: Header PHI生成後にセットされる
    /// - `None`: まだPHI生成前、または該当なし
    pub join_id: Option<ValueId>,
    /// Phase 227: Role of this carrier (LoopState or ConditionOnly)
    ///
    /// - `LoopState`: Value needed after loop (participates in exit PHI)
    /// - `ConditionOnly`: Only used for loop condition (no exit PHI)
    pub role: CarrierRole,
    /// Phase 228: Initialization policy for header PHI
    ///
    /// - `FromHost`: Use host_id value (regular carriers)
    /// - `BoolConst(false)`: Initialize with false (promoted LoopBodyLocal carriers)
    pub init: CarrierInit,
    /// Phase 78: BindingId for this carrier (dev-only)
    ///
    /// For promoted carriers (e.g., is_digit_pos), this is allocated separately
    /// by CarrierBindingAssigner. For source-derived carriers, this comes from
    /// builder.binding_map.
    ///
    /// Enables type-safe lookup: BindingId → ValueId (join_id) in ConditionEnv.
    ///
    /// # Example
    ///
    /// ```ignore
    /// // Source-derived carrier
    /// CarrierVar {
    ///     name: "sum",
    ///     binding_id: Some(BindingId(5)), // from builder.binding_map["sum"]
    ///     ..
    /// }
    ///
    /// // Promoted carrier
    /// CarrierVar {
    ///     name: "is_digit_pos",
    ///     binding_id: Some(BindingId(10)), // allocated by CarrierBindingAssigner
    ///     ..
    /// }
    /// ```
    #[cfg(feature = "normalized_dev")]
    pub binding_id: Option<BindingId>,
}

impl CarrierVar {
    /// Create a new CarrierVar with default LoopState role
    ///
    /// This is the primary constructor for CarrierVar. Use this instead of
    /// struct literal syntax to ensure role defaults to LoopState.
    pub fn new(name: String, host_id: ValueId) -> Self {
        Self {
            name,
            host_id,
            join_id: None,
            role: CarrierRole::LoopState,
            init: CarrierInit::FromHost, // Phase 228: Default to FromHost
            #[cfg(feature = "normalized_dev")]
            binding_id: None, // Phase 78: No BindingId by default
        }
    }

    /// Create a CarrierVar with explicit role
    pub fn with_role(name: String, host_id: ValueId, role: CarrierRole) -> Self {
        Self {
            name,
            host_id,
            join_id: None,
            role,
            init: CarrierInit::FromHost, // Phase 228: Default to FromHost
            #[cfg(feature = "normalized_dev")]
            binding_id: None, // Phase 78: No BindingId by default
        }
    }

    /// Phase 228: Create a CarrierVar with explicit role and init policy
    pub fn with_role_and_init(
        name: String,
        host_id: ValueId,
        role: CarrierRole,
        init: CarrierInit,
    ) -> Self {
        Self {
            name,
            host_id,
            join_id: None,
            role,
            init,
            #[cfg(feature = "normalized_dev")]
            binding_id: None, // Phase 78: No BindingId by default
        }
    }
}

/// Complete carrier information for a loop
#[derive(Debug, Clone)]
pub struct CarrierInfo {
    /// Loop control variable name (e.g., "i")
    pub loop_var_name: String,
    /// Loop control variable ValueId in host
    pub loop_var_id: ValueId,
    /// Additional carrier variables (e.g., sum, printed)
    pub carriers: Vec<CarrierVar>,
    /// Phase 171-C-5: Trim pattern helper (if this CarrierInfo was created from Trim promotion)
    pub trim_helper: Option<crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper>,
    /// Phase 224: Promoted LoopBodyLocal variables (e.g., "digit_pos" promoted to "is_digit_pos")
    ///
    /// These variables were originally LoopBodyLocal but have been promoted to carriers
    /// during condition promotion (e.g., DigitPosPromoter). The lowerer should skip
    /// LoopBodyLocal checks for these variables.
    ///
    /// Phase 229: Naming convention for promoted carriers:
    /// - DigitPos pattern: "var" → "is_var" (e.g., "digit_pos" → "is_digit_pos")
    /// - Trim pattern: "var" → "is_var_match" (e.g., "ch" → "is_ch_match")
    ///
    /// Condition variable resolution dynamically infers the carrier name from this list.
    pub promoted_loopbodylocals: Vec<String>,

    /// Phase 76: Type-safe promotion tracking (dev-only)
    ///
    /// Maps original BindingId to promoted BindingId, eliminating name-based hacks.
    ///
    /// # Example
    ///
    /// DigitPos promotion:
    /// - Original: BindingId(5) for `"digit_pos"`
    /// - Promoted: BindingId(10) for `"is_digit_pos"`
    /// - Map entry: `promoted_bindings[BindingId(5)] = BindingId(10)`
    ///
    /// This enables type-safe resolution:
    /// ```ignore
    /// if let Some(promoted_bid) = carrier_info.promoted_bindings.get(&original_bid) {
    ///     // Lookup promoted carrier by BindingId (no string matching!)
    /// }
    /// ```
    ///
    /// # Migration Strategy (Phase 76)
    ///
    /// - **Dual Path**: BindingId lookup (NEW) OR name-based fallback (LEGACY)
    /// - **Populated by**: DigitPosPromoter, TrimLoopHelper (Phase 76)
    /// - **Used by**: ConditionEnv::resolve_var_with_binding (Phase 75+)
    /// - **Phase 77**: Remove name-based fallback after full migration
    ///
    /// # Design Notes
    ///
    /// **Q: Why BindingId map instead of name map?**
    /// - **Type Safety**: Compiler-checked binding identity (no typos)
    /// - **Shadowing-Aware**: BindingId distinguishes inner/outer scope vars
    /// - **No Name Collisions**: BindingId is unique even if names shadow
    ///
    /// **Q: Why not remove `promoted_loopbodylocals` immediately?**
    /// - **Legacy Compatibility**: Existing code uses name-based lookup
    /// - **Gradual Migration**: Phase 76 adds BindingId, Phase 77 removes name-based
    /// - **Fail-Safe**: Dual path ensures no regressions during transition
    #[cfg(feature = "normalized_dev")]
    pub promoted_bindings: BTreeMap<BindingId, BindingId>,
}

impl CarrierInfo {
    /// Phase 193-2: Create CarrierInfo from a variable_map
    ///
    /// Automatically extracts all non-loop-control variables from the host's
    /// variable_map. This eliminates manual carrier listing for simple cases.
    ///
    /// # Arguments
    ///
    /// * `loop_var_name` - Name of the loop control variable (e.g., "i")
    /// * `variable_map` - Host function's variable_map (String → ValueId)
    ///
    /// # Returns
    ///
    /// CarrierInfo with loop_var and all other variables as carriers
    ///
    /// # Example
    ///
    /// ```ignore
    /// let carrier_info = CarrierInfo::from_variable_map(
    ///     "i".to_string(),
    ///     &variable_map  // {"i": ValueId(5), "sum": ValueId(10), "count": ValueId(11)}
    /// )?;
    /// // Result: CarrierInfo with loop_var="i", carriers=[sum, count]
    /// ```
    pub fn from_variable_map(
        loop_var_name: String,
        variable_map: &BTreeMap<String, ValueId>, // Phase 222.5-D: HashMap → BTreeMap for determinism
    ) -> Result<Self, String> {
        // Find loop variable
        let loop_var_id = variable_map.get(&loop_var_name).copied().ok_or_else(|| {
            format!(
                "Loop variable '{}' not found in variable_map",
                loop_var_name
            )
        })?;

        // Collect all non-loop-var variables as carriers
        let mut carriers: Vec<CarrierVar> = variable_map
            .iter()
            .filter(|(name, _)| *name != &loop_var_name)
            .map(|(name, &id)| CarrierVar {
                name: name.clone(),
                host_id: id,
                join_id: None, // Phase 177-STRUCT-1: Set by header PHI generation
                role: CarrierRole::LoopState, // Phase 227: Default to LoopState
                init: CarrierInit::FromHost, // Phase 228: Default to FromHost
                #[cfg(feature = "normalized_dev")]
                binding_id: None, // Phase 78: Set by CarrierBindingAssigner
            })
            .collect();

        // Sort for determinism
        carriers.sort_by(|a, b| a.name.cmp(&b.name));

        Ok(CarrierInfo {
            loop_var_name,
            loop_var_id,
            carriers,
            trim_helper: None, // Phase 171-C-5: No Trim pattern by default
            promoted_loopbodylocals: Vec::new(), // Phase 224: No promoted variables by default
            #[cfg(feature = "normalized_dev")]
            promoted_bindings: BTreeMap::new(), // Phase 76: No promoted bindings by default
        })
    }

    /// Phase 193-2: Create CarrierInfo with explicit carrier list
    ///
    /// Useful when you have specific carriers in mind and want explicit control
    /// over which variables are treated as carriers.
    ///
    /// # Arguments
    ///
    /// * `loop_var_name` - Name of the loop control variable
    /// * `loop_var_id` - ValueId of the loop variable
    /// * `carrier_names` - Names of carrier variables (will look up in variable_map)
    /// * `variable_map` - Host function's variable_map for lookups
    ///
    /// # Returns
    ///
    /// CarrierInfo with only the specified carriers
    ///
    /// # Example
    ///
    /// ```ignore
    /// let carrier_info = CarrierInfo::with_explicit_carriers(
    ///     "i".to_string(),
    ///     ValueId(5),
    ///     vec!["sum".to_string(), "count".to_string()],
    ///     &variable_map
    /// )?;
    /// ```
    pub fn with_explicit_carriers(
        loop_var_name: String,
        loop_var_id: ValueId,
        carrier_names: Vec<String>,
        variable_map: &BTreeMap<String, ValueId>, // Phase 222.5-D: HashMap → BTreeMap for determinism
    ) -> Result<Self, String> {
        let mut carriers = Vec::new();

        for name in carrier_names {
            let host_id = variable_map
                .get(&name)
                .copied()
                .ok_or_else(|| format!("Carrier variable '{}' not found in variable_map", name))?;

            carriers.push(CarrierVar {
                name,
                host_id,
                join_id: None, // Phase 177-STRUCT-1: Set by header PHI generation
                role: CarrierRole::LoopState, // Phase 227: Default to LoopState
                init: CarrierInit::FromHost, // Phase 228: Default to FromHost
                #[cfg(feature = "normalized_dev")]
                binding_id: None, // Phase 78: Set by CarrierBindingAssigner
            });
        }

        // Sort for determinism
        carriers.sort_by(|a, b| a.name.cmp(&b.name));

        Ok(CarrierInfo {
            loop_var_name,
            loop_var_id,
            carriers,
            trim_helper: None, // Phase 171-C-5: No Trim pattern by default
            promoted_loopbodylocals: Vec::new(), // Phase 224: No promoted variables by default
            #[cfg(feature = "normalized_dev")]
            promoted_bindings: BTreeMap::new(), // Phase 76: No promoted bindings by default
        })
    }

    /// Phase 193-2: Create CarrierInfo with manual CarrierVar list
    ///
    /// Most explicit construction method - you provide everything directly.
    /// Useful when you already have CarrierVar structs built elsewhere.
    ///
    /// # Arguments
    ///
    /// * `loop_var_name` - Name of the loop control variable
    /// * `loop_var_id` - ValueId of the loop variable
    /// * `carriers` - Vec of already-constructed CarrierVar structs
    pub fn with_carriers(
        loop_var_name: String,
        loop_var_id: ValueId,
        mut carriers: Vec<CarrierVar>,
    ) -> Self {
        // Sort for determinism
        carriers.sort_by(|a, b| a.name.cmp(&b.name));

        Self {
            loop_var_name,
            loop_var_id,
            carriers,
            trim_helper: None, // Phase 171-C-5: No Trim pattern by default
            promoted_loopbodylocals: Vec::new(), // Phase 224: No promoted variables by default
            #[cfg(feature = "normalized_dev")]
            promoted_bindings: BTreeMap::new(), // Phase 76: No promoted bindings by default
        }
    }

    /// Phase 193-2: Get carrier count
    ///
    /// Convenience method for checking how many carriers this info has.
    pub fn carrier_count(&self) -> usize {
        self.carriers.len()
    }

    /// Phase 193-2: Check if this has multiple carriers
    ///
    /// Useful for pattern matching: "is this a multi-carrier loop?"
    pub fn is_multi_carrier(&self) -> bool {
        self.carriers.len() > 1
    }

    /// Phase 193-2: Find a carrier by name
    ///
    /// Lookup a specific carrier variable by name.
    pub fn find_carrier(&self, name: &str) -> Option<&CarrierVar> {
        self.carriers.iter().find(|c| c.name == name)
    }

    /// Phase 171-C-4: Merge carriers from another CarrierInfo
    ///
    /// Deduplicates by carrier name. If a carrier with the same name already exists,
    /// it will not be added again.
    ///
    /// # Arguments
    ///
    /// * `other` - Another CarrierInfo to merge from
    ///
    /// # Example
    ///
    /// ```ignore
    /// let mut carrier_info = CarrierInfo::from_variable_map("i", &variable_map)?;
    /// let promoted_carrier = TrimPatternInfo::to_carrier_info();
    /// carrier_info.merge_from(&promoted_carrier);
    /// ```
    pub fn merge_from(&mut self, other: &CarrierInfo) {
        for carrier in &other.carriers {
            if !self.carriers.iter().any(|c| c.name == carrier.name) {
                self.carriers.push(carrier.clone());
            }
        }
        // Maintain sorted order for determinism
        self.carriers.sort_by(|a, b| a.name.cmp(&b.name));

        // Phase 171-C-5: Also merge trim_helper if present
        if other.trim_helper.is_some() {
            self.trim_helper = other.trim_helper.clone();
        }

        // Phase 224: Merge promoted_loopbodylocals (deduplicate)
        for promoted_var in &other.promoted_loopbodylocals {
            if !self.promoted_loopbodylocals.contains(promoted_var) {
                self.promoted_loopbodylocals.push(promoted_var.clone());
            }
        }

        // Phase 76: Merge promoted_bindings (dev-only)
        #[cfg(feature = "normalized_dev")]
        {
            for (original, promoted) in &other.promoted_bindings {
                self.promoted_bindings.insert(*original, *promoted);
            }
        }
    }

    /// Phase 171-C-5: Get Trim pattern helper
    ///
    /// Returns the TrimLoopHelper if this CarrierInfo was created from Trim promotion.
    ///
    /// # Returns
    ///
    /// * `Some(&TrimLoopHelper)` - If this CarrierInfo contains Trim pattern information
    /// * `None` - If this is a regular CarrierInfo (not from Trim promotion)
    ///
    /// # Example
    ///
    /// ```ignore
    /// if let Some(helper) = carrier_info.trim_helper() {
    ///     eprintln!("Trim pattern detected: {}", helper.carrier_name);
    ///     eprintln!("Whitespace chars: {:?}", helper.whitespace_chars);
    /// }
    /// ```
    pub fn trim_helper(
        &self,
    ) -> Option<&crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper> {
        self.trim_helper.as_ref()
    }

    /// Phase 229/231: Resolve promoted LoopBodyLocal name to carrier JoinIR ValueId
    ///
    /// This helper centralizes the naming convention for promoted variables so that
    /// ScopeManager 実装がそれぞれ命名規約を再実装しなくて済むようにするよ。
    ///
    /// 命名規約:
    /// - DigitPos パターン: `"var"` → `"is_var"`（例: "digit_pos" → "is_digit_pos"）
    /// - Trim パターン   : `"var"` → `"is_var_match"`（例: "ch" → "is_ch_match"）
    ///
    /// # Arguments
    ///
    /// * `original_name` - 元の LoopBodyLocal 名（例: "digit_pos"）
    ///
    /// # Returns
    ///
    /// * `Some(ValueId)` - 対応する carrier の join_id が見つかった場合
    /// * `None` - promoted_loopbodylocals に含まれない、または join_id 未設定の場合
    ///
    /// # Phase 77: DEPRECATED
    ///
    /// This method uses fragile naming conventions ("is_*", "is_*_match") and will
    /// be removed in Phase 78+ when all call sites migrate to BindingId-based lookup.
    /// Use `resolve_promoted_with_binding()` for type-safe BindingId lookup.
    #[deprecated(
        since = "phase77",
        note = "Use resolve_promoted_with_binding() for type-safe BindingId lookup"
    )]
    pub fn resolve_promoted_join_id(&self, original_name: &str) -> Option<ValueId> {
        #[cfg(feature = "normalized_dev")]
        eprintln!(
            "[phase77/legacy/carrier_info] WARNING: Using deprecated name-based promoted lookup for '{}'",
            original_name
        );
        if !self
            .promoted_loopbodylocals
            .contains(&original_name.to_string())
        {
            return None;
        }

        let candidates = [
            format!("is_{}", original_name),       // DigitPos pattern
            format!("is_{}_match", original_name), // Trim pattern
        ];

        for carrier_name in &candidates {
            // loop_var 自身が ConditionOnly carrier として扱われるケースは現状ほぼないが、
            // 将来の拡張に備えて loop_var_name も一応チェックしておく。
            if carrier_name == &self.loop_var_name {
                if let Some(carrier) = self.carriers.iter().find(|c| c.name == self.loop_var_name) {
                    if let Some(join_id) = carrier.join_id {
                        return Some(join_id);
                    }
                }
            }

            if let Some(carrier) = self.carriers.iter().find(|c| c.name == *carrier_name) {
                if let Some(join_id) = carrier.join_id {
                    return Some(join_id);
                }
            }
        }

        None
    }

    /// Phase 76: Type-safe promoted binding resolution (dev-only)
    ///
    /// Resolves a promoted LoopBodyLocal binding via BindingId map, eliminating
    /// name-based hacks (`format!("is_{}", name)`). Falls back to legacy name-based
    /// lookup for backward compatibility during Phase 76-77 migration.
    ///
    /// # Arguments
    ///
    /// * `original_binding` - Original LoopBodyLocal's BindingId (e.g., BindingId(5) for "digit_pos")
    ///
    /// # Returns
    ///
    /// * `Some(BindingId)` - Promoted carrier's BindingId (e.g., BindingId(10) for "is_digit_pos")
    /// * `None` - No promotion mapping found
    ///
    /// # Example
    ///
    /// ```ignore
    /// // DigitPos promotion: BindingId(5) "digit_pos" → BindingId(10) "is_digit_pos"
    /// let original_bid = BindingId(5);
    /// if let Some(promoted_bid) = carrier_info.resolve_promoted_with_binding(original_bid) {
    ///     // Lookup carrier by promoted BindingId (type-safe!)
    ///     let promoted_value = condition_env.get_by_binding(promoted_bid);
    /// }
    /// ```
    ///
    /// # Migration Path (Phase 76-77)
    ///
    /// - **Phase 76**: BindingId map populated by promoters, dual path (BindingId OR name)
    /// - **Phase 77**: Remove name-based fallback, BindingId-only lookup
    ///
    /// # Design Notes
    ///
    /// **Why not merge with `resolve_promoted_join_id()`?**
    /// - Different input type: BindingId vs String
    /// - Different output: BindingId vs ValueId
    /// - Different usage: ScopeManager (BindingId) vs legacy lowerers (name)
    ///
    /// **Why BTreeMap instead of HashMap?**
    /// - Deterministic iteration (Phase 222.5-D consistency)
    /// - Debug-friendly sorted output
    #[cfg(feature = "normalized_dev")]
    pub fn resolve_promoted_with_binding(&self, original_binding: BindingId) -> Option<BindingId> {
        self.promoted_bindings.get(&original_binding).copied()
    }

    /// Phase 76: Record a promoted binding (dev-only)
    ///
    /// Helper method to populate the promoted_bindings map during promotion.
    /// Called by wrapper functions that have access to both CarrierInfo and binding_map.
    ///
    /// # Arguments
    ///
    /// * `original_binding` - Original LoopBodyLocal's BindingId
    /// * `promoted_binding` - Promoted carrier's BindingId
    ///
    /// # Example
    ///
    /// ```ignore
    /// // After DigitPosPromoter creates CarrierInfo, record the binding mapping:
    /// carrier_info.record_promoted_binding(
    ///     binding_map.get("digit_pos").copied().unwrap(),  // BindingId(5)
    ///     binding_map.get("is_digit_pos").copied().unwrap() // BindingId(10)
    /// );
    /// ```
    ///
    /// # Phase 76 Note
    ///
    /// This method is currently UNUSED because promoters (DigitPosPromoter, TrimLoopHelper)
    /// don't have access to binding_map. Actual population happens in a future phase when
    /// we integrate BindingId tracking into the promotion pipeline.
    #[cfg(feature = "normalized_dev")]
    pub fn record_promoted_binding(
        &mut self,
        original_binding: BindingId,
        promoted_binding: BindingId,
    ) {
        use super::debug_output_box::DebugOutputBox;

        // Phase 86: Use DebugOutputBox for consistent debug output
        // Allow JOINIR_TEST_DEBUG override for test-specific diagnostics
        let test_debug = std::env::var("JOINIR_TEST_DEBUG").is_ok();
        let debug = DebugOutputBox::new("binding_pilot/promoted_bindings");

        if debug.is_enabled() || test_debug {
            eprintln!(
                "[binding_pilot/promoted_bindings] {} → {}",
                original_binding, promoted_binding
            );
        }
        self.promoted_bindings
            .insert(original_binding, promoted_binding);
    }
}

/// Exit metadata returned by lowerers
///
/// This structure captures the mapping from JoinIR exit values to
/// carrier variable names, enabling dynamic binding generation.
#[derive(Debug, Clone)]
pub struct ExitMeta {
    /// Exit value bindings: (carrier_name, join_exit_value_id)
    ///
    /// Example for Pattern 4:
    /// ```
    /// vec![("sum".to_string(), ValueId(15))]
    /// ```
    /// where ValueId(15) is the k_exit parameter in JoinIR-local space.
    pub exit_values: Vec<(String, ValueId)>,
}

/// Phase 33-14: JoinFragmentMeta - Distinguishes expr result from carrier updates
///
/// ## Purpose
///
/// Separates two distinct use cases for JoinIR loops:
///
/// 1. **Expr Result Pattern** (joinir_min_loop.hako):
///    ```nyash
///    local result = loop(...) { ... }  // Loop used as expression
///    return result
///    ```
///    Here, the k_exit return value is the "expr result" that should go to exit_phi_inputs.
///
/// 2. **Carrier Update Pattern** (trim pattern):
///    ```nyash
///    loop(...) { start = start + 1 }   // Loop used for side effects
///    print(start)                      // Use carrier after loop
///    ```
///    Here, there's no "expr result" - only carrier variable updates.
///
/// ## SSA Correctness
///
/// Previously, exit_phi_inputs mixed expr results with carrier updates, causing:
/// - PHI inputs that referenced undefined remapped values
/// - SSA-undef errors in VM execution
///
/// With JoinFragmentMeta:
/// - `expr_result`: Only goes to exit_phi_inputs (generates PHI for expr value)
/// - `exit_meta`: Only goes to carrier_inputs (updates variable_map via carrier PHIs)
///
/// ## Example: Pattern 2 (joinir_min_loop.hako)
///
/// ```rust
/// JoinFragmentMeta {
///     expr_result: Some(i_exit),  // k_exit returns i as expr value
///     exit_meta: ExitMeta::single("i".to_string(), i_exit),  // Also a carrier
/// }
/// ```
///
/// ## Example: Pattern 3 (trim pattern)
///
/// ```rust
/// JoinFragmentMeta {
///     expr_result: None,  // Loop doesn't return a value
///     exit_meta: ExitMeta::multiple(vec![
///         ("start".to_string(), start_exit),
///         ("end".to_string(), end_exit),
///     ]),
/// }
/// ```
#[derive(Debug, Clone)]
pub struct JoinFragmentMeta {
    /// Expression result ValueId from k_exit (JoinIR-local)
    ///
    /// - `Some(vid)`: Loop is used as expression, k_exit's return value → exit_phi_inputs
    /// - `None`: Loop is used for side effects only, no PHI for expr value
    pub expr_result: Option<ValueId>,

    /// Carrier variable exit bindings (existing ExitMeta)
    ///
    /// Maps carrier names to their JoinIR-local exit values.
    /// These go to carrier_inputs for carrier PHI generation.
    pub exit_meta: ExitMeta,
}

impl JoinFragmentMeta {
    /// Create JoinFragmentMeta for expression result pattern
    ///
    /// Use when the loop returns a value (like `return loop(...)`).
    pub fn with_expr_result(expr_result: ValueId, exit_meta: ExitMeta) -> Self {
        Self {
            expr_result: Some(expr_result),
            exit_meta,
        }
    }

    /// Create JoinFragmentMeta for carrier-only pattern
    ///
    /// Use when the loop only updates carriers (like trim pattern).
    pub fn carrier_only(exit_meta: ExitMeta) -> Self {
        Self {
            expr_result: None,
            exit_meta,
        }
    }

    /// Create empty JoinFragmentMeta (no expr result, no carriers)
    pub fn empty() -> Self {
        Self {
            expr_result: None,
            exit_meta: ExitMeta::empty(),
        }
    }

    /// Check if this fragment has an expression result
    pub fn has_expr_result(&self) -> bool {
        self.expr_result.is_some()
    }

    /// Phase 33-14: Backward compatibility - convert to ExitMeta
    ///
    /// During migration, some code may still expect ExitMeta.
    /// This extracts just the carrier bindings.
    #[deprecated(since = "33-14", note = "Use exit_meta directly for carrier access")]
    pub fn to_exit_meta(&self) -> ExitMeta {
        self.exit_meta.clone()
    }
}

impl ExitMeta {
    /// Create new ExitMeta with no exit values
    pub fn empty() -> Self {
        Self {
            exit_values: vec![],
        }
    }

    /// Create ExitMeta with a single exit value
    pub fn single(carrier_name: String, join_value: ValueId) -> Self {
        Self {
            exit_values: vec![(carrier_name, join_value)],
        }
    }

    /// Create ExitMeta with multiple exit values
    pub fn multiple(exit_values: Vec<(String, ValueId)>) -> Self {
        Self { exit_values }
    }

    /// Phase 193-2: Get the count of exit bindings
    ///
    /// Useful for checking if this ExitMeta has any exit values.
    pub fn binding_count(&self) -> usize {
        self.exit_values.len()
    }

    /// Phase 193-2: Check if this has any exit values
    pub fn is_empty(&self) -> bool {
        self.exit_values.is_empty()
    }

    /// Phase 193-2: Find a binding by carrier name
    ///
    /// Lookup a specific exit value by carrier name.
    pub fn find_binding(&self, carrier_name: &str) -> Option<ValueId> {
        self.exit_values
            .iter()
            .find(|(name, _)| name == carrier_name)
            .map(|(_, value_id)| *value_id)
    }

    /// Phase 193-2: Add a binding to ExitMeta
    ///
    /// Convenient way to build ExitMeta incrementally.
    pub fn with_binding(mut self, carrier_name: String, join_value: ValueId) -> Self {
        self.exit_values.push((carrier_name, join_value));
        self
    }
}

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

    // Helper: Create a CarrierVar for testing
    fn test_carrier(name: &str, id: u32) -> CarrierVar {
        CarrierVar {
            name: name.to_string(),
            host_id: ValueId(id),
            join_id: None,                // Phase 177-STRUCT-1
            role: CarrierRole::LoopState, // Phase 227: Default to LoopState
            init: CarrierInit::FromHost,  // Phase 228: Default to FromHost
            #[cfg(feature = "normalized_dev")]
            binding_id: None, // Phase 78: No BindingId by default
        }
    }

    // Helper: Create a CarrierInfo for testing
    fn test_carrier_info(loop_var: &str, loop_id: u32, carriers: Vec<CarrierVar>) -> CarrierInfo {
        CarrierInfo::with_carriers(loop_var.to_string(), ValueId(loop_id), carriers)
    }

    #[test]
    fn test_merge_from_empty() {
        // Merge empty CarrierInfo should not change anything
        let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);

        let other = test_carrier_info("j", 20, vec![]);

        carrier_info.merge_from(&other);

        assert_eq!(carrier_info.carrier_count(), 1);
        assert_eq!(carrier_info.carriers[0].name, "sum");
    }

    #[test]
    fn test_merge_from_new_carrier() {
        // Merge a new carrier that doesn't exist yet
        let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);

        let other = test_carrier_info("j", 20, vec![test_carrier("count", 15)]);

        carrier_info.merge_from(&other);

        assert_eq!(carrier_info.carrier_count(), 2);
        // Should be sorted by name
        assert_eq!(carrier_info.carriers[0].name, "count"); // 'c' < 's'
        assert_eq!(carrier_info.carriers[1].name, "sum");
    }

    #[test]
    fn test_merge_from_duplicate_carrier() {
        // Merge a carrier with the same name should NOT duplicate
        let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);

        let other = test_carrier_info(
            "j",
            20,
            vec![test_carrier("sum", 999)], // Same name, different ID
        );

        carrier_info.merge_from(&other);

        // Should still have only 1 carrier (no duplication)
        assert_eq!(carrier_info.carrier_count(), 1);
        assert_eq!(carrier_info.carriers[0].name, "sum");
        // Original ID should be preserved
        assert_eq!(carrier_info.carriers[0].host_id, ValueId(10));
    }

    #[test]
    fn test_merge_from_multiple_carriers() {
        // Merge multiple carriers
        let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);

        let other = test_carrier_info(
            "j",
            20,
            vec![test_carrier("count", 15), test_carrier("product", 18)],
        );

        carrier_info.merge_from(&other);

        assert_eq!(carrier_info.carrier_count(), 3);
        // Should be sorted by name
        assert_eq!(carrier_info.carriers[0].name, "count");
        assert_eq!(carrier_info.carriers[1].name, "product");
        assert_eq!(carrier_info.carriers[2].name, "sum");
    }

    #[test]
    fn test_merge_from_preserves_determinism() {
        // Test that merge maintains sorted order
        let mut carrier_info = test_carrier_info(
            "i",
            5,
            vec![test_carrier("zebra", 30), test_carrier("alpha", 10)],
        );

        let other = test_carrier_info(
            "j",
            20,
            vec![test_carrier("beta", 15), test_carrier("gamma", 18)],
        );

        carrier_info.merge_from(&other);

        assert_eq!(carrier_info.carrier_count(), 4);
        // Should be sorted alphabetically
        assert_eq!(carrier_info.carriers[0].name, "alpha");
        assert_eq!(carrier_info.carriers[1].name, "beta");
        assert_eq!(carrier_info.carriers[2].name, "gamma");
        assert_eq!(carrier_info.carriers[3].name, "zebra");
    }

    #[test]
    fn test_merge_from_with_trim_helper() {
        // Test that trim_helper is merged
        use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper;

        let mut carrier_info = test_carrier_info("i", 5, vec![]);

        let mut other = test_carrier_info("j", 20, vec![]);
        other.trim_helper = Some(TrimLoopHelper {
            original_var: "ch".to_string(),
            carrier_name: "is_whitespace".to_string(),
            whitespace_chars: vec![" ".to_string(), "\t".to_string()],
        });

        carrier_info.merge_from(&other);

        // trim_helper should be copied
        assert!(carrier_info.trim_helper.is_some());
        let helper = carrier_info.trim_helper.as_ref().unwrap();
        assert_eq!(helper.original_var, "ch");
        assert_eq!(helper.carrier_name, "is_whitespace");
        assert_eq!(helper.whitespace_count(), 2);
    }

    #[test]
    fn test_trim_helper_accessor() {
        // Test the trim_helper() accessor method
        use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper;

        let mut carrier_info = test_carrier_info("i", 5, vec![]);

        // Initially None
        assert!(carrier_info.trim_helper().is_none());

        // Add trim_helper
        carrier_info.trim_helper = Some(TrimLoopHelper {
            original_var: "ch".to_string(),
            carrier_name: "is_whitespace".to_string(),
            whitespace_chars: vec![" ".to_string()],
        });

        // Now Some
        assert!(carrier_info.trim_helper().is_some());
        let helper = carrier_info.trim_helper().unwrap();
        assert_eq!(helper.original_var, "ch");
    }

    // ========== Phase 76: promoted_bindings tests ==========

    #[test]
    #[cfg(feature = "normalized_dev")]
    fn test_promoted_bindings_record_and_resolve() {
        use crate::mir::BindingId;

        let mut carrier_info = test_carrier_info("i", 5, vec![]);

        // Record a promotion: BindingId(5) → BindingId(10)
        carrier_info.record_promoted_binding(BindingId(5), BindingId(10));

        // Resolve should find the mapping
        assert_eq!(
            carrier_info.resolve_promoted_with_binding(BindingId(5)),
            Some(BindingId(10))
        );

        // Unknown BindingId should return None
        assert_eq!(
            carrier_info.resolve_promoted_with_binding(BindingId(99)),
            None
        );
    }

    #[test]
    #[cfg(feature = "normalized_dev")]
    fn test_promoted_bindings_multiple_mappings() {
        use crate::mir::BindingId;

        let mut carrier_info = test_carrier_info("i", 5, vec![]);

        // Record multiple promotions (e.g., DigitPos + Trim in same loop)
        carrier_info.record_promoted_binding(BindingId(5), BindingId(10)); // digit_pos → is_digit_pos
        carrier_info.record_promoted_binding(BindingId(6), BindingId(11)); // ch → is_ch_match

        // Both should resolve independently
        assert_eq!(
            carrier_info.resolve_promoted_with_binding(BindingId(5)),
            Some(BindingId(10))
        );
        assert_eq!(
            carrier_info.resolve_promoted_with_binding(BindingId(6)),
            Some(BindingId(11))
        );
    }

    #[test]
    #[cfg(feature = "normalized_dev")]
    fn test_promoted_bindings_merge() {
        use crate::mir::BindingId;

        let mut carrier_info1 = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
        carrier_info1.record_promoted_binding(BindingId(1), BindingId(2));

        let mut carrier_info2 = test_carrier_info("j", 20, vec![test_carrier("count", 15)]);
        carrier_info2.record_promoted_binding(BindingId(3), BindingId(4));

        // Merge carrier_info2 into carrier_info1
        carrier_info1.merge_from(&carrier_info2);

        // Both promoted_bindings should be present
        assert_eq!(
            carrier_info1.resolve_promoted_with_binding(BindingId(1)),
            Some(BindingId(2))
        );
        assert_eq!(
            carrier_info1.resolve_promoted_with_binding(BindingId(3)),
            Some(BindingId(4))
        );
    }

    #[test]
    #[cfg(feature = "normalized_dev")]
    fn test_promoted_bindings_default_empty() {
        use crate::mir::BindingId;

        // Newly created CarrierInfo should have empty promoted_bindings
        let carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);

        assert_eq!(
            carrier_info.resolve_promoted_with_binding(BindingId(0)),
            None
        );
    }

    #[test]
    #[cfg(feature = "normalized_dev")]
    fn test_promoted_bindings_overwrite() {
        use crate::mir::BindingId;

        let mut carrier_info = test_carrier_info("i", 5, vec![]);

        // Record initial mapping
        carrier_info.record_promoted_binding(BindingId(5), BindingId(10));

        // Overwrite with new mapping (should replace)
        carrier_info.record_promoted_binding(BindingId(5), BindingId(20));

        // Should return the new value
        assert_eq!(
            carrier_info.resolve_promoted_with_binding(BindingId(5)),
            Some(BindingId(20))
        );
    }
}
