use crate::path_utils::resolve_symlink_write_paths;
use crate::path_utils::write_atomically;
use anyhow::Context;
use codex_config::CONFIG_TOML_FILE;
use codex_config::types::McpServerConfig;
use codex_config::types::SessionPickerViewMode;
use codex_config::types::ToolSuggestDisabledTool;
use codex_features::FEATURES;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::openai_models::ReasoningEffort;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use tokio::task;
use toml_edit::ArrayOfTables;
use toml_edit::DocumentMut;
use toml_edit::Item as TomlItem;
use toml_edit::Table as TomlTable;
use toml_edit::value;

const NOTICE_TABLE_KEY: &str = "notice";

mod document_helpers;

/// Discrete config mutations supported by the persistence engine.
#[derive(Clone, Debug)]
pub enum ConfigEdit {
    /// Update the active (or default) model selection and optional reasoning effort.
    SetModel {
        model: Option<String>,
        effort: Option<ReasoningEffort>,
    },
    /// Update the service tier preference for future turns.
    SetServiceTier { service_tier: Option<String> },
    /// Update the active (or default) model personality.
    SetModelPersonality { personality: Option<Personality> },
    /// Toggle the acknowledgement flag under `[notice]`.
    SetNoticeHideFullAccessWarning(bool),
    /// Toggle the Windows world-writable directories warning acknowledgement flag.
    SetNoticeHideWorldWritableWarning(bool),
    /// Toggle the rate limit model nudge acknowledgement flag.
    SetNoticeHideRateLimitModelNudge(bool),
    /// Toggle the model migration prompt acknowledgement flag.
    SetNoticeHideModelMigrationPrompt(String, bool),
    /// Toggle the home external config migration prompt acknowledgement flag.
    SetNoticeHideExternalConfigMigrationPromptHome(bool),
    /// Record when the home external config migration prompt was last shown.
    SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(i64),
    /// Toggle the project external config migration prompt acknowledgement flag.
    SetNoticeHideExternalConfigMigrationPromptProject(String, bool),
    /// Record when the project external config migration prompt was last shown.
    SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(String, i64),
    /// Record that a migration prompt was shown for an old->new model mapping.
    RecordModelMigrationSeen { from: String, to: String },
    /// Replace the entire `[mcp_servers]` table.
    ReplaceMcpServers(BTreeMap<String, McpServerConfig>),
    /// Add a disabled tool suggestion under `[tool_suggest].disabled_tools`.
    AddToolSuggestDisabledTool(ToolSuggestDisabledTool),
    /// Set or clear a skill config entry under `[[skills.config]]` by path.
    SetSkillConfig { path: PathBuf, enabled: bool },
    /// Set or clear a skill config entry under `[[skills.config]]` by name.
    SetSkillConfigByName { name: String, enabled: bool },
    /// Set trust_level under `[projects."<path>"]`,
    /// migrating inline tables to explicit tables.
    SetProjectTrustLevel { path: PathBuf, level: TrustLevel },
    /// Set the value stored at the exact dotted path.
    SetPath {
        segments: Vec<String>,
        value: TomlItem,
    },
    /// Remove the value stored at the exact dotted path.
    ClearPath { segments: Vec<String> },
}

#[derive(Clone, Debug, PartialEq, Eq)]
enum SkillConfigSelector {
    Name(String),
    Path(PathBuf),
}

/// Produces a config edit that sets `[tui].theme = "<name>"`.
pub fn syntax_theme_edit(name: &str) -> ConfigEdit {
    ConfigEdit::SetPath {
        segments: vec!["tui".to_string(), "theme".to_string()],
        value: value(name.to_string()),
    }
}

/// Produces a config edit that sets [tui].pet = "<name>".
pub fn tui_pet_edit(name: &str) -> ConfigEdit {
    ConfigEdit::SetPath {
        segments: vec!["tui".to_string(), "pet".to_string()],
        value: value(name.to_string()),
    }
}

