use anyhow::{Context, Result, anyhow, bail};
use std::collections::HashSet;
use std::fmt::Write as _;
use std::path::Path;
use toml::Value as TomlValue;
use vtcode_core::config::loader::{ConfigManager, VTCodeConfig};
use vtcode_core::config::{ConfigReadRequest, ConfigService};
use vtcode_core::config::{constants::defaults, constants::model_helpers};
use vtcode_core::llm::{auto_lightweight_model, lightweight_model_choices};
use vtcode_core::ui::theme;

use crate::agent::runloop::unified::config_section_headings::{
    heading_for_path, normalize_config_path,
};

use super::SettingsPaletteState;
use super::docs::{FIELD_DOCS, FieldDoc};
use super::path::{PathToken, get_node, get_node_mut, parse_path_tokens};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ScalarOperation {
    Toggle,
    Increment,
    Decrement,
    CycleNext,
    CyclePrev,
}

pub(super) fn mutate_draft_and_persist<F>(
    state: &mut SettingsPaletteState,
    mutator: F,
) -> Result<()>
where
    F: FnOnce(&mut TomlValue) -> Result<()>,
{
    let previous_draft = state.draft.clone();
    mutate_draft(state, mutator)?;
    if let Err(err) = persist_draft(state) {
        state.draft = previous_draft;
        return Err(err);
    }
    Ok(())
}

pub(super) fn reload_state_from_disk(state: &mut SettingsPaletteState) -> Result<()> {
    if let Ok(response) = ConfigService::read(ConfigReadRequest {
        workspace: state.workspace.clone(),
        runtime_overrides: Vec::new(),
    }) && let Ok(config) = serde_json::from_value::<VTCodeConfig>(response.effective_config)
    {
        state.draft = config;
        if state.source_path.exists() {
            state.source_label = format!("Configuration source: {}", state.source_path.display());
        } else {
            state.source_label = no_config_source_label(&state.workspace);
        }
        return Ok(());
    }

    if state.source_path.exists() {
        let manager = ConfigManager::load_from_file(&state.source_path)
            .with_context(|| format!("Failed to load {}", state.source_path.display()))?;
        state.draft = manager.config().clone();
        state.source_label = format!("Configuration source: {}", state.source_path.display());
        return Ok(());
    }

    let manager = ConfigManager::load_from_workspace(&state.workspace)
        .context("Failed to reload runtime defaults")?;
    state.draft = manager.config().clone();
    state.source_label = no_config_source_label(&state.workspace);
    Ok(())
}

pub(super) fn no_config_source_label(workspace: &Path) -> String {
    format!(
        "No vtcode.toml found for {}. Draft starts from runtime defaults.",
        workspace.display()
    )
}

pub(super) fn add_array_item(root: &mut TomlValue, path: &str) -> Result<()> {
    let node =
        get_node_mut(root, path).ok_or_else(|| anyhow!("Array path '{}' was not found", path))?;

    let TomlValue::Array(values) = node else {
        bail!("Path '{}' is not an array", path);
    };

    let value = default_array_item(path, values);
    values.push(value);
    Ok(())
}

fn default_array_item(path: &str, existing: &[TomlValue]) -> TomlValue {
    if normalize_config_path(path) == "agent.codex_app_server.args" {
        return TomlValue::String("app-server".to_string());
    }

    if normalize_config_path(path) == "custom_providers" {
        return default_custom_provider_item(existing);
    }

    existing
        .first()
        .cloned()
        .unwrap_or_else(|| TomlValue::String(String::new()))
}

fn default_custom_provider_item(existing: &[TomlValue]) -> TomlValue {
    let mut used_names: HashSet<String> = HashSet::new();
    for value in existing {
        let TomlValue::Table(table) = value else {
            continue;
        };
        if let Some(name) = table.get("name").and_then(TomlValue::as_str) {
            used_names.insert(name.to_ascii_lowercase());
        }
    }

    let mut suffix = existing.len().max(1);
    let name = loop {
        let candidate = format!("custom-provider-{}", suffix);
        if !used_names.contains(&candidate) {
            break candidate;
        }
        suffix += 1;
    };

    let mut table = toml::map::Map::new();
    table.insert("name".to_string(), TomlValue::String(name));
    table.insert(
        "display_name".to_string(),
        TomlValue::String(format!("Custom Provider {}", suffix)),
    );
    table.insert(
        "base_url".to_string(),
        TomlValue::String("https://llm.example/v1".to_string()),
    );
    table.insert("api_key_env".to_string(), TomlValue::String(String::new()));
    table.insert("model".to_string(), TomlValue::String(String::new()));
    TomlValue::Table(table)
}

