use anyhow::{Context, Result, anyhow};
use log::{debug, error, info, warn};
use regex::Regex;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use crate::cli::GenerateArgs;
use crate::metadata;

#[derive(Debug, Clone)]
pub struct Dto {
    pub name: String,
    pub version: String,
    pub minor_version: String,
    pub identifier: String,
    pub file_path: PathBuf,
    pub metadata: HashMap<String, String>, // Legacy single-value metadata
    pub multi_value_metadata: metadata::DtoMetadata, // New multi-value metadata
}

pub fn run(args: &GenerateArgs) -> Result<()> {
    let template_dir = args
        .template_dir
        .clone()
        .unwrap_or_else(|| args.input_dir.clone());

    // 1. DTO Discovery
    let dtos = discover_dtos(&args.input_dir, args.list_file.as_deref())?;
    if dtos.is_empty() {
        warn!("No DTOs found to process.");
        return Ok(());
    }
    info!("Discovered {} DTOs to process.", dtos.len());
    for dto in &dtos {
        info!(
            "  - {}: {} (minor: {})",
            dto.identifier,
            dto.file_path.display(),
            dto.minor_version
        );
    }

    // 2. Configuration and Validation
    let config = load_config(&args.input_dir)?;
    validate_config(&dtos, &config)?;
    info!("Configuration loaded and validated successfully.");

    // 3. Template Processing (Per-DTO)
    process_per_dto(
        &dtos,
        &config,
        &template_dir,
        &args.output_dir,
        !args.no_gitignore,
    )?;

    // 4. Template Processing (Foreach-DTO)
    process_foreach_dto(
        &dtos,
        &config,
        &template_dir,
        &args.output_dir,
        !args.no_gitignore,
    )?;

    Ok(())
}

fn discover_dtos(input_dir: &Path, list_file: Option<&Path>) -> Result<Vec<Dto>> {
    // Accept any casing convention: kebab-case, PascalCase, snake_case, etc.
    // Examples: store-link.v1.proto, StoreLink.v1.proto, store_link.v1.proto, storelink.v1.proto
    let dto_regex =
        Regex::new(r"^(?P<name>[A-Za-z][A-Za-z0-9_-]*)\.v(?P<version>[0-9]+)\.[a-z][a-z]*$")
            .unwrap();
    let minor_version_regex = Regex::new(r"minor version (\d+)").unwrap();
    let mut dtos = Vec::new();

    let files_to_process: Vec<String> = if let Some(list_file_path) = list_file {
        info!("Reading DTO list from {}", list_file_path.display());
        fs::read_to_string(list_file_path)
            .with_context(|| {
                format!(
                    "Failed to read DTO list file '{}' as UTF-8. \
                    The file may contain invalid UTF-8 characters or be encoded in a different format.",
                    list_file_path.display()
                )
            })?
            .lines()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect()
    } else {
        info!("Scanning for DTOs in {}", input_dir.display());
        fs::read_dir(input_dir)?
            .filter_map(Result::ok)
            .map(|entry| entry.file_name().into_string().unwrap_or_default())
            .collect()
    };

    for file_name in files_to_process {
        let file_path = input_dir.join(&file_name);
        if !file_path.is_file() {
            warn!("Skipping non-existent file from list: {file_name}");
            continue;
        }

        if let Some(captures) = dto_regex.captures(&file_name) {
            let name = captures.name("name").unwrap().as_str().to_string();
            let version_num = captures.name("version").unwrap().as_str();
            let version = format!("v{version_num}");
            let identifier = format!("{name}.{version}");

            let content = fs::read_to_string(&file_path)
                .with_context(|| {
                    format!(
                        "Failed to read DTO file '{}' as UTF-8. This usually means:\n  \
                        - The file contains binary data or non-UTF-8 text\n  \
                        - The file is encoded in a different character set (e.g., Latin-1, Windows-1252)\n  \
                        - The file is corrupted\n  \
                        Try checking the file encoding or converting it to UTF-8.",
                        file_path.display()
                    )
                })?;
            let minor_version = minor_version_regex
                .captures(&content)
                .and_then(|caps| caps.get(1))
                .map(|m| m.as_str().to_string())
                .context(format!("Could not find minor version in {file_name}"))?;

            dtos.push(Dto {
                name,
                version,
                minor_version,
                identifier,
                file_path: file_path.clone(),
                metadata: extract_dto_metadata(&file_path, &content)?,
                multi_value_metadata: extract_multi_value_metadata(&file_path, &content)?,
            });
        } else {
            warn!("Skipping file with invalid name format: {file_name}");
        }
    }

    Ok(dtos)
}

/// Helper function to extract metadata from a DTO file
fn extract_dto_metadata(file_path: &Path, content: &str) -> Result<HashMap<String, String>> {
    match metadata::extract_metadata(file_path, content) {
        Ok(meta) => Ok(meta),
        Err(_) => {
            // If metadata extraction fails, return empty HashMap and continue
            // This allows the processor to work even if metadata extraction has issues
            Ok(HashMap::new())
        }
    }
}

/// Helper function to extract multi-value metadata from a DTO file
fn extract_multi_value_metadata(file_path: &Path, content: &str) -> Result<metadata::DtoMetadata> {
    match metadata::extract_multi_value_metadata(file_path, content) {
        Ok(meta) => Ok(meta),
        Err(_) => {
            // If metadata extraction fails, return empty DtoMetadata and continue
            Ok(metadata::DtoMetadata::new(file_path.to_path_buf()))
        }
    }
}

type Config = HashMap<String, HashMap<String, String>>;

fn load_config(input_dir: &Path) -> Result<Config> {
    let mut config: Config = HashMap::new();
    let config_regex = Regex::new(r"^__(.*)__\.csv$").unwrap();

    for entry in fs::read_dir(input_dir)? {
        let entry = entry?;
        let file_name = entry.file_name();
        let file_name_str = file_name.to_string_lossy();

        if let Some(captures) = config_regex.captures(&file_name_str) {
            let placeholder = format!("__{}__", &captures[1]);
            info!("Loading config from {file_name_str}");
            let mut reader = csv::ReaderBuilder::new()
                .has_headers(false)
                .flexible(true)
                .from_path(entry.path())
                .with_context(|| {
                    format!(
                        "Failed to read CSV config file '{}'. This usually means:\n  \
                        - The file contains invalid UTF-8 characters\n  \
                        - The file is encoded in a different character set\n  \
                        - The file is corrupted or not a valid CSV\n  \
                        Try checking the file encoding or converting it to UTF-8.",
                        entry.path().display()
                    )
                })?;

            for result in reader.records() {
                match result {
                    Ok(record) => {
                        if record.len() >= 2 {
                            let dto_identifier = record[0].trim().to_string();
                            let value = record[1].trim().to_string();
                            config
                                .entry(placeholder.clone())
                                .or_default()
                                .insert(dto_identifier, value);
                        }
                    }
                    Err(e) => {
                        warn!("Skipping invalid line in {file_name_str}: {e}");
                    }
                }
            }
        }
    }
    Ok(config)
}

