#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]

//! Inline TUI editor for `~/.capo/agent/settings.toml`.
//!
//! See spec §4 in `docs/superpowers/specs/2026-05-21-capo-v0.8-design.md`.
//! Pi-style hardcoded row inventory (NOT reflection) — 17 fields across
//! 5 sections. State machine: list-mode vs edit-mode; F2 saves via
//! Command::ApplySettings (rebuild-first ordering).

use std::collections::HashMap;

use capo_agent::settings::Settings;

/// One displayable row in the editor: either a section header (not
/// selectable) or an editable field.
#[derive(Debug, Clone)]
pub enum SettingsRow {
    Section {
        label: &'static str,
    },
    Field {
        section: &'static str,
        key: &'static str,
        value: SettingsValue,
        kind: FieldKind,
    },
}

/// Editable field-value carrier. Mirrors the simple types in `Settings`.
#[derive(Debug, Clone, PartialEq)]
pub enum SettingsValue {
    String(String),
    U32(u32),
    Usize(usize),
    F32(f32),
    Bool(bool),
}

/// What kind of editor the field's row offers when the user presses Enter.
/// Numeric kinds carry their own min bound — different fields have
/// different floors (e.g. `streaming_throttle_ms` accepts 0 for "no
/// throttle"; `max_tokens` must be > 0). NOT a blanket "must be > 0"
/// rule.
#[derive(Debug, Clone)]
pub enum FieldKind {
    PlainText,
    Url,
    NumericU32 { min: u32, max: Option<u32> },
    NumericUsize { min: usize },
    NumericF32 { min: f32, max: f32 },
    Bool,
    Enum { options: &'static [&'static str] },
}

/// Per-row edit-mode cursor. `None` = list mode.
#[derive(Debug, Clone)]
pub enum EditCursor {
    Text { buffer: String, cursor_byte: usize },
    Toggle,
    EnumPicker { current_idx: usize },
}

/// The full editor state — owned by `BottomPane::Settings(...)`.
#[derive(Debug)]
pub struct SettingsState {
    pub original: Settings,
    pub rows: Vec<SettingsRow>,
    pub selected: usize,
    pub edit: Option<EditCursor>,
    pub errors: HashMap<usize, String>,
    pub save_blocked_reason: Option<String>,
}

impl SettingsState {
    pub fn new(settings: Settings) -> Self {
        let rows = rows_from_settings(&settings);
        // First selectable row (skip the initial section header).
        let selected = rows
            .iter()
            .position(|r| matches!(r, SettingsRow::Field { .. }))
            .unwrap_or(0);
        Self {
            original: settings,
            rows,
            selected,
            edit: None,
            errors: HashMap::new(),
            save_blocked_reason: None,
        }
    }
}

const PROVIDER_OPTIONS: &[&str] = &[
    "anthropic",
    "openai",
    "gemini",
    "claude-code",
    "codex-cli",
    "gemini-cli",
];

const LOG_LEVEL_OPTIONS: &[&str] = &["trace", "debug", "info", "warn", "error"];

