//! Form execution command implementation

use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use typedialog_core::form_parser;
use typedialog_core::helpers;
use typedialog_core::i18n::LocaleLoader;
use typedialog_core::nickel::{NickelCli, TemplateEngine};
use typedialog_core::prelude::*;
use unic_langid::LanguageIdentifier;

use super::helpers::{extract_nickel_defaults, flatten_json_object, print_results};

/// 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()
}

/// 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)
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()
}

#[allow(clippy::too_many_arguments)]
pub async fn execute_form(
    config: PathBuf,
    template: Option<PathBuf>,
    defaults: Option<PathBuf>,
    format: &str,
    output_file: &Option<PathBuf>,
    cli_locale: &Option<String>,
    redact: bool,
    encrypt: bool,
    encryption_backend: String,
    key_file: Option<PathBuf>,
    vault_addr: Option<String>,
    vault_token: Option<String>,
    vault_key_path: Option<String>,
) -> Result<()> {
    let form = form_parser::load_form(&config)?;
    let base_dir = config.parent().unwrap_or_else(|| std::path::Path::new("."));

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

    // 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);
        NickelCli::verify()?;

        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: HashMap<String, serde_json::Value> = if is_ncl {
            let value = NickelCli::export(&defaults_path)?;
            match value {
                serde_json::Value::Object(map) => {
                    let extracted = extract_nickel_defaults(&map, &form.fields);
                    let flattened = flatten_json_object(&map);
                    let mut combined = extracted;
                    for (k, v) in flattened {
                        combined.entry(k).or_insert(v);
                    }
                    combined
                }
                _ => {
                    return Err(Error::validation_failed(
                        "Defaults .ncl must export to a JSON object".to_string(),
                    ))
                }
            }
        } 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) => {
                    let extracted = extract_nickel_defaults(&map, &form.fields);
                    let flattened = flatten_json_object(&map);
                    let mut combined = extracted;
                    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
            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) => {
                    let extracted = extract_nickel_defaults(&map, &form.fields);
                    let flattened = flatten_json_object(&map);
                    let mut combined = extracted;
                    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!(
                "[DEBUG] Loaded {} default field values",
                defaults_json.len()
            );
            for key in defaults_json.keys().take(5) {
                eprintln!("[DEBUG]   - {}", key);
            }
            if defaults_json.len() > 5 {
                eprintln!("[DEBUG]   ... and {} more", defaults_json.len() - 5);
            }
        }
        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
    };

    // Auto-detect backend
    let backend_type = BackendFactory::auto_detect();
    let mut backend = BackendFactory::create(backend_type)?;
    let form_fields = form.fields.clone();

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

    // Generate output
    if let Some(template_path) = template {
        let mut engine = TemplateEngine::new();
        let nickel_output = engine.render_file(template_path.as_path(), &results, None)?;

        if let Some(output_path_input) = output_file {
            // Resolve output path with cascading search
            let path = resolve_file_path(output_path_input, base_dir, true);
            fs::write(&path, &nickel_output).map_err(Error::io)?;
        } else {
            println!("{}", nickel_output);
        }
    } else {
        let encryption_context = if redact {
            helpers::EncryptionContext::redact_only()
        } else if encrypt {
            let mut backend_config = std::collections::HashMap::new();
            if let Some(key) = key_file {
                backend_config.insert("key_file".to_string(), key.to_string_lossy().to_string());
            }
            if let Some(addr) = vault_addr {
                backend_config.insert("vault_addr".to_string(), addr);
            }
            if let Some(token) = vault_token {
                backend_config.insert("vault_token".to_string(), token);
            }
            if let Some(path) = vault_key_path {
                backend_config.insert("vault_key_path".to_string(), path);
            }
            helpers::EncryptionContext::encrypt_with(&encryption_backend, backend_config)
        } else {
            helpers::EncryptionContext::noop()
        };

        let config = TypeDialogConfig::default();
        print_results(
            &results,
            &actual_format,
            output_file,
            &form_fields,
            &encryption_context,
            config.encryption.as_ref(),
        )?;
    }

    Ok(())
}
