//! Internal helpers for scaffolding: file/script writing, .gitignore management,
//! and project.toml generation.

use std::fs;
use std::path::Path;

use super::detect::detect_components_toml;
use super::templates::{DEFAULT_AGENTS_TOML, DEFAULT_PROJECT_SETTINGS_TOML};

pub(super) fn generate_project_toml(root: &Path) -> String {
    // TOML requires bare `key = value` settings to appear before any table
    // header. Emitting components (which open with `[[component]]`) first
    // would make every commented top-level setting below them silently
    // attach to the last `[[component]]` table once uncommented, instead of
    // the top-level document — so settings must come first.
    let components = detect_components_toml(root);
    format!("{DEFAULT_PROJECT_SETTINGS_TOML}\n{components}")
}

pub(super) fn write_file_if_missing(path: &Path, content: &str) -> Result<(), String> {
    if path.exists() {
        return Ok(());
    }
    fs::write(path, content).map_err(|e| format!("Failed to write file: {}", e))?;
    Ok(())
}

/// Write `content` to `path` if missing, then ensure the file is executable.
pub(super) fn write_script_if_missing(path: &Path, content: &str) -> Result<(), String> {
    write_file_if_missing(path, content)?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(path)
            .map_err(|e| format!("Failed to read permissions for {}: {}", path.display(), e))?
            .permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms)
            .map_err(|e| format!("Failed to set permissions on {}: {}", path.display(), e))?;
    }

    Ok(())
}

/// Write (or idempotently update) `.huskies/.gitignore` with Story Kit–specific
/// ignore patterns for files that live inside the `.huskies/` directory.
/// Patterns are relative to `.huskies/` as git resolves `.gitignore` files
/// relative to the directory that contains them.
pub(super) fn write_story_kit_gitignore(root: &Path) -> Result<(), String> {
    // Entries that belong inside .huskies/.gitignore (relative to .huskies/).
    let entries = [
        "bot.toml",
        "matrix_store/",
        "matrix_device_id",
        "matrix_history.json",
        "timers.json",
        "worktrees/",
        "merge_workspace/",
        "coverage/",
        "logs/",
        "token_usage.jsonl",
        "wizard_state.json",
        "store.json",
        "pipeline.db",
        "*.db",
    ];

    let gitignore_path = root.join(".huskies").join(".gitignore");
    let existing = if gitignore_path.exists() {
        fs::read_to_string(&gitignore_path)
            .map_err(|e| format!("Failed to read .huskies/.gitignore: {}", e))?
    } else {
        String::new()
    };

    let missing: Vec<&str> = entries
        .iter()
        .copied()
        .filter(|e| !existing.lines().any(|l| l.trim() == *e))
        .collect();

    if missing.is_empty() {
        return Ok(());
    }

    let mut new_content = existing;
    if !new_content.is_empty() && !new_content.ends_with('\n') {
        new_content.push('\n');
    }
    for entry in missing {
        new_content.push_str(entry);
        new_content.push('\n');
    }

    fs::write(&gitignore_path, new_content)
        .map_err(|e| format!("Failed to write .huskies/.gitignore: {}", e))?;

    Ok(())
}

/// Append root-level Story Kit entries to the project `.gitignore`.
/// Only `.huskies_port` and `.mcp.json` remain here because they live at
/// the project root and git does not support `../` patterns in `.gitignore`
/// files, so they cannot be expressed in `.huskies/.gitignore`.
/// `store.json` is excluded via `.huskies/.gitignore` since it now lives
/// inside the `.huskies/` directory.
pub(super) fn append_root_gitignore_entries(root: &Path) -> Result<(), String> {
    let entries = [".huskies_port", ".mcp.json", "PLAN.md"];

    let gitignore_path = root.join(".gitignore");
    let existing = if gitignore_path.exists() {
        fs::read_to_string(&gitignore_path)
            .map_err(|e| format!("Failed to read .gitignore: {}", e))?
    } else {
        String::new()
    };

    let missing: Vec<&str> = entries
        .iter()
        .copied()
        .filter(|e| !existing.lines().any(|l| l.trim() == *e))
        .collect();

    if missing.is_empty() {
        return Ok(());
    }

    let mut new_content = existing;
    if !new_content.is_empty() && !new_content.ends_with('\n') {
        new_content.push('\n');
    }
    for entry in missing {
        new_content.push_str(entry);
        new_content.push('\n');
    }

    fs::write(&gitignore_path, new_content)
        .map_err(|e| format!("Failed to write .gitignore: {}", e))?;

    Ok(())
}

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

    #[test]
    fn generate_project_toml_includes_components_but_not_agents() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").unwrap();

        let toml = generate_project_toml(dir.path());
        // Component section should be present
        assert!(toml.contains("[[component]]"));
        assert!(toml.contains("name = \"server\""));
        // Agent sections must NOT be in project.toml — they go in agents.toml
        assert!(!toml.contains("[[agent]]"));
    }

    #[test]
    fn default_agents_toml_has_coder_qa_mergemaster() {
        assert!(DEFAULT_AGENTS_TOML.contains("[[agent]]"));
        assert!(DEFAULT_AGENTS_TOML.contains("stage = \"coder\""));
        assert!(DEFAULT_AGENTS_TOML.contains("stage = \"qa\""));
        assert!(DEFAULT_AGENTS_TOML.contains("stage = \"mergemaster\""));
    }
}