/// Produces a config edit that sets `[tui].session_picker_view = "<mode>"`.
pub fn session_picker_view_edit(mode: SessionPickerViewMode) -> ConfigEdit {
    ConfigEdit::SetPath {
        segments: vec!["tui".to_string(), "session_picker_view".to_string()],
        value: value(mode.to_string()),
    }
}

/// Produces a config edit that sets `[tui].status_line` to an explicit ordered list.
///
/// The array is written even when it is empty so "hide the status line" stays
/// distinct from "unset, so use defaults".
pub fn status_line_items_edit(items: &[String]) -> ConfigEdit {
    let array = items.iter().cloned().collect::<toml_edit::Array>();

    ConfigEdit::SetPath {
        segments: vec!["tui".to_string(), "status_line".to_string()],
        value: TomlItem::Value(array.into()),
    }
}

/// Produces a config edit that sets `[tui].status_line_use_colors`.
pub fn status_line_use_colors_edit(enabled: bool) -> ConfigEdit {
    ConfigEdit::SetPath {
        segments: vec!["tui".to_string(), "status_line_use_colors".to_string()],
        value: value(enabled),
    }
}

/// Produces a config edit that sets `[tui].terminal_title` to an explicit ordered list.
///
/// The array is written even when it is empty so "disabled title updates" stays
/// distinct from "unset, so use defaults".
pub fn terminal_title_items_edit(items: &[String]) -> ConfigEdit {
    let array = items.iter().cloned().collect::<toml_edit::Array>();

    ConfigEdit::SetPath {
        segments: vec!["tui".to_string(), "terminal_title".to_string()],
        value: TomlItem::Value(array.into()),
    }
}

fn keymap_binding_value(keys: &[String]) -> TomlItem {
    if let [key] = keys {
        value(key.to_string())
    } else {
        let array = keys.iter().cloned().collect::<toml_edit::Array>();
        TomlItem::Value(array.into())
    }
}

/// Produces a config edit that replaces one root-level TUI keymap binding list.
pub fn keymap_bindings_edit(context: &str, action: &str, keys: &[String]) -> ConfigEdit {
    ConfigEdit::SetPath {
        segments: vec![
            "tui".to_string(),
            "keymap".to_string(),
            context.to_string(),
            action.to_string(),
        ],
        value: keymap_binding_value(keys),
    }
}

/// Produces a config edit that replaces one root-level TUI keymap binding.
pub fn keymap_binding_edit(context: &str, action: &str, key: &str) -> ConfigEdit {
    keymap_bindings_edit(context, action, &[key.to_string()])
}

/// Produces a config edit that removes one root-level TUI keymap binding.
pub fn keymap_binding_clear_edit(context: &str, action: &str) -> ConfigEdit {
    ConfigEdit::ClearPath {
        segments: vec![
            "tui".to_string(),
            "keymap".to_string(),
            context.to_string(),
            action.to_string(),
        ],
    }
}

pub fn model_availability_nux_count_edits(shown_count: &HashMap<String, u32>) -> Vec<ConfigEdit> {
    let mut shown_count_entries: Vec<_> = shown_count.iter().collect();
    shown_count_entries.sort_unstable_by_key(|(left, _)| *left);

    let mut edits = vec![ConfigEdit::ClearPath {
        segments: vec!["tui".to_string(), "model_availability_nux".to_string()],
    }];
    for (model_slug, count) in shown_count_entries {
        edits.push(ConfigEdit::SetPath {
            segments: vec![
                "tui".to_string(),
                "model_availability_nux".to_string(),
                model_slug.clone(),
            ],
            value: value(i64::from(*count)),
        });
    }

    edits
}

struct ConfigDocument {
    doc: DocumentMut,
}

#[derive(Copy, Clone)]
enum TraversalMode {
    Create,
    Existing,
}

impl ConfigDocument {
    fn new(doc: DocumentMut) -> Self {
        Self { doc }
    }

