//! Minimal Normalized JoinIR model (Phase 26-H.B).
//!
//! テスト専用の極小サブセット。Pattern1 の while だけを Structured → Normalized に
//! 変換して遊ぶための足場だよ。本線の Structured→MIR 経路には影響しない。

use std::collections::{BTreeMap, HashSet};

use crate::mir::join_ir::{
    BinOpKind, CompareOp, ConstValue, JoinContId, JoinFuncId, JoinFunction, JoinInst, JoinIrPhase,
    JoinModule, MirLikeInst, UnaryOp,
};
use crate::mir::ValueId;
#[cfg(feature = "normalized_dev")]
use std::collections::HashMap;
#[cfg(feature = "normalized_dev")]
use std::panic::{catch_unwind, AssertUnwindSafe};

#[cfg(feature = "normalized_dev")]
pub mod dev_env;
#[cfg(feature = "normalized_dev")]
pub mod dev_fixtures;
#[cfg(feature = "normalized_dev")]
pub mod fixtures;
#[cfg(feature = "normalized_dev")]
pub mod loop_step_inspector;
#[cfg(feature = "normalized_dev")]
pub mod shape_guard;
#[cfg(feature = "normalized_dev")]
use crate::mir::join_ir::normalized::shape_guard::NormalizedDevShape;

/// 環境レイアウト（最小）。
#[derive(Debug, Clone)]
pub struct EnvLayout {
    pub id: u32,
    pub fields: Vec<EnvField>,
}

#[derive(Debug, Clone)]
pub struct EnvField {
    pub name: String,
    pub ty: Option<crate::mir::MirType>,
    pub value_id: Option<ValueId>,
}

/// 正規化済み関数 ID
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct JpFuncId(pub u32);

/// 正規化済み関数（Kont 兼用、is_kont で区別）。
#[derive(Debug, Clone)]
pub struct JpFunction {
    pub id: JpFuncId,
    pub name: String,
    pub env_layout: Option<u32>,
    pub body: Vec<JpInst>,
    pub is_kont: bool,
}

/// 正規化済み命令（最小セット）。
#[derive(Debug, Clone)]
pub enum JpInst {
    Let {
        dst: ValueId,
        op: JpOp,
        args: Vec<ValueId>,
    },
    EnvLoad {
        dst: ValueId,
        env: ValueId,
        field: usize,
    },
    EnvStore {
        env: ValueId,
        field: usize,
        src: ValueId,
    },
    TailCallFn {
        target: JpFuncId,
        env: Vec<ValueId>,
    },
    TailCallKont {
        target: JpFuncId,
        env: Vec<ValueId>,
    },
    If {
        cond: ValueId,
        then_target: JpFuncId,
        else_target: JpFuncId,
        env: Vec<ValueId>,
    },
}

/// 演算（Let 用の最小サブセット）。
#[derive(Debug, Clone)]
pub enum JpOp {
    Const(ConstValue),
    BinOp(BinOpKind),
    Unary(UnaryOp),
    Compare(CompareOp),
    BoxCall {
        box_name: String,
        method: String,
    },
    /// 三項演算子（cond ? then : else）
    Select,
}

/// Normalized JoinIR モジュール（テスト専用）。
#[derive(Debug, Clone)]
pub struct NormalizedModule {
    pub functions: BTreeMap<JpFuncId, JpFunction>,
    pub entry: Option<JpFuncId>,
    pub env_layouts: Vec<EnvLayout>,
    pub phase: JoinIrPhase,
    /// Structured に戻すためのスナップショット（テスト専用）。
    pub structured_backup: Option<JoinModule>,
}

impl NormalizedModule {
    pub fn to_structured(&self) -> Option<JoinModule> {
        self.structured_backup.clone()
    }
}

#[cfg(feature = "normalized_dev")]
fn verify_normalized_pattern1(module: &NormalizedModule) -> Result<(), String> {
    if module.phase != JoinIrPhase::Normalized {
        return Err("[joinir/normalized-dev] pattern1: phase must be Normalized".to_string());
    }

    // Env field bounds check
    if let Some(env) = module.env_layouts.get(0) {
        let field_count = env.fields.len();
        for func in module.functions.values() {
            for inst in &func.body {
                match inst {
                    JpInst::EnvLoad { field, .. } | JpInst::EnvStore { field, .. } => {
                        if *field >= field_count {
                            return Err(format!(
                                "[joinir/normalized-dev] pattern1: env field out of range: {} (fields={})",
                                field, field_count
                            ));
                        }
                    }
                    _ => {}
                }
            }
        }
    }

    for func in module.functions.values() {
        for inst in &func.body {
            match inst {
                JpInst::Let { .. }
                | JpInst::EnvLoad { .. }
                | JpInst::EnvStore { .. }
                | JpInst::TailCallFn { .. }
                | JpInst::TailCallKont { .. }
                | JpInst::If { .. } => {}
            }
        }

        // Tail: allow TailCall or If only
        if let Some(last) = func.body.last() {
            match last {
                JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {}
                _ => {
                    return Err(format!(
                        "[joinir/normalized-dev] pattern1: function '{}' does not end with tail call/if",
                        func.name
                    ))
                }
            }
        }
    }

    Ok(())
}

