//! 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}\"")),
        ],
    );
    super::ncl_write::atomic_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)?;
    super::ncl_write::atomic_write(path, &updated)?;
    Ok(next_id)
}

/// Update editable fields (title, kind, priority, detail) for an item.
pub struct ItemUpdate<'a> {
    pub title: &'a str,
    pub kind: &'a str,
    pub priority: &'a str,
    pub status: &'a str,
    pub detail: &'a str,
    /// Comma-separated ADR ids; empty string = leave unchanged.
    pub related_adrs: &'a str,
    /// Comma-separated mode ids; empty string = leave unchanged.
    pub related_modes: &'a str,
    /// Enum value without tick; empty string = leave unchanged.
    pub graduates_to: &'a str,
    pub today: &'a str,
}

pub fn update_item(path: &Path, id: &str, upd: &ItemUpdate<'_>) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)?;

    let adrs_val = csv_to_ncl_array(upd.related_adrs);
    let modes_val = csv_to_ncl_array(upd.related_modes);

    let mut fields: Vec<(&str, String)> = vec![
        ("title", format!("\"{}\"", escape_ncl(upd.title))),
        ("kind", format!("'{}", upd.kind)),
        ("priority", format!("'{}", upd.priority)),
        ("status", format!("'{}", upd.status)),
        ("detail", format!("\"{}\"", escape_ncl(upd.detail))),
        ("updated", format!("\"{}\"", upd.today)),
    ];
    if !upd.related_adrs.is_empty() {
        fields.push(("related_adrs", adrs_val));
    }
    if !upd.related_modes.is_empty() {
        fields.push(("related_modes", modes_val));
    }
    if !upd.graduates_to.is_empty() {
        fields.push(("graduates_to", format!("'{}", upd.graduates_to)));
    }

    let pairs: Vec<(&str, &str)> = fields.iter().map(|(k, v)| (*k, v.as_str())).collect();
    let updated = mutate_item_fields(&content, id, &pairs);
    super::ncl_write::atomic_write(path, &updated)?;
    Ok(())
}

/// Convert `"adr-001,adr-002"` → `["adr-001", "adr-002"]` (Nickel array
/// literal).
fn csv_to_ncl_array(csv: &str) -> String {
    if csv.trim().is_empty() {
        return "[]".to_string();
    }
    let items: Vec<String> = csv
        .split(',')
        .map(|s| format!("\"{}\"", escape_ncl(s.trim())))
        .collect();
    format!("[{}]", items.join(", "))
}

/// Remove the item block with the given id from the items array.
pub fn delete_item(path: &Path, id: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let updated = remove_item_block(&content, id)?;
    super::ncl_write::atomic_write(path, &updated)?;
    Ok(())
}

// ── 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)
}

/// Advance `i` past the `},` closing line and an optional trailing blank.
/// Returns the new index after skipping.
fn skip_past_block_close(lines: &[&str], mut i: usize) -> usize {
    while i < lines.len() {
        let closing = lines[i].trim();
        i += 1;
        if closing == "}," {
            break;
        }
    }
    if lines.get(i).is_some_and(|l| l.trim().is_empty()) {
        i += 1;
    }
    i
}

/// Remove the entire `{ ... },` block that contains `id = "<id>"`.
///
/// Scans for the opening `{` on the line before the id field (or the same
/// block boundary), collects lines until the closing `},`, and drops them.
fn remove_item_block(content: &str, id: &str) -> anyhow::Result<String> {
    let id_needle = format!("\"{}\"", id);
    let lines: Vec<&str> = content.lines().collect();
    let mut result: Vec<&str> = Vec::with_capacity(lines.len());
    let mut i = 0;

    while i < lines.len() {
        let trimmed = lines[i].trim();
        let is_block_start = trimmed == "{"
            && (1..=4).any(|d| {
                lines
                    .get(i + d)
                    .is_some_and(|l| l.contains(&id_needle) && l.contains('='))
            });
        if is_block_start {
            i = skip_past_block_close(&lines, i);
            continue;
        }
        result.push(lines[i]);
        i += 1;
    }

    let out = result.join("\n");
    if content.ends_with('\n') && !out.ends_with('\n') {
        Ok(out + "\n")
    } else {
        Ok(out)
    }
}

/// 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");
    }

    #[tokio::test]
    async fn concurrent_add_item_produces_unique_ids() {
        use std::path::PathBuf;
        use std::sync::Arc;

        use tempfile::NamedTempFile;

        const MINIMAL_BACKLOG: &str = "{\n  items = [\n  ],\n}\n";
        const TASKS: usize = 6;

        let lock = Arc::new(super::super::ncl_write::NclWriteLock::new());
        let file = NamedTempFile::new().unwrap();
        std::fs::write(file.path(), MINIMAL_BACKLOG).unwrap();
        let path: Arc<PathBuf> = Arc::new(file.path().to_path_buf());

        let handles: Vec<_> = (0..TASKS)
            .map(|i| {
                let lock = Arc::clone(&lock);
                let path = Arc::clone(&path);
                tokio::spawn(async move {
                    let _guard = lock.acquire(&path).await;
                    add_item(
                        &path,
                        &format!("Task {i}"),
                        "Bug",
                        "High",
                        "detail",
                        "2026-03-13",
                    )
                })
            })
            .collect();

        let mut ids: Vec<String> = Vec::new();
        for h in handles {
            ids.push(h.await.unwrap().unwrap());
        }

        ids.sort();
        let unique_count = {
            let mut d = ids.clone();
            d.dedup();
            d.len()
        };
        assert_eq!(
            unique_count, TASKS,
            "concurrent add_item must produce unique IDs, got: {ids:?}"
        );
    }

    #[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}"
        );
    }
}