fn validate_config(dtos: &[Dto], config: &Config) -> Result<()> {
    let mut missing_entries = Vec::new();
    for placeholder in config.keys() {
        for dto in dtos {
            if !config[placeholder].contains_key(&dto.identifier) {
                missing_entries.push(format!(
                    "DTO '{}' is missing from configuration file for '{}'",
                    dto.identifier, placeholder
                ));
            }
        }
    }

    if !missing_entries.is_empty() {
        for entry in &missing_entries {
            log::error!("{entry}");
        }
        return Err(anyhow!(
            "Configuration validation failed. {} missing entries found.",
            missing_entries.len()
        ));
    }

    Ok(())
}

fn process_per_dto(
    dtos: &[Dto],
    config: &Config,
    template_dir: &Path,
    output_dir: &Path,
    respect_gitignore: bool,
) -> Result<()> {
    info!("Starting Per-DTO template processing...");

    for dto in dtos {
        info!("Processing DTO '{}'", dto.identifier);

        // Create a single temporary directory for the DTO's processing
        let temp_dir = tempfile::Builder::new()
            .prefix(&format!("gen-{}-", dto.identifier))
            .tempdir()?;
        let temp_path = temp_dir.path();

        // Copy all template files into the temporary directory (respecting gitignore)
        copy_dir_all_with_gitignore(template_dir, temp_path, respect_gitignore)?;

        // Build placeholders and perform replacements
        let mut placeholders = build_placeholders(dto, config, &dto.multi_value_metadata);
        placeholders.insert(
            "__dtofilepath__".to_string(),
            dto.file_path.to_string_lossy().to_string(),
        );

        replace_placeholders_in_tree(temp_path, &placeholders, &dto.multi_value_metadata)?;

        // Merge the processed files into the final output directory
        copy_dir_all_with_gitignore(temp_path, output_dir, false)?; // Don't apply gitignore to processed output
    }

    info!("Per-DTO template processing complete.");
    Ok(())
}

fn process_foreach_dto(
    dtos: &[Dto],
    config: &Config,
    template_dir: &Path,
    output_dir: &Path,
    respect_gitignore: bool,
) -> Result<()> {
    info!("Starting Foreach-DTO template processing...");
    let start_marker = "FOREACH-DTO-START";
    let end_marker = "FOREACH-DTO-END";

    // Create file iterator based on gitignore setting
    let walker = if respect_gitignore {
        info!("Respecting .gitignore files in template directory");
        ignore::WalkBuilder::new(template_dir)
            .git_ignore(true)
            .git_global(true)
            .git_exclude(true)
            .hidden(false)
            .build()
    } else {
        info!("Ignoring .gitignore files (--no-gitignore flag set)");
        ignore::WalkBuilder::new(template_dir)
            .git_ignore(false)
            .git_global(false)
            .git_exclude(false)
            .hidden(false)
            .build()
    };

    for entry in walker {
        let entry = entry?;
        if !entry.file_type().is_some_and(|ft| ft.is_file()) {
            continue;
        }

        let template_path = entry.path();
        let mut content = match fs::read_to_string(template_path) {
            Ok(c) => c,
            Err(e) => {
                if e.kind() == std::io::ErrorKind::InvalidData {
                    warn!(
                        "Skipping template file '{}' due to UTF-8 encoding error: {}. \
                        The file may contain binary data or be encoded in a different format.",
                        template_path.display(),
                        e
                    );
                }
                continue; // Skip non-text files or files with encoding issues
            }
        };

        if content.contains(start_marker) && content.contains(end_marker) {
            info!(
                "Processing FOREACH-DTO block in {}",
                template_path.display()
            );

            while let (Some(start_marker_idx), Some(end_marker_idx)) =
                (content.find(start_marker), content.find(end_marker))
            {
                // Region to be replaced: from the start of the start_marker line to the start of the end_marker line.
                let replacement_start =
                    content[..start_marker_idx].rfind('\n').map_or(0, |i| i + 1);
                let end_of_end_marker = end_marker_idx + end_marker.len();
                let replacement_end = content[end_of_end_marker..]
                    .find('\n')
                    .map_or(content.len(), |i| end_of_end_marker + i);

                // Template content: from the end of the start_marker line to the start of the end_marker line.
                let template_start = content[start_marker_idx..]
                    .find('\n')
                    .map_or(end_marker_idx, |i| start_marker_idx + i + 1);
                let template_end = content[..end_marker_idx]
                    .rfind('\n')
                    .map_or(template_start, |i| i + 1);

                let sub_template = if template_start < template_end {
                    content[template_start..template_end].to_string()
                } else {
                    String::new()
                };

                let mut all_dtos_content = String::new();

                for (i, dto) in dtos.iter().enumerate() {
                    let placeholders = build_placeholders(dto, config, &dto.multi_value_metadata);
                    let mut processed_sub = sub_template.clone();

                    // Process FOREACH-UNIQUE-DTO-VALUE blocks FIRST (Phase 3 User Story 1)
                    processed_sub = match process_foreach_unique_dto_value(
                        &processed_sub,
                        &dto.multi_value_metadata,
                        &placeholders,
                    ) {
                        Ok(expanded) => expanded,
                        Err(e) => {
                            error!(
                                "Failed to process FOREACH-UNIQUE-DTO-VALUE blocks for DTO '{}': {}",
                                dto.name, e
                            );
                            processed_sub // Keep original content on error
                        }
                    };

                    // Process IFDEF-DTO-VALUE blocks (conditional inclusion)
                    processed_sub = match process_ifdef_dto_value(
                        &processed_sub,
                        &dto.multi_value_metadata,
                        &placeholders,
                    ) {
                        Ok(expanded) => expanded,
                        Err(e) => {
                            error!(
                                "Failed to process IFDEF-DTO-VALUE blocks for DTO '{}': {}",
                                dto.name, e
                            );
                            processed_sub // Keep original content on error
                        }
                    };

                    // Process FOREACH-DTO-VALUE blocks SECOND (before substituting placeholders)
                    // This ensures that __idpattern__ etc. in FOREACH-DTO-VALUE-START markers aren't replaced
                    processed_sub = match process_foreach_dto_value(
                        &processed_sub,
                        &dto.multi_value_metadata,
                        &placeholders,
                    ) {
                        Ok(expanded) => expanded,
                        Err(e) => {
                            error!(
                                "Failed to process FOREACH-DTO-VALUE blocks for DTO '{}': {}",
                                dto.name, e
                            );
                            processed_sub // Keep original content on error
                        }
                    };

                    // Then do regular placeholder substitution
                    for (key, value) in &placeholders {
                        processed_sub = processed_sub.replace(key, value);
                    }

                    all_dtos_content.push_str(&processed_sub);
                    if i < dtos.len() - 1 {
                        // If there is no newline between blocks, add one.
                        if !sub_template.ends_with('\n') && !sub_template.ends_with("\r\n") {
                            all_dtos_content.push('\n');
                        }
                    }
                }

                // Trim trailing newline from generated content as the replacement range preserves it.
                let final_content = all_dtos_content.trim_end_matches(['\r', '\n']);
                content.replace_range(replacement_start..replacement_end, final_content);
            }

            let relative_path = template_path.strip_prefix(template_dir)?;
            let output_path = output_dir.join(relative_path);
            if let Some(parent) = output_path.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::write(&output_path, content)?;
            info!("Generated aggregate file {}", output_path.display());
        }
    }

    info!("Foreach-DTO template processing complete.");
    Ok(())
}

