//! Web Backend using axum and HTMX for web-based forms
//!
//! This backend provides a REST API server with HTML forms using HTMX.
//! Forms are served over HTTP and can be accessed from any web browser.

use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle;

use super::{FormBackend, RenderContext};
use crate::error::{ErrorWrapper, Result};
use crate::form_parser::{DisplayItem, FieldDefinition, FieldType};

/// Type alias for complete form submission channel
type CompleteFormChannel = Arc<RwLock<Option<oneshot::Sender<HashMap<String, Value>>>>>;

/// Roundtrip context for nickel-roundtrip mode
#[derive(Clone)]
pub struct RoundtripContext {
    pub initial_values: HashMap<String, Value>,
    pub output_path: PathBuf,
    pub input_nickel: String,
}

/// Shared form state accessible to all handlers (A-SHARED-STATE pattern)
#[derive(Clone)]
pub struct WebFormState {
    #[allow(dead_code)]
    form: Arc<RwLock<Option<crate::form_parser::FormDefinition>>>,
    current_field: Arc<RwLock<Option<FieldDefinition>>>,
    field_channels: Arc<RwLock<HashMap<String, oneshot::Sender<Value>>>>,
    results: Arc<RwLock<HashMap<String, Value>>>,
    display_buffer: Arc<RwLock<Vec<String>>>,
    /// Channel for complete form submission (used in Complete display mode)
    complete_form_tx: CompleteFormChannel,
    /// Flag indicating if we're in complete form mode (all fields at once)
    is_complete_mode: Arc<RwLock<bool>>,
    /// Cached fields for complete mode rendering
    complete_fields: Arc<RwLock<Vec<FieldDefinition>>>,
    /// Cached display items (section headers, etc.) for complete mode
    complete_items: Arc<RwLock<Vec<DisplayItem>>>,
    /// Base directory for lazy loading fragments (FASE 4)
    base_dir: Arc<RwLock<Option<PathBuf>>>,
    /// Roundtrip context (only set in nickel-roundtrip mode)
    roundtrip_context: Arc<RwLock<Option<RoundtripContext>>>,
}

impl WebFormState {
    fn new() -> Self {
        WebFormState {
            form: Arc::new(RwLock::new(None)),
            current_field: Arc::new(RwLock::new(None)),
            field_channels: Arc::new(RwLock::new(HashMap::new())),
            results: Arc::new(RwLock::new(HashMap::new())),
            display_buffer: Arc::new(RwLock::new(Vec::new())),
            complete_form_tx: Arc::new(RwLock::new(None)),
            is_complete_mode: Arc::new(RwLock::new(false)),
            complete_fields: Arc::new(RwLock::new(Vec::new())),
            complete_items: Arc::new(RwLock::new(Vec::new())),
            base_dir: Arc::new(RwLock::new(None)),
            roundtrip_context: Arc::new(RwLock::new(None)),
        }
    }

    /// Set roundtrip context for nickel-roundtrip mode
    pub async fn set_roundtrip_context(&self, context: RoundtripContext) {
        let mut ctx = self.roundtrip_context.write().await;
        *ctx = Some(context);
    }
}

/// Web Backend implementation using axum
pub struct WebBackend {
    port: u16,
    state: Option<Arc<WebFormState>>,
    server_handle: Option<JoinHandle<()>>,
    shutdown_tx: Option<oneshot::Sender<()>>,
}

#[derive(Deserialize, Default)]
struct FieldSubmission {
    #[serde(default)]
    value: String,
    field_type: String,
    #[serde(default)]
    values: String, // Changed to String - will be comma-separated values
}

impl WebBackend {
    /// Create a new Web backend instance with the specified port
    pub fn new(port: u16) -> Self {
        WebBackend {
            port,
            state: None,
            server_handle: None,
            shutdown_tx: None,
        }
    }

    /// Get access to the web form state (for setting roundtrip context)
    pub fn get_state(&self) -> Option<Arc<WebFormState>> {
        self.state.clone()
    }
}

#[cfg(feature = "web")]
use axum::{
    extract::{Path, Query, State},
    http::{HeaderMap, StatusCode},
    response::IntoResponse,
    routing::{get, post},
    Form, Json, Router,
};

#[async_trait]
impl FormBackend for WebBackend {
    async fn initialize(&mut self) -> Result<()> {
        #[cfg(feature = "web")]
        {
            use std::net::SocketAddr;

            // Create shared state (A-SHARED-STATE guideline)
            let form_state = Arc::new(WebFormState::new());
            self.state = Some(form_state.clone());

            // Graceful shutdown channel
            let (shutdown_tx, shutdown_rx) = oneshot::channel();
            self.shutdown_tx = Some(shutdown_tx);

            // Router composition (A-ROUTER-COMPOSITION guideline)
            let app = Router::new()
                .route("/", get(index_handler))
                .route("/api/form", get(get_form_handler))
                .route("/api/form/dynamic", get(get_dynamic_form_handler))
                .route("/api/field/{name}", post(submit_field_handler))
                .route("/api/form/complete", post(submit_complete_form_handler))
                .route("/api/form/download", get(download_config_handler))
                .route("/api/nickel/to-form", post(nickel_to_form_handler))
                .route("/api/nickel/template", post(nickel_template_handler))
                .route("/api/nickel/roundtrip", post(nickel_roundtrip_handler))
                .with_state(form_state);

            // Server startup with graceful shutdown
            let addr = SocketAddr::from(([127, 0, 0, 1], self.port));
            let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| {
                ErrorWrapper::validation_failed(format!(
                    "Failed to bind to port {}: {}",
                    self.port, e
                ))
            })?;

            let server = axum::serve(listener, app).with_graceful_shutdown(async {
                shutdown_rx.await.ok();
            });

            let handle = tokio::spawn(async move {
                if let Err(e) = server.await {
                    eprintln!("Server error: {}", e);
                }
            });

            self.server_handle = Some(handle);
            let url = format!("http://localhost:{}", self.port);
            println!("Web UI available at {}", url);
            tokio::time::sleep(Duration::from_millis(500)).await;

            // Open browser — best-effort, never fatal.
            let url_clone = url.clone();
            tokio::task::spawn_blocking(move || {
                #[cfg(target_os = "macos")]
                drop(std::process::Command::new("open").arg(&url_clone).status());
                #[cfg(target_os = "linux")]
                drop(
                    std::process::Command::new("xdg-open")
                        .arg(&url_clone)
                        .status(),
                );
                #[cfg(target_os = "windows")]
                drop(
                    std::process::Command::new("cmd")
                        .args(["/c", "start", "", &url_clone])
                        .status(),
                );
            });

            Ok(())
        }

        #[cfg(not(feature = "web"))]
        {
            Err(ErrorWrapper::validation_failed(
                "Web feature not enabled. Enable with --features web".to_string(),
            ))
        }
    }

    async fn render_display_item(&self, item: &DisplayItem, context: &RenderContext) -> Result<()> {
        // Check conditional rendering
        if let Some(condition) = &item.when {
            if !evaluate_condition(condition, &context.results) {
                return Ok(());
            }
        }

        if let Some(state) = &self.state {
            let mut buffer = state.display_buffer.write().await;

            // Build HTML with custom border support
            let mut html = String::new();
            let margin_left = item.margin_left.unwrap_or(0);
            let border_margin_left = item.border_margin_left.unwrap_or(0);
            let content_margin_left = item.content_margin_left.unwrap_or(2);

            let style = format!("margin-left: {}ch;", margin_left);
            let border_style = format!(
                "margin-left: {}ch; font-family: monospace;",
                border_margin_left
            );
            let content_style = format!("margin-left: {}ch;", content_margin_left);

            // Render top border if specified
            if item.border_top.unwrap_or(false) {
                let border_left = item.border_top_l.as_deref().unwrap_or("╭");
                let border_char = item.border_top_char.as_deref().unwrap_or("─");
                let border_len = item.border_top_len.unwrap_or(40);
                let border_right = item.border_top_r.as_deref().unwrap_or("╮");
                let border_line = format!(
                    "{}{}{}",
                    border_left,
                    border_char.repeat(border_len),
                    border_right
                );
                html.push_str(&format!(
                    "<div style='{}'><pre>{}</pre></div>\n",
                    border_style,
                    html_escape(&border_line)
                ));
            }

            // Render title
            if let Some(title) = &item.title {
                html.push_str(&format!(
                    "<div style='{}'><h3>{}</h3></div>\n",
                    content_style,
                    html_escape(title)
                ));
            }

            // Render content
            if let Some(content) = &item.content {
                html.push_str(&format!(
                    "<div style='{}'><p>{}</p></div>\n",
                    content_style,
                    html_escape(content)
                ));
            }

            // Render bottom border if specified
            if item.border_bottom.unwrap_or(false) {
                let border_left = item.border_bottom_l.as_deref().unwrap_or("╰");
                let border_char = item.border_bottom_char.as_deref().unwrap_or("─");
                let border_len = item.border_bottom_len.unwrap_or(40);
                let border_right = item.border_bottom_r.as_deref().unwrap_or("╯");
                let border_line = format!(
                    "{}{}{}",
                    border_left,
                    border_char.repeat(border_len),
                    border_right
                );
                html.push_str(&format!(
                    "<div style='{}'><pre>{}</pre></div>\n",
                    border_style,
                    html_escape(&border_line)
                ));
            }

            html.push_str(&format!("<div style='{}'></div>\n", style));
            buffer.push(html);
        }

        Ok(())
    }

    async fn execute_field(
        &self,
        field: &FieldDefinition,
        _context: &RenderContext,
    ) -> Result<Value> {
        let state = self
            .state
            .as_ref()
            .ok_or_else(|| ErrorWrapper::validation_failed("Server not initialized"))?;

        // Create oneshot channel for field submission
        let (tx, rx) = oneshot::channel();

        // Store field info and channel
        {
            let mut current = state.current_field.write().await;
            *current = Some(field.clone());

            let mut channels = state.field_channels.write().await;
            channels.insert(field.name.clone(), tx);
        }

        // Wait for field submission with timeout
        match tokio::time::timeout(Duration::from_secs(300), rx).await {
            Ok(Ok(value)) => {
                // Validate required fields
                if field.required.unwrap_or(false) && value.is_null() {
                    return self.execute_field(field, _context).await;
                }

                // Store result
                {
                    let mut results = state.results.write().await;
                    results.insert(field.name.clone(), value.clone());
                }

                // Clear current field
                {
                    let mut current = state.current_field.write().await;
                    *current = None;
                }

                Ok(value)
            }
            Ok(Err(_)) => Err(ErrorWrapper::cancelled_no_context()),
            Err(_) => Err(ErrorWrapper::validation_failed(
                "Field submission timeout".to_string(),
            )),
        }
    }

    async fn execute_form_complete(
        &mut self,
        form: &crate::form_parser::FormDefinition,
        base_dir: &std::path::Path,
        items: Vec<DisplayItem>,
        fields: Vec<FieldDefinition>,
        initial_values: Option<HashMap<String, Value>>,
    ) -> Result<HashMap<String, Value>> {
        let state = self
            .state
            .as_ref()
            .ok_or_else(|| ErrorWrapper::validation_failed("Server not initialized"))?;

        // Initialize results with initial values (for field rendering)
        {
            let mut is_complete = state.is_complete_mode.write().await;
            *is_complete = true;

            let mut cached_fields = state.complete_fields.write().await;
            *cached_fields = fields.clone();

            let mut cached_items = state.complete_items.write().await;
            *cached_items = items.clone();

            let mut stored_base_dir = state.base_dir.write().await;
            *stored_base_dir = Some(base_dir.to_path_buf());

            let mut stored_form = state.form.write().await;
            *stored_form = Some(form.clone());

            // Initialize results with initial values
            let mut results = state.results.write().await;
            *results = initial_values.clone().unwrap_or_default();
            if !results.is_empty() {
                eprintln!(
                    "[web] Complete form initialized with {} default values",
                    results.len()
                );
            }
        }

        // Create oneshot channel for complete form submission
        let (tx, rx) = oneshot::channel();
        {
            let mut complete_tx = state.complete_form_tx.write().await;
            *complete_tx = Some(tx);
        }

        // Clear field channels (but keep results with initial values)
        {
            let mut channels = state.field_channels.write().await;
            channels.clear();
        }

        // Wait for complete form submission with timeout
        match tokio::time::timeout(Duration::from_secs(300), rx).await {
            Ok(Ok(mut all_results)) => {
                // Validate required fields
                for field in &fields {
                    if field.required.unwrap_or(false) {
                        if let Some(value) = all_results.get(&field.name) {
                            if value.is_null()
                                || (value.is_string() && value.as_str().unwrap_or("").is_empty())
                            {
                                // Required field is empty - would need to show error and retry
                                // For now, just ensure field is in results
                                all_results.insert(field.name.clone(), Value::Null);
                            }
                        } else {
                            all_results.insert(field.name.clone(), Value::Null);
                        }
                    }
                }

                // Merge back initial values for fields not submitted by user
                if let Some(init_vals) = initial_values {
                    for (k, v) in init_vals.iter() {
                        if !all_results.contains_key(k) {
                            all_results.insert(k.clone(), v.clone());
                        }
                    }
                }

                // Store results in state for reference
                {
                    let mut results = state.results.write().await;
                    *results = all_results.clone();
                }

                // Clear complete mode flag
                {
                    let mut is_complete = state.is_complete_mode.write().await;
                    *is_complete = false;
                }

                Ok(all_results)
            }
            Ok(Err(_)) => Err(ErrorWrapper::cancelled_no_context()),
            Err(_) => Err(ErrorWrapper::validation_failed(
                "Form submission timeout".to_string(),
            )),
        }
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }

    async fn shutdown(&mut self) -> Result<()> {
        if let Some(tx) = self.shutdown_tx.take() {
            let _ = tx.send(());
        }

        if let Some(handle) = self.server_handle.take() {
            match tokio::time::timeout(Duration::from_secs(5), handle).await {
                Ok(Ok(())) => Ok(()),
                Ok(Err(e)) => Err(ErrorWrapper::validation_failed(format!(
                    "Server join error: {}",
                    e
                ))),
                Err(_) => {
                    // Timeout - server didn't shutdown gracefully
                    Err(ErrorWrapper::validation_failed(
                        "Server shutdown timeout".to_string(),
                    ))
                }
            }
        } else {
            Ok(())
        }
    }

    fn is_available() -> bool {
        #[cfg(feature = "web")]
        {
            true
        }

        #[cfg(not(feature = "web"))]
        {
            false
        }
    }

    fn name(&self) -> &str {
        "web"
    }
}