    fn apply(&mut self, edit: &ConfigEdit) -> anyhow::Result<bool> {
        match edit {
            ConfigEdit::SetModel { model, effort } => Ok({
                let mut mutated = false;
                mutated |= self.write_optional_value(
                    &["model"],
                    model.as_ref().map(|model_value| value(model_value.clone())),
                );
                mutated |= self.write_optional_value(
                    &["model_reasoning_effort"],
                    effort.as_ref().map(|effort| value(effort.to_string())),
                );
                mutated
            }),
            ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_optional_value(
                &["service_tier"],
                service_tier.as_ref().map(|service_tier| {
                    // Keep the legacy config spelling stable. Runtime values use
                    // `priority`, but config.toml continues to store it as `fast`.
                    let config_value = match ServiceTier::from_request_value(service_tier) {
                        Some(ServiceTier::Fast) => "fast",
                        Some(ServiceTier::Flex) => "flex",
                        None => service_tier.as_str(),
                    };
                    value(config_value)
                }),
            )),
            ConfigEdit::SetModelPersonality { personality } => Ok(self.write_optional_value(
                &["personality"],
                personality.map(|personality| value(personality.to_string())),
            )),
            ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value(
                &[NOTICE_TABLE_KEY, "hide_full_access_warning"],
                value(*acknowledged),
            )),
            ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged) => Ok(self.write_value(
                &[NOTICE_TABLE_KEY, "hide_world_writable_warning"],
                value(*acknowledged),
            )),
            ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged) => Ok(self.write_value(
                &[NOTICE_TABLE_KEY, "hide_rate_limit_model_nudge"],
                value(*acknowledged),
            )),
            ConfigEdit::SetNoticeHideModelMigrationPrompt(migration_config, acknowledged) => {
                Ok(self.write_value(
                    &[NOTICE_TABLE_KEY, migration_config.as_str()],
                    value(*acknowledged),
                ))
            }
            ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(acknowledged) => Ok(self
                .write_value(
                    &[
                        NOTICE_TABLE_KEY,
                        "external_config_migration_prompts",
                        "home",
                    ],
                    value(*acknowledged),
                )),
            ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(timestamp) => {
                Ok(self.write_value(
                    &[
                        NOTICE_TABLE_KEY,
                        "external_config_migration_prompts",
                        "home_last_prompted_at",
                    ],
                    value(*timestamp),
                ))
            }
            ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject(
                project,
                acknowledged,
            ) => Ok(self.write_value(
                &[
                    NOTICE_TABLE_KEY,
                    "external_config_migration_prompts",
                    "projects",
                    project.as_str(),
                ],
                value(*acknowledged),
            )),
            ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(
                project,
                timestamp,
            ) => Ok(self.write_value(
                &[
                    NOTICE_TABLE_KEY,
                    "external_config_migration_prompts",
                    "project_last_prompted_at",
                    project.as_str(),
                ],
                value(*timestamp),
            )),
            ConfigEdit::RecordModelMigrationSeen { from, to } => Ok(self.write_value(
                &[NOTICE_TABLE_KEY, "model_migrations", from.as_str()],
                value(to.clone()),
            )),
            ConfigEdit::ReplaceMcpServers(servers) => Ok(self.replace_mcp_servers(servers)),
            ConfigEdit::AddToolSuggestDisabledTool(disabled_tool) => {
                Ok(self.add_tool_suggest_disabled_tool(disabled_tool))
            }
            ConfigEdit::SetSkillConfig { path, enabled } => {
                Ok(self.set_skill_config(SkillConfigSelector::Path(path.clone()), *enabled))
            }
            ConfigEdit::SetSkillConfigByName { name, enabled } => {
                Ok(self.set_skill_config(SkillConfigSelector::Name(name.clone()), *enabled))
            }
            ConfigEdit::SetPath { segments, value } => Ok(self.insert(segments, value.clone())),
            ConfigEdit::ClearPath { segments } => Ok(self.clear_owned(segments)),
            ConfigEdit::SetProjectTrustLevel { path, level } => {
                // Delegate to the existing, tested logic in config.rs to
                // ensure tables are explicit and migration is preserved.
                crate::config::set_project_trust_level_inner(
                    &mut self.doc,
                    path.as_path(),
                    *level,
                )?;
                Ok(true)
            }
        }
    }

    fn write_optional_value(&mut self, segments: &[&str], value: Option<TomlItem>) -> bool {
        match value {
            Some(item) => self.write_value(segments, item),
            None => self.clear(segments),
        }
    }

    fn write_value(&mut self, segments: &[&str], value: TomlItem) -> bool {
        let resolved = segments
            .iter()
            .map(|segment| (*segment).to_string())
            .collect::<Vec<_>>();
        self.insert(&resolved, value)
    }

    fn clear(&mut self, segments: &[&str]) -> bool {
        let resolved = segments
            .iter()
            .map(|segment| (*segment).to_string())
            .collect::<Vec<_>>();
        self.remove(&resolved)
    }

    fn add_tool_suggest_disabled_tool(&mut self, disabled_tool: &ToolSuggestDisabledTool) -> bool {
        let disabled_tools_item = self
            .doc
            .get("tool_suggest")
            .and_then(|item| item.as_table_like())
            .and_then(|table| table.get("disabled_tools"));
        let existing_from_array = disabled_tools_item
            .and_then(|item| item.as_value())
            .and_then(|value| value.as_array())
            .into_iter()
            .flat_map(|array| array.iter())
            .filter_map(document_helpers::parse_tool_suggest_disabled_tool);
        let existing_from_tables = disabled_tools_item
            .and_then(|item| match item {
                TomlItem::ArrayOfTables(array) => Some(array),
                _ => None,
            })
            .into_iter()
            .flat_map(|array| array.iter())
            .filter_map(document_helpers::parse_tool_suggest_disabled_tool_table);

        let mut seen = HashSet::new();
        let disabled_tools = existing_from_array
            .chain(existing_from_tables)
            .chain(std::iter::once(disabled_tool.clone()))
            .filter_map(|disabled_tool| disabled_tool.normalized())
            .filter(|disabled_tool| seen.insert(disabled_tool.clone()))
            .collect::<Vec<_>>();
        self.write_value(
            &["tool_suggest", "disabled_tools"],
            document_helpers::tool_suggest_disabled_tools_value(&disabled_tools),
        )
    }

    fn clear_owned(&mut self, segments: &[String]) -> bool {
        self.remove(segments)
    }

    fn replace_mcp_servers(&mut self, servers: &BTreeMap<String, McpServerConfig>) -> bool {
        if servers.is_empty() {
            return self.clear(&["mcp_servers"]);
        }

        let root = self.doc.as_table_mut();
        if !root.contains_key("mcp_servers") {
            root.insert(
                "mcp_servers",
                TomlItem::Table(document_helpers::new_implicit_table()),
            );
        }

        let Some(item) = root.get_mut("mcp_servers") else {
            return false;
        };

        if document_helpers::ensure_table_for_write(item).is_none() {
            *item = TomlItem::Table(document_helpers::new_implicit_table());
        }

        let Some(table) = item.as_table_mut() else {
            return false;
        };

        let keys_to_remove: Vec<String> = table
            .iter()
            .map(|(key, _)| key.to_string())
            .filter(|key| !servers.contains_key(key.as_str()))
            .collect();

        for key in keys_to_remove {
            table.remove(&key);
        }

        for (name, config) in servers {
            if let Some(existing) = table.get_mut(name.as_str()) {
                if let TomlItem::Value(value) = existing
                    && let Some(inline) = value.as_inline_table_mut()
                {
                    let replacement = document_helpers::serialize_mcp_server_inline(config);
                    document_helpers::merge_inline_table(inline, replacement);
                } else {
                    *existing = document_helpers::serialize_mcp_server(config);
                }
            } else {
                table.insert(name, document_helpers::serialize_mcp_server(config));
            }
        }

        true
    }

    fn set_skill_config(&mut self, selector: SkillConfigSelector, enabled: bool) -> bool {
        let selector = match selector {
            SkillConfigSelector::Name(name) => SkillConfigSelector::Name(name.trim().to_string()),
            SkillConfigSelector::Path(path) => {
                SkillConfigSelector::Path(PathBuf::from(normalize_skill_config_path(&path)))
            }
        };
        if matches!(&selector, SkillConfigSelector::Name(name) if name.is_empty()) {
            return false;
        }
        let mut remove_skills_table = false;
        let mut mutated = false;

        {
            let root = self.doc.as_table_mut();
            let skills_item = match root.get_mut("skills") {
                Some(item) => item,
                None => {
                    if enabled {
                        return false;
                    }
                    root.insert(
                        "skills",
                        TomlItem::Table(document_helpers::new_implicit_table()),
                    );
                    let Some(item) = root.get_mut("skills") else {
                        return false;
                    };
                    item
                }
            };

            if document_helpers::ensure_table_for_write(skills_item).is_none() {
                if enabled {
                    return false;
                }
                *skills_item = TomlItem::Table(document_helpers::new_implicit_table());
            }
            let Some(skills_table) = skills_item.as_table_mut() else {
                return false;
            };

            let config_item = match skills_table.get_mut("config") {
                Some(item) => item,
                None => {
                    if enabled {
                        return false;
                    }
                    skills_table.insert("config", TomlItem::ArrayOfTables(ArrayOfTables::new()));
                    let Some(item) = skills_table.get_mut("config") else {
                        return false;
                    };
                    item
                }
            };

            if !matches!(config_item, TomlItem::ArrayOfTables(_)) {
                if enabled {
                    return false;
                }
                *config_item = TomlItem::ArrayOfTables(ArrayOfTables::new());
            }

            let TomlItem::ArrayOfTables(overrides) = config_item else {
                return false;
            };

            let existing_index = overrides.iter().enumerate().find_map(|(idx, table)| {
                skill_config_selector_from_table(table)
                    .filter(|value| value == &selector)
                    .map(|_| idx)
            });

            if enabled {
                if let Some(index) = existing_index {
                    overrides.remove(index);
                    mutated = true;
                    if overrides.is_empty() {
                        skills_table.remove("config");
                        if skills_table.is_empty() {
                            remove_skills_table = true;
                        }
                    }
                }
            } else if let Some(index) = existing_index {
                for (idx, table) in overrides.iter_mut().enumerate() {
                    if idx == index {
                        write_skill_config_selector(table, &selector);
                        table["enabled"] = value(false);
                        mutated = true;
                        break;
                    }
                }
            } else {
                let mut entry = TomlTable::new();
                entry.set_implicit(false);
                write_skill_config_selector(&mut entry, &selector);
                entry["enabled"] = value(false);
                overrides.push(entry);
                mutated = true;
            }
        }

        if remove_skills_table {
            let root = self.doc.as_table_mut();
            root.remove("skills");
        }

        mutated
    }

    fn insert(&mut self, segments: &[String], value: TomlItem) -> bool {
        let Some((last, parents)) = segments.split_last() else {
            return false;
        };

        let Some(parent) = self.descend(parents, TraversalMode::Create) else {
            return false;
        };

        let mut value = value;
        if let Some(existing) = parent.get(last) {
            Self::preserve_decor(existing, &mut value);
        }
        parent[last] = value;
        true
    }

    fn remove(&mut self, segments: &[String]) -> bool {
        let Some((last, parents)) = segments.split_last() else {
            return false;
        };

        let Some(parent) = self.descend(parents, TraversalMode::Existing) else {
            return false;
        };

        parent.remove(last).is_some()
    }

    fn descend(&mut self, segments: &[String], mode: TraversalMode) -> Option<&mut TomlTable> {
        let mut current = self.doc.as_table_mut();

        for segment in segments {
            match mode {
                TraversalMode::Create => {
                    if !current.contains_key(segment.as_str()) {
                        current.insert(
                            segment.as_str(),
                            TomlItem::Table(document_helpers::new_implicit_table()),
                        );
                    }

                    let item = current.get_mut(segment.as_str())?;
                    current = document_helpers::ensure_table_for_write(item)?;
                }
                TraversalMode::Existing => {
                    let item = current.get_mut(segment.as_str())?;
                    current = document_helpers::ensure_table_for_read(item)?;
                }
            }
        }

        Some(current)
    }

    fn preserve_decor(existing: &TomlItem, replacement: &mut TomlItem) {
        match (existing, replacement) {
            (TomlItem::Table(existing_table), TomlItem::Table(replacement_table)) => {
                replacement_table
                    .decor_mut()
                    .clone_from(existing_table.decor());
                for (key, existing_item) in existing_table.iter() {
                    if let (Some(existing_key), Some(mut replacement_key)) =
                        (existing_table.key(key), replacement_table.key_mut(key))
                    {
                        replacement_key
                            .leaf_decor_mut()
                            .clone_from(existing_key.leaf_decor());
                        replacement_key
                            .dotted_decor_mut()
                            .clone_from(existing_key.dotted_decor());
                    }
                    if let Some(replacement_item) = replacement_table.get_mut(key) {
                        Self::preserve_decor(existing_item, replacement_item);
                    }
                }
            }
            (TomlItem::Value(existing_value), TomlItem::Value(replacement_value)) => {
                replacement_value
                    .decor_mut()
                    .clone_from(existing_value.decor());
            }
            _ => {}
        }
    }
}

