//! Story service — domain logic for creating, updating, and managing pipeline work items.
//!
//! Extracted from `http/mcp/story_tools.rs` following the conventions in
//! `docs/architecture/service-modules.md`:
//! - `mod.rs` (this file) — public API, typed [`Error`], orchestration
//! - `io.rs` — the ONLY place that performs side effects (story file I/O)
//! - `criteria.rs` — pure criterion parsing and validation
//! - `lifecycle.rs` — pure story-ID and lifecycle helpers
//! - `front_matter.rs` — pure front-matter field validation
//! - `validation.rs` — pure story content validation
//!
//! # State-Mutation Inventory (Axis 3)
//!
//! The story service orchestrates the following state writes across all
//! lifecycle operations:
//!
//! ## CRDT Writes
//! - `crdt_state::write_item` — on story/bug/spike/refactor creation (via `http::workflow`)
//! - `crdt_state::evict_item` — on story deletion or purge (tombstone op)
//! - `crdt_state::move_item` — on stage transitions (backlog → current → qa → merge → done → archived)
//!
//! ## Filesystem Shadow Writes
//! - `.huskies/work/1_backlog/<id>.md` — written on story/bug/spike/refactor creation
//! - `.huskies/work/<stage>/<id>.md` — moved between stages on lifecycle transitions
//! - `.huskies/work/<stage>/<id>.md` front matter — updated by `update_story`, `check_criterion`,
//!   `edit_criterion`, `add_criterion`, `remove_criterion`, `accept_story`, `reject_qa`
//! - `.huskies/bugs/archive/<id>.md` — written on `close_bug`
//! - `.huskies/work/4_merge/<id>.md` merge_failure field — written by `report_merge_failure`
//!
//! ## Database Shadow Table
//! - `pipeline_items` row — updated on stage transitions and item creation/deletion
//! - `content_store` entry — updated on story content changes, deleted on purge/delete

/// Pure criterion parsing, checkbox toggling, and validation.
pub mod criteria;
/// Pure front-matter field validation (stage names, agent assignments).
pub mod front_matter;
/// Side-effectful story file I/O — read, write, move, and delete.
pub mod io;
/// Pure story-ID helpers and lifecycle state transitions.
pub mod lifecycle;
/// Pure story content validation rules.
pub mod validation;

pub use criteria::parse_test_cases;
#[allow(unused_imports)]
pub use front_matter::{is_valid_stage, stage_alias_to_dir};
#[allow(unused_imports)]
pub use lifecycle::{has_valid_id_prefix, story_number};
#[allow(unused_imports)]
pub use validation::{is_valid_story_name, validate_criterion_index};

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

/// Typed errors returned by `service::story` functions.
///
/// HTTP handlers map these to status codes:
/// - [`Error::NotFound`] → 404 Not Found
/// - [`Error::Validation`] → 400 Bad Request
/// - [`Error::Conflict`] → 409 Conflict
/// - [`Error::Io`] → 500 Internal Server Error
/// - [`Error::UpstreamFailure`] → 500 Internal Server Error
#[allow(dead_code)]
#[derive(Debug)]
pub enum Error {
    /// The requested story or work item was not found.
    NotFound(String),
    /// A required argument is missing or has an invalid value.
    Validation(String),
    /// The operation cannot proceed due to a conflicting state (e.g. unmerged branch).
    Conflict(String),
    /// A filesystem read or write operation failed.
    Io(String),
    /// An upstream dependency (agents, CRDT, git) returned an unexpected error.
    UpstreamFailure(String),
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotFound(msg) => write!(f, "Not found: {msg}"),
            Self::Validation(msg) => write!(f, "Validation error: {msg}"),
            Self::Conflict(msg) => write!(f, "Conflict: {msg}"),
            Self::Io(msg) => write!(f, "I/O error: {msg}"),
            Self::UpstreamFailure(msg) => write!(f, "Upstream failure: {msg}"),
        }
    }
}

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

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

    #[test]
    fn error_display_not_found() {
        let e = Error::NotFound("story 42 not found".to_string());
        assert!(e.to_string().contains("Not found"));
    }

    #[test]
    fn error_display_validation() {
        let e = Error::Validation("name is required".to_string());
        assert!(e.to_string().contains("Validation error"));
    }

    #[test]
    fn error_display_conflict() {
        let e = Error::Conflict("unmerged changes".to_string());
        assert!(e.to_string().contains("Conflict"));
    }

    #[test]
    fn error_display_io() {
        let e = Error::Io("story file write failed".to_string());
        assert!(e.to_string().contains("I/O error"));
    }

    #[test]
    fn error_display_upstream_failure() {
        let e = Error::UpstreamFailure("CRDT eviction failed".to_string());
        assert!(e.to_string().contains("Upstream failure"));
    }
}
