use crate::app_config::AppType;
use crate::services::ProviderService;
use serde_json::{json, Value};

use super::codex_config::{
    build_codex_provider_config_toml, clean_codex_provider_key, update_codex_config_snippet,
};
use super::{
    ClaudeApiFormat, GeminiAuthType, ProviderAddFormState, UsageQueryTemplate,
    OPENCLAW_DEFAULT_API_PROTOCOL, OPENCLAW_DEFAULT_USER_AGENT,
};

impl ProviderAddFormState {
    pub fn to_provider_json_value(&self) -> Value {
        let mut provider_obj = match self.extra.clone() {
            Value::Object(map) => map,
            _ => serde_json::Map::new(),
        };

        provider_obj.insert("id".to_string(), json!(self.id.value.trim()));
        provider_obj.insert("name".to_string(), json!(self.name.value.trim()));

        upsert_optional_trimmed(
            &mut provider_obj,
            "websiteUrl",
            self.website_url.value.as_str(),
        );
        upsert_optional_trimmed(&mut provider_obj, "notes", self.notes.value.as_str());

        self.update_provider_meta(&mut provider_obj);

        let settings_value = provider_obj
            .entry("settingsConfig".to_string())
            .or_insert_with(|| json!({}));
        if !settings_value.is_object() {
            *settings_value = json!({});
        }
        let settings_obj = settings_value
            .as_object_mut()
            .expect("settingsConfig must be a JSON object");

        match self.app_type {
            AppType::Claude => {
                let env_value = settings_obj
                    .entry("env".to_string())
                    .or_insert_with(|| json!({}));
                if !env_value.is_object() {
                    *env_value = json!({});
                }
                let env_obj = env_value
                    .as_object_mut()
                    .expect("env must be a JSON object");
                set_or_remove_trimmed(env_obj, "ANTHROPIC_AUTH_TOKEN", &self.claude_api_key.value);
                set_or_remove_trimmed(env_obj, "ANTHROPIC_BASE_URL", &self.claude_base_url.value);
                if self.claude_model_config_touched {
                    set_or_remove_trimmed(env_obj, "ANTHROPIC_MODEL", &self.claude_model.value);
                    set_or_remove_trimmed(
                        env_obj,
                        "ANTHROPIC_REASONING_MODEL",
                        &self.claude_reasoning_model.value,
                    );
                    set_or_remove_trimmed(
                        env_obj,
                        "ANTHROPIC_DEFAULT_HAIKU_MODEL",
                        &self.claude_haiku_model.value,
                    );
                    set_or_remove_trimmed(
                        env_obj,
                        "ANTHROPIC_DEFAULT_SONNET_MODEL",
                        &self.claude_sonnet_model.value,
                    );
                    set_or_remove_trimmed(
                        env_obj,
                        "ANTHROPIC_DEFAULT_OPUS_MODEL",
                        &self.claude_opus_model.value,
                    );
                    env_obj.remove("ANTHROPIC_SMALL_FAST_MODEL");
                }
                settings_obj.remove("api_format");
                settings_obj.remove("openrouter_compat_mode");
                if self.claude_hide_attribution && self.claude_hide_attribution_touched {
                    settings_obj.insert(
                        "attribution".to_string(),
                        json!({
                            "commit": "",
                            "pr": ""
                        }),
                    );
                } else if self.claude_hide_attribution_touched {
                    settings_obj.remove("attribution");
                }
            }
            AppType::Codex => {
                if self.is_codex_official_provider() {
                    let existing_config = settings_obj
                        .get("config")
                        .and_then(|value| value.as_str())
                        .unwrap_or("");
                    let cleaned_config =
                        crate::codex_config::strip_codex_provider_config_text(existing_config)
                            .map_err(|_| ())
                            .unwrap_or_else(|_| existing_config.trim().to_string());
                    settings_obj.insert("config".to_string(), Value::String(cleaned_config));

                    let auth_value = settings_obj
                        .entry("auth".to_string())
                        .or_insert_with(|| json!({}));
                    if !auth_value.is_object() {
                        *auth_value = json!({});
                    }
                } else {
                    let provider_key =
                        clean_codex_provider_key(self.id.value.trim(), self.name.value.trim());
                    let base_url = self.codex_base_url.value.trim().trim_end_matches('/');
                    let model = if self.codex_model.is_blank() {
                        "gpt-5.4"
                    } else {
                        self.codex_model.value.trim()
                    };

                    let existing_config = settings_obj
                        .get("config")
                        .and_then(|value| value.as_str())
                        .unwrap_or("");
                    let base_config = if existing_config.trim().is_empty() {
                        build_codex_provider_config_toml(
                            &provider_key,
                            base_url,
                            model,
                            self.codex_wire_api,
                        )
                    } else {
                        existing_config.to_string()
                    };
                    let config_toml = update_codex_config_snippet(
                        &base_config,
                        base_url,
                        model,
                        self.codex_wire_api,
                        self.codex_requires_openai_auth,
                        self.codex_env_key.value.trim(),
                    );
                    settings_obj.insert("config".to_string(), Value::String(config_toml));

                    let api_key = self.codex_api_key.value.trim();
                    if api_key.is_empty() {
                        if let Some(auth_obj) = settings_obj
                            .get_mut("auth")
                            .and_then(|value| value.as_object_mut())
                        {
                            auth_obj.remove("OPENAI_API_KEY");
                            if auth_obj.is_empty() {
                                settings_obj.remove("auth");
                            }
                        } else {
                            settings_obj.remove("auth");
                        }
                    } else {
                        let auth_value = settings_obj
                            .entry("auth".to_string())
                            .or_insert_with(|| json!({}));
                        if !auth_value.is_object() {
                            *auth_value = json!({});
                        }
                        let auth_obj = auth_value
                            .as_object_mut()
                            .expect("auth must be a JSON object");
                        auth_obj.insert("OPENAI_API_KEY".to_string(), json!(api_key));
                    }
                }
            }
            AppType::Gemini => {
                let env_value = settings_obj
                    .entry("env".to_string())
                    .or_insert_with(|| json!({}));
                if !env_value.is_object() {
                    *env_value = json!({});
                }
                let env_obj = env_value
                    .as_object_mut()
                    .expect("env must be a JSON object");

                match self.gemini_auth_type {
                    GeminiAuthType::OAuth => {
                        env_obj.remove("GEMINI_API_KEY");
                        env_obj.remove("GOOGLE_GEMINI_BASE_URL");
                        env_obj.remove("GEMINI_BASE_URL");
                        env_obj.remove("GEMINI_MODEL");
                    }
                    GeminiAuthType::ApiKey => {
                        set_or_remove_trimmed(
                            env_obj,
                            "GEMINI_API_KEY",
                            &self.gemini_api_key.value,
                        );
                        set_or_remove_trimmed(
                            env_obj,
                            "GOOGLE_GEMINI_BASE_URL",
                            &self.gemini_base_url.value,
                        );
                        set_or_remove_trimmed(env_obj, "GEMINI_MODEL", &self.gemini_model.value);
                    }
                }
            }
            AppType::OpenCode => {
                let npm_package = self.opencode_npm_package.value.trim();
                settings_obj.insert(
                    "npm".to_string(),
                    json!(if npm_package.is_empty() {
                        "@ai-sdk/openai-compatible"
                    } else {
                        npm_package
                    }),
                );

                let options_value = settings_obj
                    .entry("options".to_string())
                    .or_insert_with(|| json!({}));
                if !options_value.is_object() {
                    *options_value = json!({});
                }
                let options_obj = options_value
                    .as_object_mut()
                    .expect("options must be a JSON object");
                set_or_remove_trimmed(options_obj, "apiKey", &self.opencode_api_key.value);
                set_or_remove_trimmed(options_obj, "baseURL", &self.opencode_base_url.value);
                if options_obj.is_empty() {
                    settings_obj.remove("options");
                }

                let mut models_value = settings_obj
                    .remove("models")
                    .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
                if !models_value.is_object() {
                    models_value = Value::Object(serde_json::Map::new());
                }
                let models_obj = models_value
                    .as_object_mut()
                    .expect("models must be a JSON object");

                let current_model_id = self.opencode_primary_model_id();
                if let Some(original_id) = self.opencode_model_original_id.as_deref() {
                    if current_model_id.as_deref() != Some(original_id) {
                        models_obj.remove(original_id);
                    }
                }

                if let Some(model_id) = current_model_id {
                    let mut model_obj = match models_obj.remove(&model_id) {
                        Some(Value::Object(map)) => map,
                        _ => serde_json::Map::new(),
                    };
                    let model_name = self.opencode_model_name.value.trim().to_string();
                    model_obj.insert(
                        "name".to_string(),
                        json!(if model_name.is_empty() {
                            model_id.as_str()
                        } else {
                            model_name.as_str()
                        }),
                    );

                    let limit_value = model_obj
                        .entry("limit".to_string())
                        .or_insert_with(|| Value::Object(serde_json::Map::new()));
                    if !limit_value.is_object() {
                        *limit_value = Value::Object(serde_json::Map::new());
                    }
                    let limit_obj = limit_value
                        .as_object_mut()
                        .expect("limit must be a JSON object");

                    set_or_remove_u64(
                        limit_obj,
                        "context",
                        &self.opencode_model_context_limit.value,
                    );
                    set_or_remove_u64(limit_obj, "output", &self.opencode_model_output_limit.value);
                    if limit_obj.is_empty() {
                        model_obj.remove("limit");
                    }

                    models_obj.insert(model_id, Value::Object(model_obj));
                }

                if !models_obj.is_empty() {
                    settings_obj.insert("models".to_string(), models_value);
                }
            }
            AppType::Hermes => {
                for legacy_key in ["api", "apiKey", "apiMode", "baseUrl", "baseURL", "endpoint"] {
                    settings_obj.remove(legacy_key);
                }

                settings_obj.insert("api_mode".to_string(), json!(self.hermes_api_mode_value()));

                let base_url = self
                    .hermes_base_url
                    .value
                    .trim()
                    .trim_end_matches('/')
                    .to_string();
                set_or_remove_trimmed(settings_obj, "base_url", &base_url);
                set_or_remove_trimmed(settings_obj, "api_key", &self.hermes_api_key.value);

                if self.hermes_models.is_empty() {
                    settings_obj.remove("models");
                } else {
                    settings_obj.insert(
                        "models".to_string(),
                        Value::Array(self.hermes_models.clone()),
                    );
                }

                set_or_remove_f64(
                    settings_obj,
                    "rate_limit_delay",
                    &self.hermes_rate_limit_delay.value,
                );
            }
            AppType::OpenClaw => {
                settings_obj.remove("npm");
                settings_obj.remove("options");
                settings_obj.remove("api_key");
                settings_obj.remove("base_url");

                set_or_remove_trimmed(settings_obj, "apiKey", &self.opencode_api_key.value);
                set_or_remove_trimmed(settings_obj, "baseUrl", &self.opencode_base_url.value);

                let api_value = self.opencode_npm_package.value.trim();
                settings_obj.insert(
                    "api".to_string(),
                    json!(if api_value.is_empty() {
                        OPENCLAW_DEFAULT_API_PROTOCOL
                    } else {
                        api_value
                    }),
                );

                let mut headers_obj = match settings_obj.remove("headers") {
                    Some(Value::Object(map)) => map,
                    _ => serde_json::Map::new(),
                };
                if self.openclaw_user_agent {
                    headers_obj
                        .entry("User-Agent".to_string())
                        .or_insert_with(|| json!(OPENCLAW_DEFAULT_USER_AGENT));
                } else {
                    headers_obj.remove("User-Agent");
                }
                if headers_obj.is_empty() {
                    settings_obj.remove("headers");
                } else {
                    settings_obj.insert("headers".to_string(), Value::Object(headers_obj));
                }

                let mut models = if self.openclaw_models.is_empty() {
                    match settings_obj.remove("models") {
                        Some(Value::Array(items)) => items,
                        _ => Vec::new(),
                    }
                } else {
                    self.openclaw_models.clone()
                };

                let model_id = self.openclaw_primary_model_id();
                match model_id {
                    Some(model_id) => {
                        let mut original_index = self
                            .opencode_model_original_id
                            .as_deref()
                            .and_then(|original_id| openclaw_model_index(&models, original_id));

                        if let Some(existing_index) = openclaw_model_index(&models, &model_id) {
                            if Some(existing_index) != original_index {
                                models.remove(existing_index);
                                if let Some(index) = original_index.as_mut() {
                                    if existing_index < *index {
                                        *index = index.saturating_sub(1);
                                    }
                                }
                            }
                        }

                        let target_index =
                            original_index.or_else(|| openclaw_model_index(&models, &model_id));

                        let mut model_obj = target_index
                            .and_then(|index| models.get(index).cloned())
                            .and_then(|value| value.as_object().cloned())
                            .unwrap_or_default();

                        model_obj.insert("id".to_string(), json!(model_id.clone()));

                        let model_name = self.opencode_model_name.value.trim();
                        if model_name.is_empty() {
                            model_obj.remove("name");
                        } else {
                            model_obj.insert("name".to_string(), json!(model_name));
                        }

                        let context_value = self.opencode_model_context_limit.value.trim();
                        if context_value.is_empty() {
                            model_obj.remove("contextWindow");
                            model_obj.remove("context_window");
                        } else if let Ok(context_window) = context_value.parse::<u32>() {
                            model_obj.remove("context_window");
                            model_obj.insert("contextWindow".to_string(), json!(context_window));
                        }

                        let updated_model = Value::Object(model_obj);
                        if let Some(index) = target_index {
                            models[index] = updated_model;
                        } else {
                            models.push(updated_model);
                        }
                    }
                    None => {
                        if let Some(original_id) = self.opencode_model_original_id.as_deref() {
                            if let Some(index) = openclaw_model_index(&models, original_id) {
                                models.remove(index);
                            }
                        }
                    }
                }

                if models.is_empty() {
                    settings_obj.remove("models");
                } else {
                    settings_obj.insert("models".to_string(), Value::Array(models));
                }
            }
        }

        Value::Object(provider_obj)
    }