/// Pattern1 専用: Normalized → Structured への簡易逆変換。
pub fn normalized_pattern1_to_structured(norm: &NormalizedModule) -> JoinModule {
    assert_eq!(
        norm.phase,
        JoinIrPhase::Normalized,
        "normalized_pattern1_to_structured expects Normalized phase"
    );

    let env_layout = norm
        .env_layouts
        .get(0)
        .expect("normalized_pattern1_to_structured: missing env layout");

    let mut module = JoinModule::new();

    for (jp_id, jp_fn) in &norm.functions {
        let params: Vec<ValueId> = jp_fn
            .env_layout
            .and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id))
            .unwrap_or(env_layout)
            .fields
            .iter()
            .enumerate()
            .map(|(idx, f)| f.value_id.unwrap_or(ValueId(idx as u32)))
            .collect();

        let mut func = JoinFunction::new(JoinFuncId(jp_id.0), jp_fn.name.clone(), params);

        for inst in &jp_fn.body {
            match inst {
                JpInst::Let { dst, op, args } => match op {
                    JpOp::Const(v) => func.body.push(JoinInst::Compute(MirLikeInst::Const {
                        dst: *dst,
                        value: v.clone(),
                    })),
                    JpOp::BoxCall { box_name, method } => {
                        func.body.push(JoinInst::Compute(MirLikeInst::BoxCall {
                            dst: Some(*dst),
                            box_name: box_name.clone(),
                            method: method.clone(),
                            args: args.clone(),
                        }))
                    }
                    JpOp::BinOp(op) => func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
                        dst: *dst,
                        op: *op,
                        lhs: args.get(0).copied().unwrap_or(ValueId(0)),
                        rhs: args.get(1).copied().unwrap_or(ValueId(0)),
                    })),
                    JpOp::Unary(op) => func.body.push(JoinInst::Compute(MirLikeInst::UnaryOp {
                        dst: *dst,
                        op: *op,
                        operand: args.get(0).copied().unwrap_or(ValueId(0)),
                    })),
                    JpOp::Select => func.body.push(JoinInst::Compute(MirLikeInst::Select {
                        dst: *dst,
                        cond: args.get(0).copied().unwrap_or(ValueId(0)),
                        then_val: args.get(1).copied().unwrap_or(ValueId(0)),
                        else_val: args.get(2).copied().unwrap_or(ValueId(0)),
                    })),
                    JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare {
                        dst: *dst,
                        op: *op,
                        lhs: args.get(0).copied().unwrap_or(ValueId(0)),
                        rhs: args.get(1).copied().unwrap_or(ValueId(0)),
                    })),
                },
                JpInst::TailCallFn { target, env } => func.body.push(JoinInst::Call {
                    func: JoinFuncId(target.0),
                    args: env.clone(),
                    k_next: None,
                    dst: None,
                }),
                JpInst::TailCallKont { target, env } => func.body.push(JoinInst::Jump {
                    cont: JoinContId(target.0),
                    args: env.clone(),
                    cond: None,
                }),
                JpInst::If {
                    cond,
                    then_target,
                    else_target,
                    env,
                } => {
                    // Jump to then_target on cond, else jump to else_target (Pattern1 minimal)
                    func.body.push(JoinInst::Jump {
                        cont: JoinContId(then_target.0),
                        args: env.clone(),
                        cond: Some(*cond),
                    });
                    func.body.push(JoinInst::Jump {
                        cont: JoinContId(else_target.0),
                        args: env.clone(),
                        cond: None,
                    });
                }
                JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } => {
                    // Not used in Pattern1 minimal; ignore for now
                }
            }
        }

        module.add_function(func);
    }

    module.entry = norm.entry.map(|e| JoinFuncId(e.0));
    module.phase = JoinIrPhase::Structured;
    module
}