fn build_placeholders(
    dto: &Dto,
    config: &Config,
    multi_value_metadata: &metadata::DtoMetadata,
) -> HashMap<String, String> {
    let mut placeholders = HashMap::new();

    // Add all case variants for DtoName
    // Detect convention from filename and generate all variants
    use crate::case_transform::{
        detect_convention, detect_word_boundaries, to_camel_case as case_to_camel_case,
        to_kebab_case, to_lower_snake_case, to_lowercase as case_to_lowercase,
        to_pascal_case as case_to_pascal_case, to_screaming_snake_case,
        to_uppercase as case_to_uppercase,
    };

    if let Some(name_convention) = detect_convention(&dto.name) {
        let name_words = detect_word_boundaries(&dto.name, name_convention);
        placeholders.insert("__dtoname__".to_string(), case_to_lowercase(&name_words));
        placeholders.insert("__DTONAME__".to_string(), case_to_uppercase(&name_words));
        placeholders.insert("__DtoName__".to_string(), case_to_pascal_case(&name_words));
        placeholders.insert("__dtoName__".to_string(), case_to_camel_case(&name_words));
        placeholders.insert("__dto_name__".to_string(), to_lower_snake_case(&name_words));
        placeholders.insert(
            "__DTO_NAME__".to_string(),
            to_screaming_snake_case(&name_words),
        );
        placeholders.insert("__dto-name__".to_string(), to_kebab_case(&name_words));
    } else {
        // Fallback for single word or undetectable convention
        let dtoname_lower = dto.name.to_lowercase();
        placeholders.insert("__dtoname__".to_string(), dtoname_lower.clone());
        placeholders.insert("__DTONAME__".to_string(), dto.name.to_uppercase());
        placeholders.insert("__DtoName__".to_string(), to_pascal_case(&dto.name));
        placeholders.insert("__dtoName__".to_string(), dtoname_lower.clone());
        placeholders.insert("__dto_name__".to_string(), dtoname_lower.clone());
        placeholders.insert("__DTO_NAME__".to_string(), dto.name.to_uppercase());
        placeholders.insert("__dto-name__".to_string(), dtoname_lower.clone());
    }

    // Add all case variants for DtoVersion
    // Version is always v{number}, so just provide lowercase/uppercase variants
    let dtoversion_lower = dto.version.to_lowercase();
    placeholders.insert("__dtoversion__".to_string(), dtoversion_lower.clone());
    placeholders.insert("__DTOVERSION__".to_string(), dto.version.to_uppercase());
    placeholders.insert("__DtoVersion__".to_string(), to_pascal_case(&dto.version));
    placeholders.insert("__dtoVersion__".to_string(), dtoversion_lower.clone());
    placeholders.insert("__dto_version__".to_string(), dtoversion_lower.clone());
    placeholders.insert("__DTO_VERSION__".to_string(), dto.version.to_uppercase());
    placeholders.insert("__dto-version__".to_string(), dtoversion_lower.clone());

    // Add DtoMinorVersion (single word, so fewer variants)
    placeholders.insert("__dtominorversion__".to_string(), dto.minor_version.clone());

    for (placeholder_key, values_map) in config {
        if let Some(value) = values_map.get(&dto.identifier) {
            placeholders.insert(placeholder_key.clone(), value.clone());
        }
    }

    // Add single-value placeholders from multi_value_metadata
    // These already have all case variants generated during extraction
    for (variant_key, placeholder) in &multi_value_metadata.placeholders {
        if matches!(placeholder.arity, metadata::PlaceholderArity::SingleValue) {
            // Get the first (and only) value for single-value placeholders
            if let Some(first_value) = placeholder.values.first() {
                let placeholder_key = format!("__{variant_key}__");
                placeholders.insert(placeholder_key, first_value.value.clone());
            }
        }
    }

    // Also add legacy metadata (for backward compatibility)
    // But exclude any that are multi-value placeholders
    let multi_value_placeholder_names: std::collections::HashSet<String> = multi_value_metadata
        .placeholders
        .iter()
        .filter(|(_, p)| matches!(p.arity, metadata::PlaceholderArity::MultiValue))
        .map(|(name, _)| name.clone())
        .collect();

    for (name, value) in &dto.metadata {
        // Only add single-value placeholders from legacy metadata
        // that aren't already in multi_value_metadata
        let normalized = name.to_lowercase().replace("_", "").replace("-", "");
        if !multi_value_placeholder_names.contains(&normalized) {
            let placeholder_key = format!("__{name}__");
            // Don't overwrite variants already added from multi_value_metadata
            placeholders.entry(placeholder_key).or_insert(value.clone());
        }
    }

    placeholders
}

fn to_pascal_case(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
    }
}