pub(super) fn pop_array_item(root: &mut TomlValue, path: &str) -> Result<()> {
    let node =
        get_node_mut(root, path).ok_or_else(|| anyhow!("Array path '{}' was not found", path))?;

    let TomlValue::Array(values) = node else {
        bail!("Path '{}' is not an array", path);
    };

    if values.pop().is_none() {
        bail!("Array '{}' is already empty", path);
    }

    Ok(())
}

pub(super) fn apply_scalar_operation(
    root: &mut TomlValue,
    path: &str,
    operation: ScalarOperation,
) -> Result<()> {
    let precomputed_cycle_options = matches!(
        operation,
        ScalarOperation::CycleNext | ScalarOperation::CyclePrev
    )
    .then(|| resolve_cycle_options(Some(root), path, ""));
    let Some(node) = get_node_mut(root, path) else {
        return apply_missing_scalar_operation(root, path, operation);
    };

    match node {
        TomlValue::Boolean(value) => {
            if operation != ScalarOperation::Toggle {
                bail!("{} supports toggle only", path);
            }
            *value = !*value;
            Ok(())
        }
        TomlValue::Integer(value) => {
            match operation {
                ScalarOperation::Increment => *value = value.saturating_add(1),
                ScalarOperation::Decrement => *value = value.saturating_sub(1),
                _ => bail!("{} supports numeric increment/decrement", path),
            }
            Ok(())
        }
        TomlValue::Float(value) => {
            match operation {
                ScalarOperation::Increment => *value += 0.1,
                ScalarOperation::Decrement => *value -= 0.1,
                _ => bail!("{} supports numeric increment/decrement", path),
            }
            Ok(())
        }
        TomlValue::String(current) => {
            let options = precomputed_cycle_options
                .clone()
                .unwrap_or_else(|| resolve_cycle_options(None, path, current));
            if options.is_empty() {
                bail!("{} has no predefined values to cycle", path);
            }
            let next = cycle_string_option(current, &options, operation)?;
            *current = next;
            Ok(())
        }
        _ => bail!("{} is not a scalar setting", path),
    }
}

fn apply_missing_scalar_operation(
    root: &mut TomlValue,
    path: &str,
    operation: ScalarOperation,
) -> Result<()> {
    match operation {
        ScalarOperation::CycleNext | ScalarOperation::CyclePrev => {
            let mut options = resolve_cycle_options(Some(root), path, "");
            if options.is_empty() {
                bail!("Settings path '{}' was not found", path);
            }
            options.sort();
            options.dedup();
            let value = match operation {
                ScalarOperation::CycleNext => options.first().cloned(),
                ScalarOperation::CyclePrev => options.last().cloned(),
                _ => None,
            }
            .ok_or_else(|| anyhow!("{} has no predefined values to cycle", path))?;
            insert_missing_string_value(root, path, value)?;
            Ok(())
        }
        _ => bail!("Settings path '{}' was not found", path),
    }
}

fn insert_missing_string_value(root: &mut TomlValue, path: &str, value: String) -> Result<()> {
    let tokens = parse_path_tokens(path)?;
    if tokens.is_empty() {
        bail!("Settings path '{}' was not found", path);
    }

    let mut current = root;
    for token in &tokens[..tokens.len() - 1] {
        match token {
            PathToken::Key(key) => {
                let TomlValue::Table(table) = current else {
                    bail!("Parent path for '{}' is not a table", path);
                };
                current = table
                    .entry(key.clone())
                    .or_insert_with(|| TomlValue::Table(toml::map::Map::new()));
            }
            PathToken::Index(_) => bail!("Cannot create missing array path '{}'", path),
        }
    }

    match tokens.last() {
        Some(PathToken::Key(key)) => {
            let TomlValue::Table(table) = current else {
                bail!("Parent path for '{}' is not a table", path);
            };
            table.insert(key.clone(), TomlValue::String(value));
            Ok(())
        }
        Some(PathToken::Index(_)) => bail!("Cannot create missing array path '{}'", path),
        None => bail!("Settings path '{}' was not found", path),
    }
}