    pub fn to_provider_json_value_with_common_config(
        &self,
        common_snippet: &str,
    ) -> Result<Value, String> {
        let mut provider_value = self.to_provider_json_value();
        let snippet = common_snippet.trim();
        if snippet.is_empty() {
            return Ok(provider_value);
        }
        if !ProviderAddFormState::supports_common_config(&self.app_type) {
            return Ok(provider_value);
        }

        let provider: crate::provider::Provider = serde_json::from_value(provider_value.clone())
            .map_err(|e| crate::cli::i18n::texts::tui_toast_invalid_json(&e.to_string()))?;
        let effective_settings = ProviderService::build_effective_live_snapshot(
            &self.app_type,
            &provider,
            Some(snippet),
            true,
        )
        .map_err(|e| e.to_string())?;

        if let Some(settings_value) = provider_value
            .as_object_mut()
            .and_then(|obj| obj.get_mut("settingsConfig"))
        {
            *settings_value = effective_settings;
        }

        Ok(provider_value)
    }

    fn update_provider_meta(&self, provider_obj: &mut serde_json::Map<String, Value>) {
        let should_write_common_config_meta = self.should_write_common_config_meta();
        let should_write_claude_api_format = matches!(
            self.claude_api_format,
            ClaudeApiFormat::OpenAiChat | ClaudeApiFormat::OpenAiResponses
        ) && matches!(self.app_type, AppType::Claude)
            && !self.is_claude_official_provider();

        if !should_write_common_config_meta
            && !should_write_claude_api_format
            && !self.has_usage_script_meta()
            && !provider_obj.get("meta").is_some_and(Value::is_object)
        {
            return;
        }

        let meta_value = provider_obj
            .entry("meta".to_string())
            .or_insert_with(|| json!({}));
        if !meta_value.is_object() {
            *meta_value = json!({});
        }

        let Some(meta_obj) = meta_value.as_object_mut() else {
            return;
        };

        meta_obj.remove("applyCommonConfig");
        if should_write_common_config_meta {
            meta_obj.insert(
                "commonConfigEnabled".to_string(),
                json!(self.include_common_config),
            );
        } else {
            meta_obj.remove("commonConfigEnabled");
        }

        if matches!(self.app_type, AppType::Claude) {
            match self.claude_api_format {
                _ if self.is_claude_official_provider() => {
                    meta_obj.remove("apiFormat");
                }
                ClaudeApiFormat::Anthropic => {
                    meta_obj.remove("apiFormat");
                }
                ClaudeApiFormat::OpenAiChat => {
                    meta_obj.insert("apiFormat".to_string(), json!("openai_chat"));
                }
                ClaudeApiFormat::OpenAiResponses => {
                    meta_obj.insert("apiFormat".to_string(), json!("openai_responses"));
                }
            }
        }

        self.update_usage_script_meta(meta_obj);

        if meta_obj.is_empty() {
            provider_obj.remove("meta");
        }
    }

