// Copyright (c) 2026 Joydev GmbH (joydev.com)
// SPDX-License-Identifier: MIT

use std::fs;
use std::io::Write;
use std::path::Path;
use std::sync::atomic::AtomicBool;

use joy_core::ai_templates;

use crate::color;

static QUIET: AtomicBool = AtomicBool::new(false);

macro_rules! qprintln {
    ($($arg:tt)*) => {
        if !QUIET.load(std::sync::atomic::Ordering::Relaxed) {
            println!($($arg)*);
        }
    };
}

const VISION_TEMPLATE: &str = include_str!("../../docs/vision/README.md");
const ARCHITECTURE_TEMPLATE: &str = include_str!("../../docs/architecture/README.md");
const CONTRIBUTING_TEMPLATE: &str = include_str!("../../docs/CONTRIBUTING.md");

const JOY_BLOCK_START: &str = "<!-- joy:start -->";
const JOY_BLOCK_END: &str = "<!-- joy:end -->";

#[derive(clap::Args)]
pub struct AiArgs {
    #[command(subcommand)]
    command: AiCommand,
}

#[derive(clap::Subcommand)]
enum AiCommand {
    /// Initialize AI tool integration for new tools
    Init(InitArgs),
    /// Update AI tool files to current Joy version
    Update(UpdateArgs),
    /// Remove AI tool configurations from this project
    Reset(ResetArgs),
}

#[derive(clap::Args, Default)]
struct InitArgs {
    /// Path to the architecture doc (e.g. docs/architecture/README.md). Skips the prompt.
    #[arg(long, value_hint = clap::ValueHint::FilePath)]
    architecture: Option<String>,

    /// Path to the vision doc (e.g. docs/vision/README.md). Skips the prompt.
    #[arg(long, value_hint = clap::ValueHint::FilePath)]
    vision: Option<String>,

    /// Path to the contributing doc (e.g. CONTRIBUTING.md). Skips the prompt.
    #[arg(long, value_hint = clap::ValueHint::FilePath)]
    contributing: Option<String>,
}

#[derive(clap::Args)]
struct UpdateArgs {
    /// Only check if files are up to date (exit code 2 if stale), do not write
    #[arg(long)]
    check: bool,
}

#[derive(clap::Args)]
struct ResetArgs {
    /// Only reset a specific tool (claude, qwen, vibe, copilot)
    #[arg(long)]
    tool: Option<String>,

    /// Skip confirmation prompt
    #[arg(long, short)]
    force: bool,
}

pub fn run(args: AiArgs) -> anyhow::Result<()> {
    match args.command {
        AiCommand::Init(a) => ai_init(a),
        AiCommand::Update(a) => update(a),
        AiCommand::Reset(a) => reset(a),
    }
}

/// Run the AI init flow with default prompts. Used by the `joy` welcome
/// wizard after a fresh `joy init`.
pub fn run_init_default() -> anyhow::Result<()> {
    ai_init(InitArgs::default())
}

fn ai_init(args: InitArgs) -> anyhow::Result<()> {
    let root = joy_core::store::find_project_root(&std::env::current_dir()?)
        .ok_or_else(|| anyhow::anyhow!("No Joy project found (run `joy init` first)"))?;

    println!("{}", color::header("AI Init"));
    println!();

    // Ensure project.defaults.yaml exists
    joy_core::embedded::sync_files(&root, joy_core::init::PROJECT_FILES)?;

    check_docs(&root, &args)?;
    let configured_tools = setup_new_tools(&root)?;
    update_gitignore(&root, &configured_tools)?;
    check_nested_projects(&root)?;

    let msg = format!(
        "AI integration complete -- {}",
        color::plural(configured_tools.len(), "tool")
    );
    println!("{}", color::footer(&msg));
    Ok(())
}

fn update(args: UpdateArgs) -> anyhow::Result<()> {
    let root = joy_core::store::find_project_root(&std::env::current_dir()?)
        .ok_or_else(|| anyhow::anyhow!("No Joy project found (run `joy init` first)"))?;

    let dry_run = args.check;
    let header = if dry_run { "AI Status" } else { "AI Update" };

    println!("{}", color::header(header));
    println!();

    if !dry_run {
        joy_core::embedded::sync_files(&root, joy_core::init::PROJECT_FILES)?;
    }

    println!("{}", color::section("AI Tools"));

    let mut tool_count = 0;
    let mut has_issues = false;
    let mut configured_tools: Vec<&str> = Vec::new();

    for (name, id, detect, configure) in ALL_TOOLS {
        let installed = detect();
        let configured = is_tool_configured(&root, id);
        if configured {
            tool_count += 1;
            configured_tools.push(id);
            let member_id = format!("ai:{id}@joy");
            if dry_run {
                let stale = is_tool_stale(&root, id, &member_id)?;
                if stale {
                    has_issues = true;
                    println!(
                        "  {}{:<24} {}",
                        color::warn_mark(),
                        name,
                        color::warning("outdated")
                    );
                } else {
                    println!(
                        "  {}{:<24} {}",
                        color::check_mark(),
                        name,
                        color::inactive("up to date")
                    );
                }
            } else {
                QUIET.store(true, std::sync::atomic::Ordering::Relaxed);
                let changed = configure(&root, &member_id)?;
                QUIET.store(false, std::sync::atomic::Ordering::Relaxed);
                let status = if changed {
                    color::success("updated")
                } else {
                    color::inactive("up to date")
                };
                println!("  {}{:<24} {}", color::check_mark(), name, status);
            }
        } else if installed {
            println!(
                "  {}{:<24} {}",
                color::warn_mark(),
                name,
                color::warning("installed, not configured")
            );
        } else {
            println!("    {:<24} {}", name, color::inactive("not installed"));
        }
    }

    if tool_count == 0 && !dry_run {
        println!(
            "  {}No configured AI tools found. Run {} first.",
            color::warn_mark(),
            color::label("joy ai init")
        );
    }

    // Sync gitignore entries for configured tools
    if !dry_run {
        update_gitignore(&root, &configured_tools)?;
    }

    println!();
    let msg = if dry_run {
        format!(
            "{} · {}",
            if has_issues {
                format!("files need update -- run {}", color::label("joy ai update"))
            } else {
                "all up to date".to_string()
            },
            color::plural(tool_count, "tool")
        )
    } else {
        format!(
            "AI update complete -- {}",
            color::plural(tool_count, "tool")
        )
    };
    println!("{}", color::footer(&msg));

    if dry_run && has_issues {
        std::process::exit(2);
    }
    Ok(())
}

