//! Wizard service — domain logic for the multi-step project setup wizard.
//!
//! Follows the conventions from `docs/architecture/service-modules.md`:
//! - `mod.rs` (this file) — public API, typed [`Error`], and orchestration
//! - `io.rs` — the ONLY place that performs side effects (filesystem reads/writes)
//! - `state_machine.rs` — pure helpers: bare-project detection, generation hints,
//!   step classification — all unit-testable without tempdirs or an async runtime

pub(crate) mod io;
/// Pure wizard state-machine helpers — bare-project detection, step classification.
pub mod state_machine;

use crate::io::wizard::{StepStatus, WizardState, WizardStep, format_wizard_state};
use crate::validation::check_boilerplate_user_story;
use std::path::Path;

// ── Error type ────────────────────────────────────────────────────────────────

/// Typed errors returned by `service::wizard` functions.
///
/// HTTP handlers map these to status codes:
/// - [`Error::NotActive`] → 404 Not Found
/// - [`Error::InvalidStateTransition`] → 400 Bad Request
/// - [`Error::MissingInput`] → 400 Bad Request
/// - [`Error::InvalidContent`] → 400 Bad Request
/// - [`Error::GenerationFailure`] → 500 Internal Server Error
/// - [`Error::PersistenceFailure`] → 500 Internal Server Error
#[derive(Debug)]
#[allow(dead_code)]
pub enum Error {
    /// No wizard is active (`wizard_state.json` not found).
    NotActive,
    /// The requested state transition is invalid (e.g. wrong step order).
    InvalidStateTransition(String),
    /// Required input was absent (e.g. content not staged before confirming).
    MissingInput(String),
    /// Staged content failed validation (e.g. boilerplate user-story template).
    InvalidContent(String),
    /// The LLM or agent failed to generate content for the step.
    GenerationFailure(String),
    /// A filesystem read or write operation failed.
    PersistenceFailure(String),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotActive => {
                write!(f, "No wizard active. Run `huskies init` to begin setup.")
            }
            Self::InvalidStateTransition(msg) => write!(f, "Invalid state transition: {msg}"),
            Self::MissingInput(msg) => write!(f, "Missing input: {msg}"),
            Self::InvalidContent(msg) => write!(f, "Invalid content: {msg}"),
            Self::GenerationFailure(msg) => write!(f, "Generation failed: {msg}"),
            Self::PersistenceFailure(msg) => write!(f, "Persistence error: {msg}"),
        }
    }
}

/// Mark `step` as confirmed and advance the wizard.
///
/// Enforces sequential ordering — only the current step may be confirmed.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::InvalidStateTransition`] if `step` is not the current step.
/// - [`Error::PersistenceFailure`] if saving state fails.
pub fn mark_step_confirmed(root: &Path, step: WizardStep) -> Result<WizardState, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;
    state
        .confirm_step(step)
        .map_err(Error::InvalidStateTransition)?;
    io::save(&state, root)?;
    Ok(state)
}

/// Mark `step` as skipped and advance the wizard.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::InvalidStateTransition`] if `step` is not the current step.
/// - [`Error::PersistenceFailure`] if saving state fails.
pub fn mark_step_skipped(root: &Path, step: WizardStep) -> Result<WizardState, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;
    state
        .skip_step(step)
        .map_err(Error::InvalidStateTransition)?;
    io::save(&state, root)?;
    Ok(state)
}

// ── Public API — used by MCP tool handlers ─────────────────────────────────

/// Return the current wizard state as a human-readable summary.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
pub fn status(root: &Path) -> Result<String, Error> {
    let state = io::load(root).ok_or(Error::NotActive)?;
    Ok(format_wizard_state(&state))
}