fn replace_placeholders_in_tree(
    dir: &Path,
    placeholders: &HashMap<String, String>,
    multi_value_metadata: &metadata::DtoMetadata,
) -> Result<()> {
    for entry in walkdir::WalkDir::new(dir)
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        // Replace in file content
        if path.is_file() {
            let mut content = fs::read_to_string(path).with_context(|| {
                format!(
                    "Failed to read template file '{}' as UTF-8 during placeholder replacement. \
                        The file may contain invalid UTF-8 characters.",
                    path.display()
                )
            })?;

            // FIRST: Process FOREACH-UNIQUE-DTO-VALUE blocks (Phase 3 User Story 1)
            if content.contains("FOREACH-UNIQUE-DTO-VALUE-START") {
                match process_foreach_unique_dto_value(&content, multi_value_metadata, placeholders)
                {
                    Ok(processed) => content = processed,
                    Err(e) => {
                        return Err(anyhow!(
                            "Failed to process FOREACH-UNIQUE-DTO-VALUE blocks in file '{}': {}",
                            path.display(),
                            e
                        ));
                    }
                }
            }

            // Process IFDEF/IFANY/IFNDEF/IFNO-DTO-VALUE blocks - conditional inclusion based on placeholder existence
            // Must happen AFTER FOREACH-UNIQUE but BEFORE FOREACH-DTO-VALUE
            if content.contains("IFDEF-DTO-VALUE-START")
                || content.contains("IFANY-DTO-VALUE-START")
                || content.contains("IFNDEF-DTO-VALUE-START")
                || content.contains("IFNO-DTO-VALUE-START")
            {
                match process_ifdef_dto_value(&content, multi_value_metadata, placeholders) {
                    Ok(processed) => content = processed,
                    Err(e) => {
                        return Err(anyhow!(
                            "Failed to process conditional DTO-VALUE blocks in file '{}': {}",
                            path.display(),
                            e
                        ));
                    }
                }
            }

            // SECOND: Process FOREACH-DTO-VALUE blocks BEFORE any other placeholder substitution
            // This prevents placeholders in marker lines from being replaced
            if content.contains("FOREACH-DTO-VALUE-START") {
                match process_foreach_dto_value(&content, multi_value_metadata, placeholders) {
                    Ok(processed) => content = processed,
                    Err(e) => {
                        return Err(anyhow!(
                            "Failed to process FOREACH-DTO-VALUE blocks in file '{}': {}",
                            path.display(),
                            e
                        ));
                    }
                }
            }

            // Build a set of multi-value placeholder names to exclude from regular replacement
            let multi_value_placeholder_keys: std::collections::HashSet<String> =
                multi_value_metadata
                    .placeholders
                    .iter()
                    .filter(|(_, p)| matches!(p.arity, metadata::PlaceholderArity::MultiValue))
                    .map(|(name, _)| format!("__{name}__"))
                    .collect();

            // THEN: Substitute all remaining placeholders EXCEPT:
            // - __dtofilecontents__ and __dtofilepath__ (special handling)
            // - Multi-value placeholders (already handled in FOREACH blocks)
            for (key, value) in placeholders {
                if key != "__dtofilecontents__"
                    && key != "__dtofilepath__"
                    && !multi_value_placeholder_keys.contains(key)
                {
                    content = content.replace(key, value);
                }
            }

            // Then, handle __dtofilecontents__ insertion after other substitutions are done
            // This ensures the DTO file contents are inserted as-is without any placeholder substitution
            let dto_file_placeholder = "__dtofilecontents__";
            if content.contains(dto_file_placeholder) {
                if let Some(dto_path_str) = placeholders.get("__dtofilepath__") {
                    let dto_content = fs::read_to_string(dto_path_str).with_context(|| {
                        format!(
                            "Failed to read DTO file '{dto_path_str}' as UTF-8 for placeholder replacement. \
                                The file may contain invalid UTF-8 characters."
                        )
                    })?;
                    content = content.replace(dto_file_placeholder, &dto_content);
                }
            }
            fs::write(path, content)?;
        }
    }

    // Replace in directory and file names (bottom-up)
    let entries: Vec<_> = walkdir::WalkDir::new(dir)
        .sort_by_file_name()
        .into_iter()
        .filter_map(Result::ok)
        .collect();

    for entry in entries.into_iter().rev() {
        let path = entry.path();
        let original_name = path.file_name().unwrap().to_string_lossy().to_string();
        let mut new_name = original_name.clone();

        for (key, value) in placeholders {
            new_name = new_name.replace(key, value);
        }

        if original_name != new_name {
            let new_path = path.with_file_name(new_name);
            fs::rename(path, &new_path)?;
            info!("Renamed {} to {}", path.display(), new_path.display());
        }
    }

    Ok(())
}

// ============================================================================
// Phase 3: FOREACH-DTO-VALUE Processing (US1 - T012, T013)
// ============================================================================