/// Hardcoded row inventory — 17 fields + 5 section headers = 22 rows.
/// See spec §4.1 for the field list and validation rules.
pub fn rows_from_settings(s: &Settings) -> Vec<SettingsRow> {
    vec![
        SettingsRow::Section { label: "model" },
        SettingsRow::Field {
            section: "model",
            key: "provider",
            value: SettingsValue::String(s.model.provider.clone()),
            kind: FieldKind::Enum {
                options: PROVIDER_OPTIONS,
            },
        },
        SettingsRow::Field {
            section: "model",
            key: "name",
            value: SettingsValue::String(s.model.name.clone()),
            kind: FieldKind::PlainText,
        },
        SettingsRow::Field {
            section: "model",
            key: "max_tokens",
            value: SettingsValue::U32(s.model.max_tokens),
            kind: FieldKind::NumericU32 { min: 1, max: None },
        },
        SettingsRow::Section { label: "anthropic" },
        SettingsRow::Field {
            section: "anthropic",
            key: "base_url",
            value: SettingsValue::String(s.anthropic.base_url.clone()),
            kind: FieldKind::Url,
        },
        SettingsRow::Section { label: "ui" },
        SettingsRow::Field {
            section: "ui",
            key: "theme",
            value: SettingsValue::String(s.ui.theme.clone()),
            kind: FieldKind::PlainText,
        },
        SettingsRow::Field {
            section: "ui",
            key: "streaming_throttle_ms",
            value: SettingsValue::U32(s.ui.streaming_throttle_ms),
            kind: FieldKind::NumericU32 {
                min: 0,
                max: Some(10_000),
            },
        },
        SettingsRow::Field {
            section: "ui",
            key: "footer_show_cost",
            value: SettingsValue::Bool(s.ui.footer_show_cost),
            kind: FieldKind::Bool,
        },
        SettingsRow::Field {
            section: "ui",
            key: "hide_thinking_blocks",
            value: SettingsValue::Bool(s.ui.hide_thinking_blocks),
            kind: FieldKind::Bool,
        },
        SettingsRow::Field {
            section: "ui",
            key: "collapse_tool_output_by_default",
            value: SettingsValue::Bool(s.ui.collapse_tool_output_by_default),
            kind: FieldKind::Bool,
        },
        SettingsRow::Field {
            section: "ui",
            key: "collapse_tool_args_max_lines",
            value: SettingsValue::U32(s.ui.collapse_tool_args_max_lines),
            kind: FieldKind::NumericU32 { min: 0, max: None },
        },
        SettingsRow::Field {
            section: "ui",
            key: "collapse_history_after",
            value: SettingsValue::U32(s.ui.collapse_history_after),
            kind: FieldKind::NumericU32 { min: 0, max: None },
        },
        SettingsRow::Section { label: "session" },
        SettingsRow::Field {
            section: "session",
            key: "autosave",
            value: SettingsValue::Bool(s.session.autosave),
            kind: FieldKind::Bool,
        },
        SettingsRow::Field {
            section: "session",
            key: "compact_at_context_pct",
            value: SettingsValue::F32(s.session.compact_at_context_pct),
            kind: FieldKind::NumericF32 { min: 0.0, max: 1.0 },
        },
        SettingsRow::Field {
            section: "session",
            key: "max_context_tokens",
            value: SettingsValue::Usize(s.session.max_context_tokens),
            kind: FieldKind::NumericUsize { min: 1 },
        },
        SettingsRow::Field {
            section: "session",
            key: "keep_turns",
            value: SettingsValue::Usize(s.session.keep_turns),
            kind: FieldKind::NumericUsize { min: 1 },
        },
        SettingsRow::Section { label: "logging" },
        SettingsRow::Field {
            section: "logging",
            key: "level",
            value: SettingsValue::String(s.logging.level.clone()),
            kind: FieldKind::Enum {
                options: LOG_LEVEL_OPTIONS,
            },
        },
        SettingsRow::Field {
            section: "logging",
            key: "file",
            value: SettingsValue::String(s.logging.file.clone()),
            kind: FieldKind::PlainText,
        },
    ]
}