/// Check if a tool's generated files are up to date by re-rendering expected
/// content and comparing against on-disk files.
fn is_tool_stale(root: &Path, tool: &str, member_id: &str) -> anyhow::Result<bool> {
    let workflow = ai_templates::load_workflow()?;
    let agents = ai_templates::load_agents()?;

    // Check SKILL.md (all tools except copilot)
    let skill_path = match tool {
        "claude" => Some(root.join(".claude/skills/joy/SKILL.md")),
        "qwen" => Some(root.join(".qwen/skills/joy/SKILL.md")),
        "vibe" => Some(root.join(".vibe/skills/joy/SKILL.md")),
        _ => None,
    };
    if let Some(path) = skill_path {
        let expected = ai_templates::render_skill(&workflow)?;
        if !file_matches(&path, &expected) {
            return Ok(true);
        }
    }

    // Check setup.md (all tools except copilot)
    let setup_path = match tool {
        "claude" => Some(root.join(".claude/skills/joy/setup.md")),
        "qwen" => Some(root.join(".qwen/skills/joy/setup.md")),
        "vibe" => Some(root.join(".vibe/skills/joy/setup.md")),
        _ => None,
    };
    if let Some(path) = setup_path {
        if !file_matches(&path, ai_templates::setup_instructions()) {
            return Ok(true);
        }
    }

    // Check instruction files (joy-block content)
    let block_path = match tool {
        "claude" => Some(root.join(".claude/CLAUDE.md")),
        "qwen" => Some(root.join(".qwen/QWEN.md")),
        "copilot" => Some(root.join(".github/copilot-instructions.md")),
        _ => None,
    };
    if let Some(path) = block_path {
        let has_skill = tool != "copilot";
        let expected_block = render_managed_block(member_id, has_skill)?;
        if !joy_block_matches(&path, &expected_block) {
            return Ok(true);
        }
    }

    // Check copilot prompt
    if tool == "copilot" {
        let expected = ai_templates::render_copilot_prompt(&workflow)?;
        if !file_matches(&root.join(".github/prompts/joy.prompt.md"), &expected) {
            return Ok(true);
        }
    }

    // Check agent files
    for agent in &agents {
        if !ai_templates::agent_applicable_to_tool(agent, tool) {
            continue;
        }
        if let Some(filename) = ai_templates::agent_filename(agent, tool) {
            let expected = ai_templates::render_agent(agent, &workflow, tool)?;
            let agents_dir = match tool {
                "claude" => ".claude/agents",
                "qwen" => ".qwen/agents",
                "vibe" => ".vibe/agents",
                "copilot" => ".github/agents",
                _ => continue,
            };
            if !file_matches(&root.join(agents_dir).join(&filename), &expected) {
                return Ok(true);
            }
        }
    }

    Ok(false)
}

/// Check if a file's content matches the expected content exactly.
fn file_matches(path: &Path, expected: &str) -> bool {
    match fs::read_to_string(path) {
        Ok(content) => content == expected,
        Err(_) => false,
    }
}

/// Check if the joy-block inside a file matches the expected block content.
fn joy_block_matches(path: &Path, expected_block: &str) -> bool {
    let content = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(_) => return false,
    };
    let expected_wrapped = format!("{}\n{}\n{}", JOY_BLOCK_START, expected_block, JOY_BLOCK_END);
    content.contains(&expected_wrapped)
}

/// One configurable doc the project tracks for AI tools.
struct DocSpec {
    /// Logical key under `project.docs.*` (architecture / vision / contributing).
    key: &'static str,
    /// Human label shown in the prompt.
    label: &'static str,
    /// Why this doc helps the AI (shown when offering a template stub).
    purpose: &'static str,
    /// Built-in default path used when nothing is configured / scanned.
    default_path: &'static str,
    /// Candidate paths to scan in priority order. Used to suggest existing docs
    /// in repos that already follow a different convention.
    candidates: &'static [&'static str],
    /// Embedded template content to seed a missing file.
    template: &'static str,
}

const DOC_SPECS: &[DocSpec] = &[
    DocSpec {
        key: "vision",
        label: "Vision",
        purpose: "product goals and design decisions",
        default_path: joy_core::model::project::Docs::DEFAULT_VISION,
        candidates: &[
            "docs/dev/vision/README.md",
            "docs/vision/README.md",
            "docs/vision.md",
            "VISION.md",
        ],
        template: VISION_TEMPLATE,
    },
    DocSpec {
        key: "architecture",
        label: "Architecture",
        purpose: "technical stack and structure",
        default_path: joy_core::model::project::Docs::DEFAULT_ARCHITECTURE,
        candidates: &[
            "docs/dev/architecture/README.md",
            "docs/architecture/README.md",
            "docs/architecture.md",
            "ARCHITECTURE.md",
        ],
        template: ARCHITECTURE_TEMPLATE,
    },
    DocSpec {
        key: "contributing",
        label: "Contributing",
        purpose: "coding conventions and commit messages",
        default_path: joy_core::model::project::Docs::DEFAULT_CONTRIBUTING,
        candidates: &[
            "CONTRIBUTING.md",
            "docs/CONTRIBUTING.md",
            ".github/CONTRIBUTING.md",
        ],
        template: CONTRIBUTING_TEMPLATE,
    },
];