/// Pattern2 専用のミニ変換（最小サブセット: ループ変数1つ + break、acc などの LoopState を 1 個まで＋ホスト 1 個まで）。
///
/// 制約:
/// - structured.phase は Structured であること
/// - main/loop_step/k_exit の 3 関数構成（joinir_min_loop 相当）
pub fn normalize_pattern2_minimal(structured: &JoinModule) -> NormalizedModule {
    assert!(
        structured.is_structured(),
        "normalize_pattern2_minimal: expected Structured JoinIR"
    );

    // Minimal guardrail: Pattern2 mini should have main/loop_step/k_exit only, with 1 loop param.
    let func_count = structured.functions.len();
    let loop_func = structured
        .functions
        .values()
        .find(|f| f.name == "loop_step")
        .or_else(|| structured.functions.get(&JoinFuncId::new(1)))
        .expect("normalize_pattern2_minimal: loop_step not found");

    assert!(
        func_count == 3,
        "normalize_pattern2_minimal: expected 3 functions (entry/loop_step/k_exit) but got {}",
        func_count
    );
    let param_max = {
        #[allow(unused_mut)]
        let mut max = 3;
        #[cfg(feature = "normalized_dev")]
        {
            let shapes = shape_guard::supported_shapes(structured);
            if shapes
                .iter()
                .any(|s| matches!(s, NormalizedDevShape::JsonparserAtoiMini))
            {
                max = max.max(8);
            }
            if shapes
                .iter()
                .any(|s| matches!(s, NormalizedDevShape::JsonparserAtoiReal))
            {
                max = max.max(10);
            }
            if shapes
                .iter()
                .any(|s| matches!(s, NormalizedDevShape::JsonparserParseNumberReal))
            {
                max = max.max(12);
            }
            if shapes
                .iter()
                .any(|s| matches!(s, NormalizedDevShape::JsonparserSkipWsReal))
            {
                max = max.max(6);
            }
            if shapes.iter().any(|s| {
                matches!(
                    s,
                    NormalizedDevShape::Pattern4ContinueMinimal
                        | NormalizedDevShape::JsonparserParseArrayContinueSkipWs
                        | NormalizedDevShape::JsonparserParseObjectContinueSkipWs
                )
            }) {
                max = max.max(6);
            }
            if shapes.iter().any(|s| {
                matches!(
                    s,
                    NormalizedDevShape::Pattern3IfSumMinimal
                        | NormalizedDevShape::Pattern3IfSumMulti
                        | NormalizedDevShape::Pattern3IfSumJson
                        | NormalizedDevShape::SelfhostIfSumP3
                        | NormalizedDevShape::SelfhostIfSumP3Ext
                        | NormalizedDevShape::SelfhostStmtCountP3
                        | NormalizedDevShape::SelfhostDetectFormatP3
                )
            }) {
                max = max.max(6);
            }
        }
        max
    };
    assert!(
        (1..=param_max).contains(&loop_func.params.len()),
        "normalize_pattern2_minimal: expected 1..={} params (loop var + carriers + optional host)",
        param_max
    );

    let jump_conds = loop_func
        .body
        .iter()
        .filter(|inst| matches!(inst, JoinInst::Jump { cond: Some(_), .. }))
        .count();
    let tail_calls = loop_func
        .body
        .iter()
        .filter(|inst| matches!(inst, JoinInst::Call { k_next: None, .. }))
        .count();
    assert!(
        jump_conds >= 1 && tail_calls >= 1,
        "normalize_pattern2_minimal: expected at least one conditional jump and one tail call"
    );

    let mut functions = BTreeMap::new();
    let mut env_layouts = Vec::new();

    for (fid, func) in &structured.functions {
        let env_layout_id = if func.params.is_empty() {
            None
        } else {
            let id = env_layouts.len() as u32;
            env_layouts.push(EnvLayout {
                id,
                fields: func
                    .params
                    .iter()
                    .enumerate()
                    .map(|(idx, vid)| EnvField {
                        name: format!("field{}", idx),
                        ty: None,
                        value_id: Some(*vid),
                    })
                    .collect(),
            });
            Some(id)
        };

        let mut body = Vec::new();
        for inst in &func.body {
            match inst {
                JoinInst::Compute(MirLikeInst::Const { dst, value }) => body.push(JpInst::Let {
                    dst: *dst,
                    op: JpOp::Const(value.clone()),
                    args: vec![],
                }),
                JoinInst::Compute(MirLikeInst::BoxCall {
                    dst,
                    box_name,
                    method,
                    args,
                }) => {
                    if let Some(dst) = dst {
                        body.push(JpInst::Let {
                            dst: *dst,
                            op: JpOp::BoxCall {
                                box_name: box_name.clone(),
                                method: method.clone(),
                            },
                            args: args.clone(),
                        })
                    }
                }
                JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => {
                    body.push(JpInst::Let {
                        dst: *dst,
                        op: JpOp::BinOp(*op),
                        args: vec![*lhs, *rhs],
                    })
                }
                JoinInst::Compute(MirLikeInst::UnaryOp { dst, op, operand }) => {
                    body.push(JpInst::Let {
                        dst: *dst,
                        op: JpOp::Unary(*op),
                        args: vec![*operand],
                    })
                }
                JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs }) => {
                    body.push(JpInst::Let {
                        dst: *dst,
                        op: JpOp::Compare(*op),
                        args: vec![*lhs, *rhs],
                    })
                }
                JoinInst::Compute(MirLikeInst::Select {
                    dst,
                    cond,
                    then_val,
                    else_val,
                }) => body.push(JpInst::Let {
                    dst: *dst,
                    op: JpOp::Select,
                    args: vec![*cond, *then_val, *else_val],
                }),
                JoinInst::Jump { cont, args, cond } => {
                    if let Some(cond_val) = cond {
                        body.push(JpInst::If {
                            cond: *cond_val,
                            then_target: JpFuncId(cont.0),
                            else_target: JpFuncId(loop_func.id.0),
                            env: args.clone(),
                        });
                    } else {
                        body.push(JpInst::TailCallKont {
                            target: JpFuncId(cont.0),
                            env: args.clone(),
                        });
                    }
                }
                JoinInst::Select {
                    dst,
                    cond,
                    then_val,
                    else_val,
                    ..
                } => {
                    body.push(JpInst::Let {
                        dst: *dst,
                        op: JpOp::Select,
                        args: vec![*cond, *then_val, *else_val],
                    });
                }
                JoinInst::Call {
                    func, args, k_next, ..
                } => {
                    if k_next.is_none() {
                        body.push(JpInst::TailCallFn {
                            target: JpFuncId(func.0),
                            env: args.clone(),
                        });
                    }
                }
                JoinInst::MethodCall {
                    dst,
                    receiver,
                    method,
                    args,
                    ..
                } => {
                    let mut call_args = Vec::with_capacity(args.len() + 1);
                    call_args.push(*receiver);
                    call_args.extend(args.iter().copied());
                    body.push(JpInst::Let {
                        dst: *dst,
                        op: JpOp::BoxCall {
                            box_name: "unknown".to_string(),
                            method: method.clone(),
                        },
                        args: call_args,
                    });
                }
                _ => {
                    // Ret / other instructions are ignored in this minimal prototype
                }
            }
        }

        functions.insert(
            JpFuncId(fid.0),
            JpFunction {
                id: JpFuncId(fid.0),
                name: func.name.clone(),
                env_layout: env_layout_id,
                body,
                is_kont: func.name.starts_with("k_"),
            },
        );
    }

    let norm = NormalizedModule {
        functions,
        entry: structured.entry.map(|e| JpFuncId(e.0)),
        env_layouts,
        phase: JoinIrPhase::Normalized,
        structured_backup: Some(structured.clone()),
    };

    #[cfg(feature = "normalized_dev")]
    {
        verify_normalized_pattern2(&norm, param_max).expect("normalized Pattern2 verifier");
    }

    norm
}

