#![allow(clippy::result_large_err)]

//! typedialog-web - Web server for interactive forms
//!
//! A web server tool for creating interactive forms accessible via HTTP.
//! Uses axum web framework with HTMX for dynamic form interactions.

use clap::{Parser, Subcommand};
use std::fs;
use std::path::{Path, PathBuf};
use typedialog_core::cli_common;
use typedialog_core::config::load_backend_config;
use typedialog_core::form_parser;
use typedialog_core::helpers;
use typedialog_core::i18n::LocaleLoader;
use typedialog_core::prelude::*;
use unic_langid::LanguageIdentifier;

#[derive(Parser)]
#[command(
    name = "typedialog-web",
    version,
    about = "Web server for interactive forms",
    long_about = cli_common::WEB_MAIN_LONG_ABOUT
)]
struct Args {
    #[command(subcommand)]
    command: Option<Commands>,

    /// Web backend configuration file (TOML)
    ///
    /// If provided, uses this file exclusively.
    /// If not provided, searches: ~/.config/typedialog/web/{TYPEDIALOG_ENV}.toml → ~/.config/typedialog/web/config.toml → defaults
    #[arg(global = true, short = 'c', long, value_name = "FILE")]
    backend_config: Option<PathBuf>,

    /// Path to TOML form configuration file
    config: Option<PathBuf>,

    /// Port to listen on (can also be set via TYPEDIALOG_PORT env var)
    #[arg(short, long, default_value = "8080")]
    port: u16,

    /// Path to JSON file with default field values
    #[arg(long)]
    defaults: Option<PathBuf>,

    /// Output file for form results
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Output format: json, yaml, text, toml
    #[arg(short = 'f', long, default_value = "json")]
    format: String,

    /// Locale override for form localization
    #[arg(short, long, help = cli_common::LOCALE_FLAG_HELP)]
    locale: Option<String>,

    /// Automatically open the form in the default web browser
    #[arg(long)]
    open: bool,
}

#[derive(Subcommand)]
enum Commands {
    /// Execute interactive form from TOML configuration via HTTP server
    Form {
        /// Path to TOML form configuration file
        config: PathBuf,

        /// Path to JSON file with default field values
        #[arg(long)]
        defaults: Option<PathBuf>,
    },

    /// Complete roundtrip: .ncl → form → .ncl (interactive via HTTP)
    #[command(name = "nickel-roundtrip")]
    NickelRoundtrip {
        /// Path to input Nickel file (.ncl)
        input: PathBuf,

        /// Path to form definition (.toml)
        form: PathBuf,

        /// Path to output Nickel file (.ncl)
        #[arg(long)]
        output: PathBuf,

        /// Optional Tera template file (.ncl.j2) for rendering output
        #[arg(long)]
        ncl_template: Option<PathBuf>,

        /// Disable validation with nickel typecheck
        #[arg(long)]
        no_validate: bool,

        /// Print verbose progress messages
        #[arg(short, long)]
        verbose: bool,

        /// Automatically open the form in the default web browser
        #[arg(long)]
        open: bool,
    },
}

/// Resolve a file path with cascading search relative to form directory
///
/// Search order:
/// 1. If absolute path → use directly
/// 2. If path exists as-is (relative to cwd) → use it
/// 3. Try relative to form's directory → use if exists
/// 4. For output files → prefer form's directory
/// 5. For input files → return original (will fail with clear error)
///
/// Returns the resolved PathBuf.
fn resolve_file_path(path: &Path, form_base_dir: &Path, is_output: bool) -> PathBuf {
    // 1. If absolute, use it directly
    if path.is_absolute() {
        return path.to_path_buf();
    }

    // 2. Check if path exists as-is (relative to cwd)
    if path.exists() {
        return path.to_path_buf();
    }

    // 3. Try relative to form's directory
    let form_relative = form_base_dir.join(path);
    if form_relative.exists() {
        return form_relative;
    }

    // 4. For output files, prefer form directory even if doesn't exist yet
    if is_output {
        return form_relative;
    }

    // 5. Return original path (will fail with clear error for input files)
    path.to_path_buf()
}

/// Recursively flatten nested JSON objects into a single-level map
/// Converts {"a": {"b": {"c": "value"}}} to {"a_b_c": "value"}
fn flatten_json_object(
    obj: &serde_json::Map<String, serde_json::Value>,
) -> std::collections::HashMap<String, serde_json::Value> {
    let mut result = std::collections::HashMap::new();
    flatten_recursive(obj, "", &mut result);
    result
}