fn check_docs(root: &Path, args: &InitArgs) -> anyhow::Result<()> {
    use joy_core::model::Project;

    println!("{}", color::section("Documentation"));

    let project_path = joy_core::store::joy_dir(root).join(joy_core::store::PROJECT_FILE);
    let mut project: Project = joy_core::store::read_yaml(&project_path)?;
    let mut project_changed = false;
    let mut all_found = true;

    for spec in DOC_SPECS {
        let configured = match spec.key {
            "vision" => project.docs.vision.clone(),
            "architecture" => project.docs.architecture.clone(),
            "contributing" => project.docs.contributing.clone(),
            _ => unreachable!(),
        };
        let flag_override = match spec.key {
            "vision" => args.vision.clone(),
            "architecture" => args.architecture.clone(),
            "contributing" => args.contributing.clone(),
            _ => unreachable!(),
        };

        let chosen = resolve_doc_path(root, spec, configured.as_deref(), flag_override.as_deref())?;

        // Persist non-default choices so AI tools and future runs see them.
        let to_store = if chosen == spec.default_path {
            None
        } else {
            Some(chosen.clone())
        };
        if to_store != configured {
            match spec.key {
                "vision" => project.docs.vision = to_store,
                "architecture" => project.docs.architecture = to_store,
                "contributing" => project.docs.contributing = to_store,
                _ => unreachable!(),
            }
            project_changed = true;
        }

        let full = root.join(&chosen);
        if full.is_file() {
            println!("  {}{}", color::check_mark(), chosen);
        } else {
            println!("  {}{}", color::cross_mark(), color::warning(&chosen));
            let name = chosen.rsplit('/').next().unwrap_or(&chosen);
            print!(
                "    {} helps AI understand your {}. Create template? [Y/n] ",
                name, spec.purpose
            );
            std::io::stdout().flush()?;
            let mut input = String::new();
            std::io::stdin().read_line(&mut input)?;
            let trimmed = input.trim();
            if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("y") {
                if let Some(parent) = full.parent() {
                    fs::create_dir_all(parent)?;
                }
                fs::write(&full, spec.template)?;
                println!(
                    "    {}Created {} (template -- your AI tool will help fill it in)",
                    color::check_mark(),
                    chosen
                );
            }
            all_found = false;
        }
    }

    if project_changed {
        joy_core::store::write_yaml_preserve(&project_path, &project)?;
    }

    if !all_found {
        println!(
            "\n  {}Your AI tool will offer to fill in empty templates on first use.",
            color::warn_mark()
        );
    }
    println!();

    Ok(())
}

/// Pick a suggestion for a doc path: the first candidate that exists, or the
/// built-in default if none match. Does no other IO.
fn suggested_doc_path<'a>(root: &Path, spec: &'a DocSpec) -> &'a str {
    spec.candidates
        .iter()
        .find(|p| root.join(p).is_file())
        .copied()
        .unwrap_or(spec.default_path)
}

/// Decide which path to use for a given doc spec.
///
/// Precedence:
/// 1. Flag value passed to `joy ai init` (non-interactive).
/// 2. Path already stored in `project.docs` (no prompt unless missing on disk).
/// 3. Interactive prompt with a suggestion (first existing candidate, else default).
fn resolve_doc_path(
    root: &Path,
    spec: &DocSpec,
    configured: Option<&str>,
    flag: Option<&str>,
) -> anyhow::Result<String> {
    if let Some(flag_value) = flag {
        let trimmed = flag_value.trim();
        if !trimmed.is_empty() {
            return Ok(trimmed.to_string());
        }
    }
    if let Some(value) = configured {
        return Ok(value.to_string());
    }

    let suggestion = suggested_doc_path(root, spec);
    print!("    {} doc path [{}]: ", spec.label, suggestion);
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let trimmed = input.trim();
    if trimmed.is_empty() {
        Ok(suggestion.to_string())
    } else {
        Ok(trimmed.to_string())
    }
}