pub(super) fn resolve_cycle_options(
    root: Option<&TomlValue>,
    path: &str,
    current: &str,
) -> Vec<String> {
    match normalize_config_path(path).as_str() {
        "agent.codex_app_server.command" => {
            return codex_sidecar_cycle_options(current, "codex");
        }
        "agent.codex_app_server.args[]" => {
            return codex_sidecar_cycle_options(current, "app-server");
        }
        _ => {}
    }

    if normalize_config_path(path) == "agent.theme" {
        return theme::available_themes()
            .into_iter()
            .map(str::to_string)
            .collect();
    }
    if normalize_config_path(path) == "agent.small_model.model" {
        return lightweight_model_cycle_options(root, current);
    }

    FIELD_DOCS
        .lookup(path)
        .map(|doc| doc.options.clone())
        .filter(|options| !options.is_empty())
        .unwrap_or_else(|| {
            if current.is_empty() {
                Vec::new()
            } else {
                vec![current.to_string()]
            }
        })
}

fn codex_sidecar_cycle_options(current: &str, default: &str) -> Vec<String> {
    let mut options = vec![default.to_string()];
    let trimmed = current.trim();
    if !trimmed.is_empty() && trimmed != default {
        options.push(trimmed.to_string());
    }
    options
}

fn lightweight_model_cycle_options(root: Option<&TomlValue>, current: &str) -> Vec<String> {
    let provider = root
        .and_then(|value| get_node(value, "agent.provider"))
        .and_then(TomlValue::as_str)
        .filter(|value| !value.trim().is_empty())
        .unwrap_or(defaults::DEFAULT_PROVIDER);
    let main_model = root
        .and_then(|value| get_node(value, "agent.default_model"))
        .and_then(TomlValue::as_str)
        .filter(|value| !value.trim().is_empty())
        .map(str::to_string)
        .or_else(|| model_helpers::default_for(provider).map(str::to_string))
        .unwrap_or_else(|| current.to_string());

    let mut options = vec![String::new(), main_model.clone()];
    options.extend(lightweight_model_choices(provider, &main_model));
    if !current.trim().is_empty() {
        options.push(current.to_string());
    }

    let auto_model = auto_lightweight_model(provider, &main_model);
    let mut deduped = Vec::new();
    for option in options {
        if deduped
            .iter()
            .any(|existing: &String| existing.eq_ignore_ascii_case(option.as_str()))
        {
            continue;
        }
        deduped.push(option);
    }

    if let Some(auto_index) = deduped
        .iter()
        .position(|value| value.eq_ignore_ascii_case(auto_model.as_str()))
    {
        let auto = deduped.remove(auto_index);
        deduped.insert(1, auto);
    }

    deduped
}

fn cycle_string_option(
    current: &str,
    options: &[String],
    operation: ScalarOperation,
) -> Result<String> {
    if options.is_empty() {
        bail!("No cycle options available")
    }

    let mut ordered = options.to_vec();
    ordered.sort();
    ordered.dedup();

    let current_index = ordered
        .iter()
        .position(|entry| entry == current)
        .unwrap_or(0);
    let next_index = match operation {
        ScalarOperation::CycleNext => (current_index + 1) % ordered.len(),
        ScalarOperation::CyclePrev => {
            if current_index == 0 {
                ordered.len() - 1
            } else {
                current_index - 1
            }
        }
        _ => bail!("Invalid string cycle operation"),
    };

    Ok(ordered[next_index].clone())
}

pub(super) fn mutate_draft<F>(state: &mut SettingsPaletteState, mutator: F) -> Result<()>
where
    F: FnOnce(&mut TomlValue) -> Result<()>,
{
    let mut draft_value = TomlValue::try_from(state.draft.clone())
        .context("Failed to serialize draft configuration")?;
    mutator(&mut draft_value)?;

    let parsed: VTCodeConfig = draft_value
        .try_into()
        .context("Updated draft configuration is invalid")?;
    parsed
        .validate()
        .context("Updated draft configuration failed validation")?;

    state.draft = parsed;
    Ok(())
}

fn write_commented_config(path: &Path, config: &VTCodeConfig) -> Result<()> {
    let content = render_commented_config(config)?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
    }
    std::fs::write(path, content)
        .with_context(|| format!("Failed to write configuration file {}", path.display()))
}

