//! MCP tool implementations for the interactive setup wizard.
//!
//! These tools allow Claude Code (and other MCP clients) to drive the setup
//! wizard entirely from the terminal without requiring the web UI or chat bot.
//!
//! Typical flow:
//! 1. `wizard_status`  — inspect current state
//! 2. `wizard_generate` — read the codebase and call again with `content` to
//!    stage generated text for review
//! 3. `wizard_confirm`  — write staged content to disk and advance the wizard
//! 4. `wizard_skip`     — skip a step that does not apply
//! 5. `wizard_retry`    — discard staged content and regenerate from scratch

use crate::http::context::AppContext;
use crate::io::wizard::{StepStatus, WizardState, WizardStep, format_wizard_state};
use serde_json::Value;
use std::fs;
use std::path::Path;

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

/// Return the filesystem path (relative to `project_root`) for a step's output.
///
/// Returns `None` for `Scaffold` since that step has no single output file — it
/// creates the full `.storkit/` directory structure and is handled by
/// `storkit init` before the server starts.
pub(crate) fn step_output_path(project_root: &Path, step: WizardStep) -> Option<std::path::PathBuf> {
    match step {
        WizardStep::Context => Some(
            project_root
                .join(".storkit")
                .join("specs")
                .join("00_CONTEXT.md"),
        ),
        WizardStep::Stack => Some(
            project_root
                .join(".storkit")
                .join("specs")
                .join("tech")
                .join("STACK.md"),
        ),
        WizardStep::TestScript => Some(project_root.join("script").join("test")),
        WizardStep::ReleaseScript => Some(project_root.join("script").join("release")),
        WizardStep::TestCoverage => Some(project_root.join("script").join("test_coverage")),
        WizardStep::Scaffold => None,
    }
}

pub(crate) fn is_script_step(step: WizardStep) -> bool {
    matches!(
        step,
        WizardStep::TestScript | WizardStep::ReleaseScript | WizardStep::TestCoverage
    )
}

/// Write `content` to `path` only when the file does not already exist.
///
/// Existing files (including `CLAUDE.md`) are never overwritten — the wizard
/// appends or skips per the acceptance criteria.  For script steps the file is
/// also made executable after writing.
pub(crate) fn write_if_missing(path: &Path, content: &str, executable: bool) -> Result<bool, String> {
    if path.exists() {
        return Ok(false); // already present — skip silently
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .map_err(|e| format!("Failed to create directory {}: {e}", parent.display()))?;
    }
    fs::write(path, content)
        .map_err(|e| format!("Failed to write {}: {e}", path.display()))?;

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

    Ok(true)
}

/// Serialise a `WizardStep` to its snake_case string (e.g. `"test_script"`).
fn step_slug(step: WizardStep) -> String {
    serde_json::to_value(step)
        .ok()
        .and_then(|v| v.as_str().map(String::from))
        .unwrap_or_default()
}

// ── MCP tool handlers ─────────────────────────────────────────────────────────

/// `wizard_status` — return current wizard state as a human-readable summary.
pub(super) fn tool_wizard_status(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    let state =
        WizardState::load(&root).ok_or("No wizard active. Run `storkit init` to begin setup.")?;
    Ok(format_wizard_state(&state))
}