fn reset(args: ResetArgs) -> anyhow::Result<()> {
    let root = joy_core::store::find_project_root(&std::env::current_dir()?)
        .ok_or_else(|| anyhow::anyhow!("No Joy project found (run `joy init` first)"))?;

    let all_tools: &[(&str, &str, &[&str])] = &[
        ("Claude Code", "claude", &[".claude/"]),
        ("Qwen Code", "qwen", &[".qwen/"]),
        ("Mistral Vibe", "vibe", &[".vibe/"]),
        (
            "GitHub Copilot",
            "copilot",
            &[
                ".github/copilot-instructions.md",
                ".github/agents/",
                ".github/prompts/",
            ],
        ),
    ];

    let tools: Vec<_> = if let Some(ref filter) = args.tool {
        let found = all_tools.iter().find(|(_, id, _)| id == filter);
        match found {
            Some(t) => vec![*t],
            None => {
                let valid: Vec<_> = all_tools.iter().map(|(_, id, _)| *id).collect();
                anyhow::bail!("unknown tool: {filter}\nknown tools: {}", valid.join(", "));
            }
        }
    } else {
        all_tools.to_vec()
    };

    // Collect what exists
    let mut to_remove: Vec<(&str, &str)> = Vec::new();
    for (name, _, paths) in &tools {
        for path in *paths {
            let full = root.join(path);
            if full.exists() {
                to_remove.push((name, path));
            }
        }
    }

    if to_remove.is_empty() {
        // No files to remove, but check for orphaned members
        let project_path = joy_core::store::joy_dir(&root).join(joy_core::store::PROJECT_FILE);
        if let Ok(mut project) =
            joy_core::store::read_yaml::<joy_core::model::Project>(&project_path)
        {
            let mut cleaned = false;
            for (_, id, _) in &tools {
                let member_id = format!("ai:{id}@joy");
                if project.members.remove(&member_id).is_some() {
                    println!(
                        "  {}{:<24} orphaned member removed",
                        color::check_mark(),
                        member_id
                    );
                    cleaned = true;
                }
            }
            if cleaned {
                joy_core::store::write_yaml_preserve(&project_path, &project)?;
            } else {
                println!("{}No AI tool configurations found.", color::check_mark());
            }
        } else {
            println!("{}No AI tool configurations found.", color::check_mark());
        }
        return Ok(());
    }

    println!("{}", color::header("AI Reset"));
    println!();
    println!("Will remove:");
    for (name, path) in &to_remove {
        println!("  {}{:<24} {}", color::cross_mark(), name, path);
    }

    if !args.force {
        println!();
        print!("Proceed? [y/N] ");
        std::io::stdout().flush()?;
        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        let trimmed = input.trim();
        if !trimmed.eq_ignore_ascii_case("y") {
            println!("Aborted.");
            return Ok(());
        }
    }

    for (name, path) in &to_remove {
        let full = root.join(path);
        if full.is_dir() {
            fs::remove_dir_all(&full)?;
        } else {
            fs::remove_file(&full)?;
        }
        println!("  {}{:<24} removed", color::check_mark(), name);
    }

    // Remove AI members from project.yaml for reset tools
    let project_path = joy_core::store::joy_dir(&root).join(joy_core::store::PROJECT_FILE);
    if let Ok(mut project) = joy_core::store::read_yaml::<joy_core::model::Project>(&project_path) {
        let mut project_changed = false;
        for (_, id, paths) in &tools {
            let was_removed = paths
                .iter()
                .any(|p| to_remove.iter().any(|(_, tp)| tp == p));
            if was_removed {
                let member_id = format!("ai:{id}@joy");
                if project.members.remove(&member_id).is_some() {
                    println!("  {}{:<24} member removed", color::check_mark(), member_id);
                    project_changed = true;
                    // Remove AI member's session and per-human delegation key file.
                    if let Ok(project_id) = joy_core::auth::session::project_id(&root) {
                        let _ = joy_core::auth::session::remove_session(&project_id, &member_id);
                        let _ = joy_core::auth::delegation::remove_delegation_key(
                            &project_id,
                            &member_id,
                        );
                    }
                    // Remove delegation entries for this AI member from all
                    // human members in project.yaml.
                    for (_, m) in project.members.iter_mut() {
                        m.ai_delegations.remove(&member_id);
                    }
                }
            }
        }
        if project_changed {
            joy_core::store::write_yaml_preserve(&project_path, &project)?;
        }
    }

    // If no AI tools remain, update gitignore and clean up .joy/ai/
    let any_remaining = all_tools
        .iter()
        .any(|(_, id, _)| is_tool_configured(&root, id));
    if !any_remaining {
        joy_core::init::update_gitignore_block(&root, joy_core::init::GITIGNORE_BASE_ENTRIES)?;

        // Remove .joy/ai/ directory, preserving jobs/ if non-empty
        let ai_dir = joy_core::store::joy_dir(&root).join("ai");
        if ai_dir.exists() {
            let jobs_dir = ai_dir.join("jobs");
            let jobs_has_content = jobs_dir.is_dir()
                && fs::read_dir(&jobs_dir)
                    .map(|mut d| d.next().is_some())
                    .unwrap_or(false);

            if jobs_has_content {
                // Remove everything in ai/ except jobs/
                for entry in fs::read_dir(&ai_dir)? {
                    let entry = entry?;
                    if entry.file_name() != "jobs" {
                        let path = entry.path();
                        if path.is_dir() {
                            fs::remove_dir_all(&path)?;
                        } else {
                            fs::remove_file(&path)?;
                        }
                    }
                }
                println!(
                    "  {}{:<24} cleaned (jobs/ preserved)",
                    color::check_mark(),
                    ".joy/ai/"
                );
            } else {
                fs::remove_dir_all(&ai_dir)?;
                println!("  {}{:<24} removed", color::check_mark(), ".joy/ai/");
            }
        }
    }

    let count = tools
        .iter()
        .filter(|(_, _, paths)| {
            paths
                .iter()
                .any(|p| to_remove.iter().any(|(_, tp)| tp == p))
        })
        .count();
    println!(
        "{}",
        color::footer(&format!("{} reset", color::plural(count, "tool")))
    );
    Ok(())
}

type ToolEntry = (
    &'static str,                            // display name
    &'static str,                            // id
    fn() -> bool,                            // detect: is the tool installed?
    fn(&Path, &str) -> anyhow::Result<bool>, // configure
);

fn detect_claude() -> bool {
    which("claude")
}
fn detect_qwen() -> bool {
    which("qwen") || which("qwen-code")
}
fn detect_vibe() -> bool {
    which("vibe")
}
fn detect_copilot() -> bool {
    which("copilot") || which("gh")
}

const ALL_TOOLS: &[ToolEntry] = &[
    ("Claude Code", "claude", detect_claude, configure_claude),
    ("Qwen Code", "qwen", detect_qwen, configure_qwen),
    ("Mistral Vibe", "vibe", detect_vibe, configure_vibe),
    (
        "GitHub Copilot",
        "copilot",
        detect_copilot,
        configure_copilot,
    ),
];

/// Set up only NEW (not yet configured) tools. Returns list of all configured tool IDs.
fn setup_new_tools(root: &Path) -> anyhow::Result<Vec<&'static str>> {
    println!("{}", color::section("AI Tools"));

    let mut configured_tools: Vec<&'static str> = Vec::new();
    let mut newly_configured = 0;

    // Load project for member registration
    let project_path = joy_core::store::joy_dir(root).join(joy_core::store::PROJECT_FILE);
    let mut project: joy_core::model::Project = joy_core::store::read_yaml(&project_path)?;
    let mut project_changed = false;

    for (name, id, detect, configure) in ALL_TOOLS {
        if !detect() {
            continue;
        }
        let already = is_tool_configured(root, id);
        let member_id = format!("ai:{id}@joy");
        let should_register;

        if already {
            println!(
                "  {}{:<24} {}",
                color::check_mark(),
                name,
                color::inactive("already configured")
            );
            configured_tools.push(*id);
            // Backfill the member entry when the tool was configured outside
            // joy ai init (e.g. by joy ai update or a copied .claude/ dir).
            should_register = true;
        } else {
            print!("  {}{:<24} configure? [Y/n] ", color::warn_mark(), name);
            if confirm_default_yes()? {
                configure(root, &member_id)?;
                configured_tools.push(*id);
                newly_configured += 1;
                should_register = true;
            } else {
                should_register = false;
            }
        }

        if should_register && !project.members.contains_key(&member_id) {
            let ai_defaults = joy_core::store::load_ai_defaults(root);
            let ai_caps = if ai_defaults.capabilities.is_empty() {
                joy_core::model::item::Capability::work_capabilities()
            } else {
                ai_defaults.capabilities.clone()
            };
            project.members.insert(
                member_id.clone(),
                joy_core::model::project::Member::new(
                    joy_core::model::project::MemberCapabilities::Specific({
                        use joy_core::model::project::CapabilityConfig;
                        let mut map = std::collections::BTreeMap::new();
                        for cap in ai_caps {
                            map.insert(cap, CapabilityConfig::default());
                        }
                        map
                    }),
                ),
            );
            project_changed = true;
            println!(
                "  {}{:<24} {}",
                color::check_mark(),
                member_id,
                color::success("registered as member")
            );
        }
    }

    if project_changed {
        joy_core::store::write_yaml_preserve(&project_path, &project)?;
    }

    if configured_tools.is_empty() {
        println!("  {}No supported AI tools detected.", color::warn_mark());
        println!(
            "  {}",
            color::inactive("Supported: Claude Code, Qwen Code, Mistral Vibe, GitHub Copilot")
        );
        println!(
            "  {}",
            color::inactive("Install one and re-run `joy ai init`.")
        );
    } else if newly_configured == 0 {
        println!(
            "\n  {}All tools already configured. Use {} to update files.",
            color::warn_mark(),
            color::label("joy ai update")
        );
    }

    println!();
    Ok(configured_tools)
}