/// T012: Process FOREACH-DTO-VALUE blocks in template content
/// T013: Implement iteration logic - repeat block for each value
/// T017: Accept additional placeholders for expansion within iterations
/// T018: Substitute all placeholder types within block content
pub fn process_foreach_dto_value(
    template_content: &str,
    metadata: &metadata::DtoMetadata,
    additional_placeholders: &HashMap<String, String>,
) -> Result<String> {
    use log::debug;

    let start_marker = "FOREACH-DTO-VALUE-START";
    let end_marker = "FOREACH-DTO-VALUE-END";

    let mut result = String::new();
    let mut remaining = template_content;

    loop {
        // Find next FOREACH-DTO-VALUE block
        if let Some(start_marker_pos) = remaining.find(start_marker) {
            // Find the start of the line containing the START marker (may be in a comment)
            let line_start = remaining[..start_marker_pos]
                .rfind('\n')
                .map_or(0, |i| i + 1);

            // Add everything before the START marker line (with placeholder substitution)
            let mut before_block = remaining[..line_start].to_string();
            for (placeholder_key, placeholder_value) in additional_placeholders {
                before_block = before_block.replace(placeholder_key, placeholder_value);
            }
            result.push_str(&before_block);

            // Extract placeholder name from the START marker line
            let after_start = &remaining[start_marker_pos + start_marker.len()..];
            let marker_line_end = after_start.find('\n').unwrap_or(after_start.len());
            let marker_line = &after_start[..marker_line_end];
            let placeholder_with_delimiters = marker_line.trim();

            // Extract the first token (placeholder name) from the line
            // This handles cases like "@* FOREACH-DTO-VALUE-START __name__ *@" where there are trailing characters
            let first_token = placeholder_with_delimiters
                .split_whitespace()
                .next()
                .unwrap_or("");

            // Remove __ delimiters if present
            let placeholder_name = first_token.trim_start_matches("__").trim_end_matches("__");

            // Block content starts after the START marker line
            let block_content_start = start_marker_pos + start_marker.len() + marker_line_end + 1;
            let search_content = &remaining[block_content_start..];

            if let Some(end_marker_pos) = search_content.find(end_marker) {
                // Extract block content (everything between START line and END line)
                // Find the start of the line containing the END marker
                let end_line_start = search_content[..end_marker_pos]
                    .rfind('\n')
                    .map_or(0, |i| i + 1);
                let block_content = &search_content[..end_line_start];

                debug!("Processing FOREACH-DTO-VALUE block for placeholder: {placeholder_name}");

                // Get placeholder from metadata (if it exists)
                if let Some(placeholder) = metadata.placeholders.get(placeholder_name) {
                    // Placeholder exists - iterate over its values (may be 0, 1, or many)
                    // Find all multi-value placeholders that appear on the same lines as this one
                    // These are "companion" placeholders that should be iterated together
                    let companion_placeholders =
                        find_companion_placeholders(placeholder_name, metadata);

                    debug!(
                        "Found {} companion placeholders: {:?}",
                        companion_placeholders.len(),
                        companion_placeholders
                    );

                    // T013: Iterate over all values (0, 1, or N)
                    for (i, value) in placeholder.values.iter().enumerate() {
                        // T017: Build full placeholder map for this iteration
                        // Start with the iteration placeholder
                        let mut iteration_placeholders = HashMap::new();

                        // T088/T089: Add case variants by looking up variant placeholders with matching line number
                        // metadata.placeholders has keys like: "AliasName", "aliasname", "ALIASNAME", "alias-name", etc.
                        // Each contains values from all iterations with line numbers
                        // We filter to only include values from the current line number
                        add_case_variants_from_metadata(
                            placeholder_name,
                            value.line_number,
                            &mut iteration_placeholders,
                            &metadata.placeholders,
                        );

                        // Add companion placeholders from the same line (same index)
                        for companion_name in &companion_placeholders {
                            // Add case variants for companion by looking up variant placeholders
                            add_case_variants_from_metadata(
                                companion_name,
                                value.line_number,
                                &mut iteration_placeholders,
                                &metadata.placeholders,
                            );
                        }

                        // T017: Add all additional placeholders (single-value, etc.)
                        // BUT skip the iteration placeholder and companion placeholders to avoid overriding them
                        for (key, val) in additional_placeholders {
                            if !iteration_placeholders.contains_key(key) {
                                iteration_placeholders.insert(key.clone(), val.clone());
                            }
                        }

                        // T018: Substitute all placeholders in block content
                        // Debug: dump the entire placeholder map
                        debug!("=== Iteration {i} placeholder map ===");
                        let mut sorted_keys: Vec<_> = iteration_placeholders.keys().collect();
                        sorted_keys.sort();
                        for key in sorted_keys {
                            debug!("  {} => {}", key, iteration_placeholders.get(key).unwrap());
                        }
                        debug!("=== End placeholder map ===");

                        let mut iteration_result = block_content.to_string();
                        for (placeholder_key, placeholder_value) in &iteration_placeholders {
                            iteration_result =
                                iteration_result.replace(placeholder_key, placeholder_value);
                        }

                        result.push_str(&iteration_result);
                    }

                    debug!(
                        "Expanded {} iterations for placeholder {}",
                        placeholder.values.len(),
                        placeholder_name
                    );
                } else {
                    // Placeholder doesn't exist in metadata - treat as 0 iterations (empty expansion)
                    // This is valid: FOREACH-DTO-VALUE supports 0, 1, or N values
                    debug!(
                        "Placeholder '{placeholder_name}' not found in metadata - expanding to empty (0 iterations)"
                    );
                    // Don't add anything to result - the block is simply omitted
                }

                // Move past the END marker line
                // Find the end of the line containing the END marker
                let end_marker_abs_pos = block_content_start + end_marker_pos;
                let after_end_marker = &remaining[end_marker_abs_pos + end_marker.len()..];
                let end_line_end = after_end_marker
                    .find('\n')
                    .map_or(after_end_marker.len(), |i| i + 1);

                // Update remaining to skip past the END marker line
                remaining = &remaining[end_marker_abs_pos + end_marker.len() + end_line_end..];
            } else {
                return Err(anyhow!(
                    "Unclosed FOREACH-DTO-VALUE block for placeholder '{}'",
                    placeholder_name
                ));
            }
        } else {
            // No more FOREACH-DTO-VALUE blocks - substitute placeholders in remaining content
            let mut final_content = remaining.to_string();
            for (placeholder_key, placeholder_value) in additional_placeholders {
                final_content = final_content.replace(placeholder_key, placeholder_value);
            }
            result.push_str(&final_content);
            break;
        }
    }

    Ok(result)
}