fn normalize_skill_config_path(path: &Path) -> String {
    dunce::canonicalize(path)
        .unwrap_or_else(|_| path.to_path_buf())
        .to_string_lossy()
        .to_string()
}

fn skill_config_selector_from_table(table: &TomlTable) -> Option<SkillConfigSelector> {
    let path = table
        .get("path")
        .and_then(|item| item.as_str())
        .map(Path::new)
        .map(|path| SkillConfigSelector::Path(PathBuf::from(normalize_skill_config_path(path))));
    let name = table
        .get("name")
        .and_then(|item| item.as_str())
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .map(|name| SkillConfigSelector::Name(name.to_string()));

    match (path, name) {
        (Some(selector), None) | (None, Some(selector)) => Some(selector),
        _ => None,
    }
}

fn write_skill_config_selector(table: &mut TomlTable, selector: &SkillConfigSelector) {
    match selector {
        SkillConfigSelector::Name(name) => {
            table.remove("path");
            table["name"] = value(name.clone());
        }
        SkillConfigSelector::Path(path) => {
            table.remove("name");
            table["path"] = value(path.to_string_lossy().to_string());
        }
    }
}

/// Persist edits using a blocking strategy.
pub fn apply_blocking(codex_home: &Path, edits: &[ConfigEdit]) -> anyhow::Result<()> {
    let config_path = codex_home.join(CONFIG_TOML_FILE);
    apply_blocking_to_resolved_file(&config_path, edits)
}