/// Render the managed block (identity + instructions with workflow) for a tool's instruction file.
fn render_managed_block(member_id: &str, has_skill: bool) -> anyhow::Result<String> {
    let workflow = ai_templates::load_workflow()?;
    let joy_block = ai_templates::render_joy_block(member_id, has_skill)?;
    let instructions = ai_templates::render_instructions(&workflow)?;
    Ok(format!("{}\n\n{}", joy_block, instructions))
}

/// Render SKILL.md with workflow context.
fn render_skill() -> anyhow::Result<String> {
    let workflow = ai_templates::load_workflow()?;
    ai_templates::render_skill(&workflow).map_err(Into::into)
}

/// Remove and recreate Joy-managed subdirectories for a tool.
/// Preserves user-owned files (instruction files, settings.json).
fn clean_managed_dirs(root: &Path, dirs: &[&str]) {
    for dir in dirs {
        let path = root.join(dir);
        if path.is_dir() {
            let _ = fs::remove_dir_all(&path);
        }
    }
}

/// Generate agent files for a tool into the given directory.
fn generate_agents(root: &Path, tool: &str, agents_dir: &str) -> anyhow::Result<bool> {
    let workflow = ai_templates::load_workflow()?;
    let agents = ai_templates::load_agents()?;
    let mut changed = false;

    for agent in &agents {
        if !ai_templates::agent_applicable_to_tool(agent, tool) {
            continue;
        }
        if let Some(filename) = ai_templates::agent_filename(agent, tool) {
            let content = ai_templates::render_agent(agent, &workflow, tool)?;
            let path = root.join(agents_dir).join(&filename);
            changed |= write_if_changed(&path, &content)?;
            qprintln!("    {}{}/{}", color::check_mark(), agents_dir, filename);
        }
    }
    Ok(changed)
}

/// Ensure `env.JOY_SESSION` is set in a JSON settings object.
fn set_session_env(settings: &mut serde_json::Value, session_value: &str) {
    let env = settings
        .as_object_mut()
        .unwrap()
        .entry("env")
        .or_insert_with(|| serde_json::json!({}));
    env.as_object_mut()
        .unwrap()
        .insert("JOY_SESSION".to_string(), serde_json::json!(session_value));
}

/// Write the given `JOY_SESSION` env value into the AI tool's settings file
/// so that subshells spawned by the tool pick up the live session. Called
/// from `joy auth --token` after a fresh session is created. Returns Ok(())
/// even when no tool settings file exists (e.g. the tool was never init'd),
/// since that is a valid state for a project that only runs AI from eval.
pub(crate) fn write_session_env_for_member(
    root: &Path,
    member_id: &str,
    env_value: &str,
) -> anyhow::Result<()> {
    let tool = member_id
        .strip_prefix("ai:")
        .and_then(|s| s.split_once('@'))
        .map(|(t, _)| t)
        .unwrap_or("");
    match tool {
        "claude" => write_json_session_env(&root.join(".claude/settings.json"), env_value),
        "qwen" => write_json_session_env(&root.join(".qwen/settings.json"), env_value),
        "copilot" => write_json_session_env(&root.join(".github/copilot/settings.json"), env_value),
        "vibe" => write_dotenv_session(&root.join(".vibe/.env"), env_value),
        _ => Ok(()),
    }
}

fn write_json_session_env(path: &Path, value: &str) -> anyhow::Result<()> {
    if !path.is_file() {
        return Ok(());
    }
    let content = fs::read_to_string(path)?;
    let mut settings: serde_json::Value =
        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}));
    set_session_env(&mut settings, value);
    let rendered = serde_json::to_string_pretty(&settings)?;
    let _ = write_if_changed(path, &format!("{rendered}\n"))?;
    Ok(())
}

fn write_dotenv_session(path: &Path, value: &str) -> anyhow::Result<()> {
    if !path.is_file() {
        // If no .env file exists yet the AI tool hasn't been init'd for this
        // project, nothing to persist into. The interactive eval path still
        // sets the env var in the current shell.
        return Ok(());
    }
    let existing = fs::read_to_string(path)?;
    let content = if existing.contains("JOY_SESSION=") {
        existing
            .lines()
            .map(|l| {
                if l.starts_with("JOY_SESSION=") {
                    format!("JOY_SESSION={value}")
                } else {
                    l.to_string()
                }
            })
            .collect::<Vec<_>>()
            .join("\n")
            + "\n"
    } else {
        format!("{}\nJOY_SESSION={value}\n", existing.trim_end())
    };
    let _ = write_if_changed(path, &content)?;
    Ok(())
}

fn configure_claude(root: &Path, member_id: &str) -> anyhow::Result<bool> {
    let claude_dir = root.join(".claude");
    fs::create_dir_all(&claude_dir)?;
    clean_managed_dirs(root, &[".claude/agents", ".claude/skills/joy"]);
    let mut changed = false;

    let claude_md = claude_dir.join("CLAUDE.md");
    changed |= update_with_joy_block(&claude_md, &render_managed_block(member_id, true)?)?;
    qprintln!("    {}.claude/CLAUDE.md", color::check_mark());

    let skill_path = claude_dir.join("skills/joy/SKILL.md");
    changed |= write_if_changed(&skill_path, &render_skill()?)?;
    qprintln!("    {}.claude/skills/joy/SKILL.md", color::check_mark());

    let setup_path = claude_dir.join("skills/joy/setup.md");
    changed |= write_if_changed(&setup_path, ai_templates::setup_instructions())?;
    qprintln!("    {}.claude/skills/joy/setup.md", color::check_mark());

    changed |= generate_agents(root, "claude", ".claude/agents")?;
    changed |= update_claude_permissions(root, member_id)?;

    Ok(changed)
}