fn flatten_recursive(
    obj: &serde_json::Map<String, serde_json::Value>,
    prefix: &str,
    result: &mut std::collections::HashMap<String, serde_json::Value>,
) {
    for (key, value) in obj.iter() {
        let new_key = if prefix.is_empty() {
            key.clone()
        } else {
            format!("{}_{}", prefix, key)
        };

        match value {
            serde_json::Value::Object(nested) => {
                // Recursively flatten nested objects
                flatten_recursive(nested, &new_key, result);
            }
            serde_json::Value::Array(arr) => {
                // For arrays, just store them as-is with their key
                result.insert(new_key, serde_json::Value::Array(arr.clone()));
            }
            _ => {
                // Keep primitive values (string, number, bool, null)
                result.insert(new_key, value.clone());
            }
        }
    }
}

/// Extract common field values from Nickel schema export structure
/// Maps from nested structure (e.g., tracker.core.http.bind_address) to form field names
fn extract_value_by_path(obj: &serde_json::Value, path: &[String]) -> Option<serde_json::Value> {
    if path.is_empty() {
        return Some(obj.clone());
    }

    let key = &path[0];
    let remaining = &path[1..];

    match obj {
        serde_json::Value::Object(map) => map
            .get(key)
            .and_then(|v| extract_value_by_path(v, remaining)),
        _ => None,
    }
}

fn extract_nickel_defaults(
    obj: &serde_json::Map<String, serde_json::Value>,
    form_fields: &[form_parser::FieldDefinition],
) -> std::collections::HashMap<String, serde_json::Value> {
    let mut result = std::collections::HashMap::new();

    // Extract values using nickel_path from each field (handles arrays and complex structures)
    for field in form_fields {
        if let Some(nickel_path) = &field.nickel_path {
            if let Some(value) =
                extract_value_by_path(&serde_json::Value::Object(obj.clone()), nickel_path)
            {
                result.insert(field.name.clone(), value);
            }
        }
    }

    result
}

#[tokio::main]
async fn main() -> Result<()> {
    // Initialize tracing subscriber with env filter (respects RUST_LOG)
    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
        )
        .init();

    let args = Args::parse();

    // Load configuration with CLI override
    let config_path = args.backend_config.as_deref();
    let _config =
        load_backend_config::<TypeDialogConfig>("web", config_path, TypeDialogConfig::default())?;

    if let Some(path) = config_path {
        eprintln!("📋 Using config: {}", path.display());
    }

    match args.command {
        Some(Commands::Form { config, defaults }) => {
            execute_form(
                config,
                defaults,
                args.port,
                args.output,
                &args.format,
                &args.locale,
                args.open,
            )
            .await?;
        }
        Some(Commands::NickelRoundtrip {
            input,
            form,
            output,
            ncl_template,
            no_validate,
            verbose,
            open,
        }) => {
            nickel_roundtrip_cmd(
                input,
                form,
                output,
                ncl_template,
                !no_validate,
                verbose,
                open,
            )
            .await?;
        }
        None => {
            let config = args.config.ok_or_else(|| {
                Error::validation_failed("Please provide a form configuration file")
            })?;
            execute_form(
                config,
                args.defaults,
                args.port,
                args.output,
                &args.format,
                &args.locale,
                args.open,
            )
            .await?;
        }
    }

    Ok(())
}

/// Detect output format from filename extension
///
/// Returns the format string based on file extension:
/// - `.toml` → "toml"
/// - `.yaml` or `.yml` → "yaml"
/// - `.json` → "json"
/// - `.txt` → "text"
/// - default → provided format
fn detect_output_format(output_path: Option<&PathBuf>, default_format: &str) -> String {
    output_path
        .and_then(|p| p.extension())
        .and_then(|ext| ext.to_str())
        .and_then(|ext| match ext.to_lowercase().as_str() {
            "toml" => Some("toml"),
            "yaml" | "yml" => Some("yaml"),
            "json" => Some("json"),
            "txt" => Some("text"),
            _ => None,
        })
        .unwrap_or(default_format)
        .to_string()
}