/// HTTP Handlers (A-EXTRACTORS-FIRST pattern from AXUM.md)
/// Each handler uses State extractor for type-safe access to shared state
#[cfg(feature = "web")]
async fn index_handler(State(state): State<Arc<WebFormState>>) -> impl IntoResponse {
    // Get form name from state
    let form_name = {
        let form_opt = state.form.read().await;
        form_opt
            .as_ref()
            .map(|f| f.name.as_str())
            .unwrap_or("Form")
            .to_string()
    };

    let html = format!(
        r#"<!DOCTYPE html>
<html>
<head>
    <title>{}</title>
    <style>
        body {{ font-family: monospace; background: #1e1e1e; color: #d4d4d4; margin: 20px; }}
        .form-container {{ max-width: 600px; margin: 0 auto; }}
        .display-item {{ margin: 20px 0; border-left: 3px solid #007acc; padding-left: 10px; }}
        .field {{ margin: 15px 0; }}
        input, select, textarea {{ background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; padding: 8px; }}
        button {{ background: #007acc; color: white; border: none; padding: 8px 16px; cursor: pointer; }}
        button:hover {{ background: #005a9e; }}
    </style>
    <script>
    function collectMultiselectValues(event, fieldName) {{
        const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
        const values = Array.from(checkboxes).map(cb => cb.value);
        document.getElementById('values_' + fieldName).value = values.join(',');
    }}
    </script>
</head>
<body>
    <div class="form-container" id="main">
        <h1>{}</h1>
    </div>
    <script>
    // Load initial form - server waits until form is ready
    document.addEventListener('DOMContentLoaded', function() {{
        fetch('/api/form')
            .then(r => r.text())
            .then(html => {{
                const container = document.getElementById('main');

                // Create temporary container to parse HTML
                const temp = document.createElement('div');
                temp.innerHTML = html;

                // Extract and execute scripts
                const scripts = temp.querySelectorAll('script');
                const scriptContents = Array.from(scripts).map(s => s.textContent);

                // Remove scripts from content before inserting
                scripts.forEach(s => s.remove());

                // Insert non-script content
                container.innerHTML += temp.innerHTML;

                // Execute scripts one by one
                scriptContents.forEach(function(content) {{
                    const script = document.createElement('script');
                    script.textContent = content;
                    document.body.appendChild(script);
                }});

                // After form is loaded and scripts executed, filter dependent options
                // This ensures primary_language only shows languages selected in detected_languages
                setTimeout(function() {{
                    if (typeof filterDependentOptions === 'function') {{
                        filterDependentOptions();
                    }}
                }}, 100);
            }});
    }});

    // Dynamic options filtering based on data-options-from
    function filterDependentOptions() {{
        // First, update all hidden fields for multiselect from their checkboxes
        document.querySelectorAll('[id^="values_"]').forEach(hiddenField => {{
            const fieldName = hiddenField.id.replace('values_', '');
            const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
            const values = Array.from(checkboxes).map(cb => cb.value);
            hiddenField.value = values.join(',');
        }});

        // Find all select fields with data-options-from attribute
        document.querySelectorAll('select[data-options-from]').forEach(targetSelect => {{
            const sourceFieldName = targetSelect.getAttribute('data-options-from');

            // Store original options if not already stored
            if (!targetSelect.dataset.allOptions) {{
                const allOptions = Array.from(targetSelect.options).map(opt => ({{
                    value: opt.value,
                    text: opt.textContent
                }}));
                targetSelect.dataset.allOptions = JSON.stringify(allOptions);
            }}

            // Find the source field (could be multiselect checkboxes or select)
            const sourceHiddenField = document.getElementById('values_' + sourceFieldName);
            const sourceSelect = document.querySelector('select[data-fieldname="' + sourceFieldName + '"]');

            let selectedValues = [];

            if (sourceHiddenField) {{
                // Multiselect with checkboxes
                const valuesStr = sourceHiddenField.value;
                selectedValues = valuesStr ? valuesStr.split(',').map(v => v.trim()) : [];
            }} else if (sourceSelect) {{
                // Regular select
                selectedValues = [sourceSelect.value];
            }}

            // Get all original options
            const allOptions = JSON.parse(targetSelect.dataset.allOptions);

            // Filter options to only include those in selectedValues
            const filteredOptions = allOptions.filter(opt => selectedValues.includes(opt.value));

            // Save current value - check both selected option and current value
            let currentValue = targetSelect.value;
            if (!currentValue) {{
                // If no value selected yet, check for option with 'selected' attribute
                const selectedOption = targetSelect.querySelector('option[selected]');
                if (selectedOption) {{
                    currentValue = selectedOption.value;
                }}
            }}

            // Clear and repopulate options
            targetSelect.innerHTML = '';

            // Determine which value should be selected
            let valueToSelect = '';
            if (currentValue && filteredOptions.some(opt => opt.value === currentValue)) {{
                valueToSelect = currentValue;
            }} else if (filteredOptions.length > 0) {{
                valueToSelect = filteredOptions[0].value;
            }}

            // Rebuild options and mark the selected one
            filteredOptions.forEach(opt => {{
                const option = document.createElement('option');
                option.value = opt.value;
                option.textContent = opt.text;
                if (opt.value === valueToSelect) {{
                    option.selected = true;
                }}
                targetSelect.appendChild(option);
            }});
        }});
    }}

    // Reactive form field updates
    document.addEventListener('change', function(e) {{
        // First, handle dynamic options filtering
        if (e.target.matches('input[type=checkbox][data-checkbox-group], select')) {{
            filterDependentOptions();
        }}

        if (e.target.matches('select, input[type=checkbox], input[type=radio]')) {{
            const form = document.querySelector('form#complete-form');
            if (!form) return;

            const formData = new FormData(form);
            const params = new URLSearchParams(formData);

            fetch('/api/form/dynamic?' + params)
                .then(r => r.text())
                .then(html => {{
                    document.getElementById('form-fields').innerHTML = html;
                    // After HTML replacement, populate all multiselect hidden fields
                    // This ensures values are preserved when rendered HTML replaces old HTML
                    document.querySelectorAll('[id^="values_"]').forEach(hiddenField => {{
                        const fieldName = hiddenField.id.replace('values_', '');
                        const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
                        const values = Array.from(checkboxes).map(cb => cb.value);
                        hiddenField.value = values.join(',');
                    }});
                    // Re-run option filtering after dynamic update
                    filterDependentOptions();
                }})
                .catch(err => console.error('Failed to update form:', err));
        }}
    }});
    </script>
</body>
</html>"#,
        html_escape(&form_name),
        html_escape(&form_name)
    );

    Html(html)
}

#[cfg(feature = "web")]
async fn get_form_handler(State(state): State<Arc<WebFormState>>) -> impl IntoResponse {
    // Poll until form is ready (max ~2.5 seconds)
    // IMPORTANT: Wait for is_complete_mode to be true, don't exit early
    for _ in 0..50 {
        let is_complete = *state.is_complete_mode.read().await;
        if is_complete {
            break;
        }
        tokio::time::sleep(Duration::from_millis(50)).await;
    }

    let is_complete = *state.is_complete_mode.read().await;

    // In complete mode, display items are rendered inline with fields
    // In field-by-field mode, display items are shown separately
    let display_html = if is_complete {
        String::new() // Don't show display_buffer in complete mode
    } else {
        let display_items = state.display_buffer.read().await;
        display_items.iter().cloned().collect::<Vec<_>>().join("\n")
    };

    let field_html = if is_complete {
        // Complete form mode: render all fields in a single form
        let fields = state.complete_fields.read().await;
        let items = state.complete_items.read().await;
        let results = state.results.read().await;
        let base_dir = state.base_dir.read().await;
        let base_dir_path = base_dir
            .as_ref()
            .map(|p| p.as_path())
            .unwrap_or_else(|| std::path::Path::new("."));
        render_complete_form(&items, &fields, &results, base_dir_path)
    } else {
        // Field-by-field mode: render single current field
        let current_field = state.current_field.read().await;
        if let Some(field) = current_field.as_ref() {
            render_field_to_html(field)
        } else {
            "<div class='complete'><h2>Form submitted!</h2></div>".to_string()
        }
    };

    Html(format!(
        r#"<div class="display-items">{}</div><div class="current-field">{}</div>"#,
        display_html, field_html
    ))
}

/// Dynamic form endpoint for vanilla JS polling
/// Returns updated field HTML based on current results (reactive rendering)
/// Accepts query parameters containing current form field values
#[cfg(feature = "web")]
async fn get_dynamic_form_handler(
    State(state): State<Arc<WebFormState>>,
    Query(params): Query<HashMap<String, String>>,
) -> (StatusCode, Html<String>) {
    let is_complete = *state.is_complete_mode.read().await;

    if !is_complete {
        // Only support dynamic rendering in complete form mode
        return (
            StatusCode::BAD_REQUEST,
            Html("<div>Dynamic form only available in complete mode</div>".to_string()),
        );
    }

    // Convert query parameters to results HashMap for condition evaluation
    // JavaScript sends current field values as query parameters
    // Multiselect fields use "_values" suffix and contain comma-separated values
    let mut results: HashMap<String, Value> = HashMap::new();
    for (key, value) in params {
        if key.ends_with("_values") {
            // Parse comma-separated multiselect values into array
            let field_name = key.strip_suffix("_values").unwrap_or(&key);
            let values: Vec<Value> = value
                .split(',')
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .map(|s| Value::String(s.to_string()))
                .collect();
            results.insert(field_name.to_string(), Value::Array(values));
        } else {
            results.insert(key, Value::String(value));
        }
    }

    // Get base_dir and form for recomputation
    let base_dir_opt = state.base_dir.read().await.clone();
    let form_opt = state.form.read().await.clone();

    match (base_dir_opt, form_opt) {
        (Some(base_dir), Some(form)) => {
            // Recompute visible elements based on current results
            match crate::form_parser::recompute_visible_elements(&form, &base_dir, &results) {
                Ok((visible_items, visible_fields)) => {
                    // Return ONLY field HTML (no wrapper div)
                    // Wrapper div#form-fields already exists in DOM, JavaScript replaces innerHTML
                    let (fields_html, _modals_and_scripts) =
                        render_fields_only(&visible_items, &visible_fields, &results, &base_dir);
                    (StatusCode::OK, Html(fields_html))
                }
                Err(_) => {
                    // Fallback: return all fields
                    let items = state.complete_items.read().await.clone();
                    let fields = state.complete_fields.read().await.clone();
                    let (fields_html, _modals_and_scripts) =
                        render_fields_only(&items, &fields, &results, &base_dir);
                    (StatusCode::OK, Html(fields_html))
                }
            }
        }
        _ => {
            // Fallback if state not properly initialized
            let items = state.complete_items.read().await.clone();
            let fields = state.complete_fields.read().await.clone();
            let (fields_html, _modals_and_scripts) =
                render_fields_only(&items, &fields, &HashMap::new(), std::path::Path::new("."));
            (StatusCode::OK, Html(fields_html))
        }
    }
}

#[cfg(feature = "web")]
async fn submit_field_handler(
    State(state): State<Arc<WebFormState>>,
    Path(field_name): Path<String>,
    Form(payload): Form<FieldSubmission>,
) -> impl IntoResponse {
    // Parse value based on field type
    let value = match payload.field_type.as_str() {
        "confirm" => json!(payload.value.to_lowercase() == "true"),
        "multiselect" => {
            // Parse comma-separated values into array, filter empty strings
            let values: Vec<String> = payload
                .values
                .split(',')
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string())
                .collect();
            json!(values)
        }
        "custom" => {
            // Get custom type from current field for validation
            let current_field = state.current_field.read().await;
            if let Some(field) = current_field.as_ref() {
                let custom_type = field.custom_type.as_deref().unwrap_or("String");

                // Validate custom type
                if let Err(e) = validate_custom_type_web(&payload.value, custom_type) {
                    let mut headers = HeaderMap::new();
                    headers.insert("HX-Trigger", "formUpdate".parse().unwrap());
                    return (
                        StatusCode::BAD_REQUEST,
                        headers,
                        Json(json!({"success": false, "error": e})),
                    );
                }

                // Convert to appropriate type
                parse_custom_value_web(&payload.value, custom_type)
            } else {
                json!(payload.value)
            }
        }
        "editor" => {
            // Validate required for editor
            let current_field = state.current_field.read().await;
            if let Some(field) = current_field.as_ref() {
                if field.required.unwrap_or(false) && payload.value.trim().is_empty() {
                    let mut headers = HeaderMap::new();
                    headers.insert("HX-Trigger", "formUpdate".parse().unwrap());
                    return (
                        StatusCode::BAD_REQUEST,
                        headers,
                        Json(json!({"success": false, "error": "Content cannot be empty"})),
                    );
                }
            }
            json!(payload.value)
        }
        "repeating_group" => {
            // Parse JSON array for repeating groups
            let current_field = state.current_field.read().await;

            match serde_json::from_str::<serde_json::Value>(&payload.value) {
                Ok(parsed_value) => {
                    // Validate it's an array
                    if !parsed_value.is_array() {
                        let mut headers = HeaderMap::new();
                        headers.insert("HX-Trigger", "formUpdate".parse().unwrap());
                        return (
                            StatusCode::BAD_REQUEST,
                            headers,
                            Json(json!({"success": false, "error": "Value must be a JSON array"})),
                        );
                    }

                    // Validate min/max items if field constraints exist
                    if let Some(field) = current_field.as_ref() {
                        let array_len = parsed_value.as_array().map(|a| a.len()).unwrap_or(0);

                        if let Some(min_items) = field.min_items {
                            if array_len < min_items {
                                let mut headers = HeaderMap::new();
                                headers.insert("HX-Trigger", "formUpdate".parse().unwrap());
                                return (
                                    StatusCode::BAD_REQUEST,
                                    headers,
                                    Json(
                                        json!({"success": false, "error": format!("Minimum {} items required, got {}", min_items, array_len)}),
                                    ),
                                );
                            }
                        }

                        if let Some(max_items) = field.max_items {
                            if array_len > max_items {
                                let mut headers = HeaderMap::new();
                                headers.insert("HX-Trigger", "formUpdate".parse().unwrap());
                                return (
                                    StatusCode::BAD_REQUEST,
                                    headers,
                                    Json(
                                        json!({"success": false, "error": format!("Maximum {} items allowed, got {}", max_items, array_len)}),
                                    ),
                                );
                            }
                        }
                    }

                    parsed_value
                }
                Err(e) => {
                    let mut headers = HeaderMap::new();
                    headers.insert("HX-Trigger", "formUpdate".parse().unwrap());
                    return (
                        StatusCode::BAD_REQUEST,
                        headers,
                        Json(json!({"success": false, "error": format!("Invalid JSON: {}", e)})),
                    );
                }
            }
        }
        _ => json!(payload.value),
    };

    // Send via channel
    {
        let mut channels = state.field_channels.write().await;
        if let Some(tx) = channels.remove(&field_name) {
            drop(tx.send(value.clone()));
        }
    }

    // Update results
    {
        let mut results = state.results.write().await;
        results.insert(field_name, value);
    }

    // A-TYPED-RESPONSES: Build response with HX-Trigger header
    let mut headers = HeaderMap::new();
    headers.insert("HX-Trigger", "formUpdate".parse().unwrap());

    (StatusCode::OK, headers, Json(json!({"success": true})))
}

/// Render success HTML for normal (non-roundtrip) mode
#[cfg(feature = "web")]
fn render_success_html(results: &HashMap<String, Value>) -> String {
    let field_count = results.len();

    let fields_html = if field_count > 0 {
        let mut html = String::from("<div style='margin: 20px 0;'>");
        html.push_str("<h3 style='margin: 0 0 10px 0;'>📋 Form Results:</h3>");
        html.push_str("<div style='background: #1e1e1e; padding: 15px; border-radius: 4px; font-family: monospace; max-height: 400px; overflow-y: auto;'>");

        let mut sorted_fields: Vec<_> = results.iter().collect();
        sorted_fields.sort_by_key(|(k, _)| k.as_str());

        for (field_name, value) in sorted_fields {
            let value_str = match value {
                Value::String(s) => s.clone(),
                Value::Number(n) => n.to_string(),
                Value::Bool(b) => b.to_string(),
                Value::Array(arr) => {
                    let items: Vec<String> = arr
                        .iter()
                        .map(|v| match v {
                            Value::String(s) => s.clone(),
                            other => other.to_string(),
                        })
                        .collect();
                    format!("[{}]", items.join(", "))
                }
                Value::Null => "(empty)".to_string(),
                Value::Object(_) => "{...}".to_string(),
            };

            html.push_str(&format!(
                r#"<div style="margin: 8px 0; padding: 8px; background: #2d2d30; border-radius: 3px;">
                    <div style="color: #dcdcaa; font-weight: bold;">{}</div>
                    <div style="margin-left: 15px; color: #4ec9b0; margin-top: 4px;">{}</div>
                </div>"#,
                html_escape(field_name),
                html_escape(&value_str)
            ));
        }

        html.push_str("</div></div>");
        html
    } else {
        String::from("<p style='color: #808080;'>No fields submitted</p>")
    };

    format!(
        r#"<!DOCTYPE html>
<html>
<head>
    <title>Form Submitted Successfully</title>
    <meta charset="UTF-8">
    <style>
        body {{
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #1e1e1e;
            color: #d4d4d4;
            margin: 0;
            padding: 40px 20px;
        }}
        .container {{
            max-width: 900px;
            margin: 0 auto;
            background: #252526;
            padding: 40px;
            border-radius: 8px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
        }}
        h1 {{
            color: #4ec9b0;
            margin: 0 0 10px 0;
        }}
        .header {{
            border-bottom: 2px solid #007acc;
            padding-bottom: 20px;
            margin-bottom: 30px;
        }}
        .stat-row {{
            display: flex;
            justify-content: space-between;
            margin: 10px 0;
            padding: 10px;
            background: #1e1e1e;
            border-radius: 4px;
        }}
        .actions {{
            margin-top: 30px;
            padding-top: 20px;
            border-top: 1px solid #3e3e42;
        }}
        .btn {{
            display: inline-block;
            padding: 10px 20px;
            margin: 5px;
            background: #007acc;
            color: white;
            text-decoration: none;
            border-radius: 4px;
            border: none;
            cursor: pointer;
        }}
        .btn:hover {{
            background: #005a9e;
        }}
        .auto-close {{
            color: #808080;
            font-size: 12px;
            margin-top: 30px;
            text-align: center;
        }}
    </style>
    <script>
        let countdown = 30;
        function updateCountdown() {{
            document.getElementById('countdown').textContent = countdown;
            countdown--;
            if (countdown < 0) {{
                window.close();
            }}
        }}
        setInterval(updateCountdown, 1000);
    </script>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>✅ Form Submitted Successfully!</h1>
        </div>

        <div class="stat-row">
            <span>📊 <strong>Fields Submitted:</strong></span>
            <span>{} fields</span>
        </div>

        {}

        <div class="actions">
            <button class="btn" onclick="window.location.reload()">🔄 Submit Another</button>
            <button class="btn" onclick="window.close()">❌ Close</button>
        </div>

        <div class="auto-close">
            ⏱️  This window will auto-close in <span id="countdown">30</span> seconds...
        </div>
    </div>
</body>
</html>"#,
        field_count, fields_html
    )
}

#[cfg(feature = "web")]
async fn submit_complete_form_handler(
    State(state): State<Arc<WebFormState>>,
    Form(payload): Form<HashMap<String, String>>,
) -> impl IntoResponse {
    // Parse all form fields into a results map
    let mut all_results = HashMap::new();

    // Process each submitted field
    for (key, value) in payload.iter() {
        // Skip metadata fields
        if key.starts_with("_") {
            continue;
        }

        // Handle multiselect fields (comma-separated values)
        if key.ends_with("_values") {
            let field_name = key.strip_suffix("_values").unwrap_or(key);
            let values: Vec<String> = value
                .split(',')
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string())
                .collect();
            all_results.insert(field_name.to_string(), json!(values));
        } else if !key.ends_with("_type") {
            // Regular fields - store value as string (will be converted later)
            all_results.insert(key.clone(), json!(value));
        }
    }

    // Send through channel
    {
        let mut complete_tx = state.complete_form_tx.write().await;
        if let Some(tx) = complete_tx.take() {
            drop(tx.send(all_results.clone()));
        }
    }

    // Update state results
    {
        let mut results = state.results.write().await;
        *results = all_results.clone();
    }

    // Check if we're in roundtrip mode
    let roundtrip_ctx = state.roundtrip_context.read().await;
    if let Some(ctx) = roundtrip_ctx.as_ref() {
        // Roundtrip mode: return HTML summary page
        use crate::nickel::summary::RoundtripSummary;

        let summary = RoundtripSummary::from_values(
            &ctx.initial_values,
            &all_results,
            None, // Validation will be done after template rendering
            ctx.output_path.display().to_string(),
        );

        let html = summary.render_html();
        use axum::body::Body;
        use axum::response::Response;

        Response::builder()
            .status(StatusCode::OK)
            .header("Content-Type", "text/html; charset=utf-8")
            .body(Body::from(html))
            .unwrap()
    } else {
        // Normal mode: return HTML success page
        use axum::body::Body;
        use axum::response::Response;

        let html = render_success_html(&all_results);

        Response::builder()
            .status(StatusCode::OK)
            .header("Content-Type", "text/html; charset=utf-8")
            .body(Body::from(html))
            .unwrap()
    }
}

#[cfg(feature = "web")]
async fn download_config_handler(State(state): State<Arc<WebFormState>>) -> impl IntoResponse {
    use axum::http::header;

    // Get roundtrip context to find output path
    let roundtrip_ctx = state.roundtrip_context.read().await;
    if let Some(ctx) = roundtrip_ctx.as_ref() {
        // Read the generated config file
        match std::fs::read_to_string(&ctx.output_path) {
            Ok(content) => {
                let filename = ctx
                    .output_path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("config.ncl");

                let mut headers = HeaderMap::new();
                headers.insert(
                    header::CONTENT_DISPOSITION,
                    format!("attachment; filename=\"{}\"", filename)
                        .parse()
                        .unwrap(),
                );
                headers.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap());

                (StatusCode::OK, headers, content)
            }
            Err(_) => (
                StatusCode::NOT_FOUND,
                HeaderMap::new(),
                "Config file not found".to_string(),
            ),
        }
    } else {
        (
            StatusCode::BAD_REQUEST,
            HeaderMap::new(),
            "Download only available in roundtrip mode".to_string(),
        )
    }
}

#[cfg(feature = "web")]
use axum::response::Html;

/// Render all fields in a single form for complete mode
fn render_complete_form(
    items: &[DisplayItem],
    fields: &[FieldDefinition],
    results: &HashMap<String, Value>,
    base_dir: &std::path::Path,
) -> String {
    let (fields_html, modals_and_scripts) = render_form_fields(items, fields, results, base_dir);

    // Generate global repeating group script (injected once, manages all repeating groups independently)
    let global_repeating_group_script = render_global_repeating_group_script();

    // FASE 4: HTMX polling for reactive rendering
    // Form wrapper stays static, only fields div is updated dynamically
    // Modals and scripts are placed OUTSIDE the form to avoid nested forms
    format!(
        r##"<form id="complete-form" method="POST" action="/api/form/complete" hx-post="/api/form/complete">
            <div style="border: 1px solid #3e3e42; padding: 20px; background: #252526; border-radius: 4px;">
                {}
                <button type="submit" style="background: #007acc; color: white; border: none; padding: 10px 20px; width: 100%; font-weight: bold;">
                    Submit
                </button>
            </div>
        </form>
        {}
        {}"##,
        fields_html, modals_and_scripts, global_repeating_group_script
    )
}

/// Render just the fields wrapper for reactive updates (JavaScript handles reactivity)
/// Returns (fields_html, modals_and_scripts) where modals_and_scripts should be placed outside the form
fn render_form_fields(
    items: &[DisplayItem],
    fields: &[FieldDefinition],
    results: &HashMap<String, Value>,
    base_dir: &std::path::Path,
) -> (String, String) {
    let (fields_html, modals_and_scripts) = render_fields_only(items, fields, results, base_dir);

    // Wrapper div for vanilla JS reactivity
    // JavaScript addEventListener listens for change events and updates this div's innerHTML
    let wrapped_fields = format!(
        r##"<div id="form-fields">
            {}
        </div>"##,
        fields_html
    );

    (wrapped_fields, modals_and_scripts)
}

/// Render field HTML only (no wrapper, for dynamic updates via HTMX)
/// Intercalates display items (section headers) with fields based on order
/// Returns (fields_html, modals_and_scripts) where modals_and_scripts contains all modals and JavaScript from RepeatingGroups
fn render_fields_only(
    items: &[DisplayItem],
    fields: &[FieldDefinition],
    results: &HashMap<String, Value>,
    base_dir: &std::path::Path,
) -> (String, String) {
    let mut html = String::new();
    let mut modals_and_scripts = String::new();

    // Create a sorted list of (order, element_type) tuples
    let mut elements: Vec<(usize, String)> = Vec::new();

    // Add items with their order
    for (idx, item) in items.iter().enumerate() {
        elements.push((item.order, format!("item:{}", idx)));
    }

    // Add fields with their order
    for field in fields {
        elements.push((field.order, format!("field:{}", field.name)));
    }

    // Sort by order
    elements.sort_by_key(|(order, _)| *order);

    // Render elements in order
    for (_, element_type) in elements {
        if let Some(item_idx) = element_type.strip_prefix("item:") {
            let idx: usize = item_idx.parse().unwrap();
            if let Some(item) = items.get(idx) {
                html.push_str(&render_display_item_html(item));
            }
        } else if let Some(field_name) = element_type.strip_prefix("field:") {
            if let Some(field) = fields.iter().find(|f| f.name == field_name) {
                let (field_html, field_modals_scripts) =
                    render_field_for_complete_form(field, results, base_dir);
                html.push_str(&field_html);
                modals_and_scripts.push_str(&field_modals_scripts);
            }
        }
    }

    (html, modals_and_scripts)
}

/// Render a display item as HTML
fn render_display_item_html(item: &DisplayItem) -> String {
    match item.item_type.as_str() {
        "section_header" => {
            let mut html = String::new();
            let margin = item.margin_left.unwrap_or(0) * 2;
            let border_top = item.border_top.unwrap_or(false);
            let border_bottom = item.border_bottom.unwrap_or(false);
            let title = item.title.as_deref().unwrap_or("");

            if border_top {
                html.push_str(&format!(
                    "<div style='margin-left: {}ch; font-family: monospace;'><pre>╭────────────────────────────────────────╮</pre></div>\n",
                    margin
                ));
            }

            html.push_str(&format!(
                "<div style='margin-left: {}ch;'><h3>{}</h3></div>\n",
                margin + 2,
                html_escape(title)
            ));

            if border_bottom {
                html.push_str(&format!(
                    "<div style='margin-left: {}ch; font-family: monospace;'><pre>╰────────────────────────────────────────╯</pre></div>\n",
                    margin
                ));
            }

            html.push_str("<div style='margin-left: 0ch;'></div>\n\n");
            html
        }
        "text" => {
            let content = item.content.as_deref().unwrap_or("");
            let indent = item.margin_left.unwrap_or(0);
            format!(
                "<div style='margin-left: {}ch;'>{}</div>\n",
                indent * 2,
                html_escape(content)
            )
        }
        "border" => {
            let indent = item.margin_left.unwrap_or(0);
            let style = item.content.as_deref().unwrap_or("single");
            let border_char = match style {
                "thick" => "━",
                "double" => "═",
                _ => "─",
            };
            format!(
                "<div style='margin-left: {}ch; font-family: monospace;'><pre>{}</pre></div>\n",
                indent * 2,
                border_char.repeat(40)
            )
        }
        _ => String::new(),
    }
}

/// Render multiselect field with support for different display modes
/// Modes: "list" (default), "grid", "dropdown"
fn render_multiselect_html(field: &FieldDefinition, selected_values: &[String]) -> String {
    let field_name = &field.name;
    let display_mode = field.display_mode.as_deref().unwrap_or("list");
    let searchable = field.searchable.unwrap_or(false);

    match display_mode {
        "grid" => {
            // Grid layout (2-3 columns)
            let options_html = field
                .options
                .iter()
                .map(|opt| {
                    let checked = if selected_values.contains(&opt.value) {
                        "checked"
                    } else {
                        ""
                    };
                    format!(
                        r#"<label style="display: inline-block; margin: 10px 15px; cursor: pointer; min-width: 150px;">
                    <input type="checkbox" data-checkbox-group="{}" value="{}" {}> {}
                </label>"#,
                        html_escape(field_name),
                        html_escape(&opt.value),
                        checked,
                        html_escape(opt.display_label())
                    )
                })
                .collect::<Vec<_>>()
                .join("\n");

            format!(
                r#"<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; padding: 10px; background: #1e1e1e; border: 1px solid #3e3e42;">
                    {}
                </div>"#,
                options_html
            )
        }
        "dropdown" => {
            // Dropdown select with optional search
            let options_html = field
                .options
                .iter()
                .map(|opt| {
                    let selected = if selected_values.contains(&opt.value) {
                        "selected"
                    } else {
                        ""
                    };
                    format!(
                        "<option value=\"{}\" {}>{}</option>",
                        html_escape(&opt.value),
                        selected,
                        html_escape(opt.display_label())
                    )
                })
                .collect::<Vec<_>>()
                .join("\n");

            let search_attr = if searchable {
                r#"data-searchable="true""#
            } else {
                ""
            };

            format!(
                r#"<select multiple name="{}_values" id="select_{}" {} style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;">
                    {}
                </select>"#,
                html_escape(field_name),
                html_escape(field_name),
                search_attr,
                options_html
            )
        }
        _ => {
            // Default "list" mode: vertical checkboxes
            field
                .options
                .iter()
                .map(|opt| {
                    let checked = if selected_values.contains(&opt.value) {
                        "checked"
                    } else {
                        ""
                    };
                    format!(
                        r#"<label style="display: block; margin: 5px 0; cursor: pointer;">
                    <input type="checkbox" data-checkbox-group="{}" value="{}" {}> {}
                </label>"#,
                        html_escape(field_name),
                        html_escape(&opt.value),
                        checked,
                        html_escape(opt.display_label())
                    )
                })
                .collect::<Vec<_>>()
                .join("\n")
        }
    }
}

/// Render a single field for inclusion in the complete form (without individual submit button)
/// Includes state injection for selected/checked attributes based on current results
/// Returns (field_html, modals_and_scripts) where modals_and_scripts contains modals/JS for RepeatingGroup
fn render_field_for_complete_form(
    field: &FieldDefinition,
    results: &HashMap<String, Value>,
    base_dir: &std::path::Path,
) -> (String, String) {
    let (field_html, modals_and_scripts) = match field.field_type {
        FieldType::Text => {
            let placeholder = field.placeholder.as_deref().unwrap_or("");
            let default = field.default.as_deref().unwrap_or("");
            // Use value from results if available (e.g., from initial_values), otherwise use default
            let current_value = results
                .get(&field.name)
                .and_then(|v| v.as_str())
                .unwrap_or(default);

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <input type="text" name="{}" placeholder="{}" value="{}" style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {} >
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    html_escape(placeholder),
                    html_escape(current_value),
                    if field.required.unwrap_or(false) {
                        "required"
                    } else {
                        ""
                    }
                ),
                String::new(),
            )
        }
        FieldType::Confirm => {
            // Get current value from results for state preservation
            // Handle both boolean values and string "true"/"false"
            let current_value = results
                .get(&field.name)
                .and_then(|v| {
                    if let Some(b) = v.as_bool() {
                        Some(b)
                    } else {
                        v.as_str().map(|s| s == "true")
                    }
                })
                .unwrap_or(false);

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <div style="display: flex; gap: 10px;">
                    <label style="display: flex; align-items: center; gap: 5px;">
                        <input type="radio" name="{}" value="true" {}> Yes
                    </label>
                    <label style="display: flex; align-items: center; gap: 5px;">
                        <input type="radio" name="{}" value="false" {}> No
                    </label>
                </div>
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    if current_value { "checked" } else { "" },
                    html_escape(&field.name),
                    if !current_value { "checked" } else { "" }
                ),
                String::new(),
            )
        }
        FieldType::Password => {
            let placeholder = field.placeholder.as_deref().unwrap_or("");
            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <input type="password" name="{}" placeholder="{}" autocomplete="current-password" style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {} >
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    html_escape(placeholder),
                    if field.required.unwrap_or(false) {
                        "required"
                    } else {
                        ""
                    }
                ),
                String::new(),
            )
        }
        FieldType::Select => {
            // Get current value from results for state preservation
            let current_value = results
                .get(&field.name)
                .and_then(|v| v.as_str())
                .unwrap_or("");

            let options = if field.options.is_empty() {
                String::new()
            } else {
                field
                    .options
                    .iter()
                    .map(|opt| {
                        let selected = if opt.value == current_value {
                            "selected"
                        } else {
                            ""
                        };
                        format!(
                            "<option value=\"{}\" {}>{}</option>",
                            html_escape(&opt.value),
                            selected,
                            html_escape(opt.display_label())
                        )
                    })
                    .collect::<Vec<_>>()
                    .join("\n")
            };

            let options_from_attr = if let Some(ref source_field) = field.options_from {
                format!(" data-options-from=\"{}\"", html_escape(source_field))
            } else {
                String::new()
            };

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <select name="{}" data-fieldname="{}" {} style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {}>
                    <option value="">-- Select --</option>
                    {}
                </select>
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    html_escape(&field.name),
                    options_from_attr,
                    if field.required.unwrap_or(false) {
                        "required"
                    } else {
                        ""
                    },
                    options
                ),
                String::new(),
            )
        }
        FieldType::MultiSelect => {
            let field_name = &field.name;
            let display_mode = field.display_mode.as_deref().unwrap_or("list");

            // Get current selected values from results for state preservation
            // If no results, use default value
            let selected_values: Vec<String> = results
                .get(field_name)
                .and_then(|v| v.as_array())
                .map(|arr| {
                    arr.iter()
                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
                        .collect()
                })
                .or_else(|| {
                    // Use default if available and results don't have this field
                    field.default.as_ref().map(|default| {
                        default
                            .split(',')
                            .map(|s| s.trim().to_string())
                            .filter(|s| !s.is_empty())
                            .collect()
                    })
                })
                .unwrap_or_default();

            let options_html = render_multiselect_html(field, &selected_values);

            // Generate appropriate script based on display mode
            let script = if display_mode == "dropdown" {
                // For dropdown/select mode, handle change event differently
                format!(
                    r#"<script>
                document.addEventListener('change', function(e) {{
                    if (e.target.id === 'select_{}') {{
                        const select = e.target;
                        const values = Array.from(select.selectedOptions).map(o => o.value);
                        document.getElementById('values_{}').value = values.join(',');
                        // Stop and re-trigger for reactive form update
                        e.stopImmediatePropagation();
                        setTimeout(function() {{
                            e.target.dispatchEvent(new Event('change', {{ bubbles: true }}));
                        }}, 0);
                    }}
                }});
                document.addEventListener('submit', function(e) {{
                    if (e.target.id === 'complete-form') {{
                        const select = document.getElementById('select_{}');
                        if (select) {{
                            const values = Array.from(select.selectedOptions).map(o => o.value);
                            document.getElementById('values_{}').value = values.join(',');
                        }}
                    }}
                }});
                </script>"#,
                    html_escape(field_name),
                    html_escape(field_name),
                    html_escape(field_name),
                    html_escape(field_name)
                )
            } else {
                // For checkbox modes (list, grid)
                format!(
                    r#"<script>
                document.addEventListener('change', function(e) {{
                    if (e.target.matches('[data-checkbox-group="{}"]')) {{
                        collectMultiselectValues(e, '{}');
                        e.stopImmediatePropagation();
                        setTimeout(function() {{
                            e.target.dispatchEvent(new Event('change', {{ bubbles: true }}));
                        }}, 0);
                    }}
                }});
                document.addEventListener('submit', function(e) {{
                    if (e.target.id === 'complete-form') {{
                        collectMultiselectValues(e, '{}');
                    }}
                }});
                </script>"#,
                    html_escape(field_name),
                    html_escape(field_name),
                    html_escape(field_name)
                )
            };

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <input type="hidden" name="{}_values" id="values_{}" value="">
                <div style="border: 1px solid #3e3e42; padding: 10px; background: #1e1e1e;">
                    {}
                </div>
                {}
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(field_name),
                    html_escape(field_name),
                    options_html,
                    script
                ),
                String::new(),
            )
        }
        FieldType::Custom => {
            let custom_type = field.custom_type.as_deref().unwrap_or("String");
            let placeholder = field.placeholder.as_deref().unwrap_or("");
            let default = field.default.as_deref().unwrap_or("");
            let type_hint = format!("Type: {}", custom_type);

            // Use value from results if available (e.g., from initial_values), otherwise use default
            let current_value = results
                .get(&field.name)
                .and_then(|v| v.as_str().map(|s| s.to_string()))
                .or_else(|| {
                    results
                        .get(&field.name)
                        .map(|v| v.to_string().trim_matches('"').to_string())
                })
                .unwrap_or_else(|| default.to_string());

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <input type="text" name="{}" value="{}" placeholder="{}" title="{}" style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {} >
                <small style="color: #888; display: block; margin-top: 5px;">{}</small>
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    html_escape(&current_value),
                    html_escape(placeholder),
                    html_escape(&type_hint),
                    if field.required.unwrap_or(false) {
                        "required"
                    } else {
                        ""
                    },
                    html_escape(&type_hint)
                ),
                String::new(),
            )
        }
        FieldType::Editor => {
            let rows = 10;
            let cols = 60;
            let prefix_text = field.prefix_text.as_deref().unwrap_or("");

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <textarea name="{}" rows="{}" cols="{}" style="width: 100%; font-family: monospace; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; padding: 10px; box-sizing: border-box;" {} >{}</textarea>
                <small style="color: #888; display: block; margin-top: 5px;">File type: {}</small>
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    rows,
                    cols,
                    if field.required.unwrap_or(false) {
                        "required"
                    } else {
                        ""
                    },
                    html_escape(prefix_text),
                    html_escape(field.file_extension.as_deref().unwrap_or("text"))
                ),
                String::new(),
            )
        }
        FieldType::Date => {
            let default_value = field.default.as_deref().unwrap_or("");
            // Use value from results if available (e.g., from initial_values), otherwise use default
            let current_value = results
                .get(&field.name)
                .and_then(|v| v.as_str())
                .unwrap_or(default_value);
            let min = field.min_date.as_deref().unwrap_or("");
            let max = field.max_date.as_deref().unwrap_or("");

            (
                format!(
                    r#"<div class="field" style="margin-bottom: 15px;">
                <label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
                <input type="date" name="{}" value="{}" min="{}" max="{}" style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {} >
            </div>"#,
                    html_escape(&field.prompt),
                    html_escape(&field.name),
                    html_escape(current_value),
                    html_escape(min),
                    html_escape(max),
                    if field.required.unwrap_or(false) {
                        "required"
                    } else {
                        ""
                    }
                ),
                String::new(),
            )
        }
        FieldType::RepeatingGroup => render_repeating_group_field(field, results, base_dir),
    };

    (field_html, modals_and_scripts)
}