fn update_claude_permissions(root: &Path, _member_id: &str) -> anyhow::Result<bool> {
    let settings_path = root.join(".claude/settings.json");

    let mut settings: serde_json::Value = if settings_path.is_file() {
        let content = fs::read_to_string(&settings_path)?;
        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
    } else {
        serde_json::json!({})
    };

    // Permissions
    let permissions = settings
        .as_object_mut()
        .unwrap()
        .entry("permissions")
        .or_insert_with(|| serde_json::json!({}));
    let allow = permissions
        .as_object_mut()
        .unwrap()
        .entry("allow")
        .or_insert_with(|| serde_json::json!([]));
    let allow_arr = allow.as_array_mut().unwrap();
    for perm in ["Bash(joy *)", "Bash(jot *)"] {
        if !allow_arr.iter().any(|v| v.as_str() == Some(perm)) {
            allow_arr.push(serde_json::json!(perm));
        }
    }

    // JOY_SESSION is written by `joy auth --token` once a real session
    // exists. Init-time writes would be useless placeholders (ADR-033).

    let json = serde_json::to_string_pretty(&settings)?;
    let changed = write_if_changed(&settings_path, &format!("{json}\n"))?;
    qprintln!("    {}.claude/settings.json", color::check_mark());

    Ok(changed)
}

fn configure_qwen(root: &Path, member_id: &str) -> anyhow::Result<bool> {
    let qwen_dir = root.join(".qwen");
    fs::create_dir_all(&qwen_dir)?;
    clean_managed_dirs(root, &[".qwen/agents", ".qwen/skills/joy"]);
    let mut changed = false;

    let qwen_md = qwen_dir.join("QWEN.md");
    changed |= update_with_joy_block(&qwen_md, &render_managed_block(member_id, true)?)?;
    qprintln!("    {}.qwen/QWEN.md", color::check_mark());

    let skill_path = qwen_dir.join("skills/joy/SKILL.md");
    changed |= write_if_changed(&skill_path, &render_skill()?)?;
    qprintln!("    {}.qwen/skills/joy/SKILL.md", color::check_mark());

    let setup_path = qwen_dir.join("skills/joy/setup.md");
    changed |= write_if_changed(&setup_path, ai_templates::setup_instructions())?;
    qprintln!("    {}.qwen/skills/joy/setup.md", color::check_mark());

    changed |= generate_agents(root, "qwen", ".qwen/agents")?;
    changed |= update_qwen_permissions(root, member_id)?;

    Ok(changed)
}

fn update_qwen_permissions(root: &Path, _member_id: &str) -> anyhow::Result<bool> {
    let settings_path = root.join(".qwen/settings.json");

    let mut settings: serde_json::Value = if settings_path.is_file() {
        let content = fs::read_to_string(&settings_path)?;
        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
    } else {
        serde_json::json!({})
    };

    // Permissions
    let tools = settings
        .as_object_mut()
        .unwrap()
        .entry("tools")
        .or_insert_with(|| serde_json::json!({}));
    let allowed = tools
        .as_object_mut()
        .unwrap()
        .entry("allowed")
        .or_insert_with(|| serde_json::json!([]));
    let allowed_arr = allowed.as_array_mut().unwrap();
    for perm in ["run_shell_command(joy)", "run_shell_command(jot)"] {
        if !allowed_arr.iter().any(|v| v.as_str() == Some(perm)) {
            allowed_arr.push(serde_json::json!(perm));
        }
    }

    // JOY_SESSION populated by `joy auth --token` (ADR-033).

    let json = serde_json::to_string_pretty(&settings)?;
    let changed = write_if_changed(&settings_path, &format!("{json}\n"))?;
    qprintln!("    {}.qwen/settings.json", color::check_mark());

    Ok(changed)
}

fn configure_vibe(root: &Path, _member_id: &str) -> anyhow::Result<bool> {
    let vibe_dir = root.join(".vibe");
    fs::create_dir_all(&vibe_dir)?;
    clean_managed_dirs(root, &[".vibe/agents", ".vibe/skills/joy"]);
    let mut changed = false;

    let skill_path = vibe_dir.join("skills/joy/SKILL.md");
    changed |= write_if_changed(&skill_path, &render_skill()?)?;
    qprintln!("    {}.vibe/skills/joy/SKILL.md", color::check_mark());

    let setup_path = vibe_dir.join("skills/joy/setup.md");
    changed |= write_if_changed(&setup_path, ai_templates::setup_instructions())?;
    qprintln!("    {}.vibe/skills/joy/setup.md", color::check_mark());

    changed |= generate_agents(root, "vibe", ".vibe/agents")?;

    // Create an empty .vibe/.env if missing so `joy auth --token` has a
    // file to update. JOY_SESSION itself is written by that command (ADR-033).
    let env_path = vibe_dir.join(".env");
    if !env_path.is_file() {
        changed |= write_if_changed(&env_path, "")?;
        qprintln!("    {}.vibe/.env", color::check_mark());
    }

    let config_path = vibe_dir.join("config.toml");
    changed |= ensure_vibe_bash_always(&config_path)?;
    qprintln!("    {}.vibe/config.toml", color::check_mark());

    Ok(changed)
}

