//! In-place mutations of reflection/qa.ncl.
//!
//! Mirrors backlog_ncl.rs — line-level surgery on a predictable Nickel
//! structure. The QA store has a single `entries` array of `QaEntry` records.

use std::path::Path;

/// Append a new Q&A entry to reflection/qa.ncl.
///
/// Returns the generated id (`qa-NNN`).
pub fn add_entry(
    path: &Path,
    question: &str,
    answer: &str,
    actor: &str,
    created_at: &str,
    tags: &[String],
    related: &[String],
) -> anyhow::Result<String> {
    let content = std::fs::read_to_string(path)?;
    let next_id = next_entry_id(&content);

    let block = format!(
        r#"    {{
      id         = "{id}",
      question   = "{question}",
      answer     = "{answer}",
      actor      = "{actor}",
      created_at = "{created_at}",
      tags       = {tags},
      related    = {related},
      verified   = false,
    }},
"#,
        id = next_id,
        question = escape_ncl(question),
        answer = escape_ncl(answer),
        actor = escape_ncl(actor),
        created_at = escape_ncl(created_at),
        tags = ncl_string_array(tags),
        related = ncl_string_array(related),
    );

    let updated = insert_before_entries_close(&content, &block)?;
    super::ncl_write::atomic_write(path, &updated)?;
    Ok(next_id)
}

/// Update `question` and `answer` fields for the entry with `id`.
pub fn update_entry(path: &Path, id: &str, question: &str, answer: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let updated = mutate_entry_fields(
        &content,
        id,
        &[
            ("question", &format!("\"{}\"", escape_ncl(question))),
            ("answer", &format!("\"{}\"", escape_ncl(answer))),
        ],
    );
    super::ncl_write::atomic_write(path, &updated)?;
    Ok(())
}

/// Remove the entry block with `id` from the entries array.
pub fn remove_entry(path: &Path, id: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let updated = delete_entry_block(&content, id)?;
    super::ncl_write::atomic_write(path, &updated)?;
    Ok(())
}

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

/// Replace field values inside the entry block identified by `id`.
///
/// For each `(field, new_value)`, finds the `field = ...,` line inside the
/// block and substitutes the value in-place.
fn mutate_entry_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;
        }

        let trimmed = line.trim_start();
        let replacement = fields.iter().find_map(|(field, new_val)| {
            if !trimmed.starts_with(field) {
                return None;
            }
            let eq_pos = trimmed.find('=')?;
            let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
            let before_eq = trimmed[..eq_pos].trim_end();
            Some(format!("{}{} = {},", indent, before_eq, new_val))
        });
        result.push(replacement.unwrap_or_else(|| line.to_string()));

        if trimmed == "}," {
            in_block = false;
        }
    }

    result.join("\n")
}

/// Remove the block containing `id = "qa-NNN"`.
///
/// Scans line by line, tracking `    {` opens and `    },` closes. Removes the
/// entire block (from the `    {` through `    },` inclusive) that contains the
/// id needle.
fn delete_entry_block(content: &str, id: &str) -> anyhow::Result<String> {
    let id_needle = format!("\"{}\"", id);
    let lines: Vec<&str> = content.lines().collect();
    let n = lines.len();

    // Find the line index containing the id field.
    let id_line = lines
        .iter()
        .position(|l| l.contains(&id_needle) && l.contains('='))
        .ok_or_else(|| anyhow::anyhow!("entry id {} not found in qa.ncl", id))?;

    // Scan backward from id_line to find `    {` (block open — exactly 4 spaces +
    // `{`).
    let block_start = (0..=id_line)
        .rev()
        .find(|&i| lines[i].trim() == "{")
        .ok_or_else(|| anyhow::anyhow!("could not find block open for entry {}", id))?;

    // Scan forward from id_line to find `    },` (block close — trim == `},`).
    let block_end = (id_line..n)
        .find(|&i| lines[i].trim() == "},")
        .ok_or_else(|| anyhow::anyhow!("could not find block close for entry {}", id))?;

    // Reconstruct without [block_start..=block_end].
    let mut result = Vec::with_capacity(n - (block_end - block_start + 1));
    for (i, line) in lines.iter().enumerate() {
        if i < block_start || i > block_end {
            result.push(*line);
        }
    }
    Ok(result.join("\n"))
}