/// `wizard_generate` — mark the current step as generating or stage content.
///
/// Call with no `content` argument to mark the step as `Generating` and
/// receive a hint describing what to generate.  Call again with a `content`
/// argument (the generated file body) to stage it for review; the step will
/// transition to `AwaitingConfirmation`.  Content is **not** written to disk
/// until `wizard_confirm` is called.
pub(super) fn tool_wizard_generate(args: &Value, ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    let mut state = WizardState::load(&root).ok_or("No wizard active.")?;

    if state.completed {
        return Ok("Wizard is already complete.".to_string());
    }

    let current_idx = state.current_step_index();
    let step = state.steps[current_idx].step;

    // If content is provided, stage it for confirmation.
    if let Some(content) = args.get("content").and_then(|v| v.as_str()) {
        state.set_step_status(
            step,
            StepStatus::AwaitingConfirmation,
            Some(content.to_string()),
        );
        state
            .save(&root)
            .map_err(|e| format!("Failed to save wizard state: {e}"))?;
        return Ok(format!(
            "Content staged for '{}'. Run `wizard_confirm` to write it to disk, `wizard_retry` to regenerate, or `wizard_skip` to skip.",
            step.label()
        ));
    }

    // No content provided — mark as generating and return a hint.
    state.set_step_status(step, StepStatus::Generating, None);
    state
        .save(&root)
        .map_err(|e| format!("Failed to save wizard state: {e}"))?;

    let hint = generation_hint(step, &root);
    let slug = step_slug(step);

    Ok(format!(
        "Step '{}' marked as generating.\n\n{hint}\n\nOnce you have the content, call `wizard_generate` again with a `content` argument (or PUT /wizard/step/{slug}/content). Then call `wizard_confirm` to write it to disk.",
        step.label(),
    ))
}

/// Return true if the project directory has no meaningful source files.
fn is_bare_project(project_root: &Path) -> bool {
    let dominated_by_storkit = std::fs::read_dir(project_root)
        .ok()
        .map(|entries| {
            let names: Vec<String> = entries
                .filter_map(|e| e.ok())
                .map(|e| e.file_name().to_string_lossy().to_string())
                .collect();
            // A bare project only has storkit scaffolding and no real code
            names.iter().all(|n| {
                n.starts_with('.')
                    || n == "CLAUDE.md"
                    || n == "LICENSE"
                    || n == "README.md"
                    || n == "script"
                    || n == "store.json"
            })
        })
        .unwrap_or(true);
    dominated_by_storkit
}

/// Return a generation hint for a step based on the project root.
fn generation_hint(step: WizardStep, project_root: &Path) -> String {
    let bare = is_bare_project(project_root);

    match step {
        WizardStep::Context => {
            if bare {
                "This is a bare project with no existing code. Ask the user what they want \
                 to build — the project's purpose, goals, target users, and key features. \
                 Then generate `.storkit/specs/00_CONTEXT.md` from their answers covering:\n\
                 - High-level goal of the project\n\
                 - Core features\n\
                 - Domain concepts and entities\n\
                 - Glossary of abbreviations and technical terms".to_string()
            } else {
                "Read the project source tree and generate a `.storkit/specs/00_CONTEXT.md` describing:\n\
                 - High-level goal of the project\n\
                 - Core features\n\
                 - Domain concepts and entities\n\
                 - Glossary of abbreviations and technical terms".to_string()
            }
        }
        WizardStep::Stack => {
            if bare {
                "This is a bare project with no existing code. Ask the user what language, \
                 frameworks, and tools they plan to use. Then generate `.storkit/specs/tech/STACK.md` \
                 from their answers covering:\n\
                 - Language, frameworks, and runtimes\n\
                 - Coding standards and linting rules\n\
                 - Quality gates (commands that must pass before merging)\n\
                 - Approved libraries and their purpose".to_string()
            } else {
                "Read the project source tree and generate a `.storkit/specs/tech/STACK.md` describing:\n\
                 - Language, frameworks, and runtimes\n\
                 - Coding standards and linting rules\n\
                 - Quality gates (commands that must pass before merging)\n\
                 - Approved libraries and their purpose".to_string()
            }
        }
        WizardStep::TestScript => {
            let has_cargo = project_root.join("Cargo.toml").exists();
            let has_pkg = project_root.join("package.json").exists();
            let has_pnpm = project_root.join("pnpm-lock.yaml").exists();
            let mut cmds = Vec::new();
            if has_cargo {
                cmds.push("cargo nextest run");
            }
            if has_pkg {
                cmds.push(if has_pnpm { "pnpm test" } else { "npm test" });
            }
            if cmds.is_empty() {
                "Generate a `script/test` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs the project's test suite.".to_string()
            } else {
                format!(
                    "Generate a `script/test` shell script (#!/usr/bin/env bash, set -euo pipefail) that runs: {}",
                    cmds.join(", ")
                )
            }
        }
        WizardStep::ReleaseScript => {
            "Generate a `script/release` shell script (#!/usr/bin/env bash, set -euo pipefail) that builds and releases the project (e.g. `cargo build --release` or `npm run build`).".to_string()
        }
        WizardStep::TestCoverage => {
            "Generate a `script/test_coverage` shell script (#!/usr/bin/env bash, set -euo pipefail) that generates a test coverage report (e.g. `cargo llvm-cov nextest` or `npm run coverage`).".to_string()
        }
        WizardStep::Scaffold => "Scaffold step is handled automatically by `storkit init`.".to_string(),
    }
}