    fn update_usage_script_meta(&self, meta_obj: &mut serde_json::Map<String, Value>) {
        if !self.has_usage_script_meta() && !self.usage_query_enabled && !self.usage_query_touched {
            meta_obj.remove("usage_script");
            return;
        }

        let mut script = serde_json::Map::new();
        script.insert("enabled".to_string(), json!(self.usage_query_enabled));
        script.insert("language".to_string(), json!("javascript"));
        script.insert("code".to_string(), json!(self.usage_query_code.as_str()));
        script.insert(
            "timeout".to_string(),
            json!(normalize_usage_timeout(&self.usage_query_timeout.value)),
        );
        script.insert(
            "templateType".to_string(),
            json!(self.usage_query_template.as_str()),
        );
        script.insert(
            "autoQueryInterval".to_string(),
            json!(normalize_usage_interval(
                &self.usage_query_auto_interval.value
            )),
        );

        match self.usage_query_template {
            UsageQueryTemplate::General => {
                set_or_remove_trimmed(&mut script, "apiKey", &self.usage_query_api_key.value);
                set_or_remove_trimmed(&mut script, "baseUrl", &self.usage_query_base_url.value);
            }
            UsageQueryTemplate::NewApi => {
                set_or_remove_trimmed(&mut script, "baseUrl", &self.usage_query_base_url.value);
                set_or_remove_trimmed(
                    &mut script,
                    "accessToken",
                    &self.usage_query_access_token.value,
                );
                set_or_remove_trimmed(&mut script, "userId", &self.usage_query_user_id.value);
            }
            UsageQueryTemplate::TokenPlan => {
                set_or_remove_trimmed(
                    &mut script,
                    "codingPlanProvider",
                    &self.usage_query_coding_plan_provider.value,
                );
            }
            UsageQueryTemplate::Custom
            | UsageQueryTemplate::GitHubCopilot
            | UsageQueryTemplate::Balance => {}
        }

        meta_obj.insert("usage_script".to_string(), Value::Object(script));
    }