/// Process IFDEF-DTO-VALUE blocks - conditionally includes content if placeholder is defined
///
/// Similar to FOREACH-DTO-VALUE but doesn't iterate - just includes/excludes the block.
/// If the placeholder exists (has at least one value), the block content is included once.
/// If the placeholder doesn't exist, the entire block (including markers) is removed.
///
/// # Arguments
/// * `template_content` - Template content possibly containing IFDEF-DTO-VALUE blocks
/// * `metadata` - DTO metadata containing placeholder definitions
/// * `additional_placeholders` - Map of additional placeholders (e.g., __DtoName__)
///
/// # Returns
/// * `Result<String>` - Processed template with IFDEF blocks resolved
fn process_ifdef_dto_value(
    template_content: &str,
    metadata: &metadata::DtoMetadata,
    _additional_placeholders: &HashMap<String, String>,
) -> Result<String> {
    use log::debug;

    // Support IFDEF/IFANY (include if exists) and IFNDEF/IFNO (include if NOT exists)
    let start_markers = [
        ("IFDEF-DTO-VALUE-START", "IFDEF-DTO-VALUE-END", true), // include if exists
        ("IFANY-DTO-VALUE-START", "IFANY-DTO-VALUE-END", true), // include if exists
        ("IFNDEF-DTO-VALUE-START", "IFNDEF-DTO-VALUE-END", false), // include if NOT exists
        ("IFNO-DTO-VALUE-START", "IFNO-DTO-VALUE-END", false),  // include if NOT exists
    ];

    let mut result = String::new();
    let mut remaining = template_content;

    loop {
        // Find next conditional block (whichever comes first)
        let mut earliest_start_pos = None;
        let mut matched_start_marker = "";
        let mut matched_end_marker = "";
        let mut include_if_exists = true;

        for (start_marker, end_marker, if_exists) in &start_markers {
            if let Some(pos) = remaining.find(start_marker) {
                if earliest_start_pos.is_none() || pos < earliest_start_pos.unwrap() {
                    earliest_start_pos = Some(pos);
                    matched_start_marker = start_marker;
                    matched_end_marker = end_marker;
                    include_if_exists = *if_exists;
                }
            }
        }

        if let Some(start_marker_pos) = earliest_start_pos {
            // Find the start of the line containing the START marker
            let line_start = remaining[..start_marker_pos]
                .rfind('\n')
                .map_or(0, |i| i + 1);

            // Add everything before the START marker line (no placeholder substitution yet)
            result.push_str(&remaining[..line_start]);

            // Extract placeholder name from the START marker line
            let after_start = &remaining[start_marker_pos + matched_start_marker.len()..];
            let marker_line_end = after_start.find('\n').unwrap_or(after_start.len());
            let marker_line = &after_start[..marker_line_end];
            let placeholder_with_delimiters = marker_line.trim();

            // Extract the first token (placeholder name) from the line
            // This handles cases like "@* IFANY-DTO-VALUE-START __name__ *@" where there are trailing characters
            let first_token = placeholder_with_delimiters
                .split_whitespace()
                .next()
                .unwrap_or("");

            // Remove __ delimiters if present
            let placeholder_name = first_token.trim_start_matches("__").trim_end_matches("__");

            // Debug: Show what placeholders are available
            debug!("=== IFDEF processing for placeholder: {placeholder_name} ===");
            debug!("Available placeholders in metadata.placeholders:");
            let mut sorted_keys: Vec<_> = metadata.placeholders.keys().collect();
            sorted_keys.sort();
            for key in &sorted_keys {
                let ph = metadata.placeholders.get(*key).unwrap();
                debug!("  {} -> {} values", key, ph.values.len());
            }
            debug!("=== End available placeholders ===");

            // Block content starts after the START marker line
            let block_content_start =
                start_marker_pos + matched_start_marker.len() + marker_line_end + 1;
            let search_content = &remaining[block_content_start..];

            if let Some(end_marker_pos) = search_content.find(matched_end_marker) {
                // Extract block content (everything between START line and END line)
                let end_line_start = search_content[..end_marker_pos]
                    .rfind('\n')
                    .map_or(0, |i| i + 1);
                let block_content = &search_content[..end_line_start];

                let marker_type = if include_if_exists {
                    "IFDEF/IFANY"
                } else {
                    "IFNDEF/IFNO"
                };
                debug!(
                    "Processing {marker_type}-DTO-VALUE block for placeholder: {placeholder_name}"
                );

                // Check if placeholder exists in metadata
                let placeholder_exists = metadata.placeholders.contains_key(placeholder_name);

                // Include block based on condition:
                // - IFDEF/IFANY: include if exists
                // - IFNDEF/IFNO: include if NOT exists
                let should_include = if include_if_exists {
                    placeholder_exists
                } else {
                    !placeholder_exists
                };

                if should_include {
                    // Include the block content (without markers)
                    // Don't substitute placeholders yet - that happens later
                    debug!(
                        "Placeholder '{placeholder_name}' {}: including {marker_type} block",
                        if placeholder_exists {
                            "exists"
                        } else {
                            "does not exist"
                        }
                    );
                    result.push_str(block_content);
                } else {
                    // Omit entire block including markers
                    debug!(
                        "Placeholder '{placeholder_name}' {}: excluding {marker_type} block",
                        if placeholder_exists {
                            "exists"
                        } else {
                            "does not exist"
                        }
                    );
                    // Don't add anything to result - the block is omitted
                }

                // Move past the END marker line
                let end_marker_abs_pos = block_content_start + end_marker_pos;
                let after_end_marker = &remaining[end_marker_abs_pos + matched_end_marker.len()..];
                let end_line_end = after_end_marker
                    .find('\n')
                    .map_or(after_end_marker.len(), |i| i + 1);

                // Update remaining to skip past the END marker line
                remaining =
                    &remaining[end_marker_abs_pos + matched_end_marker.len() + end_line_end..];
            } else {
                return Err(anyhow!(
                    "Unclosed {} block for placeholder '{}'",
                    matched_start_marker.replace("-START", ""),
                    placeholder_name
                ));
            }
        } else {
            // No more conditional blocks - add remaining content
            result.push_str(remaining);
            break;
        }
    }

    Ok(result)
}

/// Find all multi-value placeholders that share line numbers with the given placeholder
/// These are "companion" placeholders that should be iterated together in FOREACH loops
fn find_companion_placeholders(
    primary_placeholder_name: &str,
    metadata: &metadata::DtoMetadata,
) -> Vec<String> {
    use std::collections::{HashMap, HashSet};

    let mut companions = Vec::new();

    // Normalize placeholder name for comparison (lowercase, no separators)
    // This helps identify case variants of the same logical placeholder
    let normalize =
        |name: &str| -> String { name.to_lowercase().replace("_", "").replace("-", "") };

    let primary_normalized = normalize(primary_placeholder_name);

    // Get the primary placeholder
    if let Some(primary) = metadata.placeholders.get(primary_placeholder_name) {
        // Collect all line numbers where the primary placeholder appears
        let primary_line_numbers: HashSet<usize> =
            primary.values.iter().map(|v| v.line_number).collect();

        // Track which normalized names we've already seen to avoid duplicates
        let mut seen_normalized: HashMap<String, String> = HashMap::new();
        seen_normalized.insert(
            primary_normalized.clone(),
            primary_placeholder_name.to_string(),
        );

        // Find all other multi-value placeholders
        for (name, placeholder) in &metadata.placeholders {
            let normalized = normalize(name);

            // Skip if this is a case variant of the primary placeholder
            if normalized == primary_normalized {
                continue;
            }

            // Skip if we've already seen a case variant of this placeholder
            if seen_normalized.contains_key(&normalized) {
                continue;
            }

            // Skip single-value placeholders
            if matches!(placeholder.arity, metadata::PlaceholderArity::SingleValue) {
                continue;
            }

            // Check if this placeholder shares line numbers with the primary
            let has_shared_lines = placeholder
                .values
                .iter()
                .any(|v| primary_line_numbers.contains(&v.line_number));

            if has_shared_lines {
                companions.push(name.clone());
                seen_normalized.insert(normalized, name.clone());
            }
        }
    }

    companions
}