/// Make sure .vibe/config.toml declares `[tools.bash] permission = "always"`.
/// Creates the file if missing. If the key already exists we respect the
/// user's value and leave the file alone, so a deliberate override is
/// not clobbered on every `joy ai init`.
fn ensure_vibe_bash_always(path: &Path) -> anyhow::Result<bool> {
    use toml_edit::{value, DocumentMut, Item, Table};

    let mut doc: DocumentMut = if path.is_file() {
        fs::read_to_string(path)?.parse()?
    } else {
        DocumentMut::new()
    };

    let tools = doc
        .entry("tools")
        .or_insert_with(|| Item::Table(Table::new()))
        .as_table_mut()
        .ok_or_else(|| anyhow::anyhow!(".vibe/config.toml: [tools] is not a table"))?;
    tools.set_implicit(true);

    let bash = tools
        .entry("bash")
        .or_insert_with(|| Item::Table(Table::new()))
        .as_table_mut()
        .ok_or_else(|| anyhow::anyhow!(".vibe/config.toml: [tools.bash] is not a table"))?;

    if bash.get("permission").is_some() {
        return Ok(false);
    }
    bash["permission"] = value("always");

    write_if_changed(path, &doc.to_string())
}

fn configure_copilot(root: &Path, member_id: &str) -> anyhow::Result<bool> {
    let github_dir = root.join(".github");
    fs::create_dir_all(&github_dir)?;
    clean_managed_dirs(root, &[".github/agents", ".github/prompts"]);
    let mut changed = false;

    let instructions_md = github_dir.join("copilot-instructions.md");
    changed |= update_with_joy_block(&instructions_md, &render_managed_block(member_id, false)?)?;
    qprintln!("    {}.github/copilot-instructions.md", color::check_mark());

    // Copilot skill wrapper
    let workflow = ai_templates::load_workflow()?;
    let prompt = ai_templates::render_copilot_prompt(&workflow)?;
    let prompt_path = github_dir.join("prompts/joy.prompt.md");
    changed |= write_if_changed(&prompt_path, &prompt)?;
    qprintln!("    {}.github/prompts/joy.prompt.md", color::check_mark());

    changed |= generate_agents(root, "copilot", ".github/agents")?;
    changed |= update_copilot_permissions(root, member_id)?;

    Ok(changed)
}

fn update_copilot_permissions(root: &Path, _member_id: &str) -> anyhow::Result<bool> {
    let copilot_dir = root.join(".github/copilot");
    fs::create_dir_all(&copilot_dir)?;
    let settings_path = copilot_dir.join("settings.json");

    let mut settings: serde_json::Value = if settings_path.is_file() {
        let content = fs::read_to_string(&settings_path)?;
        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
    } else {
        serde_json::json!({})
    };

    let allow = settings
        .as_object_mut()
        .unwrap()
        .entry("allowTools")
        .or_insert_with(|| serde_json::json!([]));

    let allow_arr = allow.as_array_mut().unwrap();

    for perm in ["shell(joy:*)", "shell(jot:*)"] {
        if !allow_arr.iter().any(|v| v.as_str() == Some(perm)) {
            allow_arr.push(serde_json::json!(perm));
        }
    }

    // JOY_SESSION populated by `joy auth --token` (ADR-033).

    let json = serde_json::to_string_pretty(&settings)?;
    let changed = write_if_changed(&settings_path, &format!("{json}\n"))?;
    qprintln!("    {}.github/copilot/settings.json", color::check_mark());

    Ok(changed)
}

/// Write content to a file only if it differs.
/// Returns true if the file was changed.
fn write_if_changed(path: &Path, content: &str) -> anyhow::Result<bool> {
    if path.is_file() {
        let existing = fs::read_to_string(path)?;
        if existing == content {
            return Ok(false);
        }
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, content)?;
    Ok(true)
}

fn update_with_joy_block(path: &Path, content: &str) -> anyhow::Result<bool> {
    let block = format!("{}\n{}\n{}", JOY_BLOCK_START, content, JOY_BLOCK_END);

    let new_content = if path.is_file() {
        let existing = fs::read_to_string(path)?;
        if existing.contains(JOY_BLOCK_START) && existing.contains(JOY_BLOCK_END) {
            let start = existing.find(JOY_BLOCK_START).unwrap();
            let end = existing.find(JOY_BLOCK_END).unwrap() + JOY_BLOCK_END.len();
            let mut updated = String::new();
            updated.push_str(&existing[..start]);
            updated.push_str(&block);
            updated.push_str(&existing[end..]);
            updated
        } else {
            format!("{}\n\n{}", existing.trim_end(), block)
        }
    } else {
        format!("{}\n", block)
    };

    write_if_changed(path, &new_content)
}

fn confirm_default_yes() -> anyhow::Result<bool> {
    std::io::stdout().flush()?;
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let trimmed = input.trim();
    Ok(trimmed.is_empty() || trimmed.eq_ignore_ascii_case("y"))
}