/// Drive content generation for the current wizard step.
///
/// Call with `content = None` to mark the step as `Generating` and receive a
/// generation hint. Call with `content = Some(text)` to stage the content for
/// review (transitions to `AwaitingConfirmation`). Content is not written to
/// disk until [`confirm`] is called.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::InvalidContent`] if `content` matches the boilerplate
///   "As a …, I want …, so that …" user-story template.
/// - [`Error::PersistenceFailure`] if saving state fails.
pub fn generate(root: &Path, content: Option<&str>) -> Result<String, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;

    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 let Some(c) = content {
        if let Some(err) = check_boilerplate_user_story("content", c) {
            return Err(Error::InvalidContent(err.to_string()));
        }
        state.set_step_status(step, StepStatus::AwaitingConfirmation, Some(c.to_string()));
        io::save(&state, root)?;
        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()
        ));
    }

    state.set_step_status(step, StepStatus::Generating, None);
    io::save(&state, root)?;

    let names = io::list_dir_names(root);
    let tools = io::detect_project_tools(root);
    let is_bare = state_machine::is_bare_project(&names);
    let hint = state_machine::generation_hint(step, is_bare, &tools);
    let slug = state_machine::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(),
    ))
}

/// Confirm the current step: write staged content to disk, then advance.
///
/// Content is written only when a target path exists for the step and the file
/// does not already contain real (non-template) content.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::InvalidStateTransition`] if the state transition fails.
/// - [`Error::PersistenceFailure`] if the file write or state save fails.
pub fn confirm(root: &Path) -> Result<String, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;

    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();

    let write_msg = if let (Some(c), Some(ref path)) =
        (&content, state_machine::step_output_path(root, step))
    {
        let executable = state_machine::is_script_step(step);
        match io::write_step_file(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(Error::InvalidStateTransition)?;
    io::save(&state, root)?;

    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()
        ))
    }
}

/// Skip the current step without writing any file.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::InvalidStateTransition`] if the state transition fails.
/// - [`Error::PersistenceFailure`] if saving state fails.
pub fn skip(root: &Path) -> Result<String, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;

    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(Error::InvalidStateTransition)?;
    io::save(&state, root)?;

    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()
        ))
    }
}

/// Discard staged content and reset the current step to `Pending`.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::PersistenceFailure`] if saving state fails.
pub fn retry(root: &Path) -> Result<String, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;

    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 let Some(s) = state.steps.iter_mut().find(|s| s.step == step) {
        s.status = StepStatus::Pending;
        s.content = None;
    }
    io::save(&state, root)?;

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

/// Return the current wizard state.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
#[cfg(test)]
pub fn get_state(root: &Path) -> Result<WizardState, Error> {
    io::load(root).ok_or(Error::NotActive)
}

/// Stage `content` for `step` and transition its status to `AwaitingConfirmation`.
///
/// Content is not written to disk until [`confirm`] is called.
///
/// # Errors
/// - [`Error::NotActive`] if no wizard is active.
/// - [`Error::PersistenceFailure`] if saving state fails.
#[cfg(test)]
pub fn set_step_content(
    root: &Path,
    step: WizardStep,
    content: Option<String>,
) -> Result<WizardState, Error> {
    let mut state = io::load(root).ok_or(Error::NotActive)?;
    state.set_step_status(step, StepStatus::AwaitingConfirmation, content);
    io::save(&state, root)?;
    Ok(state)
}

