//! Single entry point for pipeline state transitions.
//!
//! [`apply_transition`] is the **only** function that should mutate a story's
//! pipeline stage.  It reads the current typed stage from the CRDT, validates
//! the transition via the pure [`super::transition()`] function, writes the new
//! stage back to the CRDT, and returns a [`TransitionFired`] event for
//! downstream subscribers.

use super::{PipelineEvent, StoryId, TransitionFired, read_typed, transition};
use chrono::Utc;

/// Error type for [`apply_transition`].
#[derive(Debug)]
pub enum ApplyError {
    /// The story was not found in the CRDT.
    NotFound(String),
    /// The CRDT projection failed.
    Projection(super::ProjectionError),
    /// The transition was invalid for the current stage.
    InvalidTransition(super::TransitionError),
}

impl std::fmt::Display for ApplyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotFound(id) => write!(f, "story '{id}' not found in CRDT"),
            Self::Projection(e) => write!(f, "projection error: {e}"),
            Self::InvalidTransition(e) => write!(f, "{e}"),
        }
    }
}

impl std::error::Error for ApplyError {}

impl From<super::ProjectionError> for ApplyError {
    fn from(e: super::ProjectionError) -> Self {
        Self::Projection(e)
    }
}

impl From<super::TransitionError> for ApplyError {
    fn from(e: super::TransitionError) -> Self {
        Self::InvalidTransition(e)
    }
}

/// Apply a pipeline event to a story, validating the transition and writing
/// the new stage to the CRDT.
///
/// This is the single canonical entry point for all pipeline stage mutations.
/// Returns a [`TransitionFired`] describing what happened, suitable for
/// broadcasting to event-bus subscribers and CRDT log consumers.
///
/// An optional `content_transform` allows callers to modify the stored content
/// (e.g. clearing front-matter fields) atomically with the stage change.
pub fn apply_transition(
    story_id: &str,
    event: PipelineEvent,
    content_transform: Option<&dyn Fn(&str) -> String>,
) -> Result<TransitionFired, ApplyError> {
    let item = read_typed(story_id)?.ok_or_else(|| ApplyError::NotFound(story_id.to_string()))?;

    let before = item.stage.clone();
    let after = transition(before.clone(), event.clone())?;
    let new_dir = after.dir_name();

    // Write the new stage to the CRDT (with optional content transform).
    crate::db::move_item_stage(story_id, new_dir, content_transform);

    // Write stage-specific metadata into the shared `resume_to` register.
    // Story 984: Superseded and Rejected stages reuse `resume_to` to carry
    // their metadata (superseded_by ID and rejection reason respectively),
    // since these stages never have a resume target.
    match &after {
        super::Stage::Superseded { superseded_by, .. } => {
            crate::crdt_state::set_resume_to_raw(story_id, &superseded_by.0);
        }
        super::Stage::Rejected { reason, .. } | super::Stage::Blocked { reason } => {
            crate::crdt_state::set_resume_to_raw(story_id, reason);
        }
        // Story 1105: write the resume target so read-back can reconstruct the
        // correct variant.  Without this, the register is stale (or empty) and
        // the deserialiser falls back to Coding regardless of where the story
        // was when it was frozen.
        super::Stage::Frozen { resume_to } => {
            crate::crdt_state::set_resume_to_raw(story_id, resume_to.dir_name());
        }
        super::Stage::ReviewHold { resume_to, .. } => {
            crate::crdt_state::set_resume_to_raw(story_id, resume_to.dir_name());
        }
        _ => {}
    }

    let fired = TransitionFired {
        story_id: StoryId(story_id.to_string()),
        before,
        after,
        event,
        at: Utc::now(),
    };

    super::events::try_broadcast(&fired);
    Ok(fired)
}

/// Convenience: apply a transition, returning an `Err(String)` on failure
/// (matches the existing lifecycle function signatures).
pub fn apply_transition_str(
    story_id: &str,
    event: PipelineEvent,
    content_transform: Option<&dyn Fn(&str) -> String>,
) -> Result<TransitionFired, String> {
    apply_transition(story_id, event, content_transform).map_err(|e| e.to_string())
}

/// Freeze a story.
///
/// Story 945: `Stage::Frozen { resume_to }` is the single source of truth;
/// the previous `frozen: bool` flag has been removed.  Transitions any
/// non-terminal stage to `Stage::Frozen { resume_to: <previous stage> }`.
pub fn transition_to_frozen(story_id: &str) -> Result<(), ApplyError> {
    apply_transition(story_id, PipelineEvent::Freeze, None).map(|_| ())
}

/// Unfreeze a story.
///
/// Story 945: returns the story to the `resume_to` stage stored on
/// `Stage::Frozen`.
pub fn transition_to_unfrozen(story_id: &str) -> Result<(), ApplyError> {
    apply_transition(story_id, PipelineEvent::Unfreeze, None).map(|_| ())
}