/// Validate a single field value against its declared kind. Returns
/// Err with a user-displayable message on failure.
pub fn validate(value: &SettingsValue, kind: &FieldKind) -> Result<(), String> {
    match (value, kind) {
        (SettingsValue::String(s), FieldKind::PlainText) => {
            if s.is_empty() {
                Err("must be non-empty".to_string())
            } else {
                Ok(())
            }
        }
        (SettingsValue::String(s), FieldKind::Url) => {
            let has_scheme = s.starts_with("http://") || s.starts_with("https://");
            let has_host = s
                .split_once("://")
                .map(|(_, rest)| !rest.trim().is_empty())
                .unwrap_or(false);
            if has_scheme && has_host {
                Ok(())
            } else {
                Err("must be an http(s) URL".to_string())
            }
        }
        (SettingsValue::U32(n), FieldKind::NumericU32 { min, max }) => {
            if *n < *min {
                Err(format!("must be ≥ {min}"))
            } else if max.is_some_and(|max| *n > max) {
                let max = max.unwrap_or(u32::MAX);
                Err(format!("must be ≤ {max}"))
            } else {
                Ok(())
            }
        }
        (SettingsValue::Usize(n), FieldKind::NumericUsize { min }) => {
            if *n < *min {
                Err(format!("must be ≥ {min}"))
            } else {
                Ok(())
            }
        }
        (SettingsValue::F32(f), FieldKind::NumericF32 { min, max }) => {
            if f.is_nan() || *f < *min || *f > *max {
                Err(format!("must be between {min} and {max}"))
            } else {
                Ok(())
            }
        }
        (SettingsValue::Bool(_), FieldKind::Bool) => Ok(()),
        (SettingsValue::String(s), FieldKind::Enum { options }) => {
            if options.contains(&s.as_str()) {
                Ok(())
            } else {
                Err(format!("must be one of: {}", options.join(", ")))
            }
        }
        (_, _) => Err("internal: value/kind type mismatch".to_string()),
    }
}

/// Validate all field rows. If any fail, populate `state.errors` and
/// `state.save_blocked_reason`, return `None`. Otherwise, build a
/// `Settings` struct from the rows and return
/// `Command::ApplySettings { settings }`.
pub fn try_save(state: &mut SettingsState) -> Option<capo_agent::Command> {
    state.errors.clear();
    state.save_blocked_reason = None;

    for (idx, row) in state.rows.iter().enumerate() {
        if let SettingsRow::Field { value, kind, .. } = row {
            if let Err(msg) = validate(value, kind) {
                state.errors.insert(idx, msg);
            }
        }
    }
    if !state.errors.is_empty() {
        let n = state.errors.len();
        state.save_blocked_reason = Some(format!("fix {n} validation error(s) before saving"));
        return None;
    }

    let settings = build_settings_from_rows(&state.rows, &state.original);
    Some(capo_agent::Command::ApplySettings { settings })
}

/// Reconstruct a `Settings` struct from the editor rows, preserving any
/// fields that weren't part of the editable set by reading from `original`
/// (provides forward-compat if `Settings` grows new fields).
fn build_settings_from_rows(rows: &[SettingsRow], original: &Settings) -> Settings {
    let mut s = original.clone();
    for row in rows {
        if let SettingsRow::Field {
            section,
            key,
            value,
            ..
        } = row
        {
            apply_to_settings(&mut s, section, key, value);
        }
    }
    s
}