    pub(crate) fn should_strip_common_config_from_applied_settings_json(&self) -> bool {
        self.include_common_config && self.should_write_common_config_meta()
    }

    fn should_write_common_config_meta(&self) -> bool {
        ProviderAddFormState::supports_common_config(&self.app_type)
            && (!self.mode.is_edit()
                || self.include_common_config_touched
                || self.has_common_config_meta())
    }

    fn has_common_config_meta(&self) -> bool {
        self.extra
            .get("meta")
            .and_then(Value::as_object)
            .is_some_and(|meta| {
                meta.contains_key("commonConfigEnabled") || meta.contains_key("applyCommonConfig")
            })
    }

    pub(super) fn has_usage_script_meta(&self) -> bool {
        self.extra
            .get("meta")
            .and_then(Value::as_object)
            .is_some_and(|meta| meta.contains_key("usage_script"))
    }
}

pub(crate) fn normalize_usage_timeout(raw: &str) -> u64 {
    normalize_usage_number(raw, 10, None)
}

pub(crate) fn normalize_usage_interval(raw: &str) -> u64 {
    normalize_usage_number(raw, 0, Some(1440))
}

fn normalize_usage_number(raw: &str, fallback: u64, max: Option<u64>) -> u64 {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return fallback;
    }

    let Ok(value) = trimmed.parse::<f64>() else {
        return fallback;
    };
    if !value.is_finite() || value < 0.0 {
        return fallback;
    }

    let floored = value.floor();
    let capped = max
        .map(|max| floored.min(max as f64))
        .unwrap_or(floored)
        .max(0.0);
    capped as u64
}