/// T088/T089: Add case variants by looking up variant placeholder keys in metadata
/// For each case variant key (e.g., "aliasname", "ALIASNAME", "alias-name"),
/// find the value with matching line_number and add to the iteration map
fn add_case_variants_from_metadata(
    placeholder_name: &str,
    line_number: usize,
    map: &mut HashMap<String, String>,
    metadata_placeholders: &HashMap<String, metadata::Placeholder>,
) {
    // Normalize the placeholder name to find all variant keys
    let normalized = placeholder_name
        .to_lowercase()
        .replace("_", "")
        .replace("-", "");

    // Find all placeholder keys in metadata that normalize to the same name
    for (variant_key, variant_placeholder) in metadata_placeholders {
        let variant_normalized = variant_key.to_lowercase().replace("_", "").replace("-", "");

        if variant_normalized == normalized {
            // This is a variant of our placeholder - find the value with matching line_number
            if let Some(value) = variant_placeholder
                .values
                .iter()
                .find(|v| v.line_number == line_number)
            {
                let key = format!("__{variant_key}__");
                map.insert(key, value.value.clone());
            }
        }
    }
}

/// Phase 2 Foundational: Deduplicate placeholder values while preserving first-occurrence order
///
/// # Arguments
/// * `values` - Slice of PlaceholderValue structs (may contain duplicates)
///
/// # Returns
/// * `Vec<PlaceholderValue>` - Deduplicated values in first-occurrence order
///
/// # Contract (from contracts/README.md)
/// - MUST preserve order: output[i] appears before output[j] in input if i < j
/// - MUST remove duplicates: no two output values have the same value field
/// - MUST be idempotent: deduplicate(deduplicate(x)) == deduplicate(x)
/// - MUST preserve original PlaceholderValue metadata (line_number, paired_with)
///
/// # Complexity
/// - Time: O(N) where N = values.len()
/// - Space: O(N) additional (HashSet for tracking)
///
/// # Order Preservation Invariants (Phase 4 User Story 3 - T032, T033)
///
/// **Invariant 1: First-Occurrence Order**
/// - For any two unique values U1 and U2 in the output:
///   - If U1's first occurrence is at index i in input
///   - And U2's first occurrence is at index j in input
///   - And i < j
///   - Then U1 appears before U2 in output
/// - Example: ["zebra"@0, "apple"@1, "zebra"@2] → ["zebra", "apple"]
///
/// **Invariant 2: Stable Sort Property**
/// - The relative order of unique values is stable across multiple runs
/// - Same input always produces same output order (deterministic)
/// - Verified by test_deterministic_output_multiple_runs (T029)
///
/// **Invariant 3: Duplicate Position Irrelevance**
/// - Only the FIRST occurrence of a value determines its position
/// - Subsequent occurrences are ignored for ordering purposes
/// - Example: ["A"@0, "B"@50, "A"@100] → ["A", "B"] (not ["B", "A"])
/// - Verified by test_separated_duplicates_order_preservation (T030)
///
/// **Implementation Strategy**:
/// - Vec maintains insertion order (first occurrence order preserved)
/// - HashSet::insert() returns true only on first insertion (deduplication)
/// - This combination guarantees O(N) time with stable first-occurrence ordering
///
/// # Task: T004, T010, T032, T033
pub fn deduplicate_values(
    values: &[metadata::PlaceholderValue],
) -> Vec<metadata::PlaceholderValue> {
    use std::collections::HashSet;

    // T033: Order preservation assertion - Vec + HashSet strategy
    // This implementation guarantees first-occurrence order because:
    // 1. We iterate through values in original order (preserves input sequence)
    // 2. HashSet.insert() returns true only for first occurrence (filters duplicates)
    // 3. Vec.push() maintains insertion order (stable output order)
    let mut seen = HashSet::new();
    let mut unique_values = Vec::new();

    for value in values {
        // Check if we've seen this value string before
        // HashSet.insert() returns true only on first insertion (first occurrence)
        if seen.insert(value.value.clone()) {
            // First occurrence: add to output (preserves original position)
            unique_values.push(value.clone());
        }
        // Subsequent occurrences: skip (already in seen set, position already determined)
    }

    // T033: Post-condition assertion
    // Output maintains first-occurrence order from input
    debug_assert!(
        unique_values.len() <= values.len(),
        "Output length must be <= input length (deduplication)"
    );

    unique_values
}