#[cfg(feature = "normalized_dev")]
fn normalize_pattern2_shape(
    structured: &JoinModule,
    target_shape: NormalizedDevShape,
) -> Result<NormalizedModule, String> {
    if !structured.is_structured() {
        return Err("[normalize_p2] Not structured JoinIR".to_string());
    }

    let shapes = shape_guard::supported_shapes(structured);
    if !shapes.contains(&target_shape) {
        return Err(format!(
            "[normalize_p2] shape mismatch: expected {:?}, got {:?}",
            target_shape, shapes
        ));
    }

    Ok(normalize_pattern2_minimal(structured))
}

/// Phase 50: selfhost token-scan P2 を Normalized に載せる（dev-only）。
#[cfg(feature = "normalized_dev")]
pub fn normalize_selfhost_token_scan_p2(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern2_shape(structured, NormalizedDevShape::SelfhostTokenScanP2)
}

/// Phase 51: selfhost token-scan P2（accum 拡張）を Normalized に載せる（dev-only）。
#[cfg(feature = "normalized_dev")]
pub fn normalize_selfhost_token_scan_p2_accum(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern2_shape(structured, NormalizedDevShape::SelfhostTokenScanP2Accum)
}

/// Phase 47-A/B: Normalize Pattern3 if-sum shapes to Normalized JoinIR
#[cfg(feature = "normalized_dev")]
pub fn normalize_pattern3_if_sum_minimal(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::Pattern3IfSumMinimal)
}

/// Phase 47-B: Normalize Pattern3 if-sum multi-carrier (sum+count) shape.
#[cfg(feature = "normalized_dev")]
pub fn normalize_pattern3_if_sum_multi_minimal(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::Pattern3IfSumMulti)
}

/// Phase 47-B: Normalize JsonParser if-sum (mini) shape.
#[cfg(feature = "normalized_dev")]
pub fn normalize_pattern3_if_sum_json_minimal(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::Pattern3IfSumJson)
}

/// Phase 50: selfhost if-sum P3 を Normalized に載せる（dev-only）。
#[cfg(feature = "normalized_dev")]
pub fn normalize_selfhost_if_sum_p3(structured: &JoinModule) -> Result<NormalizedModule, String> {
    normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::SelfhostIfSumP3)
}

/// Phase 51: selfhost if-sum P3（ext 拡張）を Normalized に載せる（dev-only）。
#[cfg(feature = "normalized_dev")]
pub fn normalize_selfhost_if_sum_p3_ext(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern3_if_sum_shape(structured, NormalizedDevShape::SelfhostIfSumP3Ext)
}

#[cfg(feature = "normalized_dev")]
fn normalize_pattern3_if_sum_shape(
    structured: &JoinModule,
    target_shape: NormalizedDevShape,
) -> Result<NormalizedModule, String> {
    if !structured.is_structured() {
        return Err("[normalize_p3] Not structured JoinIR".to_string());
    }

    let shapes = shape_guard::supported_shapes(structured);
    if !shapes.contains(&target_shape) {
        return Err(format!(
            "[normalize_p3] shape mismatch: expected {:?}, got {:?}",
            target_shape, shapes
        ));
    }

    // Phase 47-B: P3 if-sum は既存の P2 ミニ正規化器で十分に表現できる
    // （Select/If/Compare/BinOp をそのまま JpInst に写す）。
    Ok(normalize_pattern2_minimal(structured))
}