fn openclaw_model_index(models: &[Value], model_id: &str) -> Option<usize> {
    models.iter().position(|model| {
        model
            .get("id")
            .and_then(Value::as_str)
            .map(|id| id == model_id)
            .unwrap_or(false)
    })
}

pub(crate) fn merge_json_values(base: &mut Value, overlay: &Value) {
    match (base, overlay) {
        (Value::Object(base_obj), Value::Object(overlay_obj)) => {
            for (overlay_key, overlay_value) in overlay_obj {
                match base_obj.get_mut(overlay_key) {
                    Some(base_value) => merge_json_values(base_value, overlay_value),
                    None => {
                        base_obj.insert(overlay_key.clone(), overlay_value.clone());
                    }
                }
            }
        }
        (base_value, overlay_value) => {
            *base_value = overlay_value.clone();
        }
    }
}

pub(crate) fn strip_common_config_from_settings(
    app_type: &AppType,
    settings_value: &mut Value,
    common_snippet: &str,
) -> Result<(), String> {
    let snippet = common_snippet.trim();
    if snippet.is_empty() {
        return Ok(());
    }

    match app_type {
        AppType::Claude | AppType::Gemini => {
            *settings_value = ProviderService::remove_common_config_from_settings_for_preview(
                app_type,
                settings_value,
                snippet,
            )
            .map_err(|e| e.to_string())?;
        }
        AppType::OpenCode | AppType::Hermes | AppType::OpenClaw => {}
        AppType::Codex => {
            *settings_value = ProviderService::remove_common_config_from_settings_for_preview(
                app_type,
                settings_value,
                snippet,
            )
            .map_err(|e| e.to_string())?;
        }
    }

    Ok(())
}

