//! Sprint domain model.
//!
//! A sprint is a project-scoped planning aggregate. Ticket membership remains
//! authoritative on [`crate::issue::Issue`].

use std::collections::HashSet;

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::ids::SprintId;
use crate::issue::IssueActor;

/// Reorderable execution position. Values start at 1.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SprintOrder(u32);

impl SprintOrder {
    /// Builds a sprint order.
    ///
    /// # Errors
    /// [`SprintError::InvalidOrder`] when `value == 0`.
    pub fn new(value: u32) -> Result<Self, SprintError> {
        if value == 0 {
            return Err(SprintError::InvalidOrder);
        }
        Ok(Self(value))
    }

    /// Returns the raw order.
    #[must_use]
    pub const fn get(self) -> u32 {
        self.0
    }
}

impl std::fmt::Display for SprintOrder {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Optimistic-concurrency version.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SprintVersion(u64);

impl SprintVersion {
    /// Initial version assigned to a newly-created sprint.
    pub const INITIAL: Self = Self(1);

    /// Builds a version.
    ///
    /// # Errors
    /// [`SprintError::InvalidVersion`] when `value == 0`.
    pub fn new(value: u64) -> Result<Self, SprintError> {
        if value == 0 {
            return Err(SprintError::InvalidVersion);
        }
        Ok(Self(value))
    }

    /// Returns the raw version.
    #[must_use]
    pub const fn get(self) -> u64 {
        self.0
    }

    /// Returns the next optimistic version.
    #[must_use]
    pub const fn next(self) -> Self {
        Self(self.0 + 1)
    }
}

impl std::fmt::Display for SprintVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Sprint lifecycle status.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SprintStatus {
    /// Planned and not active yet.
    Planned,
    /// Current sprint.
    Active,
    /// Completed sprint.
    Done,
}

impl Default for SprintStatus {
    fn default() -> Self {
        Self::Planned
    }
}

/// Sprint aggregate.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sprint {
    /// Stable UUID.
    pub id: SprintId,
    /// Reorderable execution position.
    pub order: SprintOrder,
    /// Human-readable name. Empty means UI may display `Sprint {order}`.
    pub name: String,
    /// Lifecycle status.
    pub status: SprintStatus,
    /// Creator.
    pub created_by: IssueActor,
    /// Last updater.
    pub updated_by: IssueActor,
    /// Creation time, epoch milliseconds.
    pub created_at: u64,
    /// Last update time, epoch milliseconds.
    pub updated_at: u64,
    /// Optimistic-concurrency version.
    pub version: SprintVersion,
}

impl Sprint {
    /// Builds a new sprint with version 1.
    ///
    /// # Errors
    /// [`SprintError`] when invariants are violated.
    pub fn new(
        id: SprintId,
        order: SprintOrder,
        name: impl Into<String>,
        status: Option<SprintStatus>,
        actor: IssueActor,
        now_ms: u64,
    ) -> Result<Self, SprintError> {
        let sprint = Self {
            id,
            order,
            name: name.into(),
            status: status.unwrap_or_default(),
            created_by: actor.clone(),
            updated_by: actor,
            created_at: now_ms,
            updated_at: now_ms,
            version: SprintVersion::INITIAL,
        };
        sprint.validate()?;
        Ok(sprint)
    }

    /// Rehydrates a persisted sprint.
    ///
    /// # Errors
    /// [`SprintError`] when persisted data violates invariants.
    pub fn rehydrate(sprint: Self) -> Result<Self, SprintError> {
        sprint.validate()?;
        Ok(sprint)
    }

    /// Validates invariants local to one sprint.
    ///
    /// # Errors
    /// [`SprintError`] when an invariant is violated.
    pub fn validate(&self) -> Result<(), SprintError> {
        SprintOrder::new(self.order.get())?;
        SprintVersion::new(self.version.get())?;
        Ok(())
    }

    /// Applies a mutation and increments the version.
    ///
    /// # Errors
    /// [`SprintError`] when the resulting sprint violates invariants.
    pub fn mutate(
        mut self,
        actor: IssueActor,
        now_ms: u64,
        f: impl FnOnce(&mut Self),
    ) -> Result<Self, SprintError> {
        f(&mut self);
        self.updated_by = actor;
        self.updated_at = now_ms;
        self.version = self.version.next();
        self.validate()?;
        Ok(self)
    }
}