fn persist_draft(state: &mut SettingsPaletteState) -> Result<()> {
    write_commented_config(&state.source_path, &state.draft)
        .with_context(|| format!("Failed to save {}", state.source_path.display()))?;
    Ok(())
}

pub(super) fn render_commented_config(config: &VTCodeConfig) -> Result<String> {
    let value = ConfigManager::sparse_config_value(config)
        .context("Failed to serialize configuration for comment rendering")?;

    let TomlValue::Table(root_table) = value else {
        bail!("Rendered configuration root is not a TOML table")
    };

    let mut output = String::new();
    output.push_str("# VT Code Configuration File\n");
    output.push_str("# Saved from /config with readable section headings.\n");
    output.push_str(
        "# Every field includes descriptions, defaults, and known choices where available.\n\n",
    );

    render_table_with_comments(&mut output, &root_table, None)?;
    Ok(output)
}

fn render_table_with_comments(
    output: &mut String,
    table: &toml::map::Map<String, TomlValue>,
    prefix: Option<&str>,
) -> Result<()> {
    if let Some(path) = prefix
        && !path.is_empty()
    {
        write_section_comments(output, path);
        writeln!(output, "[{}]", path).context("Failed to render table header")?;
    }

    let mut scalar_keys = Vec::new();
    let mut table_keys = Vec::new();

    for (key, value) in table {
        match value {
            TomlValue::Table(_) => table_keys.push(key),
            _ => scalar_keys.push(key),
        }
    }

    scalar_keys.sort();
    table_keys.sort();

    for key in scalar_keys {
        let Some(value) = table.get(key) else {
            continue;
        };
        let path = build_path(prefix, key);
        write_field_comments(output, &path);

        let rendered = render_key_value(key, value)?;
        output.push_str(rendered.trim_end());
        output.push_str("\n\n");
    }

    for (idx, key) in table_keys.iter().enumerate() {
        let Some(TomlValue::Table(child_table)) = table.get(*key) else {
            continue;
        };
        let path = build_path(prefix, key);
        render_table_with_comments(output, child_table, Some(&path))?;

        if idx + 1 < table_keys.len() {
            output.push('\n');
        }
    }

    Ok(())
}

fn render_key_value(key: &str, value: &TomlValue) -> Result<String> {
    let mut table = toml::map::Map::new();
    table.insert(key.to_string(), value.clone());
    toml::to_string_pretty(&TomlValue::Table(table)).context("Failed to render field to TOML")
}

fn write_section_comments(output: &mut String, path: &str) {
    let heading = heading_for_path(path);
    if !heading.title.is_empty() {
        let _ = writeln!(output, "# {}", heading.title);
    }
    if !heading.summary.is_empty() {
        push_comment_lines(output, &heading.summary);
    }
}

fn write_field_comments(output: &mut String, path: &str) {
    if let Some(doc) = FIELD_DOCS.lookup(path) {
        write_doc_comments(output, doc, true);
    }
}

fn write_doc_comments(output: &mut String, doc: &FieldDoc, include_type_and_options: bool) {
    if !doc.description.is_empty() {
        push_comment_lines(output, &doc.description);
    }
    if include_type_and_options && !doc.options.is_empty() {
        let _ = writeln!(output, "# Possible values: {}", doc.options.join(", "));
    }
    if !doc.default_value.is_empty() {
        let _ = writeln!(output, "# Default: {}", doc.default_value);
    }
    if include_type_and_options && !doc.type_name.is_empty() {
        let _ = writeln!(output, "# Type: {}", doc.type_name);
    }
}

fn push_comment_lines(output: &mut String, description: &str) {
    for line in wrap_comment(description, 100) {
        let _ = writeln!(output, "# {}", line);
    }
}

fn wrap_comment(text: &str, max_width: usize) -> Vec<String> {
    let mut lines = Vec::new();
    let mut current = String::new();

    for word in text.split_whitespace() {
        if current.is_empty() {
            current.push_str(word);
            continue;
        }

        if current.len() + 1 + word.len() > max_width {
            lines.push(current);
            current = word.to_string();
        } else {
            current.push(' ');
            current.push_str(word);
        }
    }

    if !current.is_empty() {
        lines.push(current);
    }

    lines
}

fn build_path(prefix: Option<&str>, key: &str) -> String {
    match prefix {
        Some(prefix) if !prefix.is_empty() => format!("{}.{}", prefix, key),
        _ => key.to_string(),
    }
}