/// Phase 48-A: Pattern4 (continue) minimal ループの正規化。
///
/// ガード:
/// - structured.phase は Structured であること
/// - 対象は Pattern4ContinueMinimal のシンプル continue パターン
///
/// 実装方針:
/// - Phase 48-A minimal: P2 正規化に委譲（P4 は P2 の逆制御フローなので同じインフラ利用可）
/// - TODO (Phase 48-B): P4 固有の正規化実装
///   - EnvLayout for i, count carriers
///   - HeaderCond → ContinueCheck → Updates → Tail step sequence
///   - continue = early TailCallFn (skip Updates)
#[cfg(feature = "normalized_dev")]
pub fn normalize_pattern4_continue_minimal(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern4_continue_shape(structured, NormalizedDevShape::Pattern4ContinueMinimal)
}

/// Phase 48-B: JsonParser _parse_array continue skip_ws を Normalized に載せる（dev-only）。
#[cfg(feature = "normalized_dev")]
pub fn normalize_jsonparser_parse_array_continue_skip_ws(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern4_continue_shape(
        structured,
        NormalizedDevShape::JsonparserParseArrayContinueSkipWs,
    )
}

/// Phase 48-B: JsonParser _parse_object continue skip_ws を Normalized に載せる（dev-only）。
#[cfg(feature = "normalized_dev")]
pub fn normalize_jsonparser_parse_object_continue_skip_ws(
    structured: &JoinModule,
) -> Result<NormalizedModule, String> {
    normalize_pattern4_continue_shape(
        structured,
        NormalizedDevShape::JsonparserParseObjectContinueSkipWs,
    )
}

#[cfg(feature = "normalized_dev")]
fn normalize_pattern4_continue_shape(
    structured: &JoinModule,
    target_shape: NormalizedDevShape,
) -> Result<NormalizedModule, String> {
    if !structured.is_structured() {
        return Err("[normalize_p4] Not structured JoinIR".to_string());
    }

    // Use shape detection to verify P4 shape
    let shapes = shape_guard::supported_shapes(structured);
    if !shapes.contains(&target_shape) {
        return Err(format!(
            "[normalize_p4] shape mismatch: expected {:?}, got {:?}",
            target_shape, shapes
        ));
    }

    // Phase 48-B: reuse Pattern2 minimal normalizer (continue is early tail-call).
    Ok(normalize_pattern2_minimal(structured))
}

/// Pattern2 専用: Normalized → Structured への簡易逆変換。
pub fn normalized_pattern2_to_structured(norm: &NormalizedModule) -> JoinModule {
    if let Some(backup) = norm.to_structured() {
        return backup;
    }

    let mut module = JoinModule::new();

    for (jp_id, jp_fn) in &norm.functions {
        let params: Vec<ValueId> = jp_fn
            .env_layout
            .and_then(|id| norm.env_layouts.iter().find(|layout| layout.id == id))
            .map(|layout| {
                layout
                    .fields
                    .iter()
                    .enumerate()
                    .map(|(idx, f)| f.value_id.unwrap_or(ValueId(idx as u32)))
                    .collect()
            })
            .unwrap_or_default();

        let mut func = JoinFunction::new(JoinFuncId(jp_id.0), jp_fn.name.clone(), params);

        for inst in &jp_fn.body {
            match inst {
                JpInst::Let { dst, op, args } => match op {
                    JpOp::Const(v) => func.body.push(JoinInst::Compute(MirLikeInst::Const {
                        dst: *dst,
                        value: v.clone(),
                    })),
                    JpOp::BoxCall { box_name, method } => {
                        func.body.push(JoinInst::Compute(MirLikeInst::BoxCall {
                            dst: Some(*dst),
                            box_name: box_name.clone(),
                            method: method.clone(),
                            args: args.clone(),
                        }))
                    }
                    JpOp::BinOp(op) => func.body.push(JoinInst::Compute(MirLikeInst::BinOp {
                        dst: *dst,
                        op: *op,
                        lhs: args.get(0).copied().unwrap_or(ValueId(0)),
                        rhs: args.get(1).copied().unwrap_or(ValueId(0)),
                    })),
                    JpOp::Unary(op) => func.body.push(JoinInst::Compute(MirLikeInst::UnaryOp {
                        dst: *dst,
                        op: *op,
                        operand: args.get(0).copied().unwrap_or(ValueId(0)),
                    })),
                    JpOp::Select => func.body.push(JoinInst::Compute(MirLikeInst::Select {
                        dst: *dst,
                        cond: args.get(0).copied().unwrap_or(ValueId(0)),
                        then_val: args.get(1).copied().unwrap_or(ValueId(0)),
                        else_val: args.get(2).copied().unwrap_or(ValueId(0)),
                    })),
                    JpOp::Compare(op) => func.body.push(JoinInst::Compute(MirLikeInst::Compare {
                        dst: *dst,
                        op: *op,
                        lhs: args.get(0).copied().unwrap_or(ValueId(0)),
                        rhs: args.get(1).copied().unwrap_or(ValueId(0)),
                    })),
                },
                JpInst::TailCallFn { target, env } => func.body.push(JoinInst::Call {
                    func: JoinFuncId(target.0),
                    args: env.clone(),
                    k_next: None,
                    dst: None,
                }),
                JpInst::TailCallKont { target, env } => func.body.push(JoinInst::Jump {
                    cont: JoinContId(target.0),
                    args: env.clone(),
                    cond: None,
                }),
                JpInst::If {
                    cond,
                    then_target,
                    else_target,
                    env,
                } => {
                    func.body.push(JoinInst::Jump {
                        cont: JoinContId(then_target.0),
                        args: env.clone(),
                        cond: Some(*cond),
                    });
                    func.body.push(JoinInst::Jump {
                        cont: JoinContId(else_target.0),
                        args: env.clone(),
                        cond: None,
                    });
                }
                JpInst::EnvLoad { .. } | JpInst::EnvStore { .. } => {
                    // Not used in minimal pattern2 bridge
                }
            }
        }

        module.add_function(func);
    }

    module.entry = norm.entry.map(|e| JoinFuncId(e.0));
    module.phase = JoinIrPhase::Structured;
    module
}