fn apply_blocking_to_resolved_file(
    resolved_config_file: &Path,
    edits: &[ConfigEdit],
) -> anyhow::Result<()> {
    if edits.is_empty() {
        return Ok(());
    }

    let write_paths = resolve_symlink_write_paths(resolved_config_file)?;
    let serialized = match write_paths.read_path {
        Some(path) => match std::fs::read_to_string(&path) {
            Ok(contents) => contents,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
            Err(err) => return Err(err.into()),
        },
        None => String::new(),
    };

    let doc = if serialized.is_empty() {
        DocumentMut::new()
    } else {
        serialized.parse::<DocumentMut>()?
    };

    let mut document = ConfigDocument::new(doc);
    let mut mutated = false;

    for edit in edits {
        mutated |= document.apply(edit)?;
    }

    if !mutated {
        return Ok(());
    }

    write_atomically(&write_paths.write_path, &document.doc.to_string()).with_context(|| {
        format!(
            "failed to persist config at {}",
            write_paths.write_path.display()
        )
    })?;

    Ok(())
}

/// Persist edits asynchronously by offloading the blocking writer.
///
pub async fn apply(codex_home: &Path, edits: Vec<ConfigEdit>) -> anyhow::Result<()> {
    let codex_home = codex_home.to_path_buf();
    let config_path = codex_home.join(CONFIG_TOML_FILE);
    task::spawn_blocking(move || apply_blocking_to_resolved_file(&config_path, &edits))
        .await
        .context("config persistence task panicked")?
}

