//! In-place mutations of reflection/backlog.ncl.
//!
//! The Nickel file format is predictable enough for line-level surgery.
//! We never parse the full AST — we only do targeted replacements inside
//! item blocks identified by their unique `id` field.

use std::path::Path;

/// Update `status` and `updated` fields inside the item block with `id`.
pub fn update_status(path: &Path, id: &str, new_status: &str, today: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let updated = mutate_item_fields(
        &content,
        id,
        &[
            ("status", &format!("'{new_status}")),
            ("updated", &format!("\"{today}\"")),
        ],
    );
    std::fs::write(path, updated)?;
    Ok(())
}

/// Append a new item to the items array.
///
/// Generates the next `bl-NNN` id, formats a Nickel record, and inserts it
/// before the closing `  ],` of the items array.
pub fn add_item(
    path: &Path,
    title: &str,
    kind: &str,
    priority: &str,
    detail: &str,
    today: &str,
) -> anyhow::Result<String> {
    let content = std::fs::read_to_string(path)?;
    let next_id = next_item_id(&content);

    let block = format!(
        r#"    {{
      id       = "{id}",
      title    = "{title}",
      kind     = '{kind},
      priority = '{priority},
      status   = 'Open,
      detail   = "{detail}",
      related_adrs  = [],
      related_modes = [],
      graduates_to  = 'StateTransition,
      created  = "{today}",
      updated  = "{today}",
    }},
"#,
        id = next_id,
        title = escape_ncl(title),
        detail = escape_ncl(detail),
    );

    // Insert before the closing `  ],` of the items array.
    let updated = insert_before_array_close(&content, &block)?;
    std::fs::write(path, updated)?;
    Ok(next_id)
}

// ── helpers ──────────────────────────────────────────────────────────────────

/// Replace the value of one or more fields within the item block identified by
/// `id`.
///
/// For each `(field_name, new_value)` pair, finds the line
/// `<ws>field_name<ws>=<ws>...,` inside the target block and replaces the value
/// in-place.
fn mutate_item_fields(content: &str, id: &str, fields: &[(&str, &str)]) -> String {
    let id_needle = format!("\"{}\"", id);
    let mut in_block = false;
    let mut result: Vec<String> = Vec::with_capacity(content.lines().count() + 1);

    for line in content.lines() {
        if !in_block {
            if line.contains(&id_needle) && line.contains('=') {
                in_block = true;
            }
            result.push(line.to_string());
            continue;
        }

        // Inside the target block — attempt field replacement.
        let trimmed = line.trim_start();
        let mut replaced = false;
        for (field, new_val) in fields {
            if trimmed.starts_with(field) {
                let eq_pos = trimmed.find('=').unwrap_or(usize::MAX);
                // Guard: must have `=` and next non-whitespace char after `=` is the value.
                if eq_pos < trimmed.len() {
                    let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
                    // Preserve original field alignment by keeping spacing up to `=`.
                    let before_eq = &trimmed[..eq_pos];
                    result.push(format!(
                        "{}{} {} {},",
                        indent,
                        before_eq.trim_end(),
                        "=",
                        new_val
                    ));
                    replaced = true;
                    break;
                }
            }
        }
        if !replaced {
            result.push(line.to_string());
        }

        // Detect end of block: a line whose trimmed form is `},`
        if trimmed == "}," {
            in_block = false;
        }
    }

    result.join("\n")
}

/// Find the highest `bl-NNN` id and return `bl-(NNN+1)` zero-padded to 3
/// digits.
fn next_item_id(content: &str) -> String {
    let max = content
        .lines()
        .filter_map(|line| {
            let t = line.trim();
            let rest = t.strip_prefix("id")?;
            let val = rest.split('"').nth(1)?;
            let num_str = val.strip_prefix("bl-")?;
            num_str.parse::<u32>().ok()
        })
        .max()
        .unwrap_or(0);
    format!("bl-{:03}", max + 1)
}

/// Insert `block` before the first occurrence of `  ],` (items array close).
fn insert_before_array_close(content: &str, block: &str) -> anyhow::Result<String> {
    // We look for a line whose trimmed content is `],` — this is the array closing.
    // To avoid matching other arrays, we require exactly two leading spaces.
    let needle = "  ],";
    let pos = content.find(needle).ok_or_else(|| {
        anyhow::anyhow!("could not locate items array closing `  ],` in backlog.ncl")
    })?;
    let mut result = String::with_capacity(content.len() + block.len());
    result.push_str(&content[..pos]);
    result.push_str(block);
    result.push_str(&content[pos..]);
    Ok(result)
}

/// Minimal escaping for string values embedded in Nickel double-quoted strings.
fn escape_ncl(s: &str) -> String {
    s.replace('\\', "\\\\").replace('"', "\\\"")
}

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

    #[test]
    fn next_id_empty() {
        assert_eq!(next_item_id(""), "bl-001");
    }

    #[test]
    fn next_id_increments() {
        let content = r#"id = "bl-003","#;
        assert_eq!(next_item_id(content), "bl-004");
    }

    #[test]
    fn mutate_status() {
        let content = "id = \"bl-001\",\nstatus   = 'Open,\nupdated  = \"2026-01-01\",\n},\n";
        let result = mutate_item_fields(
            content,
            "bl-001",
            &[("status", "'Done"), ("updated", "\"2026-03-12\"")],
        );
        assert!(result.contains("'Done"), "status not updated: {result}");
        assert!(
            result.contains("\"2026-03-12\""),
            "updated not changed: {result}"
        );
    }
}