/// Render a field to HTML based on its type (A-EXTRACTORS-FIRST supports diverse types)
fn render_field_to_html(field: &FieldDefinition) -> String {
    match field.field_type {
        FieldType::Text => {
            let placeholder = field.placeholder.as_deref().unwrap_or("");
            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="text">
                    <input type="text" name="value" placeholder="{}" autofocus required>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name),
                html_escape(placeholder)
            )
        }
        FieldType::Confirm => {
            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" style="display: flex; gap: 10px;" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="confirm">
                    <button type="submit" name="value" value="true" style="background: #28a745;">Yes</button>
                    <button type="submit" name="value" value="false" style="background: #dc3545;">No</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name)
            )
        }
        FieldType::Password => {
            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="password">
                    <input type="password" name="value" autocomplete="current-password" autofocus required>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name)
            )
        }
        FieldType::Select => {
            let options = if field.options.is_empty() {
                String::new()
            } else {
                field
                    .options
                    .iter()
                    .map(|opt| {
                        format!(
                            "<option value=\"{}\">{}</option>",
                            html_escape(&opt.value),
                            html_escape(opt.display_label())
                        )
                    })
                    .collect::<Vec<_>>()
                    .join("\n")
            };

            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="select">
                    <select name="value" autofocus required>
                        {}
                    </select>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name),
                options
            )
        }
        FieldType::MultiSelect => {
            let field_name = &field.name;
            let display_mode = field.display_mode.as_deref().unwrap_or("list");

            // Parse default values for pre-checking
            let default_values: Vec<String> = field
                .default
                .as_ref()
                .map(|default| {
                    default
                        .split(',')
                        .map(|s| s.trim().to_string())
                        .filter(|s| !s.is_empty())
                        .collect()
                })
                .unwrap_or_default();

            let options_html = render_multiselect_html(field, &default_values);

            let form_id = format!("form_{}", html_escape(field_name));

            // Generate appropriate submit button and script based on display mode
            let (submit_button, form_script) = if display_mode == "dropdown" {
                let btn = r#"<button type="submit">Submit</button>"#.to_string();
                let script = format!(
                    r#"<script>
                    document.getElementById('{}').addEventListener('submit', function(e) {{
                        const select = document.querySelector('select[name="{}_values"]');
                        if (select) {{
                            const values = Array.from(select.selectedOptions).map(o => o.value);
                            document.getElementById('values_{}').value = values.join(',');
                        }}
                    }});
                    </script>"#,
                    form_id,
                    html_escape(field_name),
                    html_escape(field_name)
                );
                (btn, script)
            } else {
                let btn = format!(
                    r#"<button type="submit" onclick="collectMultiselectValues(event, '{}')">Submit</button>"#,
                    html_escape(field_name)
                );
                let script = r#"<script>
                    function collectMultiselectValues(event, fieldName) {{
                        const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
                        const values = Array.from(checkboxes).map(cb => cb.value);
                        document.getElementById('values_' + fieldName).value = values.join(',');
                    }}
                    </script>"#.to_string();
                (btn, script)
            };

            format!(
                r#"<div class="field">
                <label>{}</label>
                <form id="{}" hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="multiselect">
                    <input type="hidden" name="values" id="values_{}" value="">
                    <div style="border: 1px solid #3e3e42; padding: 10px; background: #252526;">
                        {}
                    </div>
                    {}
                </form>
            </div>
            {}"#,
                html_escape(&field.prompt),
                form_id,
                html_escape(field_name),
                html_escape(field_name),
                options_html,
                submit_button,
                form_script
            )
        }
        FieldType::Custom => {
            let custom_type = field.custom_type.as_deref().unwrap_or("String");
            let placeholder = field.placeholder.as_deref().unwrap_or("");
            let type_hint = format!("Type: {}", custom_type);

            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="custom">
                    <input type="text" name="value" placeholder="{}" title="{}" autofocus required>
                    <small style="color: #888; display: block; margin-top: 5px;">{}</small>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name),
                html_escape(placeholder),
                html_escape(&type_hint),
                html_escape(&type_hint)
            )
        }
        FieldType::Editor => {
            let rows = 10;
            let cols = 60;
            let prefix_text = field.prefix_text.as_deref().unwrap_or("");

            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="editor">
                    <textarea name="value" rows="{}" cols="{}" style="width: 100%; font-family: monospace; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; padding: 10px;" autofocus required>{}</textarea>
                    <small style="color: #888; display: block; margin-top: 5px;">File type: {}</small>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name),
                rows,
                cols,
                html_escape(prefix_text),
                html_escape(field.file_extension.as_deref().unwrap_or("text"))
            )
        }
        FieldType::Date => {
            let value = field.default.as_deref().unwrap_or("");
            let min = field.min_date.as_deref().unwrap_or("");
            let max = field.max_date.as_deref().unwrap_or("");

            format!(
                r#"<div class="field">
                <label>{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="date">
                    <input type="date" name="value" value="{}" min="{}" max="{}" autofocus required>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&field.name),
                html_escape(value),
                html_escape(min),
                html_escape(max)
            )
        }
        FieldType::RepeatingGroup => {
            // JSON editor fallback for repeating groups (HTMX single-field form)
            // Future enhancement: dynamic HTML cards with add/remove buttons

            // Generate example hint based on fragment if available
            let example_hint = if let Some(fragment) = &field.fragment {
                format!("Array of items from: {}", fragment)
            } else {
                r#"Example: [{"field1": "value1"}, {"field2": "value2"}]"#.to_string()
            };

            let min_max_hint = match (field.min_items, field.max_items) {
                (Some(min), Some(max)) => format!(" ({}-{} items)", min, max),
                (Some(min), None) => format!(" (min: {})", min),
                (None, Some(max)) => format!(" (max: {})", max),
                (None, None) => String::new(),
            };

            format!(
                r#"<div class="field">
                <label>{}{}</label>
                <form hx-post="/api/field/{}" hx-swap="none" hx-on="htmx:afterRequest: setTimeout(() => htmx.ajax('GET', '/api/form', {{target: '#form', swap: 'innerHTML'}}), 100)">
                    <input type="hidden" name="field_type" value="repeating_group">
                    <textarea name="value" rows="12" style="width: 100%; font-family: 'Courier New', monospace; padding: 10px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42;" autofocus required>[]</textarea>
                    <small style="color: #888; display: block; margin-top: 5px;">Edit as JSON array. {}</small>
                    <button type="submit">Submit</button>
                </form>
            </div>"#,
                html_escape(&field.prompt),
                html_escape(&min_max_hint),
                html_escape(&field.name),
                html_escape(&example_hint)
            )
        }
    }
}