#[cfg(feature = "normalized_dev")]
fn verify_normalized_pattern2(
    module: &NormalizedModule,
    max_env_fields: usize,
) -> Result<(), String> {
    if module.phase != JoinIrPhase::Normalized {
        return Err("[joinir/normalized-dev] pattern2: phase must be Normalized".to_string());
    }

    let mut layout_sizes: HashMap<u32, usize> = HashMap::new();
    for layout in &module.env_layouts {
        let size = layout.fields.len();
        if !(1..=max_env_fields).contains(&size) {
            return Err(format!(
                "[joinir/normalized-dev] pattern2: expected 1..={} env fields, got {}",
                max_env_fields, size
            ));
        }
        layout_sizes.insert(layout.id, size);
    }

    for func in module.functions.values() {
        let expected_env_len = func
            .env_layout
            .and_then(|id| layout_sizes.get(&id))
            .copied();

        for inst in &func.body {
            match inst {
                JpInst::Let { .. }
                | JpInst::EnvLoad { .. }
                | JpInst::EnvStore { .. }
                | JpInst::TailCallFn { .. }
                | JpInst::TailCallKont { .. }
                | JpInst::If { .. } => {}
            }

            match inst {
                JpInst::TailCallFn { env, .. }
                | JpInst::TailCallKont { env, .. }
                | JpInst::If { env, .. } => {
                    if let Some(expected) = expected_env_len {
                        if env.is_empty() {
                            return Err("[joinir/normalized-dev] pattern2: env must not be empty"
                                .to_string());
                        }
                        let _ = expected;
                    }
                }
                _ => {}
            }
        }

        if let Some(last) = func.body.last() {
            match last {
                JpInst::TailCallFn { .. } | JpInst::TailCallKont { .. } | JpInst::If { .. } => {}
                _ => {
                    return Err(format!(
                        "[joinir/normalized-dev] pattern2: function '{}' does not end with tail call/if",
                        func.name
                    ));
                }
            }
        }
    }

    Ok(())
}

/// Pattern1 専用のミニ変換。
///
/// 制約:
/// - structured.phase は Structured であること
/// - 対象は Pattern1 のシンプル while（break/continue なし）
pub fn normalize_pattern1_minimal(structured: &JoinModule) -> NormalizedModule {
    assert!(
        structured.is_structured(),
        "normalize_pattern1_minimal: expected Structured JoinIR"
    );

    // entry/loop_step/k_exit を前提に、loop_step を拾う
    let loop_func = structured
        .functions
        .values()
        .find(|f| f.name == "loop_step")
        .or_else(|| structured.functions.get(&JoinFuncId::new(1)))
        .expect("normalize_pattern1_minimal: loop_step not found");

    // EnvLayout をざっくり作る（フィールド名は field0, field1,... で代用）
    let env_layout = EnvLayout {
        id: 0,
        fields: loop_func
            .params
            .iter()
            .enumerate()
            .map(|(idx, vid)| EnvField {
                name: format!("field{}", idx),
                ty: None,
                value_id: Some(*vid),
            })
            .collect(),
    };

    // loop_step の Compute を Let に写経（Pattern1 では Compute/Call/Ret のみ想定）
    let mut extra_konts: HashSet<JpFuncId> = HashSet::new();
    let mut jp_body = Vec::new();
    for inst in &loop_func.body {
        match inst {
            JoinInst::Compute(MirLikeInst::Const { dst, value }) => jp_body.push(JpInst::Let {
                dst: *dst,
                op: JpOp::Const(value.clone()),
                args: vec![],
            }),
            JoinInst::Compute(MirLikeInst::BoxCall {
                dst,
                box_name,
                method,
                args,
            }) => {
                if let Some(dst) = dst {
                    jp_body.push(JpInst::Let {
                        dst: *dst,
                        op: JpOp::BoxCall {
                            box_name: box_name.clone(),
                            method: method.clone(),
                        },
                        args: args.clone(),
                    })
                }
            }
            JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => {
                jp_body.push(JpInst::Let {
                    dst: *dst,
                    op: JpOp::BinOp(*op),
                    args: vec![*lhs, *rhs],
                })
            }
            JoinInst::Compute(MirLikeInst::UnaryOp { dst, op, operand }) => {
                jp_body.push(JpInst::Let {
                    dst: *dst,
                    op: JpOp::Unary(*op),
                    args: vec![*operand],
                })
            }
            JoinInst::Compute(MirLikeInst::Compare { dst, op, lhs, rhs }) => {
                jp_body.push(JpInst::Let {
                    dst: *dst,
                    op: JpOp::Compare(*op),
                    args: vec![*lhs, *rhs],
                })
            }
            // Tail recursion / exit は TailCall と If でざっくり表現
            JoinInst::Jump { cont, args, cond } => {
                if let Some(cond_val) = cond {
                    extra_konts.insert(JpFuncId(cont.0));
                    jp_body.push(JpInst::If {
                        cond: *cond_val,
                        then_target: JpFuncId(cont.0),
                        else_target: JpFuncId(loop_func.id.0),
                        env: args.clone(),
                    });
                } else {
                    extra_konts.insert(JpFuncId(cont.0));
                    jp_body.push(JpInst::TailCallKont {
                        target: JpFuncId(cont.0),
                        env: args.clone(),
                    });
                }
            }
            JoinInst::Call { func, args, .. } => jp_body.push(JpInst::TailCallFn {
                target: JpFuncId(func.0),
                env: args.clone(),
            }),
            JoinInst::Ret { value } => {
                if let Some(v) = value {
                    let kont_id = JpFuncId(loop_func.id.0 + 1);
                    extra_konts.insert(kont_id);
                    jp_body.push(JpInst::TailCallKont {
                        target: kont_id,
                        env: vec![*v],
                    });
                }
            }
            _ => {
                // Pattern1 の最小変換なので他は無視（将来拡張）
            }
        }
    }

    let loop_fn = JpFunction {
        id: JpFuncId(loop_func.id.0),
        name: loop_func.name.clone(),
        env_layout: Some(env_layout.id),
        body: jp_body,
        is_kont: false,
    };

    let mut functions = BTreeMap::new();
    functions.insert(loop_fn.id, loop_fn);

    for kont_id in extra_konts {
        functions.entry(kont_id).or_insert_with(|| JpFunction {
            id: kont_id,
            name: format!("kont_{}", kont_id.0),
            env_layout: Some(env_layout.id),
            body: Vec::new(),
            is_kont: true,
        });
    }

    let norm = NormalizedModule {
        functions,
        entry: Some(JpFuncId(loop_func.id.0)),
        env_layouts: vec![env_layout],
        phase: JoinIrPhase::Normalized,
        structured_backup: Some(structured.clone()),
    };

    #[cfg(feature = "normalized_dev")]
    {
        verify_normalized_pattern1(&norm).expect("normalized verifier");
    }

    norm
}