/// Load default values from a Nickel (.ncl) file
///
/// Exports the .ncl file to JSON, then extracts and flattens values
/// matching the form fields.
///
/// Returns a HashMap of field names to their default values.
fn load_nickel_defaults(
    ncl_path: &Path,
    form_elements: &[form_parser::FormElement],
    verbose: bool,
) -> Result<std::collections::HashMap<String, serde_json::Value>> {
    use typedialog_core::nickel::NickelCli;

    if verbose {
        eprintln!("[web] Loading default values from {}", ncl_path.display());
    }

    NickelCli::verify()?;
    let value = NickelCli::export(ncl_path)?;

    match value {
        serde_json::Value::Object(map) => {
            // Extract fields from elements (which includes fragments after expand_includes)
            let form_fields: Vec<form_parser::FieldDefinition> = form_elements
                .iter()
                .filter_map(|elem| match elem {
                    form_parser::FormElement::Field(f) => Some(f.as_ref().clone()),
                    _ => None,
                })
                .collect();

            // Extract Nickel structure AND flatten everything (combine both approaches)
            let mut combined = extract_nickel_defaults(&map, &form_fields);
            let flattened = flatten_json_object(&map);

            // Flattened values fill in gaps not covered by extraction
            for (k, v) in flattened {
                combined.entry(k).or_insert(v);
            }

            if verbose {
                eprintln!("[web] Loaded {} default field values", combined.len());
            }

            Ok(combined)
        }
        _ => Err(Error::validation_failed(
            "Defaults .ncl must export to a JSON object".to_string(),
        )),
    }
}

async fn execute_form(
    config: PathBuf,
    defaults: Option<PathBuf>,
    port: u16,
    output: Option<PathBuf>,
    format: &str,
    cli_locale: &Option<String>,
    open_browser: bool,
) -> Result<()> {
    let mut form = form_parser::load_form(&config)?;

    // Auto-detect format from output filename if not explicitly json
    let actual_format = detect_output_format(output.as_ref(), format);

    // Web backend uses unified elements array internally, migrate if using legacy format
    form.migrate_to_elements();

    // Extract base directory for resolving relative paths in includes
    let base_dir = config.parent().unwrap_or_else(|| std::path::Path::new("."));

    // form = form_parser::expand_includes(form, base_dir)?;  // handled by load_form for .ncl

    // Load default values from JSON, TOML, or .ncl file if provided
    let initial_values = if let Some(defaults_path_input) = defaults {
        // Resolve defaults path with cascading search
        let defaults_path = resolve_file_path(&defaults_path_input, base_dir, false);

        let extension = defaults_path.extension().and_then(|s| s.to_str());
        let is_ncl = extension == Some("ncl");
        let is_toml = extension == Some("toml");

        let defaults_json: std::collections::HashMap<String, serde_json::Value> = if is_ncl {
            // Use helper function to load .ncl defaults
            load_nickel_defaults(&defaults_path, &form.elements, false)?
        } else if is_toml {
            // Read TOML and convert to JSON
            let defaults_content = fs::read_to_string(&defaults_path).map_err(|e| {
                Error::validation_failed(format!("Failed to read defaults file: {}", e))
            })?;
            let toml_value: toml::Value = toml::from_str(&defaults_content).map_err(|e| {
                Error::validation_failed(format!("Failed to parse defaults TOML: {}", e))
            })?;
            // Convert TOML to JSON
            let parsed: serde_json::Value = serde_json::to_value(&toml_value).map_err(|e| {
                Error::validation_failed(format!("Failed to convert TOML to JSON: {}", e))
            })?;
            match parsed {
                serde_json::Value::Object(map) => {
                    // Extract fields from elements (which includes fragments after expand_includes)
                    let form_fields: Vec<form_parser::FieldDefinition> = form
                        .elements
                        .iter()
                        .filter_map(|elem| match elem {
                            form_parser::FormElement::Field(f) => Some(f.as_ref().clone()),
                            _ => None,
                        })
                        .collect();

                    let mut combined = extract_nickel_defaults(&map, &form_fields);
                    let flattened = flatten_json_object(&map);
                    for (k, v) in flattened {
                        combined.entry(k).or_insert(v);
                    }
                    combined
                }
                _ => {
                    return Err(Error::validation_failed(
                        "Defaults TOML must be a table/object".to_string(),
                    ))
                }
            }
        } else {
            // Read JSON directly - combine extraction and flatten
            let defaults_content = fs::read_to_string(&defaults_path).map_err(|e| {
                Error::validation_failed(format!("Failed to read defaults file: {}", e))
            })?;
            let parsed: serde_json::Value =
                serde_json::from_str(&defaults_content).map_err(|e| {
                    Error::validation_failed(format!("Failed to parse defaults JSON: {}", e))
                })?;
            match parsed {
                serde_json::Value::Object(map) => {
                    // Extract fields from elements (which includes fragments after expand_includes)
                    let form_fields: Vec<form_parser::FieldDefinition> = form
                        .elements
                        .iter()
                        .filter_map(|elem| match elem {
                            form_parser::FormElement::Field(f) => Some(f.as_ref().clone()),
                            _ => None,
                        })
                        .collect();

                    let mut combined = extract_nickel_defaults(&map, &form_fields);
                    let flattened = flatten_json_object(&map);
                    for (k, v) in flattened {
                        combined.entry(k).or_insert(v);
                    }
                    combined
                }
                _ => {
                    return Err(Error::validation_failed(
                        "Defaults must be a JSON object".to_string(),
                    ))
                }
            }
        };

        if !defaults_json.is_empty() {
            eprintln!(
                "[web] Loaded {} default field values from --defaults",
                defaults_json.len()
            );
        }
        Some(defaults_json)
    } else {
        None
    };

    // Load I18nBundle if needed
    let i18n_bundle = if form.locale.is_some() || cli_locale.is_some() {
        let config = TypeDialogConfig::default();
        let resolver = LocaleResolver::new(config.clone());
        let form_locale = form.locale.as_deref();
        let locale = resolver.resolve(cli_locale.as_deref(), form_locale);
        let fallback_locale: LanguageIdentifier = "en-US"
            .parse()
            .map_err(|_| Error::validation_failed("Invalid fallback locale".to_string()))?;
        let loader = LocaleLoader::new(config.locales_path);
        Some(I18nBundle::new(locale, fallback_locale, &loader)?)
    } else {
        None
    };

    let mut backend = BackendFactory::create(BackendType::Web { port })?;

    println!("Starting typedialog web server for form: {}", form.name);
    println!("Listening on http://localhost:{}", port);
    if let Some(ref out_path) = output {
        println!("Results will be saved to: {}", out_path.display());
    }

    // Delay browser opening to ensure backend is fully ready
    // Need enough time for execute_form_complete() to load form into state
    let open_browser_clone = open_browser;
    let port_clone = port;
    tokio::spawn(async move {
        if open_browser_clone {
            tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
            let url = format!("http://localhost:{}", port_clone);
            match webbrowser::open(&url) {
                Ok(()) => eprintln!("🌐 Opened browser at {}", url),
                Err(e) => eprintln!("⚠️  Could not open browser: {}", e),
            }
        }
    });

    let results = if let Some(ref bundle) = i18n_bundle {
        form_parser::execute_with_backend_i18n_with_defaults(
            form,
            backend.as_mut(),
            Some(bundle),
            base_dir,
            initial_values,
        )
        .await?
    } else {
        form_parser::execute_with_backend_i18n_with_defaults(
            form,
            backend.as_mut(),
            None,
            base_dir,
            initial_values,
        )
        .await?
    };

    // Save results to output file if specified
    if let Some(output_path_input) = output {
        // Resolve output path with cascading search (will create if doesn't exist)
        let out_path = resolve_file_path(&output_path_input, base_dir, true);
        let output_str = helpers::format_results(&results, &actual_format)?;
        fs::write(&out_path, output_str)
            .map_err(|e| Error::validation_failed(format!("Failed to write output file: {}", e)))?;
        eprintln!("[web] Results saved to: {}", out_path.display());
    }

    Ok(())
}