/// Find the highest `qa-NNN` id and return `qa-(NNN+1)` zero-padded to 3
/// digits.
fn next_entry_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("qa-")?;
            num_str.parse::<u32>().ok()
        })
        .max()
        .unwrap_or(0);
    format!("qa-{:03}", max + 1)
}

/// Insert `block` before the closing `  ],` of the entries array.
fn insert_before_entries_close(content: &str, block: &str) -> anyhow::Result<String> {
    let needle = "  ],";
    let pos = content.find(needle).ok_or_else(|| {
        anyhow::anyhow!("could not locate entries array closing `  ],` in qa.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)
}

/// Format a `&[String]` as a Nickel array literal: `["a", "b"]`.
fn ncl_string_array(items: &[String]) -> String {
    if items.is_empty() {
        return "[]".to_string();
    }
    let inner: Vec<String> = items
        .iter()
        .map(|s| format!("\"{}\"", escape_ncl(s)))
        .collect();
    format!("[{}]", inner.join(", "))
}

/// 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::*;

    const SAMPLE: &str = concat!(
        "let s = import \"qa\" in\n",
        "{\n",
        "  entries = [\n",
        "    {\n",
        "      id         = \"qa-001\",\n",
        "      question   = \"What is X?\",\n",
        "      answer     = \"It is Y.\",\n",
        "      actor      = \"human\",\n",
        "      created_at = \"2026-03-12\",\n",
        "      tags       = [],\n",
        "      related    = [],\n",
        "      verified   = false,\n",
        "    },\n",
        "    {\n",
        "      id         = \"qa-002\",\n",
        "      question   = \"Second?\",\n",
        "      answer     = \"Yes.\",\n",
        "      actor      = \"agent\",\n",
        "      created_at = \"2026-03-12\",\n",
        "      tags       = [],\n",
        "      related    = [],\n",
        "      verified   = false,\n",
        "    },\n",
        "  ],\n",
        "} | s.QaStore\n",
    );

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

    #[test]
    fn next_id_increments() {
        let content = r#"id = "qa-005","#;
        assert_eq!(next_entry_id(content), "qa-006");
    }

    #[test]
    fn array_empty() {
        assert_eq!(ncl_string_array(&[]), "[]");
    }

    #[test]
    fn array_values() {
        let v = vec!["a".to_string(), "b".to_string()];
        assert_eq!(ncl_string_array(&v), r#"["a", "b"]"#);
    }

    #[test]
    fn insert_before_close() {
        let content = "let s = import \"qa\" in\n{\n  entries = [\n  ],\n} | s.QaStore\n";
        let block = "    { id = \"qa-001\" },\n";
        let result = insert_before_entries_close(content, block).unwrap();
        assert!(result.contains("{ id = \"qa-001\" }"));
        assert!(result.contains("  ],"));
    }

    #[test]
    fn update_answer() {
        let updated = mutate_entry_fields(SAMPLE, "qa-001", &[("answer", "\"New answer.\"")]);
        assert!(updated.contains("\"New answer.\""), "answer not updated");
        assert!(
            updated.contains("\"Second?\""),
            "qa-002 should be untouched"
        );
    }

    #[test]
    fn delete_first_entry() {
        let updated = delete_entry_block(SAMPLE, "qa-001").unwrap();
        assert!(!updated.contains("qa-001"), "qa-001 should be removed");
        assert!(updated.contains("qa-002"), "qa-002 should remain");
    }

    #[test]
    fn delete_second_entry() {
        let updated = delete_entry_block(SAMPLE, "qa-002").unwrap();
        assert!(updated.contains("qa-001"), "qa-001 should remain");
        assert!(!updated.contains("qa-002"), "qa-002 should be removed");
    }

    #[test]
    fn delete_missing_id_errors() {
        assert!(delete_entry_block(SAMPLE, "qa-999").is_err());
    }

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

        use tempfile::NamedTempFile;

        const MINIMAL_QA: &str = "{\n  entries = [\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_QA).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_entry(
                        &path,
                        &format!("Q{i}?"),
                        "answer",
                        "agent",
                        "2026-03-13",
                        &[],
                        &[],
                    )
                })
            })
            .collect();

        let mut ids: Vec<String> = {
            let mut v = Vec::with_capacity(TASKS);
            for h in handles {
                v.push(h.await.unwrap().unwrap());
            }
            v
        };
        ids.sort();
        ids.dedup();
        assert_eq!(
            ids.len(),
            TASKS,
            "concurrent add_entry must produce unique IDs"
        );
    }
}