/// Index row used by stores and list use cases.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SprintIndexEntry {
    /// Stable sprint id.
    pub id: SprintId,
    /// Relative path to the sprint folder.
    pub path: String,
    /// Reorderable position.
    pub order: SprintOrder,
    /// Human-readable name.
    pub name: String,
    /// Lifecycle status.
    pub status: SprintStatus,
    /// Last update time.
    pub updated_at: u64,
    /// Current optimistic version.
    pub version: SprintVersion,
}

impl From<&Sprint> for SprintIndexEntry {
    fn from(sprint: &Sprint) -> Self {
        Self {
            id: sprint.id,
            path: sprint.id.to_string(),
            order: sprint.order,
            name: sprint.name.clone(),
            status: sprint.status,
            updated_at: sprint.updated_at,
            version: sprint.version,
        }
    }
}

/// Validates that sprint orders are unique and contiguous from 1.
///
/// # Errors
/// [`SprintError::DuplicateOrder`] or [`SprintError::NonContiguousOrder`].
pub fn validate_sprint_ordering<'a>(
    sprints: impl IntoIterator<Item = &'a Sprint>,
) -> Result<(), SprintError> {
    let mut orders: Vec<u32> = sprints
        .into_iter()
        .map(|sprint| sprint.order.get())
        .collect();
    orders.sort_unstable();
    let mut seen = HashSet::new();
    for order in &orders {
        if !seen.insert(*order) {
            return Err(SprintError::DuplicateOrder(*order));
        }
    }
    for (idx, order) in orders.into_iter().enumerate() {
        let expected = (idx as u32) + 1;
        if order != expected {
            return Err(SprintError::NonContiguousOrder {
                expected,
                actual: order,
            });
        }
    }
    Ok(())
}

/// Domain errors for sprint invariants and value objects.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum SprintError {
    /// Sprint order is strictly positive.
    #[error("sprint order must be greater than zero")]
    InvalidOrder,
    /// Sprint versions are strictly positive.
    #[error("sprint version must be greater than zero")]
    InvalidVersion,
    /// Two sprints share the same order.
    #[error("duplicate sprint order {0}")]
    DuplicateOrder(u32),
    /// Sprint ordering must be contiguous from 1.
    #[error("non-contiguous sprint order: expected {expected}, actual {actual}")]
    NonContiguousOrder {
        /// Expected contiguous value.
        expected: u32,
        /// Actual encountered value.
        actual: u32,
    },
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ids::SprintId;
    use uuid::Uuid;

    fn sid(n: u128) -> SprintId {
        SprintId::from_uuid(Uuid::from_u128(n))
    }

    fn sprint(order: u32) -> Sprint {
        Sprint::new(
            sid(order as u128),
            SprintOrder::new(order).unwrap(),
            "",
            None,
            IssueActor::User,
            10,
        )
        .unwrap()
    }

    #[test]
    fn sprint_defaults_to_planned_and_version_one() {
        let sprint = sprint(1);
        assert_eq!(sprint.status, SprintStatus::Planned);
        assert_eq!(sprint.version, SprintVersion::INITIAL);
    }

    #[test]
    fn order_and_version_reject_zero() {
        assert_eq!(SprintOrder::new(0), Err(SprintError::InvalidOrder));
        assert_eq!(SprintVersion::new(0), Err(SprintError::InvalidVersion));
    }

    #[test]
    fn validate_ordering_accepts_contiguous_unique_orders() {
        let a = sprint(1);
        let b = sprint(2);
        assert_eq!(validate_sprint_ordering([&b, &a]), Ok(()));
    }

    #[test]
    fn validate_ordering_rejects_duplicates_and_gaps() {
        let a = sprint(1);
        let duplicate = sprint(1);
        assert_eq!(
            validate_sprint_ordering([&a, &duplicate]),
            Err(SprintError::DuplicateOrder(1))
        );

        let c = sprint(3);
        assert_eq!(
            validate_sprint_ordering([&a, &c]),
            Err(SprintError::NonContiguousOrder {
                expected: 2,
                actual: 3,
            })
        );
    }

    #[test]
    fn mutate_increments_version_and_tracks_actor() {
        let updated = sprint(1)
            .mutate(IssueActor::System, 20, |sprint| {
                sprint.name = "Delivery".to_owned();
            })
            .unwrap();
        assert_eq!(updated.name, "Delivery");
        assert_eq!(updated.updated_by, IssueActor::System);
        assert_eq!(updated.updated_at, 20);
        assert_eq!(updated.version.get(), 2);
    }
}