/// Phase 3 User Story 1: Process FOREACH-UNIQUE-DTO-VALUE blocks with deduplication
///
/// # Arguments
/// * `template_content` - Template string containing FOREACH-UNIQUE-DTO-VALUE markers
/// * `metadata` - DtoMetadata containing all placeholder values
/// * `additional_placeholders` - Additional single-value placeholders for substitution
///
/// # Returns
/// * `Result<String>` - Processed template with deduplicated iterations
/// * `Err` - If markers are malformed or unclosed
///
/// # Contract (from contracts/README.md)
/// - MUST detect FOREACH-UNIQUE-DTO-VALUE-START and FOREACH-UNIQUE-DTO-VALUE-END markers
/// - MUST extract placeholder name from START marker line
/// - MUST deduplicate values from metadata.placeholders[name]
/// - MUST preserve companion placeholder pairing (via line_number)
/// - MUST handle zero values: omit entire block including markers
/// - MUST handle non-existent placeholder: omit block (no error)
/// - MUST propagate errors from underlying iteration logic
///
/// # Tasks: T019-T025
pub fn process_foreach_unique_dto_value(
    template_content: &str,
    metadata: &metadata::DtoMetadata,
    additional_placeholders: &HashMap<String, String>,
) -> Result<String> {
    use log::debug;

    // T020: Marker detection
    let start_marker = "FOREACH-UNIQUE-DTO-VALUE-START";
    let end_marker = "FOREACH-UNIQUE-DTO-VALUE-END";

    let mut result = String::new();
    let mut remaining = template_content;

    loop {
        // Find next FOREACH-UNIQUE-DTO-VALUE block
        if let Some(start_marker_pos) = remaining.find(start_marker) {
            // Find the start of the line containing the START marker (may be in a comment)
            let line_start = remaining[..start_marker_pos]
                .rfind('\n')
                .map_or(0, |i| i + 1);

            // Add everything before the START marker line (WITHOUT placeholder substitution)
            // DO NOT substitute placeholders here - they might be FOREACH-DTO-VALUE markers
            // that need to be processed by process_foreach_dto_value() later
            let before_block = remaining[..line_start].to_string();
            result.push_str(&before_block);

            // T021: Extract placeholder name from the START marker line
            let after_start = &remaining[start_marker_pos + start_marker.len()..];
            let marker_line_end = after_start.find('\n').unwrap_or(after_start.len());
            let marker_line = &after_start[..marker_line_end];
            let placeholder_with_delimiters = marker_line.trim();

            // Extract the first token (placeholder name) from the line
            // This handles cases like "@* FOREACH-UNIQUE-DTO-VALUE-START __name__ *@" where there are trailing characters
            let first_token = placeholder_with_delimiters
                .split_whitespace()
                .next()
                .unwrap_or("");

            // Remove __ delimiters if present
            let placeholder_name = first_token.trim_start_matches("__").trim_end_matches("__");

            // Block content starts after the START marker line
            let block_content_start = start_marker_pos + start_marker.len() + marker_line_end + 1;
            let search_content = &remaining[block_content_start..];

            if let Some(end_marker_pos) = search_content.find(end_marker) {
                // Extract block content (everything between START line and END line)
                // Find the start of the line containing the END marker
                let end_line_start = search_content[..end_marker_pos]
                    .rfind('\n')
                    .map_or(0, |i| i + 1);
                let block_content = &search_content[..end_line_start];

                debug!(
                    "Processing FOREACH-UNIQUE-DTO-VALUE block for placeholder: {placeholder_name}"
                );

                // T022: Metadata lookup and deduplication
                if let Some(placeholder) = metadata.placeholders.get(placeholder_name) {
                    // T022: Deduplicate values before iteration
                    let unique_values = deduplicate_values(&placeholder.values);

                    // T025: Log unique filtering operations
                    if placeholder.values.len() != unique_values.len() {
                        debug!(
                            "Filtered {} duplicate values for placeholder '{}' ({} total -> {} unique)",
                            placeholder.values.len() - unique_values.len(),
                            placeholder_name,
                            placeholder.values.len(),
                            unique_values.len()
                        );
                    } else {
                        debug!(
                            "No duplicates found for placeholder '{}' ({} unique values)",
                            placeholder_name,
                            unique_values.len()
                        );
                    }

                    // T024: Handle zero values edge case
                    if unique_values.is_empty() {
                        debug!(
                            "Placeholder '{placeholder_name}' has 0 values - omitting entire block including markers"
                        );
                        // Don't add anything to result - the block is simply omitted
                    } else {
                        // Find companion placeholders for paired iteration
                        let companion_placeholders =
                            find_companion_placeholders(placeholder_name, metadata);

                        debug!(
                            "Found {} companion placeholders: {:?}",
                            companion_placeholders.len(),
                            companion_placeholders
                        );

                        // T023: Iterate over unique values only
                        for value in unique_values.iter() {
                            // Build full placeholder map for this iteration
                            let mut iteration_placeholders = HashMap::new();

                            // Add case variants for the iteration placeholder by line number
                            add_case_variants_from_metadata(
                                placeholder_name,
                                value.line_number,
                                &mut iteration_placeholders,
                                &metadata.placeholders,
                            );

                            // Add case variants for companion placeholders from the same line
                            for companion_name in &companion_placeholders {
                                add_case_variants_from_metadata(
                                    companion_name,
                                    value.line_number,
                                    &mut iteration_placeholders,
                                    &metadata.placeholders,
                                );
                            }

                            // Add all additional placeholders (single-value, etc.)
                            // BUT skip the iteration placeholder and companion placeholders to avoid overriding them
                            for (key, val) in additional_placeholders {
                                if !iteration_placeholders.contains_key(key) {
                                    iteration_placeholders.insert(key.clone(), val.clone());
                                }
                            }

                            // Substitute all placeholders in block content
                            let mut iteration_result = block_content.to_string();
                            for (placeholder_key, placeholder_value) in &iteration_placeholders {
                                iteration_result =
                                    iteration_result.replace(placeholder_key, placeholder_value);
                            }

                            result.push_str(&iteration_result);
                        }

                        debug!(
                            "Expanded {} unique iterations for placeholder {}",
                            unique_values.len(),
                            placeholder_name
                        );
                    }
                } else {
                    // T024: Placeholder doesn't exist in metadata - treat as 0 iterations (empty expansion)
                    debug!(
                        "Placeholder '{placeholder_name}' not found in metadata - omitting entire block including markers (0 values)"
                    );
                    // Don't add anything to result - the block is simply omitted
                }

                // Move past the END marker line
                // Find the end of the line containing the END marker
                let end_marker_abs_pos = block_content_start + end_marker_pos;
                let after_end_marker = &remaining[end_marker_abs_pos + end_marker.len()..];
                let end_line_end = after_end_marker
                    .find('\n')
                    .map_or(after_end_marker.len(), |i| i + 1);

                // Update remaining to skip past the END marker line
                remaining = &remaining[end_marker_abs_pos + end_marker.len() + end_line_end..];
            } else {
                return Err(anyhow!(
                    "Unclosed FOREACH-UNIQUE-DTO-VALUE block for placeholder '{}'",
                    placeholder_name
                ));
            }
        } else {
            // No more FOREACH-UNIQUE-DTO-VALUE blocks
            // If we never found any blocks (result is empty), return original content unchanged
            // This prevents us from substituting placeholders that should be processed by other functions
            if result.is_empty() {
                return Ok(template_content.to_string());
            }

            // Otherwise, append remaining content WITHOUT placeholder substitution
            // Placeholder substitution happens AFTER all FOREACH processing (both UNIQUE and regular)
            // This prevents us from breaking FOREACH-DTO-VALUE markers that appear after FOREACH-UNIQUE blocks
            result.push_str(remaining);
            break;
        }
    }

    Ok(result)
}

fn copy_dir_all_with_gitignore(src: &Path, dst: &Path, respect_gitignore: bool) -> Result<()> {
    fs::create_dir_all(dst)?;

    // Create file iterator based on gitignore setting
    let walker = if respect_gitignore {
        info!(
            "Respecting .gitignore files when copying from {}",
            src.display()
        );
        ignore::WalkBuilder::new(src)
            .git_ignore(true)
            .git_global(true)
            .git_exclude(true)
            .hidden(false)
            .build()
    } else {
        ignore::WalkBuilder::new(src)
            .git_ignore(false)
            .git_global(false)
            .git_exclude(false)
            .hidden(false)
            .build()
    };

    for entry in walker {
        let entry = entry?;
        let src_path = entry.path();

        // Skip the root directory itself
        if src_path == src {
            continue;
        }

        // Calculate relative path and destination
        let relative_path = src_path.strip_prefix(src)?;
        let dst_path = dst.join(relative_path);

        if entry.file_type().is_some_and(|ft| ft.is_dir()) {
            fs::create_dir_all(&dst_path)?;
        } else if entry.file_type().is_some_and(|ft| ft.is_file()) {
            if let Some(parent) = dst_path.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(src_path, &dst_path)?;
        }
    }

    Ok(())
}