/// Fluent builder to batch config edits and apply them atomically.
#[derive(Default)]
pub struct ConfigEditsBuilder {
    config_path: PathBuf,
    edits: Vec<ConfigEdit>,
}

impl ConfigEditsBuilder {
    pub fn new(codex_home: &Path) -> Self {
        Self::for_config_path(&codex_home.join(CONFIG_TOML_FILE))
    }

    pub fn for_config(config: &crate::config::Config) -> Self {
        let config_path = config
            .config_layer_stack
            .get_user_config_file()
            .map(codex_utils_absolute_path::AbsolutePathBuf::to_path_buf)
            .unwrap_or_else(|| config.codex_home.join(CONFIG_TOML_FILE).to_path_buf());
        Self::for_config_path(&config_path)
    }

    pub fn for_config_path(config_path: &Path) -> Self {
        Self {
            config_path: config_path.to_path_buf(),
            edits: Vec::new(),
        }
    }

    pub fn set_model(mut self, model: Option<&str>, effort: Option<ReasoningEffort>) -> Self {
        self.edits.push(ConfigEdit::SetModel {
            model: model.map(ToOwned::to_owned),
            effort,
        });
        self
    }

    pub fn set_service_tier(mut self, service_tier: Option<String>) -> Self {
        self.edits.push(ConfigEdit::SetServiceTier { service_tier });
        self
    }