/// Render repeating group field with dynamic HTML cards interface
/// Provides add/edit/delete buttons and modal form for item management
/// Returns (field_html, modals_and_scripts) where modals_and_scripts contains modal overlay and JavaScript
fn render_repeating_group_field(
    field: &FieldDefinition,
    results: &HashMap<String, Value>,
    base_dir: &std::path::Path,
) -> (String, String) {
    // Load fragment to get field definitions
    let fragment_path = match &field.fragment {
        Some(path) => path,
        None => {
            return (
                format!(
                    r#"<div class="field-error" style="color: #f44336; margin: 15px 0;">
                    <strong>Error:</strong> RepeatingGroup field "{}" is missing 'fragment' attribute
                </div>"#,
                    html_escape(&field.name)
                ),
                String::new(),
            );
        }
    };

    // Resolve fragment path relative to base_dir
    let resolved_path = if std::path::Path::new(fragment_path).is_absolute() {
        std::path::Path::new(fragment_path).to_path_buf()
    } else {
        base_dir.join(fragment_path)
    };

    let fragment_form = match crate::form_parser::load_fragment_form(
        resolved_path.to_str().unwrap_or(fragment_path),
    ) {
        Ok(form) => form,
        Err(e) => {
            return (
                format!(
                    r#"<div class="field-error" style="color: #f44336; margin: 15px 0;">
                    <strong>Error loading fragment "{}":</strong> {}
                </div>"#,
                    html_escape(fragment_path),
                    html_escape(&e.to_string())
                ),
                String::new(),
            );
        }
    };

    // Extract field definitions from fragment
    let fragment_fields: Vec<&crate::form_parser::FieldDefinition> = fragment_form
        .elements
        .iter()
        .filter_map(|element| match element {
            crate::form_parser::FormElement::Field(f) => Some(f.as_ref()),
            _ => None,
        })
        .collect();

    if fragment_fields.is_empty() {
        return (
            format!(
                r#"<div class="field-error" style="color: #f44336; margin: 15px 0;">
                <strong>Error:</strong> Fragment "{}" contains no fields
            </div>"#,
                html_escape(fragment_path)
            ),
            String::new(),
        );
    }

    // Get current items from results
    let current_items: Vec<serde_json::Map<String, Value>> = results
        .get(&field.name)
        .and_then(|v| v.as_array())
        .map(|arr| arr.iter().filter_map(|v| v.as_object().cloned()).collect())
        .unwrap_or_default();

    let min_items = field.min_items.unwrap_or(0);
    let max_items = field.max_items.unwrap_or(100);
    let field_id = html_escape(&field.name);

    // Generate unique ID for this field instance
    let unique_id = format!("rg_{}", field_id);

    // Build HTML
    let mut html = String::new();

    // Container div
    html.push_str(&format!(
        r#"<div class="repeating-group-field" style="margin-bottom: 20px; border: 1px solid #3e3e42; padding: 15px; background: #1e1e1e; border-radius: 4px;">
            <label style="display: block; margin-bottom: 10px; font-weight: bold; font-size: 14px;">{}</label>
            "#,
        html_escape(&field.prompt)
    ));

    // Help text from nickel_doc or placeholder
    if let Some(doc) = &field.nickel_doc {
        html.push_str(&format!(
            r#"<small style="color: #888; display: block; margin-bottom: 10px;">{}</small>"#,
            html_escape(doc)
        ));
    }

    // Items count and limits
    html.push_str(&format!(
        r#"<div style="margin-bottom: 10px; color: #888; font-size: 13px;">
            Items: <span id="{}_count">{}</span> / {} (min: {}, max: {})
        </div>"#,
        unique_id,
        current_items.len(),
        max_items,
        min_items,
        max_items
    ));

    // Items list container
    html.push_str(&format!(
        r#"<div id="{}_list" style="margin-bottom: 15px;">"#,
        unique_id
    ));

    // Render existing items as cards
    for (idx, item) in current_items.iter().enumerate() {
        html.push_str(&render_item_card(idx, item, &fragment_fields, &unique_id));
    }

    html.push_str("</div>");

    // Add Item section with inline expandable form
    html.push_str(&render_add_item_section(&unique_id, &fragment_fields));

    // Determine uniqueness constraints for this repeating group
    // `unique` is set on the repeating group itself (field.unique), not on individual fragment fields
    let has_unique = field.unique.unwrap_or(false);
    // But `unique_key` identifies which field is the unique constraint key
    let unique_key_field = fragment_fields
        .iter()
        .find(|f| f.unique_key.unwrap_or(false))
        .map(|f| f.name.clone());
    let field_names: Vec<String> = fragment_fields.iter().map(|f| f.name.clone()).collect();

    // Hidden input to store JSON array with config data attributes
    let current_json = serde_json::to_string(&current_items).unwrap_or_else(|_| "[]".to_string());
    html.push_str(&format!(
        r#"<input type="hidden" name="{}" id="{}_data" value="{}" data-min-items="{}" data-max-items="{}" data-has-unique="{}" data-unique-key="{}" data-field-names="{}">"#,
        field_id,
        unique_id,
        html_escape(&current_json),
        min_items,
        max_items,
        has_unique,
        unique_key_field.as_deref().unwrap_or(""),
        field_names.join(",")
    ));

    // Close container div
    html.push_str("</div>");

    // Build JavaScript for managing items (inline editing, no modal)
    // Only render once at the end, not per repeating group
    let modals_and_scripts = String::new();

    (html, modals_and_scripts)
}

