use crate::runner::NyashRunner;

/// Collect using targets and strip using lines (no inlining).
/// Returns (cleaned_source, prelude_paths, imports) where:
/// - `prelude_paths` are resolved file paths to be parsed separately and AST-merged (when `NYASH_USING_AST=1`)
/// - `imports` is a HashMap mapping alias names to box types (for MirBuilder resolution)
///
/// Notes
/// - This function enforces profile policies (prod: disallow file-using; only
///   packages/aliases from nyash.toml are accepted).
/// - SSOT: Resolution sources and aliases come exclusively from nyash.toml.
/// - All runner modes use this static path to avoid logic drift.
pub fn collect_using_and_strip(
    runner: &NyashRunner,
    code: &str,
    filename: &str,
) -> Result<
    (
        String,
        Vec<String>,
        std::collections::HashMap<String, String>,
    ),
    String,
> {
    if !crate::config::env::enable_using() {
        return Ok((
            code.to_string(),
            Vec::new(),
            std::collections::HashMap::new(),
        ));
    }
    let using_ctx = runner.init_using_context();
    let prod = crate::config::env::using_is_prod();
    let strict = crate::config::env::env_bool("NYASH_USING_STRICT");
    let verbose =
        crate::config::env::cli_verbose() || crate::config::env::env_bool("NYASH_RESOLVE_TRACE");
    let ctx_dir = std::path::Path::new(filename).parent();

    let mut out = String::with_capacity(code.len());
    let mut prelude_paths: Vec<String> = Vec::new();
    // Duplicate-using detection (same target imported multiple times or alias rebound): error in all profiles
    use std::collections::HashMap;
    let mut seen_paths: HashMap<String, (String, usize)> = HashMap::new(); // canon_path -> (alias/label, first_line)
    let mut seen_aliases: HashMap<String, (String, usize)> = HashMap::new(); // alias -> (canon_path, first_line)
                                                                             // Determine if this file is inside a declared package root; if so, allow
                                                                             // internal file-using within the package even when file-using is globally disallowed.
    let filename_canon = std::fs::canonicalize(filename).ok();
    let mut inside_pkg = false;
    if let Some(ref fc) = filename_canon {
        for (_name, pkg) in &using_ctx.packages {
            let base = std::path::Path::new(&pkg.path);
            if let Ok(root) = std::fs::canonicalize(base) {
                if fc.starts_with(&root) {
                    inside_pkg = true;
                    break;
                }
            }
        }
    }
    for (lineno0, line) in code.lines().enumerate() {
        let line_no = lineno0 + 1;
        let t = line.trim_start();
        if t.starts_with("using ") {
            crate::cli_v!("[using] stripped line: {}", line);
            let rest0 = t.strip_prefix("using ").unwrap().trim();
            let rest0 = rest0.split('#').next().unwrap_or(rest0).trim();
            let rest0 = rest0.strip_suffix(';').unwrap_or(rest0).trim();
            let (target, alias_name) = if let Some(pos) = rest0.find(" as ") {
                (
                    rest0[..pos].trim().to_string(),
                    Some(rest0[pos + 4..].trim().to_string()),
                )
            } else {
                (rest0.to_string(), None)
            };
            // Strip quotes from target for alias/module lookup and path detection
            let target_unquoted = target.trim_matches('"').to_string();

            // Check if this is a known alias or module FIRST before treating as file path
            let is_known_alias_or_module = using_ctx.aliases.contains_key(&target_unquoted)
                || using_ctx
                    .pending_modules
                    .iter()
                    .any(|(k, _)| k == &target_unquoted)
                || using_ctx.packages.contains_key(&target_unquoted);

            let is_path = if is_known_alias_or_module {
                // Known alias/module - don't treat as file path even if quoted
                false
            } else {
                // SSOT: delegate path pattern check
                crate::runner::modes::common_util::resolve::path_util::is_using_target_path_unquoted(
                    &target_unquoted,
                )
            };
            if is_path {
                // SSOT: Disallow file-using at top-level; allow only for sources located
                // under a declared package root (internal package wiring), so that packages
                // can organize their modules via file paths.
                if (prod || !crate::config::env::allow_using_file()) && !inside_pkg {
                    return Err(format!(
                        "{}:{}: using: file paths are disallowed in this profile. Add it to nyash.toml [using]/[modules] and reference by name: {}\n  suggestions: using \"alias.name\" as Name  |  dev/test: set NYASH_PREINCLUDE=1 to expand includes ahead of VM\n  docs: see docs/reference/using.md",
                        filename,
                        line_no,
                        target
                    ));
                }
                let path = target_unquoted.clone();
                // Resolve relative to current file dir
                let mut p = std::path::PathBuf::from(&path);
                if p.is_relative() {
                    if let Some(dir) = ctx_dir {
                        let cand = dir.join(&p);
                        if cand.exists() {
                            p = cand;
                        }
                    }
                    // Also try NYASH_ROOT when available (repo-root relative like "apps/...")
                    if p.is_relative() {
                        if let Ok(root) = std::env::var("NYASH_ROOT") {
                            let cand = std::path::Path::new(&root).join(&p);
                            if cand.exists() {
                                p = cand;
                            }
                        } else {
                            // Fallback: guess project root from executable path (target/release/nyash)
                            if let Ok(exe) = std::env::current_exe() {
                                if let Some(root) = exe
                                    .parent()
                                    .and_then(|p| p.parent())
                                    .and_then(|p| p.parent())
                                {
                                    let cand = root.join(&p);
                                    if cand.exists() {
                                        p = cand;
                                    }
                                }
                            }
                        }
                    }
                }
                if verbose {
                    crate::runner::trace::log(format!(
                        "[using/resolve] file '{}' -> '{}'",
                        target,
                        p.display()
                    ));
                }
                let path_str = p.to_string_lossy().to_string();
                // Duplicate detection
                let canon = std::fs::canonicalize(&path_str)
                    .ok()
                    .map(|pb| pb.to_string_lossy().to_string())
                    .unwrap_or_else(|| path_str.clone());
                if let Some((prev_alias, prev_line)) = seen_paths.get(&canon) {
                    return Err(format!(
                        "using: duplicate import of '{}' at {}:{} (previous alias: '{}' first seen at line {})",
                        canon,
                        filename,
                        line_no,
                        prev_alias,
                        prev_line
                    ));
                } else {
                    seen_paths.insert(
                        canon.clone(),
                        (
                            alias_name.clone().unwrap_or_else(|| "<none>".into()),
                            line_no,
                        ),
                    );
                }
                if let Some(alias) = alias_name.clone() {
                    if let Some((prev_path, prev_line)) = seen_aliases.get(&alias) {
                        if prev_path != &canon {
                            return Err(format!(
                                "using: alias '{}' rebound at {}:{} (was '{}' first seen at line {})",
                                alias,
                                filename,
                                line_no,
                                prev_path,
                                prev_line
                            ));
                        }
                    } else {
                        seen_aliases.insert(alias, (canon, line_no));
                    }
                }
                prelude_paths.push(path_str);
                continue;
            }
            // Resolve namespaces/packages
            if prod {
                // prod: allow only names present in aliases/modules/packages (nyash.toml)
                let mut name: String = target_unquoted.clone();
                if let Some(v) = using_ctx.aliases.get(&target_unquoted) {
                    name = v.clone();
                }

                // SSOT: try central resolver first (modules/packages/relative)
                if let Ok(resolved) = crate::using::resolver::resolve_using_target_common(
                    &name,
                    &using_ctx.pending_modules,
                    &using_ctx.using_paths,
                    &using_ctx.packages,
                    ctx_dir,
                    strict,
                    verbose,
                ) {
                    if resolved.starts_with("dylib:") {
                        continue;
                    }
                    let canon = std::fs::canonicalize(&resolved)
                        .ok()
                        .map(|pb| pb.to_string_lossy().to_string())
                        .unwrap_or_else(|| resolved.clone());
                    if let Some((prev_alias, prev_line)) = seen_paths.get(&canon) {
                        return Err(format!(
                            "using: duplicate import of '{}' at {}:{} (previous alias: '{}' first seen at line {})",
                            canon, filename, line_no, prev_alias, prev_line
                        ));
                    } else {
                        seen_paths.insert(
                            canon.clone(),
                            (
                                alias_name.clone().unwrap_or_else(|| "<none>".into()),
                                line_no,
                            ),
                        );
                    }
                    if let Some(alias) = alias_name.clone() {
                        if let Some((prev_path, prev_line)) = seen_aliases.get(&alias) {
                            if prev_path != &canon {
                                return Err(format!(
                                    "using: alias '{}' rebound at {}:{} (was '{}' first seen at line {})",
                                    alias, filename, line_no, prev_path, prev_line
                                ));
                            }
                        } else {
                            seen_aliases.insert(alias, (canon, line_no));
                        }
                    }
                    prelude_paths.push(resolved);
                    continue;
                }

                // 1) modules mapping (name -> path)
                if let Some((_, mod_path)) =
                    using_ctx.pending_modules.iter().find(|(n, _)| n == &name)
                {
                    let out_path = mod_path.clone();
                    // Duplicate detection (same semantics as packages below)
                    let canon = std::fs::canonicalize(&out_path)
                        .ok()
                        .map(|pb| pb.to_string_lossy().to_string())
                        .unwrap_or_else(|| out_path.clone());
                    if let Some((prev_alias, prev_line)) = seen_paths.get(&canon) {
                        return Err(format!(
                            "using: duplicate import of '{}' at {}:{} (previous alias: '{}' first seen at line {})",
                            canon, filename, line_no, prev_alias, prev_line
                        ));
                    } else {
                        seen_paths.insert(
                            canon.clone(),
                            (
                                alias_name.clone().unwrap_or_else(|| "<none>".into()),
                                line_no,
                            ),
                        );
                    }
                    if let Some(alias) = alias_name.clone() {
                        if let Some((prev_path, prev_line)) = seen_aliases.get(&alias) {
                            if prev_path != &canon {
                                return Err(format!(
                                    "using: alias '{}' rebound at {}:{} (was '{}' first seen at line {})",
                                    alias, filename, line_no, prev_path, prev_line
                                ));
                            }
                        } else {
                            seen_aliases.insert(alias, (canon, line_no));
                        }
                    }
                    prelude_paths.push(out_path);
                }
                // 2) named packages
                else if let Some(pkg) = using_ctx.packages.get(&name) {
                    use crate::using::spec::PackageKind;
                    match pkg.kind {
                        PackageKind::Dylib => {
                            // dylib: nothing to prelude-parse; runtime loader handles it.
                        }
                        PackageKind::Package => {
                            let base = std::path::Path::new(&pkg.path);
                            let out = if let Some(m) = &pkg.main {
                                if matches!(
                                    base.extension().and_then(|s| s.to_str()),
                                    Some("nyash") | Some("hako")
                                ) {
                                    pkg.path.clone()
                                } else {
                                    base.join(m).to_string_lossy().to_string()
                                }
                            } else if matches!(
                                base.extension().and_then(|s| s.to_str()),
                                Some("nyash") | Some("hako")
                            ) {
                                pkg.path.clone()
                            } else {
                                let leaf =
                                    base.file_name().and_then(|s| s.to_str()).unwrap_or(&name);
                                base.join(format!("{}.hako", leaf))
                                    .to_string_lossy()
                                    .to_string()
                            };
                            // Duplicate detection for prod package alias resolution
                            let canon = std::fs::canonicalize(&out)
                                .ok()
                                .map(|pb| pb.to_string_lossy().to_string())
                                .unwrap_or_else(|| out.clone());
                            if let Some((prev_alias, prev_line)) = seen_paths.get(&canon) {
                                return Err(format!(
                                    "using: duplicate import of '{}' at {}:{} (previous alias: '{}' first seen at line {})",
                                    canon,
                                    filename,
                                    line_no,
                                    prev_alias,
                                    prev_line
                                ));
                            } else {
                                seen_paths.insert(
                                    canon.clone(),
                                    (
                                        alias_name.clone().unwrap_or_else(|| "<none>".into()),
                                        line_no,
                                    ),
                                );
                            }
                            if let Some(alias) = alias_name.clone() {
                                if let Some((prev_path, prev_line)) = seen_aliases.get(&alias) {
                                    if prev_path != &canon {
                                        return Err(format!(
                                            "using: alias '{}' rebound at {}:{} (was '{}' first seen at line {})",
                                            alias, filename, line_no, prev_path, prev_line
                                        ));
                                    }
                                } else {
                                    seen_aliases.insert(alias, (canon, line_no));
                                }
                            }
                            // push resolved file path for text-prelude merge
                            prelude_paths.push(out);
                        }
                    }
                } else {
                    // ⚠️ Phase 0.3: User-friendly "Did you mean?" suggestions
                    let similar: Vec<_> = using_ctx
                        .aliases
                        .keys()
                        .filter(|k| {
                            k.to_lowercase().contains(&target_unquoted.to_lowercase())
                                || target_unquoted.to_lowercase().contains(&k.to_lowercase())
                        })
                        .take(3)
                        .collect();

                    let mut err_msg = format!(
                        "{}:{}: using: '{}' not found in nyash.toml [using]/[modules]",
                        filename, line_no, target_unquoted
                    );

                    if !similar.is_empty() {
                        err_msg.push_str("\n\n💡 Did you mean:");
                        for s in similar {
                            err_msg.push_str(&format!("\n   - {}", s));
                        }
                    }

                    if using_ctx.aliases.is_empty() {
                        err_msg
                            .push_str("\n\n⚠️  No aliases loaded (check TOML parse errors above)");
                    } else {
                        err_msg.push_str(&format!(
                            "\n\nAvailable modules: {} aliases",
                            using_ctx.aliases.len()
                        ));
                    }

                    err_msg.push_str("\n\n📝 Suggestions:");
                    err_msg.push_str("\n   - Add an alias in nyash.toml: [using.aliases] YourModule = \"path/to/module\"");
                    err_msg.push_str("\n   - Use the alias: using YourModule as YourModule");
                    err_msg.push_str("\n   - Dev/test mode: NYASH_PREINCLUDE=1");
                    err_msg.push_str("\n\n🔍 Debug: NYASH_DEBUG_USING=1 for detailed logs");

                    return Err(err_msg);
                }
            } else {
                // dev/ci: allow broader resolution via resolver
                match crate::runner::pipeline::resolve_using_target(
                    &target_unquoted,
                    false,
                    &using_ctx.pending_modules,
                    &using_ctx.using_paths,
                    &using_ctx.aliases,
                    &using_ctx.packages,
                    ctx_dir,
                    strict,
                    verbose,
                ) {
                    Ok(value) => {
                        // Only file paths are candidates for AST prelude merge
                        if crate::runner::modes::common_util::resolve::path_util::is_using_target_path_unquoted(&value)
                            {
                            // Resolve relative
                            let mut p = std::path::PathBuf::from(&value);
                            if p.is_relative() {
                                if let Some(dir) = ctx_dir {
                                    let cand = dir.join(&p);
                                    if cand.exists() {
                                        p = cand;
                                    }
                                }
                                if p.is_relative() {
                                    if let Ok(root) = std::env::var("NYASH_ROOT") {
                                        let cand = std::path::Path::new(&root).join(&p);
                                        if cand.exists() {
                                            p = cand;
                                        }
                                    } else {
                                        if let Ok(exe) = std::env::current_exe() {
                                            if let Some(root) = exe
                                                .parent()
                                                .and_then(|p| p.parent())
                                                .and_then(|p| p.parent())
                                            {
                                                let cand = root.join(&p);
                                                if cand.exists() {
                                                    p = cand;
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            if verbose {
                                crate::runner::trace::log(format!(
                                    "[using/resolve] dev-file '{}' -> '{}'",
                                    value,
                                    p.display()
                                ));
                            }
                            let path_str = p.to_string_lossy().to_string();
                            let canon = std::fs::canonicalize(&path_str)
                                .ok()
                                .map(|pb| pb.to_string_lossy().to_string())
                                .unwrap_or_else(|| path_str.clone());
                            if let Some((prev_alias, prev_line)) = seen_paths.get(&canon) {
                                return Err(format!(
                                    "using: duplicate import of '{}' at {}:{} (previous alias: '{}' first seen at line {})",
                                    canon,
                                    filename,
                                    line_no,
                                    prev_alias,
                                    prev_line
                                ));
                            } else {
                                seen_paths.insert(canon.clone(), (alias_name.clone().unwrap_or_else(|| "<none>".into()), line_no));
                            }
                            if let Some(alias) = alias_name.clone() {
                                if let Some((prev_path, prev_line)) = seen_aliases.get(&alias) {
                                    if prev_path != &canon {
                                        return Err(format!(
                                            "using: alias '{}' rebound at {}:{} (was '{}' first seen at line {})",
                                            alias, filename, line_no, prev_path, prev_line
                                        ));
                                    }
                                } else {
                                    seen_aliases.insert(alias, (canon, line_no));
                                }
                            }
                            prelude_paths.push(path_str);
                        }
                    }
                    Err(e) => return Err(format!("{}:{}: using: {}", filename, line_no, e)),
                }
            }
            continue;
        }
        out.push_str(line);
        out.push('\n');
    }
    // Optional prelude boundary comment (helps manual inspection; parser ignores comments)
    if crate::config::env::env_bool("NYASH_RESOLVE_SEAM_DEBUG") {
        let mut with_marker = String::with_capacity(out.len() + 64);
        with_marker.push_str("\n/* --- using boundary (AST) --- */\n");
        with_marker.push_str(&out);
        out = with_marker;
    }
    // Phase 21.8: Build imports map from seen_aliases (alias -> alias for now)
    // This provides the MirBuilder with information about which names are valid static box references
    let mut imports = std::collections::HashMap::new();
    for (alias, _) in seen_aliases.iter() {
        imports.insert(alias.clone(), alias.clone());
    }
    Ok((out, prelude_paths, imports))
}

/// Profile-aware prelude resolution wrapper (single entrypoint).
/// - Delegates to `collect_using_and_strip` for the first pass.
/// - When AST using is enabled, resolves nested preludes via DFS and injects
///   OperatorBox preludes when available (stringify/compare/add).
/// - All runners call this helper; do not fork resolution logic elsewhere.
pub fn resolve_prelude_paths_profiled(
    runner: &NyashRunner,
    code: &str,
    filename: &str,
) -> Result<(String, Vec<String>), String> {
    // First pass: strip using from the main source and collect direct prelude paths
    let (cleaned, direct, _imports) = collect_using_and_strip(runner, code, filename)?;
    // Recursively collect nested preludes (DFS) for both AST/text merges.
    // Rationale: even when we merge via text, nested `using` inside preludes
    // must be discovered so that their definitions are present at runtime
    // (e.g., runner_min -> lower_* boxes). Previously this only ran when
    // NYASH_USING_AST=1, which caused unresolved calls in inline flows.
    let _ast_on = crate::config::env::env_bool("NYASH_USING_AST");
    let mut out: Vec<String> = Vec::new();
    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
    fn normalize_path(path: &str) -> (String, String) {
        use std::path::PathBuf;
        match PathBuf::from(path).canonicalize() {
            Ok(canon) => {
                let s = canon.to_string_lossy().to_string();
                (s.clone(), s)
            }
            Err(_) => {
                // Fall back to the original path representation.
                (path.to_string(), path.to_string())
            }
        }
    }
    fn dfs(
        runner: &NyashRunner,
        path: &str,
        out: &mut Vec<String>,
        seen: &mut std::collections::HashSet<String>,
    ) -> Result<(), String> {
        let (key, real_path) = normalize_path(path);
        if !seen.insert(key.clone()) {
            return Ok(());
        }
        // Phase 90-A: fs 系移行
        let ring0 = crate::runtime::ring0::get_global_ring0();
        let src = ring0
            .fs
            .read_to_string(std::path::Path::new(&real_path))
            .map_err(|e| format!("using: failed to read '{}': {}", real_path, e))?;
        let (_cleaned, nested, _nested_imports) =
            collect_using_and_strip(runner, &src, &real_path)?;
        for n in nested.iter() {
            dfs(runner, n, out, seen)?;
        }
        out.push(real_path);
        Ok(())
    }
    for p in direct.iter() {
        dfs(runner, p, &mut out, &mut seen)?;
    }
    // Operator Boxes prelude injection（観測“常時ON”のため）
    // stringify/compare/add は常に注入（存在時）。その他（bitwise等）は ALL 指定時のみ。
    let opbox_all = crate::config::env::env_bool("NYASH_OPERATOR_BOX_ALL")
        || crate::config::env::env_bool("NYASH_BUILDER_OPERATOR_BOX_ALL_CALL");

    if let Ok(root) = std::env::var("NYASH_ROOT") {
        let must_have = [
            "apps/lib/std/operators/stringify.hako",
            "apps/lib/std/operators/compare.hako",
            "apps/lib/std/operators/add.hako",
        ];
        for rel in must_have.iter() {
            let p = std::path::Path::new(&root).join(rel);
            if p.exists() {
                let path = p.to_string_lossy().to_string();
                if !out.iter().any(|x| x == &path) {
                    out.push(path);
                }
            }
        }
    }
    // Inject remaining arithmetic/bitwise/unary operator modules when ALL is requested
    if opbox_all {
        if let Ok(root) = std::env::var("NYASH_ROOT") {
            let rels = vec![
                "apps/lib/std/operators/sub.hako",
                "apps/lib/std/operators/mul.hako",
                "apps/lib/std/operators/div.hako",
                "apps/lib/std/operators/mod.hako",
                // Shifts / bitwise (parser tokens now supported)
                "apps/lib/std/operators/shl.hako",
                "apps/lib/std/operators/shr.hako",
                "apps/lib/std/operators/bitand.hako",
                "apps/lib/std/operators/bitor.hako",
                "apps/lib/std/operators/bitxor.hako",
                "apps/lib/std/operators/neg.hako",
                "apps/lib/std/operators/not.hako",
                "apps/lib/std/operators/bitnot.hako",
            ];
            for rel in rels {
                let p = std::path::Path::new(&root).join(rel);
                if p.exists() {
                    let path = p.to_string_lossy().to_string();
                    if !out.iter().any(|x| x == &path) {
                        out.push(path);
                    }
                }
            }
        }
    }
    // If AST merge is disabled, still return the discovered nested prelude list
    // so that the text merger can inline all dependencies. This keeps behavior
    // consistent across strategies and fixes nested `using` resolution.
    Ok((cleaned, out))
}

/// Parse prelude source files into ASTs (single helper for all runner modes).
/// - Reads each path, strips nested `using`, and parses to AST.
/// - Returns a Vec of Program ASTs (one per prelude file), preserving DFS order.
pub fn parse_preludes_to_asts(
    runner: &NyashRunner,
    prelude_paths: &[String],
) -> Result<Vec<nyash_rust::ast::ASTNode>, String> {
    let debug = crate::config::env::env_bool("NYASH_STRIP_DEBUG");
    if debug {
        eprintln!(
            "[strip-debug] parse_preludes_to_asts: {} files total",
            prelude_paths.len()
        );
        for (idx, p) in prelude_paths.iter().enumerate() {
            eprintln!("[strip-debug]   [{}] {}", idx, p);
        }
    }
    let mut out: Vec<nyash_rust::ast::ASTNode> = Vec::with_capacity(prelude_paths.len());
    for (idx, prelude_path) in prelude_paths.iter().enumerate() {
        if debug {
            eprintln!(
                "[strip-debug] [{}/{}] Processing: {}",
                idx + 1,
                prelude_paths.len(),
                prelude_path
            );
        }
        // Phase 90-A: fs 系移行
        let ring0 = crate::runtime::ring0::get_global_ring0();
        let src = ring0
            .fs
            .read_to_string(std::path::Path::new(prelude_path))
            .map_err(|e| format!("using: error reading {}: {}", prelude_path, e))?;
        let (clean_src, _nested, _nested_imports) =
            collect_using_and_strip(runner, &src, prelude_path)?;

        // IMPORTANT: Do not attempt to AST-parse .hako preludes here.
        // .hako is Hakorune surface, not Nyash AST. VM/VM-fallback paths
        // will route to text-merge when any prelude is .hako.
        if prelude_path.ends_with(".hako") {
            if debug {
                eprintln!(
                    "[strip-debug] skip AST parse for .hako prelude: {}",
                    prelude_path
                );
            }
            continue;
        }

        let clean_src = clean_src;

        // Debug: dump clean_src if NYASH_STRIP_DEBUG=1
        if debug {
            eprintln!(
                "[strip-debug] [{}/{}] About to parse: {}",
                idx + 1,
                prelude_paths.len(),
                prelude_path
            );
            eprintln!(
                "[strip-debug]   clean_src first 500 chars:\n{}\n---",
                &clean_src.chars().take(500).collect::<String>()
            );
        }

        match crate::parser::NyashParser::parse_from_string(&clean_src) {
            Ok(ast) => {
                if debug {
                    eprintln!(
                        "[strip-debug] [{}/{}] ✅ Parse SUCCESS: {}",
                        idx + 1,
                        prelude_paths.len(),
                        prelude_path
                    );
                }
                out.push(ast)
            }
            Err(e) => {
                // Always output debug info on parse failure if NYASH_STRIP_DEBUG=1
                let debug = crate::config::env::env_bool("NYASH_STRIP_DEBUG");
                eprintln!(
                    "[strip-debug] Parse FAILED for: {} (debug={})",
                    prelude_path, debug
                );
                if debug {
                    eprintln!("[strip-debug] Error: {}", e);
                    let es = format!("{}", e);
                    let lines: Vec<&str> = clean_src.lines().collect();
                    eprintln!("[strip-debug] Total lines: {}", lines.len());
                    // Try to extract error line number (e.g., "at line 451") and show local context
                    let mut printed = false;
                    if let Some(pos) = es.rfind("line ") {
                        let mut j = pos + 5; // after "line "
                        let bytes = es.as_bytes();
                        let mut n: usize = 0;
                        let mut had = false;
                        while j < bytes.len() {
                            let c = bytes[j];
                            if c >= b'0' && c <= b'9' {
                                n = n * 10 + (c - b'0') as usize;
                                j += 1;
                                had = true;
                            } else {
                                break;
                            }
                        }
                        if had {
                            let ln = if n == 0 { 1 } else { n };
                            let from = ln.saturating_sub(3);
                            let to = std::cmp::min(lines.len(), ln + 3);
                            eprintln!(
                                "[strip-debug] Context around line {} ({}..={}):",
                                ln,
                                from.max(1),
                                to
                            );
                            for i in from.max(1)..=to {
                                let mark = if i == ln { ">>" } else { "  " };
                                if let Some(line) = lines.get(i - 1) {
                                    eprintln!("{} {:4}: {}", mark, i, line);
                                }
                            }
                            printed = true;
                        }
                    }
                    if !printed {
                        eprintln!("[strip-debug] Lines 15-25:");
                        for (idx, line) in lines.iter().enumerate().skip(14).take(11) {
                            eprintln!("  {:3}: {}", idx + 1, line);
                        }
                    }
                    eprintln!("[strip-debug] Full clean_src:\n{}\n---", clean_src);
                }
                return Err(format!(
                    "Parse error in using prelude {}: {}",
                    prelude_path, e
                ));
            }
        }
    }
    if debug {
        eprintln!(
            "[strip-debug] parse_preludes_to_asts: ✅ All {} files parsed successfully",
            out.len()
        );
    }
    Ok(out)
}

/// Merge prelude ASTs with the main AST into a single Program node.
/// - Collects statements from each prelude Program in order, then appends
///   statements from the main Program.
/// - If the main AST is not a Program, returns it unchanged (defensive).
pub fn merge_prelude_asts_with_main(
    prelude_asts: Vec<nyash_rust::ast::ASTNode>,
    main_ast: &nyash_rust::ast::ASTNode,
) -> nyash_rust::ast::ASTNode {
    use nyash_rust::ast::{ASTNode, Span};
    let mut combined: Vec<ASTNode> = Vec::new();
    for a in prelude_asts.into_iter() {
        if let ASTNode::Program { statements, .. } = a {
            combined.extend(statements);
        }
    }
    if let ASTNode::Program { statements, .. } = main_ast.clone() {
        let mut all = combined;
        all.extend(statements);
        ASTNode::Program {
            statements: all,
            span: Span::unknown(),
        }
    } else {
        // Defensive: unexpected shape; preserve main AST unchanged.
        main_ast.clone()
    }
}

/// Pre-expand line-head `@name[: Type] = expr` into `local name[: Type] = expr`.
/// Minimal, safe, no semantics change. Applies only at line head (after spaces/tabs).
pub fn preexpand_at_local(src: &str) -> String {
    let mut out = String::with_capacity(src.len());
    for line in src.lines() {
        let bytes = line.as_bytes();
        let mut i = 0;
        while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
            i += 1;
        }
        if i < bytes.len() && bytes[i] == b'@' {
            // parse identifier
            let mut j = i + 1;
            if j < bytes.len() && ((bytes[j] as char).is_ascii_alphabetic() || bytes[j] == b'_') {
                j += 1;
                while j < bytes.len() {
                    let c = bytes[j] as char;
                    if c.is_ascii_alphanumeric() || c == '_' {
                        j += 1;
                    } else {
                        break;
                    }
                }
                let mut k = j;
                while k < bytes.len() && (bytes[k] == b' ' || bytes[k] == b'\t') {
                    k += 1;
                }
                if k < bytes.len() && bytes[k] == b':' {
                    k += 1;
                    while k < bytes.len() && (bytes[k] == b' ' || bytes[k] == b'\t') {
                        k += 1;
                    }
                    if k < bytes.len()
                        && ((bytes[k] as char).is_ascii_alphabetic() || bytes[k] == b'_')
                    {
                        k += 1;
                        while k < bytes.len() {
                            let c = bytes[k] as char;
                            if c.is_ascii_alphanumeric() || c == '_' {
                                k += 1;
                            } else {
                                break;
                            }
                        }
                    }
                }
                let mut eqp = k;
                while eqp < bytes.len() && (bytes[eqp] == b' ' || bytes[eqp] == b'\t') {
                    eqp += 1;
                }
                if eqp < bytes.len() && bytes[eqp] == b'=' {
                    out.push_str(&line[..i]);
                    out.push_str("local ");
                    out.push_str(&line[i + 1..eqp]);
                    out.push_str(" =");
                    out.push_str(&line[eqp + 1..]);
                    out.push('\n');
                    continue;
                }
            }
        }
        out.push_str(line);
        out.push('\n');
    }
    out
}

/// Text-based prelude merge: simpler and faster than AST merge.
/// Recursively resolves using dependencies, strips using lines from each file,
/// and concatenates prelude text followed by main source text.
/// Returns merged source text ready for compilation.
pub fn merge_prelude_text(
    runner: &NyashRunner,
    source: &str,
    filename: &str,
) -> Result<String, String> {
    let trace = crate::config::env::env_bool("NYASH_RESOLVE_TRACE");

    // First pass: collect and resolve prelude paths
    let (cleaned_main, prelude_paths) = resolve_prelude_paths_profiled(runner, source, filename)?;
    // Expand nested preludes for text-merge too (DFS) so that any `using`
    // inside prelude files (e.g., runner_min -> lower_* boxes) are also
    // included even when NYASH_USING_AST is OFF.
    let mut expanded: Vec<String> = Vec::new();
    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
    fn canonize(p: &str) -> String {
        std::fs::canonicalize(p)
            .ok()
            .map(|pb| pb.to_string_lossy().to_string())
            .unwrap_or_else(|| p.to_string())
    }
    fn dfs_text(
        runner: &NyashRunner,
        path: &str,
        out: &mut Vec<String>,
        seen: &mut std::collections::HashSet<String>,
    ) -> Result<(), String> {
        let key = canonize(path);
        if !seen.insert(key.clone()) {
            return Ok(());
        }
        // Phase 90-A: fs 系移行
        let ring0 = crate::runtime::ring0::get_global_ring0();
        let src = ring0
            .fs
            .read_to_string(std::path::Path::new(path))
            .map_err(|e| format!("using: failed to read '{}': {}", path, e))?;
        let (_cleaned, nested, _nested_imports) = collect_using_and_strip(runner, &src, path)?;
        for n in nested.iter() {
            dfs_text(runner, n, out, seen)?;
        }
        out.push(key);
        Ok(())
    }
    for p in prelude_paths.iter() {
        dfs_text(runner, p, &mut expanded, &mut seen)?;
    }
    let prelude_paths = &expanded;
    // Record for enriched diagnostics (parse error context)
    crate::runner::modes::common_util::resolve::set_last_merged_preludes(prelude_paths.clone());

    if prelude_paths.is_empty() {
        // No using statements, return original
        return Ok(source.to_string());
    }

    if trace {
        crate::runner::trace::log(format!(
            "[using/text-merge] {} prelude files for '{}'",
            prelude_paths.len(),
            filename
        ));
    }

    // Build merged text: preludes first, then main source
    let mut merged = String::new();
    let mut spans: Vec<crate::runner::modes::common_util::resolve::LineSpan> = Vec::new();
    let mut current_line: usize = 1;

    // Add preludes in DFS order
    for (idx, path) in prelude_paths.iter().enumerate() {
        // Phase 90-A: fs 系移行
        let ring0 = crate::runtime::ring0::get_global_ring0();
        let content = ring0
            .fs
            .read_to_string(std::path::Path::new(path))
            .map_err(|e| format!("using: failed to read '{}': {}", path, e))?;

        // Strip using lines from prelude and normalize
        let (cleaned_raw, _nested, _nested_imports) =
            collect_using_and_strip(runner, &content, path)?;
        let mut cleaned = normalize_text_for_inline(&cleaned_raw);
        // Hako-friendly normalize for preludes: always strip leading `local ` at line head
        // when the prelude is a .hako (or looks like Hako code). This prevents top-level
        // `local` from tripping the Nyash parser after text merge.
        if path.ends_with(".hako")
            || crate::runner::modes::common_util::hako::looks_like_hako_code(&cleaned)
        {
            cleaned = crate::runner::modes::common_util::hako::strip_local_decl(&cleaned);
        }

        if trace {
            crate::runner::trace::log(format!(
                "[using/text-merge] [{}] '{}' ({} bytes)",
                idx + 1,
                path,
                cleaned.len()
            ));
        }

        merged.push_str(&cleaned);
        merged.push('\n');

        let added = cleaned.lines().count();
        if added > 0 {
            spans.push(crate::runner::modes::common_util::resolve::LineSpan {
                file: path.clone(),
                start_line: current_line,
                line_count: added,
            });
            current_line += added + 1; // +1 for extra '\n'
        } else {
            current_line += 1;
        }
    }

    // Add boundary marker if debug mode
    if crate::config::env::env_bool("NYASH_RESOLVE_SEAM_DEBUG") {
        merged.push_str("\n/* --- using prelude/main boundary --- */\n\n");
        let boundary_lines = 3usize;
        spans.push(crate::runner::modes::common_util::resolve::LineSpan {
            file: "<prelude/main-boundary>".to_string(),
            start_line: current_line,
            line_count: boundary_lines,
        });
        current_line += boundary_lines;
    }

    // Add main source (already cleaned of using lines) and normalize
    let mut cleaned_main_norm = normalize_text_for_inline(&cleaned_main);
    // Hako-friendly normalize for main: always strip leading `local ` at line head
    // when the merged main looks like Hako code (or file is .hako as a heuristic).
    if filename.ends_with(".hako")
        || crate::runner::modes::common_util::hako::looks_like_hako_code(&cleaned_main_norm)
    {
        cleaned_main_norm =
            crate::runner::modes::common_util::hako::strip_local_decl(&cleaned_main_norm);
    }
    merged.push_str(&cleaned_main_norm);
    let main_lines = cleaned_main_norm.lines().count();
    if main_lines > 0 {
        spans.push(crate::runner::modes::common_util::resolve::LineSpan {
            file: filename.to_string(),
            start_line: current_line,
            line_count: main_lines,
        });
        current_line += main_lines;
    }
    let _ = current_line;

    if trace {
        crate::runner::trace::log(format!(
            "[using/text-merge] final merged: {} bytes ({} prelude + {} main)",
            merged.len(),
            merged.len() - cleaned_main.len(),
            cleaned_main.len()
        ));
    }

    // Optional dump of merged text for diagnostics
    if let Ok(dump_path) = std::env::var("NYASH_RESOLVE_DUMP_MERGED") {
        if !dump_path.is_empty() {
            let _ = std::fs::write(&dump_path, &merged);
        }
    }

    crate::runner::modes::common_util::resolve::set_last_text_merge_line_spans(spans);

    Ok(normalize_text_for_inline(&merged))
}

/// Minimal normalization to improve inline parser robustness.
/// - Normalize CRLF to LF
/// - Remove redundant semicolons before closing braces (`; }` → `}`)
/// - Ensure file ends with a newline
fn normalize_text_for_inline(s: &str) -> String {
    let mut out = s.replace("\r\n", "\n").replace("\r", "\n");
    // Remove `;` before `}` across line boundaries conservatively
    // pattern: `;` followed by optional spaces/newlines then `}`
    // Do a few passes to cover nested cases without regex
    for _ in 0..2 {
        let mut tmp = String::with_capacity(out.len());
        let bytes = out.as_bytes();
        let mut i = 0usize;
        while i < bytes.len() {
            if bytes[i] == b';' {
                // peek ahead skipping spaces/newlines
                let mut j = i + 1;
                while j < bytes.len() {
                    let c = bytes[j];
                    if c == b' ' || c == b'\t' || c == b'\n' {
                        j += 1;
                    } else {
                        break;
                    }
                }
                if j < bytes.len() && bytes[j] == b'}' {
                    // drop ';' (do not advance j here)
                    i += 1;
                    continue;
                }
            }
            tmp.push(bytes[i] as char);
            i += 1;
        }
        out = tmp;
    }
    if !out.ends_with('\n') {
        out.push('\n');
    }
    out
}