pub(crate) fn claude_hide_attribution_enabled(settings_config: &Value) -> bool {
    let Some(attribution) = settings_config
        .get("attribution")
        .and_then(Value::as_object)
    else {
        return false;
    };

    attribution.get("commit").and_then(Value::as_str) == Some("")
        && attribution.get("pr").and_then(Value::as_str) == Some("")
}

pub(crate) fn should_hide_provider_field(key: &str) -> bool {
    matches!(
        key,
        "category"
            | "createdAt"
            | "icon"
            | "iconColor"
            | "inFailoverQueue"
            | "meta"
            | "sortIndex"
            | "updatedAt"
    )
}

#[cfg(test)]
pub(crate) fn strip_provider_internal_fields(value: &Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut out = serde_json::Map::new();
            for (key, value) in map {
                if should_hide_provider_field(key) {
                    continue;
                }
                out.insert(key.clone(), strip_provider_internal_fields(value));
            }
            Value::Object(out)
        }
        Value::Array(items) => {
            Value::Array(items.iter().map(strip_provider_internal_fields).collect())
        }
        other => other.clone(),
    }
}

fn upsert_optional_trimmed(obj: &mut serde_json::Map<String, Value>, key: &str, raw: &str) {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        obj.remove(key);
    } else {
        obj.insert(key.to_string(), json!(trimmed));
    }
}

fn set_or_remove_trimmed(obj: &mut serde_json::Map<String, Value>, key: &str, raw: &str) {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        obj.remove(key);
    } else {
        obj.insert(key.to_string(), json!(trimmed));
    }
}

fn set_or_remove_u64(obj: &mut serde_json::Map<String, Value>, key: &str, raw: &str) {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        obj.remove(key);
    } else if let Ok(value) = trimmed.parse::<u64>() {
        obj.insert(key.to_string(), json!(value));
    } else {
        obj.remove(key);
    }
}

fn set_or_remove_f64(obj: &mut serde_json::Map<String, Value>, key: &str, raw: &str) {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        obj.remove(key);
    } else if let Ok(value) = trimmed.parse::<f64>() {
        if value.is_finite() && value >= 0.0 {
            obj.insert(key.to_string(), json!(value));
        } else {
            obj.remove(key);
        }
    } else {
        obj.remove(key);
    }
}