/// Generate global JavaScript that manages all repeating groups independently
/// Each repeating group reads its config from data-* attributes on its hidden input
fn render_global_repeating_group_script() -> String {
    r#"<script>
(function() {
    // Store controllers for each repeating group
    const controllers = {};

    // Find all repeating group hidden inputs and initialize them
    function initializeRepeatingGroups() {
        document.querySelectorAll('[id$="_data"]').forEach(function(dataInput) {
            const inputId = dataInput.id;
            const groupId = inputId.replace(/_data$/, '');

            // Read config from data attributes
            const minItems = parseInt(dataInput.getAttribute('data-min-items') || '0');
            const maxItems = parseInt(dataInput.getAttribute('data-max-items') || '999');
            const hasUnique = dataInput.getAttribute('data-has-unique') === 'true';
            const uniqueKeyField = dataInput.getAttribute('data-unique-key') || null;
            const fieldNamesStr = dataInput.getAttribute('data-field-names') || '';
            const fieldNames = fieldNamesStr ? fieldNamesStr.split(',').map(f => f.trim()) : [];

            // Create independent controller for this repeating group
            controllers[groupId] = createRepeatingGroupController(
                groupId,
                dataInput,
                minItems,
                maxItems,
                hasUnique,
                uniqueKeyField,
                fieldNames
            );
        });
    }

    // Factory function to create an independent controller for each repeating group
    function createRepeatingGroupController(groupId, dataInput, minItems, maxItems, hasUnique, uniqueKeyField, fieldNames) {
        // State closure - private to this controller
        let items = [];

        // Load initial data from hidden input
        function loadInitialData() {
            try {
                if (dataInput && dataInput.value) {
                    items = JSON.parse(dataInput.value);
                }
            } catch(e) {
                console.error('Failed to parse repeating group ' + groupId, e);
                items = [];
            }
        }

        // Update the hidden input with current items
        function updateHiddenInput() {
            dataInput.value = JSON.stringify(items);
        }

        // Get field values from a form container
        function getFormValues(container) {
            const formData = {};
            container.querySelectorAll('.field-input').forEach(function(input) {
                const fieldName = input.getAttribute('data-fieldname');
                if (fieldName) {
                    formData[fieldName] = input.value;
                }
            });
            return formData;
        }

        // Check if a new item is a duplicate
        function isDuplicate(newValues, excludeIndex) {
            for (let i = 0; i < items.length; i++) {
                if (excludeIndex !== undefined && i === excludeIndex) {
                    continue;
                }
                const existingItem = items[i];

                if (hasUnique) {
                    // Check if ALL fields match
                    let allFieldsMatch = true;
                    fieldNames.forEach(function(fieldName) {
                        if (newValues[fieldName] !== existingItem[fieldName]) {
                            allFieldsMatch = false;
                        }
                    });
                    if (allFieldsMatch) {
                        return true;
                    }
                } else if (uniqueKeyField) {
                    // Check only the unique key field
                    if (newValues[uniqueKeyField] === existingItem[uniqueKeyField]) {
                        return true;
                    }
                }
            }
            return false;
        }

        // Clear all field values in a form
        function clearFormValues(container) {
            container.querySelectorAll('.field-input').forEach(function(input) {
                input.value = '';
            });
        }

        // Generate item form HTML for edit mode
        function generateItemFormHTML(item) {
            let html = '';
            fieldNames.forEach(function(fieldName) {
                const value = item[fieldName] || '';
                html += '<div style="margin-bottom: 15px;">' +
                    '<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #d4d4d4;">' + fieldName + '</label>' +
                    '<input type="text" class="field-input" data-fieldname="' + fieldName + '" value="' + value + '" ' +
                    'style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">' +
                    '</div>';
            });

            html += '<div style="display: flex; gap: 10px; margin-top: 15px;">' +
                '<button type="button" class="save-btn" style="flex: 1; background: #28a745; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">✅ Save</button>' +
                '<button type="button" class="cancel-btn" style="flex: 1; background: #6c757d; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer;">❌ Cancel</button>' +
                '</div>';

            return html;
        }

        // Expand a container (show form)
        function expandContainer(container) {
            const summary = container.querySelector('.item-summary') || container.querySelector('.add-prompt');
            const form = container.querySelector('.item-form') || container.querySelector('.add-form');
            if (summary) {
                summary.style.display = 'none';
            }
            if (form) {
                form.style.display = 'block';
            }
            container.setAttribute('data-state', 'expanded');
        }

        // Collapse a container (hide form)
        function collapseContainer(container) {
            const summary = container.querySelector('.item-summary') || container.querySelector('.add-prompt');
            const form = container.querySelector('.item-form') || container.querySelector('.add-form');
            if (summary) summary.style.display = 'flex';
            if (form) form.style.display = 'none';
            container.setAttribute('data-state', 'collapsed');
        }

        // Update displayed values in item card
        function updateItemCard(card, newValues) {
            const summary = card.querySelector('.item-summary div');
            if (!summary) return;

            const parts = [];
            fieldNames.forEach(function(fieldName) {
                const value = newValues[fieldName] || '';
                if (value) {
                    parts.push('<strong>' + fieldName + ':</strong> ' + value);
                }
            });
            summary.innerHTML = parts.join(' | ');
        }

        // Render item card HTML (collapsed view only)
        function renderItemCardHTML(item, idx) {
            const summaryParts = [];
            fieldNames.forEach(function(fieldName) {
                if (item[fieldName]) {
                    summaryParts.push('<strong>' + fieldName + ':</strong> ' + item[fieldName]);
                }
            });

            return '<div class="item-card" data-index="' + idx + '" data-state="collapsed" style="background: #252526; border: 1px solid #3e3e42; padding: 12px; margin-bottom: 8px; border-radius: 4px;">' +
                '<div class="item-summary" style="display: flex; justify-content: space-between; align-items: center;">' +
                '<div style="flex-grow: 1;">' + summaryParts.join(' | ') + '</div>' +
                '<div style="display: flex; gap: 8px;">' +
                '<button type="button" class="edit-btn" style="background: #007acc; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">✏️ Edit</button>' +
                '<button type="button" class="delete-btn" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">🗑️ Delete</button>' +
                '</div></div>' +
                '<div class="item-form" style="display: none;"></div>' +
                '</div>';
        }

        // Refresh the entire items list
        function refreshItemList() {
            const listEl = document.getElementById(groupId + '_list');
            if (!listEl) return;

            listEl.innerHTML = items.map(function(item, idx) {
                return renderItemCardHTML(item, idx);
            }).join('');
        }

        // Check if element belongs to this repeating group
        function belongsToThisGroup(element) {
            // Walk up the DOM to find if we're inside this group's container
            let current = element;
            while (current) {
                if (current.id === dataInput.id) {
                    return true;
                }
                const nextDiv = current.closest('[id$="_list"], [class*="add-item-section"]');
                if (!nextDiv) return false;
                const parentId = nextDiv.id || '';
                if (parentId === groupId + '_list' || nextDiv.closest('[id="' + groupId + '_data"]')) {
                    return true;
                }
                current = current.parentElement;
            }
            return false;
        }

        // Update the item counter display
        function updateItemCounter() {
            const countEl = document.getElementById(groupId + '_count');
            if (countEl) {
                countEl.textContent = items.length;
            }
        }

        // Initialize this controller
        loadInitialData();
        updateItemCounter();

        // Return public API (used if needed for testing)
        return {
            getItems: function() { return items; },
            getConfig: function() { return { minItems, maxItems, hasUnique, uniqueKeyField, fieldNames }; },
            updateCounter: updateItemCounter
        };
    }

    // Global event delegation - handle all clicks and route to appropriate controller
    document.addEventListener('click', function(e) {
        // Find the nearest data input to determine which group this action belongs to
        let dataInput = e.target.closest('[id$="_data"]');
        if (!dataInput) {
            // Search upward from the clicked element
            let current = e.target;
            while (current && !dataInput) {
                current = current.parentElement;
                if (current) {
                    dataInput = current.querySelector('[id$="_data"]');
                }
            }
        }

        if (!dataInput) {
            return; // Click not related to any repeating group
        }

        const groupId = dataInput.id.replace(/_data$/, '');
        const controller = controllers[groupId];
        if (!controller) return;

        const config = controller.getConfig();
        const items = controller.getItems();

        // Add Item button
        if (e.target.closest('.add-btn')) {
            e.stopPropagation();
            e.stopImmediatePropagation();
            const section = e.target.closest('.add-item-section');
            if (section) {
                // Expand the add-item-section
                const addPrompt = section.querySelector('.add-prompt');
                const addForm = section.querySelector('.add-form');
                if (addPrompt) addPrompt.style.display = 'none';
                if (addForm) addForm.style.display = 'block';
                section.setAttribute('data-state', 'expanded');
            }
            return;
        }

        // Edit button on item card
        if (e.target.closest('.edit-btn')) {
            e.stopPropagation();
            e.stopImmediatePropagation();
            const card = e.target.closest('.item-card');
            if (card) {
                const itemForm = card.querySelector('.item-form');
                // Regenerate form if empty
                if (itemForm && itemForm.innerHTML.trim() === '') {
                    const idx = parseInt(card.getAttribute('data-index'));
                    if (idx >= 0 && idx < items.length) {
                        const item = items[idx];
                        let html = '';
                        config.fieldNames.forEach(function(fieldName) {
                            const value = item[fieldName] || '';
                            html += '<div style="margin-bottom: 15px;">' +
                                '<label style="display: block; margin-bottom: 5px; font-weight: bold; color: #d4d4d4;">' + fieldName + '</label>' +
                                '<input type="text" class="field-input" data-fieldname="' + fieldName + '" value="' + value + '" ' +
                                'style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">' +
                                '</div>';
                        });
                        html += '<div style="display: flex; gap: 10px; margin-top: 15px;">' +
                            '<button type="button" class="save-btn" style="flex: 1; background: #28a745; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">✅ Save</button>' +
                            '<button type="button" class="cancel-btn" style="flex: 1; background: #6c757d; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer;">❌ Cancel</button>' +
                            '</div>';
                        itemForm.innerHTML = html;
                    }
                }
                const summary = card.querySelector('.item-summary');
                const itemForm2 = card.querySelector('.item-form');
                if (summary) summary.style.display = 'none';
                if (itemForm2) itemForm2.style.display = 'block';
                card.setAttribute('data-state', 'expanded');
            }
            return;
        }

        // Save button on item card
        if (e.target.closest('.save-btn') && e.target.closest('.item-card')) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            const card = e.target.closest('.item-card');
            if (!card) return;

            if (card.getAttribute('data-state') === 'collapsed') return;

            const itemForm = card.querySelector('.item-form');
            if (!itemForm) return;

            const idx = parseInt(card.getAttribute('data-index'));
            if (idx < 0 || idx >= items.length) return;

            const newValues = {};
            itemForm.querySelectorAll('.field-input').forEach(function(input) {
                const fieldName = input.getAttribute('data-fieldname');
                if (fieldName) {
                    newValues[fieldName] = input.value;
                }
            });

            // Validate
            let hasError = false;
            itemForm.querySelectorAll('[data-required="true"]').forEach(function(input) {
                const fieldName = input.getAttribute('data-fieldname');
                if (!newValues[fieldName] || !newValues[fieldName].trim()) {
                    input.style.borderColor = '#dc3545';
                    hasError = true;
                } else {
                    input.style.borderColor = '';
                }
            });

            if (hasError) {
                alert('Please fill in all required fields');
                return;
            }

            // Check duplicates (exclude self)
            let isDuplicate = false;
            if (config.hasUnique || config.uniqueKeyField) {
                for (let i = 0; i < items.length; i++) {
                    if (i === idx) continue;
                    const existingItem = items[i];
                    if (config.hasUnique) {
                        let allMatch = true;
                        config.fieldNames.forEach(function(fn) {
                            if (newValues[fn] !== existingItem[fn]) allMatch = false;
                        });
                        if (allMatch) {
                            isDuplicate = true;
                            break;
                        }
                    } else if (config.uniqueKeyField && newValues[config.uniqueKeyField] === existingItem[config.uniqueKeyField]) {
                        isDuplicate = true;
                        break;
                    }
                }
            }

            if (isDuplicate) {
                const msg = config.uniqueKeyField
                    ? 'An item with this ' + config.uniqueKeyField + ' already exists'
                    : 'This item already exists';
                alert(msg);
                return;
            }

            items[idx] = newValues;
            dataInput.value = JSON.stringify(items);

            // Update display
            const summary = card.querySelector('.item-summary div');
            if (summary) {
                const parts = [];
                config.fieldNames.forEach(function(fieldName) {
                    const value = newValues[fieldName] || '';
                    if (value) {
                        parts.push('<strong>' + fieldName + ':</strong> ' + value);
                    }
                });
                summary.innerHTML = parts.join(' | ');
            }

            // Collapse
            const itemSummary = card.querySelector('.item-summary');
            const itemForm2 = card.querySelector('.item-form');
            if (itemSummary) itemSummary.style.display = 'flex';
            if (itemForm2) itemForm2.style.display = 'none';
            card.setAttribute('data-state', 'collapsed');
            return;
        }

        // Cancel button
        if (e.target.closest('.cancel-btn')) {
            e.stopPropagation();
            e.stopImmediatePropagation();
            const card = e.target.closest('.item-card');
            const section = e.target.closest('.add-item-section');
            if (card) {
                const summary = card.querySelector('.item-summary');
                const itemForm = card.querySelector('.item-form');
                if (summary) summary.style.display = 'flex';
                if (itemForm) itemForm.style.display = 'none';
                card.setAttribute('data-state', 'collapsed');
            } else if (section) {
                const form = section.querySelector('.add-form');
                if (form) {
                    form.querySelectorAll('.field-input').forEach(function(input) {
                        input.value = '';
                    });
                }
                const addPrompt = section.querySelector('.add-prompt');
                if (addPrompt) addPrompt.style.display = 'flex';
                if (form) form.style.display = 'none';
                section.setAttribute('data-state', 'collapsed');
            }
            return;
        }

        // Delete button
        if (e.target.closest('.delete-btn')) {
            e.stopPropagation();
            e.stopImmediatePropagation();
            const card = e.target.closest('.item-card');
            if (!card) return;

            if (items.length <= config.minItems) {
                alert('Minimum ' + config.minItems + ' items required');
                return;
            }

            if (confirm('Delete this item?')) {
                const idx = parseInt(card.getAttribute('data-index'));
                items.splice(idx, 1);
                dataInput.value = JSON.stringify(items);

                // Update counter
                const countEl = document.getElementById(groupId + '_count');
                if (countEl) {
                    countEl.textContent = items.length;
                }

                const listEl = document.getElementById(groupId + '_list');
                if (listEl) {
                    listEl.innerHTML = items.map(function(item, i) {
                        const summaryParts = [];
                        config.fieldNames.forEach(function(fieldName) {
                            if (item[fieldName]) {
                                summaryParts.push('<strong>' + fieldName + ':</strong> ' + item[fieldName]);
                            }
                        });

                        return '<div class="item-card" data-index="' + i + '" data-state="collapsed" style="background: #252526; border: 1px solid #3e3e42; padding: 12px; margin-bottom: 8px; border-radius: 4px;">' +
                            '<div class="item-summary" style="display: flex; justify-content: space-between; align-items: center;">' +
                            '<div style="flex-grow: 1;">' + summaryParts.join(' | ') + '</div>' +
                            '<div style="display: flex; gap: 8px;">' +
                            '<button type="button" class="edit-btn" style="background: #007acc; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">✏️ Edit</button>' +
                            '<button type="button" class="delete-btn" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">🗑️ Delete</button>' +
                            '</div></div>' +
                            '<div class="item-form" style="display: none;"></div>' +
                            '</div>';
                    }).join('');
                }
            }
            return;
        }

        // Save button on add form
        if (e.target.closest('.save-btn') && e.target.closest('.add-item-section')) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            const section = e.target.closest('.add-item-section');
            if (!section) return;

            if (section.getAttribute('data-state') === 'collapsed') return;

            const addForm = section.querySelector('.add-form');
            if (!addForm) return;

            if (items.length >= config.maxItems) {
                alert('Maximum ' + config.maxItems + ' items allowed');
                return;
            }

            const newValues = {};
            addForm.querySelectorAll('.field-input').forEach(function(input) {
                const fieldName = input.getAttribute('data-fieldname');
                if (fieldName) {
                    newValues[fieldName] = input.value;
                }
            });

            // Validate
            let hasError = false;
            addForm.querySelectorAll('[data-required="true"]').forEach(function(input) {
                const fieldName = input.getAttribute('data-fieldname');
                if (!newValues[fieldName] || !newValues[fieldName].trim()) {
                    input.style.borderColor = '#dc3545';
                    hasError = true;
                } else {
                    input.style.borderColor = '';
                }
            });

            if (hasError) {
                alert('Please fill in all required fields');
                return;
            }

            // Check duplicates
            let isDuplicate = false;
            if (config.hasUnique || config.uniqueKeyField) {
                for (let i = 0; i < items.length; i++) {
                    const existingItem = items[i];
                    if (config.hasUnique) {
                        let allMatch = true;
                        config.fieldNames.forEach(function(fn) {
                            if (newValues[fn] !== existingItem[fn]) allMatch = false;
                        });
                        if (allMatch) {
                            isDuplicate = true;
                            break;
                        }
                    } else if (config.uniqueKeyField && newValues[config.uniqueKeyField] === existingItem[config.uniqueKeyField]) {
                        isDuplicate = true;
                        break;
                    }
                }
            }

            if (isDuplicate) {
                const msg = config.uniqueKeyField
                    ? 'An item with this ' + config.uniqueKeyField + ' already exists'
                    : 'This item already exists';
                alert(msg);
                return;
            }

            items.push(newValues);
            dataInput.value = JSON.stringify(items);

            addForm.querySelectorAll('.field-input').forEach(function(input) {
                input.value = '';
            });

            const addPrompt = section.querySelector('.add-prompt');
            if (addPrompt) addPrompt.style.display = 'flex';
            if (addForm) addForm.style.display = 'none';
            section.setAttribute('data-state', 'collapsed');

            // Update counter
            const countEl = document.getElementById(groupId + '_count');
            if (countEl) {
                countEl.textContent = items.length;
            }

            const listEl = document.getElementById(groupId + '_list');
            if (listEl) {
                listEl.innerHTML = items.map(function(item, i) {
                    const summaryParts = [];
                    config.fieldNames.forEach(function(fieldName) {
                        if (item[fieldName]) {
                            summaryParts.push('<strong>' + fieldName + ':</strong> ' + item[fieldName]);
                        }
                    });

                    return '<div class="item-card" data-index="' + i + '" data-state="collapsed" style="background: #252526; border: 1px solid #3e3e42; padding: 12px; margin-bottom: 8px; border-radius: 4px;">' +
                        '<div class="item-summary" style="display: flex; justify-content: space-between; align-items: center;">' +
                        '<div style="flex-grow: 1;">' + summaryParts.join(' | ') + '</div>' +
                        '<div style="display: flex; gap: 8px;">' +
                        '<button type="button" class="edit-btn" style="background: #007acc; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">✏️ Edit</button>' +
                        '<button type="button" class="delete-btn" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">🗑️ Delete</button>' +
                        '</div></div>' +
                        '<div class="item-form" style="display: none;"></div>' +
                        '</div>';
                }).join('');
            }
            return;
        }
    });

    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeRepeatingGroups);
    } else {
        initializeRepeatingGroups();
    }
})();
</script>"#.to_string()
}