fn which(binary: &str) -> bool {
    std::process::Command::new("which")
        .arg(binary)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

/// Gitignore entries per AI tool.
const TOOL_GITIGNORE_ENTRIES: &[(&str, &[(&str, &str)])] = &[
    ("claude", &[(".claude/", "Claude Code")]),
    ("qwen", &[(".qwen/", "Qwen Code")]),
    ("vibe", &[(".vibe/", "Mistral Vibe")]),
    (
        "copilot",
        &[
            (".github/copilot-instructions.md", "GitHub Copilot"),
            (".github/copilot/", "GitHub Copilot"),
            (".github/agents/", "GitHub Copilot"),
            (".github/prompts/", "GitHub Copilot"),
        ],
    ),
];

fn update_gitignore(root: &Path, configured_tools: &[&str]) -> anyhow::Result<()> {
    use joy_core::init::GITIGNORE_BASE_ENTRIES;

    let mut entries: Vec<(&str, &str)> = GITIGNORE_BASE_ENTRIES.to_vec();

    for (tool_id, tool_entries) in TOOL_GITIGNORE_ENTRIES {
        if configured_tools.contains(tool_id) {
            entries.extend_from_slice(tool_entries);
        }
    }

    joy_core::init::update_gitignore_block(root, &entries)?;
    Ok(())
}

/// Scan subdirectories (max 2 levels) for nested Joy projects that lack AI tool config.
fn check_nested_projects(root: &Path) -> anyhow::Result<()> {
    let mut unconfigured: Vec<String> = Vec::new();

    // Collect installed tools to check against
    let tools: Vec<&str> = ALL_TOOLS
        .iter()
        .filter(|(_, _, detect, _)| detect())
        .map(|(_, id, _, _)| *id)
        .collect();

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

    // Scan 2 levels deep for .joy/project.yaml
    for entry in fs::read_dir(root)?.filter_map(|e| e.ok()) {
        let path = entry.path();
        if !path.is_dir()
            || path
                .file_name()
                .is_some_and(|n| n.to_str().is_some_and(|s| s.starts_with('.')))
        {
            continue;
        }
        check_nested_at(&path, root, &tools, &mut unconfigured);
        // Level 2
        if let Ok(sub_entries) = fs::read_dir(&path) {
            for sub_entry in sub_entries.filter_map(|e| e.ok()) {
                let sub_path = sub_entry.path();
                if !sub_path.is_dir()
                    || sub_path
                        .file_name()
                        .is_some_and(|n| n.to_str().is_some_and(|s| s.starts_with('.')))
                {
                    continue;
                }
                check_nested_at(&sub_path, root, &tools, &mut unconfigured);
            }
        }
    }

    if !unconfigured.is_empty() {
        println!("{}", color::section("Nested Projects"));
        for path in &unconfigured {
            println!("  {}{}/", color::warn_mark(), path);
        }
        println!(
            "  {}",
            color::inactive("Permissions are per-project. Run `joy ai init` in each.")
        );
        println!();
    }

    Ok(())
}

fn check_nested_at(dir: &Path, root: &Path, tools: &[&str], unconfigured: &mut Vec<String>) {
    let project_yaml = dir.join(".joy/project.yaml");
    if !project_yaml.is_file() {
        return;
    }
    // At least one installed tool must be unconfigured here
    let any_configured = tools.iter().any(|t| is_tool_configured(dir, t));
    if !any_configured {
        let relative = dir
            .strip_prefix(root)
            .unwrap_or(dir)
            .to_string_lossy()
            .to_string();
        unconfigured.push(relative);
    }
}

fn is_tool_configured(root: &Path, tool: &str) -> bool {
    match tool {
        "claude" => root.join(".claude/CLAUDE.md").is_file(),
        "qwen" => root.join(".qwen/QWEN.md").is_file(),
        "vibe" => root.join(".vibe/skills/joy/SKILL.md").is_file(),
        "copilot" => root.join(".github/copilot-instructions.md").is_file(),
        _ => false,
    }
}

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

    fn arch_spec() -> &'static DocSpec {
        DOC_SPECS.iter().find(|s| s.key == "architecture").unwrap()
    }

    #[test]
    fn ensure_vibe_bash_always_creates_new_file() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("config.toml");
        let changed = ensure_vibe_bash_always(&path).unwrap();
        assert!(changed);
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("[tools.bash]"));
        assert!(content.contains("permission = \"always\""));
    }

    #[test]
    fn ensure_vibe_bash_always_adds_to_existing_unrelated_config() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("config.toml");
        std::fs::write(&path, "[models]\ndefault = \"mistral-large\"\n").unwrap();
        let changed = ensure_vibe_bash_always(&path).unwrap();
        assert!(changed);
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("default = \"mistral-large\""));
        assert!(content.contains("permission = \"always\""));
    }

    #[test]
    fn ensure_vibe_bash_always_preserves_user_override() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("config.toml");
        std::fs::write(&path, "[tools.bash]\npermission = \"ask\"\n").unwrap();
        let changed = ensure_vibe_bash_always(&path).unwrap();
        assert!(!changed);
        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("permission = \"ask\""));
        assert!(!content.contains("\"always\""));
    }

    #[test]
    fn flag_overrides_configured_and_scan() {
        let tmp = tempfile::tempdir().unwrap();
        let result = resolve_doc_path(
            tmp.path(),
            arch_spec(),
            Some("docs/ignored.md"),
            Some("docs/from-flag.md"),
        )
        .unwrap();
        assert_eq!(result, "docs/from-flag.md");
    }

    #[test]
    fn flag_trimmed_then_used() {
        let tmp = tempfile::tempdir().unwrap();
        let result = resolve_doc_path(
            tmp.path(),
            arch_spec(),
            None,
            Some("  docs/with-spaces.md  "),
        )
        .unwrap();
        assert_eq!(result, "docs/with-spaces.md");
    }

    #[test]
    fn configured_short_circuits_prompt() {
        let tmp = tempfile::tempdir().unwrap();
        let result =
            resolve_doc_path(tmp.path(), arch_spec(), Some("ARCHITECTURE.md"), None).unwrap();
        assert_eq!(result, "ARCHITECTURE.md");
    }

    #[test]
    fn empty_flag_falls_back_to_configured() {
        let tmp = tempfile::tempdir().unwrap();
        let result = resolve_doc_path(
            tmp.path(),
            arch_spec(),
            Some("ARCHITECTURE.md"),
            Some("   "),
        )
        .unwrap();
        assert_eq!(result, "ARCHITECTURE.md");
    }

    #[test]
    fn suggestion_is_default_when_no_candidate_exists() {
        let tmp = tempfile::tempdir().unwrap();
        let suggestion = suggested_doc_path(tmp.path(), arch_spec());
        assert_eq!(
            suggestion,
            joy_core::model::project::Docs::DEFAULT_ARCHITECTURE
        );
    }

    #[test]
    fn suggestion_picks_first_existing_candidate() {
        let tmp = tempfile::tempdir().unwrap();
        // ARCHITECTURE.md is later in the candidate list than
        // docs/architecture/README.md; create only the latter and verify it wins.
        let dir = tmp.path().join("docs/architecture");
        fs::create_dir_all(&dir).unwrap();
        fs::write(dir.join("README.md"), "stub").unwrap();
        let suggestion = suggested_doc_path(tmp.path(), arch_spec());
        assert_eq!(suggestion, "docs/architecture/README.md");
    }

    #[test]
    fn suggestion_skips_missing_candidates_in_order() {
        let tmp = tempfile::tempdir().unwrap();
        // Only the very last candidate (ARCHITECTURE.md in root) exists -- it
        // should be returned despite earlier candidates being absent.
        fs::write(tmp.path().join("ARCHITECTURE.md"), "stub").unwrap();
        let suggestion = suggested_doc_path(tmp.path(), arch_spec());
        assert_eq!(suggestion, "ARCHITECTURE.md");
    }
}