async fn nickel_roundtrip_cmd(
    input: PathBuf,
    form_path: PathBuf,
    output: PathBuf,
    ncl_template: Option<PathBuf>,
    validate: bool,
    verbose: bool,
    open_browser: bool,
) -> Result<()> {
    use typedialog_core::nickel::{
        ContractParser, NickelCli, NickelTemplateContext, TemplateEngine,
    };

    if verbose {
        eprintln!("Starting Nickel roundtrip workflow with Web backend (interactive)");
    }

    // Step 1: Read input and parse contracts
    if verbose {
        eprintln!("[roundtrip] Reading input: {}", input.display());
    }

    let input_source = fs::read_to_string(&input)
        .map_err(|e| Error::validation_failed(format!("Failed to read input: {}", e)))?;

    let input_contracts = ContractParser::parse_source(&input_source)
        .map_err(|e| Error::validation_failed(format!("Failed to parse contracts: {}", e)))?;

    if verbose {
        eprintln!(
            "[roundtrip] Found {} imports",
            input_contracts.imports.len()
        );
        eprintln!(
            "[roundtrip] Found {} contract calls",
            input_contracts.field_contracts.len()
        );
    }

    // Step 2: Load form definition
    if verbose {
        eprintln!("[roundtrip] Loading form: {}", form_path.display());
    }

    let mut form = form_parser::load_form(&form_path)?;

    // Web backend uses unified elements array internally, migrate if using legacy format
    form.migrate_to_elements();

    let form_base_dir = form_path
        .parent()
        .unwrap_or_else(|| std::path::Path::new("."));

    // form = form_parser::expand_includes(form, form_base_dir)?;  // handled by load_form for .ncl

    // Step 3: Load default values from input .ncl if exists
    let initial_values = if input.exists() {
        Some(load_nickel_defaults(&input, &form.elements, verbose)?)
    } else {
        None
    };

    // Step 4: Execute form with Web backend (same path as normal execute_form)
    if verbose {
        eprintln!("[roundtrip] Starting HTTP server for interactive form");
    }

    let port = 8080;
    let mut backend = BackendFactory::create(BackendType::Web { port })?;

    // Set roundtrip context for web backend so it can show summary page
    if let Some(init_vals) = &initial_values {
        use typedialog_core::backends::web::RoundtripContext;

        // Downcast to WebBackend to access state
        let web_backend = backend
            .as_any()
            .downcast_ref::<typedialog_core::backends::web::WebBackend>()
            .ok_or_else(|| Error::validation_failed("Expected WebBackend"))?;

        if let Some(state) = web_backend.get_state() {
            let context = RoundtripContext {
                initial_values: init_vals.clone(),
                output_path: output.clone(),
                input_nickel: input_source.clone(),
            };
            state.set_roundtrip_context(context).await;
        }
    }

    println!("Starting interactive form on http://localhost:{}", port);
    println!("Complete the form and submit to continue...\n");

    // Delay browser opening to ensure backend is fully ready
    // Need enough time for execute_form_complete() to load form into state
    let open_browser_clone = open_browser;
    let port_clone = port;
    tokio::spawn(async move {
        if open_browser_clone {
            tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
            let url = format!("http://localhost:{}", port_clone);
            match webbrowser::open(&url) {
                Ok(()) => eprintln!("🌐 Opened browser at {}", url),
                Err(e) => eprintln!("⚠️  Could not open browser: {}", e),
            }
        }
    });

    // USE THE SAME EXECUTION PATH AS NORMAL WEB BACKEND
    // This respects display_mode and calls execute_form_complete() when display_mode = "complete"
    let initial_values_backup = initial_values.clone();
    let form_results = form_parser::execute_with_backend_i18n_with_defaults(
        form,
        backend.as_mut(),
        None,
        form_base_dir,
        initial_values,
    )
    .await?;

    if verbose {
        eprintln!(
            "[roundtrip] Form execution produced {} fields",
            form_results.len()
        );
    }

    // Step 4: Generate output Nickel
    if verbose {
        eprintln!("[roundtrip] Rendering output Nickel code");
    }

    let output_nickel = if let Some(template_path) = ncl_template {
        if verbose {
            eprintln!("[roundtrip] Using template: {}", template_path.display());
        }
        let mut engine = TemplateEngine::new();
        engine
            .render_file(template_path.as_path(), &form_results, None)
            .map_err(|e| Error::validation_failed(format!("Failed to render template: {}", e)))?
    } else {
        let ctx = NickelTemplateContext::from_parsed_contracts(
            input_contracts.clone(),
            form_results.clone(),
        );
        ctx.render_full_nickel()
    };

    if verbose {
        eprintln!(
            "[roundtrip] Generated {} bytes of Nickel code",
            output_nickel.len()
        );
    }

    // Step 5: Write output
    if verbose {
        eprintln!("[roundtrip] Writing output: {}", output.display());
    }

    fs::write(&output, &output_nickel)
        .map_err(|e| Error::validation_failed(format!("Failed to write output: {}", e)))?;

    // Step 6: Validate if requested
    let validation_passed = if validate {
        if verbose {
            eprintln!("[roundtrip] Validating with nickel typecheck");
        }
        match NickelCli::typecheck(&output) {
            Ok(()) => {
                if verbose {
                    eprintln!("[roundtrip] ✓ Validation passed");
                }
                true
            }
            Err(e) => {
                if verbose {
                    eprintln!("[roundtrip] ✗ Validation failed: {}", e);
                }
                false
            }
        }
    } else {
        true
    };

    // Create and print terminal summary
    use typedialog_core::nickel::summary::RoundtripSummary;

    let summary = RoundtripSummary::from_values(
        &initial_values_backup.unwrap_or_default(),
        &form_results,
        Some(validation_passed),
        output.display().to_string(),
    );

    // Print terminal summary
    print!("{}", summary.render_terminal(false));

    println!("\n✅ Configuration saved to: {}\n", output.display());
    println!("Next steps:");
    println!("  - Review the configuration: cat {}", output.display());
    println!("  - Apply CI tools: (run your CI setup command)");
    println!("  - Re-run this script anytime to update: .typedialog/ci/ci-configure.sh\n");

    if !validation_passed {
        return Err(Error::validation_failed(
            "Nickel typecheck failed on output",
        ));
    }

    Ok(())
}