/// Render the "Add Item" section with expandable form
fn render_add_item_section(
    _unique_id: &str,
    fields: &[&crate::form_parser::FieldDefinition],
) -> String {
    let mut html = r#"<div class="add-item-section" data-state="collapsed" style="background: #252526; border: 1px solid #3e3e42; padding: 12px; margin-bottom: 8px; border-radius: 4px;">
            <!-- Collapsed View: Add button -->
            <div class="add-prompt" style="display: flex; justify-content: center;">
                <button type="button" class="add-btn"
                    style="background: #28a745; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: bold;">
                    ➕ Add Item
                </button>
            </div>

            <!-- Expanded View: Add form (hidden by default) -->
            <div class="add-form" style="display: none;">"#.to_string();

    // Render form fields inline (no <form> tag, NO required attributes - use JS validation only)
    for field in fields {
        html.push_str(&format!(
            r#"<div style="margin-bottom: 15px;">
                    <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #d4d4d4;">{}{}</label>"#,
            html_escape(&field.prompt),
            if field.required.unwrap_or(false) { " *" } else { "" }
        ));

        match field.field_type {
            crate::form_parser::FieldType::Text | crate::form_parser::FieldType::Password => {
                let input_type = if field.field_type == crate::form_parser::FieldType::Password {
                    "password"
                } else {
                    "text"
                };
                let autocomplete = if field.field_type == crate::form_parser::FieldType::Password {
                    " autocomplete=\"current-password\""
                } else {
                    ""
                };
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                html.push_str(&format!(
                    r#"<input type="{}" class="field-input" data-fieldname="{}"{} placeholder="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
                    input_type,
                    field.name,
                    required_attr,
                    field.placeholder.as_deref().unwrap_or(""),
                    autocomplete
                ));
            }
            crate::form_parser::FieldType::Confirm => {
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                html.push_str(&format!(
                    r#"<select class="field-input" data-fieldname="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">
                        <option value="false">No</option>
                        <option value="true">Yes</option>
                    </select>"#,
                    field.name,
                    required_attr
                ));
            }
            crate::form_parser::FieldType::Select => {
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                let options_from_attr = if let Some(ref source_field) = field.options_from {
                    format!(" data-options-from=\"{}\"", html_escape(source_field))
                } else {
                    String::new()
                };
                html.push_str(&format!(
                    r#"<select class="field-input" data-fieldname="{}"{}{}  style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
                    field.name,
                    required_attr,
                    options_from_attr
                ));
                for opt in &field.options {
                    html.push_str(&format!(
                        r#"<option value="{}">{}</option>"#,
                        html_escape(&opt.value),
                        html_escape(opt.label.as_deref().unwrap_or(&opt.value))
                    ));
                }
                html.push_str("</select>");
            }
            _ => {
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                html.push_str(&format!(
                    r#"<input type="text" class="field-input" data-fieldname="{}"{}"
                        style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
                    field.name,
                    required_attr
                ));
            }
        }

        if let Some(doc) = &field.nickel_doc {
            html.push_str(&format!(
                r#"<small style="color: #888; display: block; margin-top: 3px;">{}</small>"#,
                html_escape(doc)
            ));
        }

        html.push_str("</div>");
    }

    // Save/Cancel buttons in expanded view
    html.push_str(
        r#"<div style="display: flex; gap: 10px; margin-top: 15px;">
                    <button type="button" class="save-btn"
                        style="flex: 1; background: #28a745; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">
                        ✅ Save
                    </button>
                    <button type="button" class="cancel-btn"
                        style="flex: 1; background: #6c757d; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer;">
                        ❌ Cancel
                    </button>
                </div>
            </div>
        </div>"#
    );

    html
}

/// Render a single item as expandable card with collapsed and expanded views
fn render_item_card(
    idx: usize,
    item: &serde_json::Map<String, Value>,
    fields: &[&crate::form_parser::FieldDefinition],
    _unique_id: &str,
) -> String {
    let mut card_html = format!(
        r#"<div class="item-card" data-index="{}" data-state="collapsed" style="background: #252526; border: 1px solid #3e3e42; padding: 12px; margin-bottom: 8px; border-radius: 4px;">
            <!-- Collapsed View: Summary + Edit/Delete buttons -->
            <div class="item-summary" style="display: flex; justify-content: space-between; align-items: center;">"#,
        idx,
    );

    // Display field values in summary
    let mut summary_parts = Vec::new();
    for field in fields {
        if let Some(value) = item.get(&field.name) {
            let value_str = match value {
                Value::String(s) => s.clone(),
                Value::Bool(b) => b.to_string(),
                Value::Number(n) => n.to_string(),
                _ => format!("{}", value),
            };
            summary_parts.push(format!(
                "<strong>{}:</strong> {}",
                html_escape(&field.prompt),
                html_escape(&value_str)
            ));
        }
    }
    card_html.push_str(&format!(
        r#"<div style="flex-grow: 1;">{}</div>"#,
        summary_parts.join(" | ")
    ));

    // Edit/Delete buttons in collapsed view
    card_html.push_str(
        r#"<div style="display: flex; gap: 8px;">
                    <button type="button" class="edit-btn"
                        style="background: #007acc; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">
                        ✏️ Edit
                    </button>
                    <button type="button" class="delete-btn"
                        style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 12px;">
                        🗑️ Delete
                    </button>
                </div>
            </div>

            <!-- Expanded View: Edit form inline (hidden by default) -->
            <div class="item-form" style="display: none; margin-top: 12px; padding-top: 12px; border-top: 1px solid #3e3e42;">"#
    );

    // Render form fields inline (no <form> tag, NO required attributes - use JS validation only)
    for field in fields {
        card_html.push_str(&format!(
            r#"<div style="margin-bottom: 15px;">
                    <label style="display: block; margin-bottom: 5px; font-weight: bold; color: #d4d4d4;">{}{}</label>"#,
            html_escape(&field.prompt),
            if field.required.unwrap_or(false) { " *" } else { "" }
        ));

        match field.field_type {
            crate::form_parser::FieldType::Text | crate::form_parser::FieldType::Password => {
                let input_type = if field.field_type == crate::form_parser::FieldType::Password {
                    "password"
                } else {
                    "text"
                };
                let autocomplete = if field.field_type == crate::form_parser::FieldType::Password {
                    " autocomplete=\"current-password\""
                } else {
                    ""
                };
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                let current_value = item.get(&field.name).and_then(|v| v.as_str()).unwrap_or("");
                card_html.push_str(&format!(
                    r#"<input type="{}" class="field-input" data-fieldname="{}"{} placeholder="{}" value="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
                    input_type,
                    field.name,
                    required_attr,
                    field.placeholder.as_deref().unwrap_or(""),
                    html_escape(current_value),
                    autocomplete
                ));
            }
            crate::form_parser::FieldType::Confirm => {
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                let current_value = item
                    .get(&field.name)
                    .and_then(|v| v.as_bool())
                    .unwrap_or(false);
                card_html.push_str(&format!(
                    r#"<select class="field-input" data-fieldname="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">
                        <option value="false" {}> No</option>
                        <option value="true" {}>Yes</option>
                    </select>"#,
                    field.name,
                    required_attr,
                    if !current_value { "selected" } else { "" },
                    if current_value { "selected" } else { "" }
                ));
            }
            crate::form_parser::FieldType::Select => {
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                let current_value = item.get(&field.name).and_then(|v| v.as_str()).unwrap_or("");
                card_html.push_str(&format!(
                    r#"<select class="field-input" data-fieldname="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
                    field.name,
                    required_attr
                ));
                for opt in &field.options {
                    let selected = if opt.value == current_value {
                        "selected"
                    } else {
                        ""
                    };
                    card_html.push_str(&format!(
                        r#"<option value="{}" {}>{}</option>"#,
                        html_escape(&opt.value),
                        selected,
                        html_escape(opt.label.as_deref().unwrap_or(&opt.value))
                    ));
                }
                card_html.push_str("</select>");
            }
            _ => {
                let required_attr = if field.required.unwrap_or(false) {
                    " data-required=\"true\""
                } else {
                    ""
                };
                let current_value = item
                    .get(&field.name)
                    .map(|v| v.to_string())
                    .unwrap_or_default();
                card_html.push_str(&format!(
                    r#"<input type="text" class="field-input" data-fieldname="{}"{} value="{}" style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
                    field.name,
                    required_attr,
                    html_escape(&current_value)
                ));
            }
        }

        if let Some(doc) = &field.nickel_doc {
            card_html.push_str(&format!(
                r#"<small style="color: #888; display: block; margin-top: 3px;">{}</small>"#,
                html_escape(doc)
            ));
        }

        card_html.push_str("</div>");
    }

    // Save/Cancel buttons in expanded view
    card_html.push_str(
        r#"<div style="display: flex; gap: 10px; margin-top: 15px;">
                    <button type="button" class="save-btn"
                        style="flex: 1; background: #28a745; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer; font-weight: bold;">
                        ✅ Save
                    </button>
                    <button type="button" class="cancel-btn"
                        style="flex: 1; background: #6c757d; color: white; border: none; padding: 10px; border-radius: 4px; cursor: pointer;">
                        ❌ Cancel
                    </button>
                </div>
            </div>
        </div>"#
    );

    card_html
}

/// HTML escape function to prevent injection
fn html_escape(s: &str) -> String {
    s.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace("\"", "&quot;")
        .replace("'", "&#39;")
}