fn apply_to_settings(s: &mut Settings, section: &str, key: &str, value: &SettingsValue) {
    match (section, key, value) {
        ("model", "provider", SettingsValue::String(v)) => s.model.provider = v.clone(),
        ("model", "name", SettingsValue::String(v)) => s.model.name = v.clone(),
        ("model", "max_tokens", SettingsValue::U32(v)) => s.model.max_tokens = *v,
        ("anthropic", "base_url", SettingsValue::String(v)) => {
            s.anthropic.base_url = v.clone();
        }
        ("ui", "theme", SettingsValue::String(v)) => s.ui.theme = v.clone(),
        ("ui", "streaming_throttle_ms", SettingsValue::U32(v)) => {
            s.ui.streaming_throttle_ms = *v;
        }
        ("ui", "footer_show_cost", SettingsValue::Bool(v)) => s.ui.footer_show_cost = *v,
        ("ui", "hide_thinking_blocks", SettingsValue::Bool(v)) => {
            s.ui.hide_thinking_blocks = *v;
        }
        ("ui", "collapse_tool_output_by_default", SettingsValue::Bool(v)) => {
            s.ui.collapse_tool_output_by_default = *v;
        }
        ("ui", "collapse_tool_args_max_lines", SettingsValue::U32(v)) => {
            s.ui.collapse_tool_args_max_lines = *v;
        }
        ("ui", "collapse_history_after", SettingsValue::U32(v)) => {
            s.ui.collapse_history_after = *v;
        }
        ("session", "autosave", SettingsValue::Bool(v)) => s.session.autosave = *v,
        ("session", "compact_at_context_pct", SettingsValue::F32(v)) => {
            s.session.compact_at_context_pct = *v;
        }
        ("session", "max_context_tokens", SettingsValue::Usize(v)) => {
            s.session.max_context_tokens = *v;
        }
        ("session", "keep_turns", SettingsValue::Usize(v)) => s.session.keep_turns = *v,
        ("logging", "level", SettingsValue::String(v)) => s.logging.level = v.clone(),
        ("logging", "file", SettingsValue::String(v)) => s.logging.file = v.clone(),
        _ => {
            // Should never happen if rows_from_settings is consistent.
            tracing::error!(
                target: "settings_editor",
                "unknown row section/key combination: {section}/{key}"
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rows_from_default_settings_has_17_fields_5_sections() {
        let s = Settings::default();
        let rows = rows_from_settings(&s);
        let sections = rows
            .iter()
            .filter(|r| matches!(r, SettingsRow::Section { .. }))
            .count();
        let fields = rows
            .iter()
            .filter(|r| matches!(r, SettingsRow::Field { .. }))
            .count();
        assert_eq!(sections, 5, "expected 5 section headers");
        assert_eq!(fields, 17, "expected 17 editable fields (v0.9 adds 4)");
    }

    #[test]
    fn state_new_selects_first_field_row_not_section() {
        let state = SettingsState::new(Settings::default());
        // selected must point at a Field, not a Section.
        match &state.rows[state.selected] {
            SettingsRow::Field { .. } => {}
            SettingsRow::Section { .. } => panic!("selected points at Section"),
        }
    }

    #[test]
    fn rows_capture_current_settings_values() {
        let mut s = Settings::default();
        s.model.name = "claude-opus-4-7".into();
        s.ui.footer_show_cost = false;
        let rows = rows_from_settings(&s);
        let model_name_row = rows
            .iter()
            .find_map(|r| match r {
                SettingsRow::Field {
                    section: "model",
                    key: "name",
                    value: SettingsValue::String(v),
                    ..
                } => Some(v.clone()),
                _ => None,
            })
            .expect("model.name row missing");
        assert_eq!(model_name_row, "claude-opus-4-7");

        let footer_row = rows
            .iter()
            .find_map(|r| match r {
                SettingsRow::Field {
                    section: "ui",
                    key: "footer_show_cost",
                    value: SettingsValue::Bool(v),
                    ..
                } => Some(*v),
                _ => None,
            })
            .expect("ui.footer_show_cost row missing");
        assert!(!footer_row);
    }

    #[test]
    fn apply_to_settings_handles_v0_9_collapse_fields() {
        let mut s = Settings::default();
        apply_to_settings(
            &mut s,
            "ui",
            "hide_thinking_blocks",
            &SettingsValue::Bool(false),
        );
        apply_to_settings(
            &mut s,
            "ui",
            "collapse_tool_output_by_default",
            &SettingsValue::Bool(false),
        );
        apply_to_settings(
            &mut s,
            "ui",
            "collapse_tool_args_max_lines",
            &SettingsValue::U32(10),
        );
        apply_to_settings(
            &mut s,
            "ui",
            "collapse_history_after",
            &SettingsValue::U32(50),
        );
        assert!(!s.ui.hide_thinking_blocks);
        assert!(!s.ui.collapse_tool_output_by_default);
        assert_eq!(s.ui.collapse_tool_args_max_lines, 10);
        assert_eq!(s.ui.collapse_history_after, 50);
    }

    #[test]
    fn validate_plain_text_rejects_empty() {
        let v = SettingsValue::String(String::new());
        assert!(validate(&v, &FieldKind::PlainText).is_err());
    }

    #[test]
    fn validate_plain_text_accepts_nonempty() {
        let v = SettingsValue::String("ok".into());
        assert!(validate(&v, &FieldKind::PlainText).is_ok());
    }

    #[test]
    fn validate_numeric_u32_rejects_below_min() {
        let v = SettingsValue::U32(0);
        assert!(validate(&v, &FieldKind::NumericU32 { min: 1, max: None }).is_err());
    }

    #[test]
    fn validate_numeric_u32_accepts_zero_when_min_zero() {
        let v = SettingsValue::U32(0);
        assert!(validate(&v, &FieldKind::NumericU32 { min: 0, max: None }).is_ok());
    }

    #[test]
    fn validate_numeric_u32_rejects_above_max() {
        let v = SettingsValue::U32(10_001);
        let kind = FieldKind::NumericU32 {
            min: 0,
            max: Some(10_000),
        };
        assert!(validate(&v, &kind).is_err());
    }

    #[test]
    fn validate_url_rejects_non_url() {
        let v = SettingsValue::String("api.anthropic.com".into());
        assert!(validate(&v, &FieldKind::Url).is_err());
    }

    #[test]
    fn validate_url_accepts_https_url() {
        let v = SettingsValue::String("https://api.anthropic.com".into());
        assert!(validate(&v, &FieldKind::Url).is_ok());
    }

    #[test]
    fn validate_numeric_f32_range_rejects_out_of_band() {
        let v = SettingsValue::F32(1.5);
        let kind = FieldKind::NumericF32 { min: 0.0, max: 1.0 };
        assert!(validate(&v, &kind).is_err());
    }

    #[test]
    fn validate_numeric_f32_range_accepts_in_band() {
        let v = SettingsValue::F32(0.85);
        let kind = FieldKind::NumericF32 { min: 0.0, max: 1.0 };
        assert!(validate(&v, &kind).is_ok());
    }

    #[test]
    fn validate_enum_rejects_unknown_option() {
        let v = SettingsValue::String("flubber".into());
        let kind = FieldKind::Enum {
            options: &["anthropic", "openai"],
        };
        assert!(validate(&v, &kind).is_err());
    }

    #[test]
    fn validate_enum_accepts_known_option() {
        let v = SettingsValue::String("openai".into());
        let kind = FieldKind::Enum {
            options: &["anthropic", "openai"],
        };
        assert!(validate(&v, &kind).is_ok());
    }

    #[test]
    fn try_save_returns_apply_settings_on_clean_state() {
        let mut state = SettingsState::new(Settings::default());
        let cmd = try_save(&mut state).expect("save should succeed");
        match cmd {
            capo_agent::Command::ApplySettings { settings } => {
                assert_eq!(settings.model.provider, Settings::default().model.provider);
            }
            other => panic!("expected ApplySettings; got {other:?}"),
        }
        assert!(state.errors.is_empty());
        assert!(state.save_blocked_reason.is_none());
    }

    #[test]
    fn try_save_blocks_with_invalid_field_and_populates_errors() {
        let mut state = SettingsState::new(Settings::default());
        let model_name_idx = state
            .rows
            .iter()
            .position(|r| {
                matches!(
                    r,
                    SettingsRow::Field {
                        section: "model",
                        key: "name",
                        ..
                    }
                )
            })
            .expect("model.name row");
        if let SettingsRow::Field { value, .. } = &mut state.rows[model_name_idx] {
            *value = SettingsValue::String(String::new());
        }

        let result = try_save(&mut state);
        assert!(result.is_none(), "save should be blocked");
        assert_eq!(state.errors.len(), 1);
        assert!(state.errors.contains_key(&model_name_idx));
        assert!(state.save_blocked_reason.is_some());
    }
}