    pub fn set_personality(mut self, personality: Option<Personality>) -> Self {
        self.edits
            .push(ConfigEdit::SetModelPersonality { personality });
        self
    }

    pub fn set_hide_full_access_warning(mut self, acknowledged: bool) -> Self {
        self.edits
            .push(ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged));
        self
    }

    pub fn set_hide_world_writable_warning(mut self, acknowledged: bool) -> Self {
        self.edits
            .push(ConfigEdit::SetNoticeHideWorldWritableWarning(acknowledged));
        self
    }

    pub fn set_hide_rate_limit_model_nudge(mut self, acknowledged: bool) -> Self {
        self.edits
            .push(ConfigEdit::SetNoticeHideRateLimitModelNudge(acknowledged));
        self
    }

    pub fn set_hide_model_migration_prompt(mut self, model: &str, acknowledged: bool) -> Self {
        self.edits
            .push(ConfigEdit::SetNoticeHideModelMigrationPrompt(
                model.to_string(),
                acknowledged,
            ));
        self
    }

    pub fn set_hide_external_config_migration_prompt_home(mut self, acknowledged: bool) -> Self {
        self.edits
            .push(ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(
                acknowledged,
            ));
        self
    }

    pub fn set_hide_external_config_migration_prompt_project(
        mut self,
        project: &str,
        acknowledged: bool,
    ) -> Self {
        self.edits.push(
            ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject(
                project.to_string(),
                acknowledged,
            ),
        );
        self
    }

    pub fn record_model_migration_seen(mut self, from: &str, to: &str) -> Self {
        self.edits.push(ConfigEdit::RecordModelMigrationSeen {
            from: from.to_string(),
            to: to.to_string(),
        });
        self
    }

    pub fn set_model_availability_nux_count(mut self, shown_count: &HashMap<String, u32>) -> Self {
        self.edits
            .extend(model_availability_nux_count_edits(shown_count));
        self
    }

    pub fn replace_mcp_servers(mut self, servers: &BTreeMap<String, McpServerConfig>) -> Self {
        self.edits
            .push(ConfigEdit::ReplaceMcpServers(servers.clone()));
        self
    }

    pub fn set_project_trust_level<P: Into<PathBuf>>(
        mut self,
        project_path: P,
        trust_level: TrustLevel,
    ) -> Self {
        self.edits.push(ConfigEdit::SetProjectTrustLevel {
            path: project_path.into(),
            level: trust_level,
        });
        self
    }

    /// Enable or disable a feature flag by key under the `[features]` table.
    ///
    /// Disabling a default-false feature clears the key instead of
    /// persisting `false`, so the config does not pin the feature once it
    /// graduates to globally enabled.
    pub fn set_feature_enabled(mut self, key: &str, enabled: bool) -> Self {
        let segments = vec!["features".to_string(), key.to_string()];
        let is_default_false_feature = FEATURES
            .iter()
            .find(|spec| spec.key == key)
            .is_some_and(|spec| !spec.default_enabled);
        if enabled || !is_default_false_feature {
            self.edits.push(ConfigEdit::SetPath {
                segments,
                value: value(enabled),
            });
        } else {
            self.edits.push(ConfigEdit::ClearPath { segments });
        }
        self
    }

    pub fn set_windows_sandbox_mode(mut self, mode: &str) -> Self {
        self.edits.push(ConfigEdit::SetPath {
            segments: vec!["windows".to_string(), "sandbox".to_string()],
            value: value(mode),
        });
        self
    }

    pub fn set_realtime_microphone(mut self, microphone: Option<&str>) -> Self {
        let segments = vec!["audio".to_string(), "microphone".to_string()];
        match microphone {
            Some(microphone) => self.edits.push(ConfigEdit::SetPath {
                segments,
                value: value(microphone),
            }),
            None => self.edits.push(ConfigEdit::ClearPath { segments }),
        }
        self
    }

    pub fn set_realtime_speaker(mut self, speaker: Option<&str>) -> Self {
        let segments = vec!["audio".to_string(), "speaker".to_string()];
        match speaker {
            Some(speaker) => self.edits.push(ConfigEdit::SetPath {
                segments,
                value: value(speaker),
            }),
            None => self.edits.push(ConfigEdit::ClearPath { segments }),
        }
        self
    }

    pub fn set_realtime_voice(mut self, voice: Option<&str>) -> Self {
        let segments = vec!["realtime".to_string(), "voice".to_string()];
        match voice {
            Some(voice) => self.edits.push(ConfigEdit::SetPath {
                segments,
                value: value(voice),
            }),
            None => self.edits.push(ConfigEdit::ClearPath { segments }),
        }
        self
    }

    pub fn clear_legacy_windows_sandbox_keys(mut self) -> Self {
        for key in [
            "experimental_windows_sandbox",
            "elevated_windows_sandbox",
            "enable_experimental_windows_sandbox",
        ] {
            let segments = vec!["features".to_string(), key.to_string()];
            self.edits.push(ConfigEdit::ClearPath { segments });
        }
        self
    }

    pub fn set_session_picker_view(mut self, mode: SessionPickerViewMode) -> Self {
        self.edits.push(ConfigEdit::SetPath {
            segments: vec!["tui".to_string(), "session_picker_view".to_string()],
            value: value(mode.to_string()),
        });
        self
    }

    pub fn with_edits<I>(mut self, edits: I) -> Self
    where
        I: IntoIterator<Item = ConfigEdit>,
    {
        self.edits.extend(edits);
        self
    }

    /// Apply edits on a blocking thread.
    pub fn apply_blocking(self) -> anyhow::Result<()> {
        apply_blocking_to_resolved_file(&self.config_path, &self.edits)
    }

    /// Apply edits asynchronously via a blocking offload.
    pub async fn apply(self) -> anyhow::Result<()> {
        task::spawn_blocking(move || {
            apply_blocking_to_resolved_file(&self.config_path, &self.edits)
        })
        .await
        .context("config persistence task panicked")?
    }
}

#[cfg(test)]
#[path = "edit_tests.rs"]
mod tests;