/// Validate custom type field input (Web backend version)
fn validate_custom_type_web(input: &str, type_name: &str) -> std::result::Result<(), String> {
    match type_name {
        "i32" => {
            input
                .parse::<i32>()
                .map_err(|_| "Expected a 32-bit integer".to_string())?;
            Ok(())
        }
        "i64" => {
            input
                .parse::<i64>()
                .map_err(|_| "Expected a 64-bit integer".to_string())?;
            Ok(())
        }
        "u32" => {
            input
                .parse::<u32>()
                .map_err(|_| "Expected an unsigned 32-bit integer".to_string())?;
            Ok(())
        }
        "u64" => {
            input
                .parse::<u64>()
                .map_err(|_| "Expected an unsigned 64-bit integer".to_string())?;
            Ok(())
        }
        "f32" => {
            input
                .parse::<f32>()
                .map_err(|_| "Expected a 32-bit floating point".to_string())?;
            Ok(())
        }
        "f64" => {
            input
                .parse::<f64>()
                .map_err(|_| "Expected a 64-bit floating point".to_string())?;
            Ok(())
        }
        "ipv4" => {
            input
                .parse::<std::net::Ipv4Addr>()
                .map_err(|_| "Expected valid IPv4 address (e.g., 192.168.1.1)".to_string())?;
            Ok(())
        }
        "ipv6" => {
            input
                .parse::<std::net::Ipv6Addr>()
                .map_err(|_| "Expected valid IPv6 address".to_string())?;
            Ok(())
        }
        "uuid" => {
            if input.len() == 36 && input.matches('-').count() == 4 {
                Ok(())
            } else {
                Err("Expected UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".to_string())
            }
        }
        "String" | "str" => Ok(()),
        "bool" => match input.to_lowercase().as_str() {
            "true" | "false" | "yes" | "no" | "y" | "n" | "1" | "0" => Ok(()),
            _ => Err("Expected: true, false, yes, no, y, n, 1, or 0".to_string()),
        },
        _ => Ok(()), // Unknown type, accept as string
    }
}

/// Parse and convert custom type field input to appropriate JSON value (Web backend version)
fn parse_custom_value_web(input: &str, type_name: &str) -> Value {
    match type_name {
        "i32" => input
            .parse::<i32>()
            .map(|v| json!(v))
            .unwrap_or_else(|_| json!(input)),
        "i64" => input
            .parse::<i64>()
            .map(|v| json!(v))
            .unwrap_or_else(|_| json!(input)),
        "u32" => input
            .parse::<u32>()
            .map(|v| json!(v))
            .unwrap_or_else(|_| json!(input)),
        "u64" => input
            .parse::<u64>()
            .map(|v| json!(v))
            .unwrap_or_else(|_| json!(input)),
        "f32" => input
            .parse::<f32>()
            .map(|v| json!(v))
            .unwrap_or_else(|_| json!(input)),
        "f64" => input
            .parse::<f64>()
            .map(|v| json!(v))
            .unwrap_or_else(|_| json!(input)),
        "bool" => match input.to_lowercase().as_str() {
            "true" | "yes" | "y" | "1" => json!(true),
            "false" | "no" | "n" | "0" => json!(false),
            _ => json!(input),
        },
        _ => json!(input), // Default to string
    }
}

/// Evaluate condition for display items (duplicate of form_parser logic for safety)
/// This provides an extra layer of filtering at render time
fn evaluate_condition(condition: &str, results: &HashMap<String, Value>) -> bool {
    let condition = condition.trim();

    // Check string operators first (word boundaries)
    let string_operators = ["contains", "startswith", "endswith"];
    for op_str in &string_operators {
        if let Some(pos) = condition.find(op_str) {
            let before_ok = pos == 0
                || !condition[..pos]
                    .chars()
                    .last()
                    .unwrap_or(' ')
                    .is_alphanumeric();
            let after_ok = pos + op_str.len() >= condition.len()
                || !condition[pos + op_str.len()..]
                    .chars()
                    .next()
                    .unwrap_or(' ')
                    .is_alphanumeric();

            if before_ok && after_ok {
                let left = condition[..pos].trim();
                let right = condition[pos + op_str.len()..].trim();

                let field_value = results.get(left).cloned().unwrap_or(Value::Null);
                let field_str = value_to_string_web(&field_value);
                let expected = parse_condition_value_web(right);
                let expected_str = value_to_string_web(&expected);

                match *op_str {
                    "contains" => return field_str.contains(&expected_str),
                    "startswith" => return field_str.starts_with(&expected_str),
                    "endswith" => return field_str.ends_with(&expected_str),
                    _ => {}
                }
            }
        }
    }

    // Parse numeric/comparison operators in order of precedence (longest first to avoid partial matches)
    let operators = [
        ("<=", "le"),
        (">=", "ge"),
        ("==", "eq"),
        ("!=", "ne"),
        (">", "gt"),
        ("<", "lt"),
    ];

    for (op_str, _op_name) in &operators {
        if let Some(pos) = condition.find(op_str) {
            let left = condition[..pos].trim();
            let right = condition[pos + op_str.len()..].trim();

            // Get the field value from results
            let field_value = results.get(left).cloned().unwrap_or(Value::Null);

            // Parse the right side as value (handle quoted strings and raw values)
            let expected = parse_condition_value_web(right);

            // Perform comparison
            match *op_str {
                "==" => return values_equal_web(&field_value, &expected),
                "!=" => return !values_equal_web(&field_value, &expected),
                ">" => {
                    return compare_values_web(&field_value, &expected)
                        == std::cmp::Ordering::Greater
                }
                "<" => {
                    return compare_values_web(&field_value, &expected) == std::cmp::Ordering::Less
                }
                ">=" => {
                    let cmp = compare_values_web(&field_value, &expected);
                    return cmp == std::cmp::Ordering::Greater || cmp == std::cmp::Ordering::Equal;
                }
                "<=" => {
                    let cmp = compare_values_web(&field_value, &expected);
                    return cmp == std::cmp::Ordering::Less || cmp == std::cmp::Ordering::Equal;
                }
                _ => {}
            }
        }
    }

    true
}

fn parse_condition_value_web(s: &str) -> Value {
    let s = s.trim();
    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
        return json!(s[1..s.len() - 1].to_string());
    }
    if let Ok(n) = s.parse::<i64>() {
        return json!(n);
    }
    if let Ok(n) = s.parse::<f64>() {
        return json!(n);
    }
    match s.to_lowercase().as_str() {
        "true" | "yes" | "1" => json!(true),
        "false" | "no" | "0" => json!(false),
        _ => json!(s.to_string()),
    }
}

fn value_to_string_web(v: &Value) -> String {
    match v {
        Value::String(s) => s.clone(),
        Value::Number(n) => n.to_string(),
        Value::Bool(b) => b.to_string(),
        Value::Null => String::new(),
        other => other.to_string(),
    }
}

fn values_equal_web(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::String(s1), Value::String(s2)) => s1 == s2,
        (Value::Number(n1), Value::Number(n2)) => n1 == n2,
        (Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
        (Value::Null, Value::Null) => true,
        (Value::String(s), Value::Number(n)) | (Value::Number(n), Value::String(s)) => s
            .parse::<f64>()
            .ok()
            .and_then(|pv| n.as_f64().map(|nv| (pv - nv).abs() < 1e-10))
            .unwrap_or(false),
        _ => false,
    }
}

fn compare_values_web(a: &Value, b: &Value) -> std::cmp::Ordering {
    use std::cmp::Ordering;
    let a_num = match a {
        Value::Number(n) => n.as_f64(),
        Value::String(s) => s.parse::<f64>().ok(),
        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
        _ => None,
    };
    let b_num = match b {
        Value::Number(n) => n.as_f64(),
        Value::String(s) => s.parse::<f64>().ok(),
        Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
        _ => None,
    };
    match (a_num, b_num) {
        (Some(an), Some(bn)) => {
            if (an - bn).abs() < 1e-10 {
                Ordering::Equal
            } else if an > bn {
                Ordering::Greater
            } else {
                Ordering::Less
            }
        }
        _ => {
            let a_str = value_to_string_web(a);
            let b_str = value_to_string_web(b);
            a_str.cmp(&b_str)
        }
    }
}