/// Write `content` to `path` if no real content already exists there.
///
/// Thin public wrapper around `io::write_step_file` for use by HTTP/chat
/// adapters that need direct file-write access without going through the full
/// confirm flow.
///
/// Returns `Ok(true)` when written, `Ok(false)` when an existing real file was
/// preserved, `Err(Error::PersistenceFailure)` on I/O error.
pub fn write_step_file(path: &Path, content: &str, executable: bool) -> Result<bool, Error> {
    io::write_step_file(path, content, executable)
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::io::wizard::WizardState;
    use tempfile::TempDir;

    fn init_wizard(dir: &TempDir) {
        std::fs::create_dir_all(dir.path().join(".huskies")).unwrap();
        WizardState::init_if_missing(dir.path());
    }

    #[test]
    fn get_state_returns_active_state() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let state = get_state(dir.path()).unwrap();
        assert_eq!(state.steps.len(), 8);
    }

    #[test]
    fn get_state_not_active_when_no_wizard() {
        let dir = TempDir::new().unwrap();
        assert!(matches!(get_state(dir.path()), Err(Error::NotActive)));
    }

    #[test]
    fn mark_step_confirmed_advances_wizard() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let state = mark_step_confirmed(dir.path(), WizardStep::Context).unwrap();
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn mark_step_confirmed_wrong_order_returns_error() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        assert!(matches!(
            mark_step_confirmed(dir.path(), WizardStep::Stack),
            Err(Error::InvalidStateTransition(_))
        ));
    }

    #[test]
    fn mark_step_skipped_advances_wizard() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let state = mark_step_skipped(dir.path(), WizardStep::Context).unwrap();
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn set_step_content_stages_content() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let state =
            set_step_content(dir.path(), WizardStep::Context, Some("# ctx".to_string())).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::AwaitingConfirmation);
        assert_eq!(state.steps[1].content.as_deref(), Some("# ctx"));
    }

    #[test]
    fn status_returns_formatted_state() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let s = status(dir.path()).unwrap();
        assert!(s.contains("Setup wizard"));
        assert!(s.contains("context"));
    }

    #[test]
    fn generate_with_content_stages_and_returns_staged_message() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let msg = generate(dir.path(), Some("# Context")).unwrap();
        assert!(msg.contains("staged"));
        let state = get_state(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::AwaitingConfirmation);
    }

    #[test]
    fn generate_rejects_boilerplate_user_story_content() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let err = generate(
            dir.path(),
            Some("As a user, I want this, so that I get value."),
        )
        .unwrap_err();
        assert!(matches!(err, Error::InvalidContent(_)));
        // Content must not be staged when rejected.
        let state = get_state(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Pending);
    }

    #[test]
    fn generate_no_content_marks_generating() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let msg = generate(dir.path(), None).unwrap();
        assert!(msg.contains("generating"));
        let state = get_state(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Generating);
    }

    #[test]
    fn confirm_writes_file_and_advances() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        generate(dir.path(), Some("# Context content")).unwrap();
        let msg = confirm(dir.path()).unwrap();
        assert!(msg.contains("confirmed"));
        let path = dir
            .path()
            .join(".huskies")
            .join("specs")
            .join("00_CONTEXT.md");
        assert!(path.exists());
        assert_eq!(std::fs::read_to_string(&path).unwrap(), "# Context content");
    }

    #[test]
    fn skip_advances_without_file_write() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        let msg = skip(dir.path()).unwrap();
        assert!(msg.contains("skipped"));
        let state = get_state(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Skipped);
        assert_eq!(state.current_step_index(), 2);
    }

    #[test]
    fn retry_resets_to_pending() {
        let dir = TempDir::new().unwrap();
        init_wizard(&dir);
        generate(dir.path(), Some("some content")).unwrap();
        let msg = retry(dir.path()).unwrap();
        assert!(msg.contains("reset"));
        let state = get_state(dir.path()).unwrap();
        assert_eq!(state.steps[1].status, StepStatus::Pending);
        assert!(state.steps[1].content.is_none());
    }

    #[test]
    fn error_display_not_active() {
        assert!(Error::NotActive.to_string().contains("No wizard active"));
    }

    #[test]
    fn error_display_invalid_state_transition() {
        let e = Error::InvalidStateTransition("wrong step".to_string());
        assert!(e.to_string().contains("Invalid state transition"));
    }

    #[test]
    fn error_display_missing_input() {
        let e = Error::MissingInput("no content".to_string());
        assert!(e.to_string().contains("Missing input"));
    }

    #[test]
    fn error_display_generation_failure() {
        let e = Error::GenerationFailure("LLM timeout".to_string());
        assert!(e.to_string().contains("Generation failed"));
    }

    #[test]
    fn error_display_persistence_failure() {
        let e = Error::PersistenceFailure("disk full".to_string());
        assert!(e.to_string().contains("Persistence error"));
    }
}