/// `wizard_confirm` — confirm the current step and write its content to disk.
///
/// If the step has staged content, the content is written to its target file
/// (only if that file does not already exist — existing files are never
/// overwritten).  The step is then marked as `Confirmed` and the wizard
/// advances to the next pending step.
pub(super) fn tool_wizard_confirm(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    let mut state = WizardState::load(&root).ok_or("No wizard active.")?;

    if state.completed {
        return Ok("Wizard is already complete.".to_string());
    }

    let current_idx = state.current_step_index();
    let step = state.steps[current_idx].step;
    let content = state.steps[current_idx].content.clone();

    // Write content to disk (only if a file path exists and the file is absent).
    let write_msg = if let (Some(c), Some(ref path)) = (&content, step_output_path(&root, step)) {
        let executable = is_script_step(step);
        match write_if_missing(path, c, executable)? {
            true => format!(" File written: `{}`.", path.display()),
            false => format!(" File `{}` already exists — skipped.", path.display()),
        }
    } else {
        String::new()
    };

    state
        .confirm_step(step)
        .map_err(|e| format!("Cannot confirm step: {e}"))?;
    state
        .save(&root)
        .map_err(|e| format!("Failed to save wizard state: {e}"))?;

    let next_idx = state.current_step_index();
    if state.completed {
        Ok(format!(
            "Step '{}' confirmed.{write_msg}\n\nSetup wizard complete! All steps done.",
            step.label()
        ))
    } else {
        let next = &state.steps[next_idx];
        Ok(format!(
            "Step '{}' confirmed.{write_msg}\n\nNext: {} — run `wizard_generate` to begin.",
            step.label(),
            next.step.label()
        ))
    }
}

/// `wizard_skip` — skip the current step without writing any file.
pub(super) fn tool_wizard_skip(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    let mut state = WizardState::load(&root).ok_or("No wizard active.")?;

    if state.completed {
        return Ok("Wizard is already complete.".to_string());
    }

    let current_idx = state.current_step_index();
    let step = state.steps[current_idx].step;

    state
        .skip_step(step)
        .map_err(|e| format!("Cannot skip step: {e}"))?;
    state
        .save(&root)
        .map_err(|e| format!("Failed to save wizard state: {e}"))?;

    let next_idx = state.current_step_index();
    if state.completed {
        Ok(format!(
            "Step '{}' skipped. Setup wizard complete!",
            step.label()
        ))
    } else {
        let next = &state.steps[next_idx];
        Ok(format!(
            "Step '{}' skipped.\n\nNext: {} — run `wizard_generate` to begin.",
            step.label(),
            next.step.label()
        ))
    }
}

/// `wizard_retry` — discard staged content and reset the current step to
/// `Pending` so it can be regenerated.
pub(super) fn tool_wizard_retry(ctx: &AppContext) -> Result<String, String> {
    let root = ctx.state.get_project_root()?;
    let mut state = WizardState::load(&root).ok_or("No wizard active.")?;

    if state.completed {
        return Ok("Wizard is already complete.".to_string());
    }

    let current_idx = state.current_step_index();
    let step = state.steps[current_idx].step;

    // Clear content and reset to pending.
    if let Some(s) = state.steps.iter_mut().find(|s| s.step == step) {
        s.status = StepStatus::Pending;
        s.content = None;
    }
    state
        .save(&root)
        .map_err(|e| format!("Failed to save wizard state: {e}"))?;

    Ok(format!(
        "Step '{}' reset to pending. Run `wizard_generate` to regenerate content.",
        step.label()
    ))
}

// ── tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::context::AppContext;
    use tempfile::TempDir;

    fn setup(dir: &TempDir) -> AppContext {
        let root = dir.path().to_path_buf();
        std::fs::create_dir_all(root.join(".storkit")).unwrap();
        WizardState::init_if_missing(&root);
        AppContext::new_test(root)
    }

    #[test]
    fn wizard_status_returns_state() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_status(&ctx).unwrap();
        assert!(result.contains("Setup wizard"));
        assert!(result.contains("context"));
    }

    #[test]
    fn wizard_status_no_wizard_returns_error() {
        let dir = TempDir::new().unwrap();
        std::fs::create_dir_all(dir.path().join(".storkit")).unwrap();
        let ctx = AppContext::new_test(dir.path().to_path_buf());
        assert!(tool_wizard_status(&ctx).is_err());
    }

    #[test]
    fn wizard_generate_marks_generating() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_generate(&serde_json::json!({}), &ctx).unwrap();
        assert!(result.contains("generating"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Generating);
    }

    #[test]
    fn wizard_generate_with_content_stages_content() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_generate(
            &serde_json::json!({"content": "# My Project"}),
            &ctx,
        )
        .unwrap();
        assert!(result.contains("staged"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::AwaitingConfirmation);
        assert_eq!(state.steps[1].content.as_deref(), Some("# My Project"));
    }

    #[test]
    fn wizard_confirm_writes_file_and_advances() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Stage content for Context step.
        tool_wizard_generate(
            &serde_json::json!({"content": "# Context content"}),
            &ctx,
        )
        .unwrap();
        let result = tool_wizard_confirm(&ctx).unwrap();
        assert!(result.contains("confirmed"));
        // File should now exist.
        let context_path = dir
            .path()
            .join(".storkit")
            .join("specs")
            .join("00_CONTEXT.md");
        assert!(context_path.exists());
        assert_eq!(
            std::fs::read_to_string(&context_path).unwrap(),
            "# Context content"
        );
        // Wizard should have advanced.
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Confirmed);
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn wizard_confirm_does_not_overwrite_existing_file() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Pre-create the specs directory and file.
        let specs_dir = dir.path().join(".storkit").join("specs");
        std::fs::create_dir_all(&specs_dir).unwrap();
        let context_path = specs_dir.join("00_CONTEXT.md");
        std::fs::write(&context_path, "original content").unwrap();

        // Stage and confirm — existing file should NOT be overwritten.
        tool_wizard_generate(
            &serde_json::json!({"content": "new content"}),
            &ctx,
        )
        .unwrap();
        let result = tool_wizard_confirm(&ctx).unwrap();
        assert!(result.contains("already exists"));
        assert_eq!(
            std::fs::read_to_string(&context_path).unwrap(),
            "original content"
        );
    }

    #[test]
    fn wizard_skip_advances_wizard() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        let result = tool_wizard_skip(&ctx).unwrap();
        assert!(result.contains("skipped"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Skipped);
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn wizard_retry_resets_to_pending() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Stage content first.
        tool_wizard_generate(
            &serde_json::json!({"content": "some content"}),
            &ctx,
        )
        .unwrap();
        let result = tool_wizard_retry(&ctx).unwrap();
        assert!(result.contains("reset"));
        let state = WizardState::load(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Pending);
        assert!(state.steps[1].content.is_none());
    }

    #[test]
    fn wizard_complete_returns_done_message() {
        let dir = TempDir::new().unwrap();
        let ctx = setup(&dir);
        // Skip all remaining steps.
        for _ in 0..5 {
            tool_wizard_skip(&ctx).unwrap();
        }
        let result = tool_wizard_status(&ctx).unwrap();
        assert!(result.contains("complete"));
    }

    #[test]
    fn format_wizard_state_shows_all_steps() {
        let mut state = WizardState::default();
        state.steps[0].status = StepStatus::Confirmed;
        let output = format_wizard_state(&state);
        assert!(output.contains("✓"));
        assert!(output.contains("Scaffold"));
        assert!(output.contains("← current"));
    }
}