/// API response for nickel-to-form operation
#[derive(serde::Serialize)]
struct NickelToFormResponse {
    success: bool,
    form_toml: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    fragments: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    i18n: Option<HashMap<String, String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

/// API response for nickel-roundtrip operation
#[derive(serde::Serialize)]
struct NickelRoundtripResponse {
    success: bool,
    output_nickel: String,
    validation_passed: Option<bool>,
    form_results_count: usize,
    imports_preserved: usize,
    contracts_preserved: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

#[cfg(feature = "web")]
use axum::extract::Multipart;

/// Handle POST /api/nickel/to-form - Convert Nickel schema to TOML form
/// Expects multipart form with:
/// - schema (file): .ncl Nickel schema file
/// - flatten (bool, optional): flatten nested records
/// - groups (bool, optional): use semantic grouping
/// - fragments (bool, optional): generate fragments
/// - i18n (bool, optional): extract i18n translations
#[cfg(feature = "web")]
async fn nickel_to_form_handler(
    State(_state): State<Arc<WebFormState>>,
    mut multipart: Multipart,
) -> Json<NickelToFormResponse> {
    use std::io::Write;
    use tempfile::NamedTempFile;

    // Extract parameters from multipart (A-EXTRACTORS-FIRST guideline)
    let mut schema_file: Option<NamedTempFile> = None;
    let mut flatten = false;
    let mut groups = false;
    let mut fragments = false;
    let mut i18n = false;

    while let Ok(Some(field)) = multipart.next_field().await {
        let field_name = field.name().unwrap_or("").to_string();

        match field_name.as_str() {
            "schema" => {
                let bytes = match field.bytes().await {
                    Ok(b) => b,
                    Err(e) => {
                        return Json(NickelToFormResponse {
                            success: false,
                            form_toml: String::new(),
                            fragments: None,
                            i18n: None,
                            error: Some(format!("Failed to read schema file: {}", e)),
                        });
                    }
                };

                match NamedTempFile::new() {
                    Ok(mut temp) => {
                        if let Err(e) = temp.write_all(&bytes) {
                            return Json(NickelToFormResponse {
                                success: false,
                                form_toml: String::new(),
                                fragments: None,
                                i18n: None,
                                error: Some(format!("Failed to write temp file: {}", e)),
                            });
                        }
                        schema_file = Some(temp);
                    }
                    Err(e) => {
                        return Json(NickelToFormResponse {
                            success: false,
                            form_toml: String::new(),
                            fragments: None,
                            i18n: None,
                            error: Some(format!("Failed to create temp file: {}", e)),
                        });
                    }
                }
            }
            "flatten" => {
                flatten = field
                    .text()
                    .await
                    .ok()
                    .map(|t| t == "true")
                    .unwrap_or(false);
            }
            "groups" => {
                groups = field
                    .text()
                    .await
                    .ok()
                    .map(|t| t == "true")
                    .unwrap_or(false);
            }
            "fragments" => {
                fragments = field
                    .text()
                    .await
                    .ok()
                    .map(|t| t == "true")
                    .unwrap_or(false);
            }
            "i18n" => {
                i18n = field
                    .text()
                    .await
                    .ok()
                    .map(|t| t == "true")
                    .unwrap_or(false);
            }
            _ => {}
        }
    }

    // Validate required schema file
    let schema_temp = match schema_file {
        Some(f) => f,
        None => {
            return Json(NickelToFormResponse {
                success: false,
                form_toml: String::new(),
                fragments: None,
                i18n: None,
                error: Some("Missing required 'schema' file".to_string()),
            });
        }
    };

    // Get path and verify nickel CLI (A-ERROR-HANDLING guideline)
    let schema_path = schema_temp.path().to_path_buf();

    // Use the nickel modules from core library
    use crate::nickel::{I18nExtractor, MetadataParser, NickelCli, TomlGenerator};

    // Verify nickel CLI is available
    if let Err(e) = NickelCli::verify() {
        return Json(NickelToFormResponse {
            success: false,
            form_toml: String::new(),
            fragments: None,
            i18n: None,
            error: Some(format!("Nickel CLI not available: {}", e)),
        });
    }

    // Extract metadata from schema
    let metadata = match NickelCli::query(&schema_path, Some("inputs")) {
        Ok(m) => m,
        Err(e) => {
            return Json(NickelToFormResponse {
                success: false,
                form_toml: String::new(),
                fragments: None,
                i18n: None,
                error: Some(format!("Failed to query schema: {}", e)),
            });
        }
    };

    // Parse into intermediate representation
    let mut schema_ir = match MetadataParser::parse(metadata) {
        Ok(ir) => ir,
        Err(e) => {
            return Json(NickelToFormResponse {
                success: false,
                form_toml: String::new(),
                fragments: None,
                i18n: None,
                error: Some(format!("Failed to parse metadata: {}", e)),
            });
        }
    };

    // Extract fragment markers if enabled
    if fragments {
        match MetadataParser::extract_fragment_markers_from_source(&schema_path) {
            Ok(markers) => MetadataParser::apply_fragment_markers(&mut schema_ir, &markers),
            Err(e) => {
                eprintln!("Warning: Failed to extract fragment markers: {}", e);
            }
        }
    }

    // Generate TOML forms
    let forms_output = if fragments && schema_ir.fields.iter().any(|f| f.fragment_marker.is_some())
    {
        match TomlGenerator::generate_with_fragments(&schema_ir, flatten, groups) {
            Ok(forms) => forms,
            Err(e) => {
                return Json(NickelToFormResponse {
                    success: false,
                    form_toml: String::new(),
                    fragments: None,
                    i18n: None,
                    error: Some(format!("Failed to generate forms: {}", e)),
                });
            }
        }
    } else {
        match TomlGenerator::generate(&schema_ir, flatten, groups) {
            Ok(form_def) => {
                let mut single = HashMap::new();
                single.insert("form.toml".to_string(), form_def);
                single
            }
            Err(e) => {
                return Json(NickelToFormResponse {
                    success: false,
                    form_toml: String::new(),
                    fragments: None,
                    i18n: None,
                    error: Some(format!("Failed to generate form: {}", e)),
                });
            }
        }
    };

    // Get main form output
    let form_toml =
        if let Some((_, form_def)) = forms_output.iter().find(|(k, _)| k.as_str() == "form.toml") {
            match toml::to_string_pretty(form_def) {
                Ok(s) => s,
                Err(e) => {
                    return Json(NickelToFormResponse {
                        success: false,
                        form_toml: String::new(),
                        fragments: None,
                        i18n: None,
                        error: Some(format!("Failed to serialize form: {}", e)),
                    });
                }
            }
        } else {
            String::new()
        };

    // Collect fragments
    let fragments_map = if forms_output.len() > 1 {
        let mut map = HashMap::new();
        for (name, form_def) in &forms_output {
            if name != "form.toml" {
                if let Ok(s) = toml::to_string_pretty(form_def) {
                    map.insert(name.clone(), s);
                }
            }
        }
        if map.is_empty() {
            None
        } else {
            Some(map)
        }
    } else {
        None
    };

    // Extract i18n if enabled
    let i18n_map = if i18n {
        match I18nExtractor::extract_and_generate(&schema_ir, &std::path::PathBuf::from("/tmp")) {
            Ok(_mapping) => {
                let mut map = HashMap::new();
                // Return empty placeholder since real i18n generation is complex
                map.insert("status".to_string(), "i18n extraction enabled".to_string());
                Some(map)
            }
            Err(e) => {
                eprintln!("Warning: Failed to extract i18n: {}", e);
                None
            }
        }
    } else {
        None
    };

    Json(NickelToFormResponse {
        success: true,
        form_toml,
        fragments: fragments_map,
        i18n: i18n_map,
        error: None,
    })
}

/// API response for template rendering
#[derive(serde::Serialize)]
struct NickelTemplateResponse {
    success: bool,
    output: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<String>,
}

/// Handle POST /api/nickel/template - Render Nickel template with form results
/// Expects multipart form with:
/// - template (file): .ncl.j2 Tera template file
/// - results (file): .json results file
#[cfg(feature = "web")]
async fn nickel_template_handler(
    State(_state): State<Arc<WebFormState>>,
    mut multipart: Multipart,
) -> Json<NickelTemplateResponse> {
    use std::io::Write;
    use tempfile::NamedTempFile;

    let mut template_file: Option<NamedTempFile> = None;
    let mut results_file: Option<NamedTempFile> = None;

    while let Ok(Some(field)) = multipart.next_field().await {
        let field_name = field.name().unwrap_or("").to_string();

        match field_name.as_str() {
            "template" => {
                let bytes = match field.bytes().await {
                    Ok(b) => b,
                    Err(e) => {
                        return Json(NickelTemplateResponse {
                            success: false,
                            output: None,
                            error: Some(format!("Failed to read template file: {}", e)),
                        });
                    }
                };

                match NamedTempFile::new() {
                    Ok(mut temp) => {
                        if let Err(e) = temp.write_all(&bytes) {
                            return Json(NickelTemplateResponse {
                                success: false,
                                output: None,
                                error: Some(format!("Failed to write temp file: {}", e)),
                            });
                        }
                        template_file = Some(temp);
                    }
                    Err(e) => {
                        return Json(NickelTemplateResponse {
                            success: false,
                            output: None,
                            error: Some(format!("Failed to create temp file: {}", e)),
                        });
                    }
                }
            }
            "results" => {
                let bytes = match field.bytes().await {
                    Ok(b) => b,
                    Err(e) => {
                        return Json(NickelTemplateResponse {
                            success: false,
                            output: None,
                            error: Some(format!("Failed to read results file: {}", e)),
                        });
                    }
                };

                match NamedTempFile::new() {
                    Ok(mut temp) => {
                        if let Err(e) = temp.write_all(&bytes) {
                            return Json(NickelTemplateResponse {
                                success: false,
                                output: None,
                                error: Some(format!("Failed to write temp file: {}", e)),
                            });
                        }
                        results_file = Some(temp);
                    }
                    Err(e) => {
                        return Json(NickelTemplateResponse {
                            success: false,
                            output: None,
                            error: Some(format!("Failed to create temp file: {}", e)),
                        });
                    }
                }
            }
            _ => {}
        }
    }

    let template_temp = match template_file {
        Some(f) => f,
        None => {
            return Json(NickelTemplateResponse {
                success: false,
                output: None,
                error: Some("Missing required 'template' file".to_string()),
            });
        }
    };

    let _results_temp = match results_file {
        Some(f) => f,
        None => {
            return Json(NickelTemplateResponse {
                success: false,
                output: None,
                error: Some("Missing required 'results' file".to_string()),
            });
        }
    };

    let template_path = template_temp.path().to_path_buf();

    use crate::nickel::TemplateEngine;

    let mut engine = TemplateEngine::new();
    match engine.render_file(
        template_path.as_path(),
        &std::collections::HashMap::new(),
        None,
    ) {
        Ok(nickel_output) => Json(NickelTemplateResponse {
            success: true,
            output: Some(nickel_output),
            error: None,
        }),
        Err(e) => Json(NickelTemplateResponse {
            success: false,
            output: None,
            error: Some(format!("Template rendering failed: {}", e)),
        }),
    }
}

/// Handle POST /api/nickel/roundtrip - Roundtrip: .ncl → results → .ncl
/// Expects multipart form with:
/// - input (file): input.ncl Nickel file
/// - results (file): results.json pre-computed form results
/// - template (file, optional): template.ncl.j2 Tera template
/// - validate (bool, optional): run nickel typecheck
///
/// NOTE: Web roundtrip is headless - it accepts pre-computed results rather than
/// executing an interactive form. For interactive roundtrip, use typedialog-tui.
#[cfg(feature = "web")]
async fn nickel_roundtrip_handler(
    State(_state): State<Arc<WebFormState>>,
    mut multipart: Multipart,
) -> Json<NickelRoundtripResponse> {
    use crate::nickel::{ContractParser, NickelCli, NickelTemplateContext, TemplateEngine};
    use std::io::Write;
    use tempfile::NamedTempFile;

    let mut input_file: Option<NamedTempFile> = None;
    let mut results_file: Option<NamedTempFile> = None;
    let mut template_file: Option<NamedTempFile> = None;
    let mut output_path = PathBuf::from("output.ncl");
    let mut validate = false;

    while let Ok(Some(field)) = multipart.next_field().await {
        let field_name = field.name().unwrap_or("").to_string();

        match field_name.as_str() {
            "input" => {
                let bytes = match field.bytes().await {
                    Ok(b) => b,
                    Err(e) => {
                        return Json(NickelRoundtripResponse {
                            success: false,
                            output_nickel: String::new(),
                            validation_passed: None,
                            form_results_count: 0,
                            imports_preserved: 0,
                            contracts_preserved: 0,
                            error: Some(format!("Failed to read input file: {}", e)),
                        });
                    }
                };

                match NamedTempFile::new() {
                    Ok(mut temp) => {
                        if let Err(e) = temp.write_all(&bytes) {
                            return Json(NickelRoundtripResponse {
                                success: false,
                                output_nickel: String::new(),
                                validation_passed: None,
                                form_results_count: 0,
                                imports_preserved: 0,
                                contracts_preserved: 0,
                                error: Some(format!("Failed to write temp file: {}", e)),
                            });
                        }
                        input_file = Some(temp);
                    }
                    Err(e) => {
                        return Json(NickelRoundtripResponse {
                            success: false,
                            output_nickel: String::new(),
                            validation_passed: None,
                            form_results_count: 0,
                            imports_preserved: 0,
                            contracts_preserved: 0,
                            error: Some(format!("Failed to create temp file: {}", e)),
                        });
                    }
                }
            }
            "results" => {
                let bytes = match field.bytes().await {
                    Ok(b) => b,
                    Err(e) => {
                        return Json(NickelRoundtripResponse {
                            success: false,
                            output_nickel: String::new(),
                            validation_passed: None,
                            form_results_count: 0,
                            imports_preserved: 0,
                            contracts_preserved: 0,
                            error: Some(format!("Failed to read results file: {}", e)),
                        });
                    }
                };

                match NamedTempFile::new() {
                    Ok(mut temp) => {
                        if let Err(e) = temp.write_all(&bytes) {
                            return Json(NickelRoundtripResponse {
                                success: false,
                                output_nickel: String::new(),
                                validation_passed: None,
                                form_results_count: 0,
                                imports_preserved: 0,
                                contracts_preserved: 0,
                                error: Some(format!("Failed to write temp file: {}", e)),
                            });
                        }
                        results_file = Some(temp);
                    }
                    Err(e) => {
                        return Json(NickelRoundtripResponse {
                            success: false,
                            output_nickel: String::new(),
                            validation_passed: None,
                            form_results_count: 0,
                            imports_preserved: 0,
                            contracts_preserved: 0,
                            error: Some(format!("Failed to create temp file: {}", e)),
                        });
                    }
                }
            }
            "template" => {
                let bytes = match field.bytes().await {
                    Ok(b) => b,
                    Err(_) => continue,
                };

                match NamedTempFile::new() {
                    Ok(mut temp) => {
                        if temp.write_all(&bytes).is_err() {
                            continue;
                        }
                        template_file = Some(temp);
                    }
                    Err(_) => continue,
                }
            }
            "validate" => {
                validate = field
                    .text()
                    .await
                    .ok()
                    .map(|t| t == "true")
                    .unwrap_or(false);
            }
            "output" => {
                if let Ok(path_str) = field.text().await {
                    output_path = PathBuf::from(path_str);
                }
            }
            _ => {}
        }
    }

    let input_temp = match input_file {
        Some(f) => f,
        None => {
            return Json(NickelRoundtripResponse {
                success: false,
                output_nickel: String::new(),
                validation_passed: None,
                form_results_count: 0,
                imports_preserved: 0,
                contracts_preserved: 0,
                error: Some("Missing required 'input' file".to_string()),
            });
        }
    };

    let results_temp = match results_file {
        Some(f) => f,
        None => {
            return Json(NickelRoundtripResponse {
                success: false,
                output_nickel: String::new(),
                validation_passed: None,
                form_results_count: 0,
                imports_preserved: 0,
                contracts_preserved: 0,
                error: Some("Missing required 'results' file".to_string()),
            });
        }
    };

    let input_path = input_temp.path().to_path_buf();
    let results_path = results_temp.path().to_path_buf();
    let template_path = template_file.as_ref().map(|t| t.path().to_path_buf());

    // Read input and parse contracts
    let input_source = match std::fs::read_to_string(&input_path) {
        Ok(src) => src,
        Err(e) => {
            return Json(NickelRoundtripResponse {
                success: false,
                output_nickel: String::new(),
                validation_passed: None,
                form_results_count: 0,
                imports_preserved: 0,
                contracts_preserved: 0,
                error: Some(format!("Failed to read input: {}", e)),
            });
        }
    };

    let input_contracts = match ContractParser::parse_source(&input_source) {
        Ok(c) => c,
        Err(e) => {
            return Json(NickelRoundtripResponse {
                success: false,
                output_nickel: String::new(),
                validation_passed: None,
                form_results_count: 0,
                imports_preserved: 0,
                contracts_preserved: 0,
                error: Some(format!("Failed to parse contracts: {}", e)),
            });
        }
    };

    // Load form results
    let results_content = match std::fs::read_to_string(&results_path) {
        Ok(c) => c,
        Err(e) => {
            return Json(NickelRoundtripResponse {
                success: false,
                output_nickel: String::new(),
                validation_passed: None,
                form_results_count: 0,
                imports_preserved: 0,
                contracts_preserved: 0,
                error: Some(format!("Failed to read results: {}", e)),
            });
        }
    };

    let form_results: HashMap<String, serde_json::Value> =
        match serde_json::from_str(&results_content) {
            Ok(r) => r,
            Err(e) => {
                return Json(NickelRoundtripResponse {
                    success: false,
                    output_nickel: String::new(),
                    validation_passed: None,
                    form_results_count: 0,
                    imports_preserved: 0,
                    contracts_preserved: 0,
                    error: Some(format!("Failed to parse results JSON: {}", e)),
                });
            }
        };

    // Generate output Nickel
    let output_nickel = if let Some(template_path) = template_path {
        let mut engine = TemplateEngine::new();
        match engine.render_file(template_path.as_path(), &form_results, None) {
            Ok(output) => output,
            Err(e) => {
                return Json(NickelRoundtripResponse {
                    success: false,
                    output_nickel: String::new(),
                    validation_passed: None,
                    form_results_count: 0,
                    imports_preserved: 0,
                    contracts_preserved: 0,
                    error: Some(format!("Failed to render template: {}", e)),
                });
            }
        }
    } else {
        let ctx = NickelTemplateContext::from_parsed_contracts(
            input_contracts.clone(),
            form_results.clone(),
        );
        ctx.render_full_nickel()
    };

    // Write output
    if let Err(e) = std::fs::write(&output_path, &output_nickel) {
        eprintln!(
            "Warning: Could not write output file {}: {}",
            output_path.display(),
            e
        );
    }

    // Validate if requested
    let validation_passed = if validate {
        match NickelCli::typecheck(&output_path) {
            Ok(()) => Some(true),
            Err(_) => Some(false),
        }
    } else {
        None
    };

    Json(NickelRoundtripResponse {
        success: true,
        output_nickel,
        validation_passed,
        form_results_count: form_results.len(),
        imports_preserved: input_contracts.imports.len(),
        contracts_preserved: input_contracts.field_contracts.len(),
        error: None,
    })
}

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

    #[test]
    fn test_web_backend_new() {
        let backend = WebBackend::new(3000);
        assert_eq!(backend.name(), "web");
        assert_eq!(backend.port, 3000);
    }

    #[test]
    fn test_web_backend_available() {
        assert!(WebBackend::is_available());
    }

    #[tokio::test]
    async fn test_web_backend_lifecycle() {
        let mut backend = WebBackend::new(3001);
        assert!(backend.initialize().await.is_ok());
        assert!(backend.shutdown().await.is_ok());
    }

    /// Test dynamic form endpoint returns correct filtered fields
    #[tokio::test]
    async fn test_web_dynamic_form_endpoint_filters_fields() {
        // Create a form with a conditional field using TOML parsing
        let toml = r#"
            name = "test"
            display_mode = "complete"

            [[fields]]
            name = "account_type"
            type = "select"
            prompt = "Account Type"
            options = ["basic", "premium"]
            default = "basic"

            [[fields]]
            name = "premium_feature"
            type = "text"
            prompt = "Premium Feature"
            when = "account_type == premium"
        "#;

        let form = crate::form_parser::parse_toml(toml).expect("Failed to parse form");

        // Create state
        let state = WebFormState::new();

        // Set form, base_dir, complete mode, and initial fields
        {
            let mut form_lock = state.form.write().await;
            *form_lock = Some(form.clone());

            let mut base_dir_lock = state.base_dir.write().await;
            *base_dir_lock = Some(std::path::PathBuf::from("/tmp"));

            let mut is_complete = state.is_complete_mode.write().await;
            *is_complete = true;

            let mut fields_lock = state.complete_fields.write().await;
            *fields_lock = form.fields.clone();
        }

        // Call the handler with current field values as query parameters
        // Simulate HTMX sending: account_type=premium
        let mut params = HashMap::new();
        params.insert("account_type".to_string(), "premium".to_string());
        let (status, Html(html_str)) =
            get_dynamic_form_handler(State(Arc::new(state)), Query(params)).await;

        // Verify the response is OK
        assert_eq!(status, StatusCode::OK, "Should return 200 OK");

        // Verify the response contains the account_type field
        assert!(
            html_str.contains("account_type"),
            "Should contain account_type field"
        );

        // With account_type == premium, premium_feature should be visible
        assert!(
            html_str.contains("premium_feature"),
            "Should contain premium_feature when condition is true"
        );
    }

    /// Test dynamic form endpoint with basic account type hides premium fields
    #[tokio::test]
    async fn test_web_dynamic_form_endpoint_hides_conditional() {
        // Create a form with a conditional field using TOML parsing
        let toml = r#"
            name = "test"
            display_mode = "complete"

            [[fields]]
            name = "account_type"
            type = "select"
            prompt = "Account Type"
            options = ["basic", "premium"]
            default = "basic"

            [[fields]]
            name = "premium_feature"
            type = "text"
            prompt = "Premium Feature"
            when = "account_type == premium"
        "#;

        let form = crate::form_parser::parse_toml(toml).expect("Failed to parse form");

        // Create state
        let state = WebFormState::new();

        // Set form, base_dir, complete mode, and initial fields
        {
            let mut form_lock = state.form.write().await;
            *form_lock = Some(form.clone());

            let mut base_dir_lock = state.base_dir.write().await;
            *base_dir_lock = Some(std::path::PathBuf::from("/tmp"));

            let mut is_complete = state.is_complete_mode.write().await;
            *is_complete = true;

            let mut fields_lock = state.complete_fields.write().await;
            *fields_lock = form.fields.clone();
        }

        // Call the handler with current field values as query parameters
        // Simulate HTMX sending: account_type=basic
        let mut params = HashMap::new();
        params.insert("account_type".to_string(), "basic".to_string());
        let (status, Html(html_str)) =
            get_dynamic_form_handler(State(Arc::new(state)), Query(params)).await;

        // Verify the response is OK
        assert_eq!(status, StatusCode::OK, "Should return 200 OK");

        // Should still have account_type field
        assert!(
            html_str.contains("account_type"),
            "Should contain account_type field"
        );

        // But NOT premium_feature since account_type == basic
        assert!(
            !html_str.contains("premium_feature"),
            "Should NOT contain premium_feature when condition is false"
        );
    }

    /// Test dynamic form endpoint handles non-complete mode gracefully
    #[tokio::test]
    async fn test_web_dynamic_form_endpoint_non_complete_mode() {
        let state = WebFormState::new();

        // NOT in complete mode
        {
            let mut is_complete = state.is_complete_mode.write().await;
            *is_complete = false;
        }

        // Call the handler with empty params (non-complete mode should fail regardless)
        let params = HashMap::new();
        let (status, _html) = get_dynamic_form_handler(State(Arc::new(state)), Query(params)).await;

        // Should return BAD_REQUEST for non-complete mode
        assert_eq!(status, StatusCode::BAD_REQUEST);
    }
}