/// Dev helper: Structured → Normalized → Structured roundtrip (Pattern1/2 minis only).
#[cfg(feature = "normalized_dev")]
pub(crate) fn normalized_dev_roundtrip_structured(
    module: &JoinModule,
) -> Result<JoinModule, String> {
    if !module.is_structured() {
        return Err("[joinir/normalized-dev] expected Structured JoinModule".to_string());
    }

    let shapes = shape_guard::supported_shapes(module);
    if shapes.is_empty() {
        return Err(
            "[joinir/normalized-dev] module shape is not supported by normalized_dev".into(),
        );
    }

    let debug = dev_env::normalized_dev_logs_enabled() && crate::config::env::joinir_dev_enabled();

    for shape in shapes {
        if debug {
            eprintln!(
                "[joinir/normalized-dev/roundtrip] attempting {:?} normalization",
                shape
            );
        }

        let attempt = match shape {
            NormalizedDevShape::Pattern1Mini => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_pattern1_minimal(module);
                normalized_pattern1_to_structured(&norm)
            })),
            NormalizedDevShape::Pattern2Mini
            | NormalizedDevShape::JsonparserSkipWsMini
            | NormalizedDevShape::JsonparserSkipWsReal
            | NormalizedDevShape::JsonparserAtoiMini
            | NormalizedDevShape::JsonparserAtoiReal
            | NormalizedDevShape::JsonparserParseNumberReal => {
                catch_unwind(AssertUnwindSafe(|| {
                    let norm = normalize_pattern2_minimal(module);
                    normalized_pattern2_to_structured(&norm)
                }))
            }
            NormalizedDevShape::SelfhostTokenScanP2 => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_selfhost_token_scan_p2(module)
                    .expect("selfhost P2 normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::SelfhostTokenScanP2Accum => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_selfhost_token_scan_p2_accum(module)
                    .expect("selfhost P2 accum normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            // Phase 47-A: P3 minimal (delegates to P2 for now, but uses proper guard)
            NormalizedDevShape::Pattern3IfSumMinimal => catch_unwind(AssertUnwindSafe(|| {
                let norm =
                    normalize_pattern3_if_sum_minimal(module).expect("P3 normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::Pattern3IfSumMulti => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_pattern3_if_sum_multi_minimal(module)
                    .expect("P3 multi normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::Pattern3IfSumJson => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_pattern3_if_sum_json_minimal(module)
                    .expect("P3 json normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::SelfhostIfSumP3 => catch_unwind(AssertUnwindSafe(|| {
                let norm =
                    normalize_selfhost_if_sum_p3(module).expect("selfhost P3 normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::SelfhostIfSumP3Ext => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_selfhost_if_sum_p3_ext(module)
                    .expect("selfhost P3 ext normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            // Phase 53: selfhost P2/P3 practical variations (delegate to existing normalizers)
            NormalizedDevShape::SelfhostArgsParseP2 => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_pattern2_minimal(module);
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::SelfhostStmtCountP3 => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_selfhost_if_sum_p3_ext(module)
                    .expect("selfhost stmt_count P3 normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            // Phase 54: selfhost P2/P3 shape growth (delegate to existing normalizers)
            NormalizedDevShape::SelfhostVerifySchemaP2 => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_pattern2_minimal(module);
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::SelfhostDetectFormatP3 => catch_unwind(AssertUnwindSafe(|| {
                let norm = normalize_selfhost_if_sum_p3_ext(module)
                    .expect("selfhost detect_format P3 normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            // Phase 48-A: P4 minimal (delegates to P2 for now, but uses proper guard)
            NormalizedDevShape::Pattern4ContinueMinimal => catch_unwind(AssertUnwindSafe(|| {
                let norm =
                    normalize_pattern4_continue_minimal(module).expect("P4 normalization failed");
                normalized_pattern2_to_structured(&norm)
            })),
            NormalizedDevShape::JsonparserParseArrayContinueSkipWs => {
                catch_unwind(AssertUnwindSafe(|| {
                    let norm = normalize_jsonparser_parse_array_continue_skip_ws(module)
                        .expect("P4 array normalization failed");
                    normalized_pattern2_to_structured(&norm)
                }))
            }
            NormalizedDevShape::JsonparserParseObjectContinueSkipWs => {
                catch_unwind(AssertUnwindSafe(|| {
                    let norm = normalize_jsonparser_parse_object_continue_skip_ws(module)
                        .expect("P4 object normalization failed");
                    normalized_pattern2_to_structured(&norm)
                }))
            }
            // Phase 89: Continue + Early Return pattern (dev-only, delegates to P2 for now)
            NormalizedDevShape::PatternContinueReturnMinimal => {
                catch_unwind(AssertUnwindSafe(|| {
                    let norm = normalize_pattern2_minimal(module);
                    normalized_pattern2_to_structured(&norm)
                }))
            }
            // Phase 90: Parse String Composite pattern (dev-only, delegates to P2 for now)
            NormalizedDevShape::ParseStringCompositeMinimal => {
                catch_unwind(AssertUnwindSafe(|| {
                    let norm = normalize_pattern2_minimal(module);
                    normalized_pattern2_to_structured(&norm)
                }))
            }
        };

        match attempt {
            Ok(structured) => {
                if debug {
                    eprintln!(
                        "[joinir/normalized-dev/roundtrip] {:?} normalization succeeded (functions={})",
                        shape,
                        structured.functions.len()
                    );
                }
                return Ok(structured);
            }
            Err(_) => {
                if debug {
                    eprintln!(
                        "[joinir/normalized-dev/roundtrip] {:?} normalization failed (unsupported)",
                        shape
                    );
                }
            }
        }
    }

    Err("[joinir/normalized-dev] all normalization attempts failed".into())
}

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

    fn build_structured_pattern1() -> JoinModule {
        let mut module = JoinModule::new();
        let mut loop_fn = JoinFunction::new(
            JoinFuncId::new(1),
            "loop_step".to_string(),
            vec![ValueId(10)],
        );

        loop_fn.body.push(JoinInst::Compute(MirLikeInst::Const {
            dst: ValueId(11),
            value: ConstValue::Integer(0),
        }));
        loop_fn.body.push(JoinInst::Compute(MirLikeInst::BinOp {
            dst: ValueId(12),
            op: BinOpKind::Add,
            lhs: ValueId(10),
            rhs: ValueId(11),
        }));
        loop_fn.body.push(JoinInst::Jump {
            cont: JoinContId(2),
            args: vec![ValueId(12)],
            cond: Some(ValueId(12)), // dummy
        });

        let mut k_exit =
            JoinFunction::new(JoinFuncId::new(2), "k_exit".to_string(), vec![ValueId(12)]);
        k_exit.body.push(JoinInst::Ret {
            value: Some(ValueId(12)),
        });

        module.entry = Some(loop_fn.id);
        module.add_function(loop_fn);
        module.add_function(k_exit);
        module
    }

    #[test]
    fn normalized_pattern1_minimal_smoke() {
        let structured = build_structured_pattern1();
        let normalized = normalize_pattern1_minimal(&structured);
        assert_eq!(normalized.phase, JoinIrPhase::Normalized);
        assert!(!normalized.env_layouts.is_empty());
        assert!(!normalized.functions.is_empty());

        #[cfg(feature = "normalized_dev")]
        {
            verify_normalized_pattern1(&normalized).expect("verifier should pass");
        }

        let restored = normalized.to_structured().expect("backup");
        assert!(restored.is_structured());
        assert_eq!(restored.functions.len(), structured.functions.len());
    }

    #[test]
    fn normalized_pattern1_roundtrip_structured_equivalent() {
        let structured = build_structured_pattern1();
        let normalized = normalize_pattern1_minimal(&structured);
        let reconstructed = normalized_pattern1_to_structured(&normalized);

        assert!(reconstructed.is_structured());
        assert_eq!(reconstructed.functions.len(), structured.functions.len());
    }
}
