//! Sprint use cases.

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use domain::{
    Clock, DomainEvent, EventBus, IdGenerator, IssueActor, IssueListFilter, IssueRef, IssueStore,
    IssueVersion, Project, Sprint, SprintId, SprintIndexEntry, SprintOrder, SprintStatus,
    SprintStore, SprintVersion,
};

use crate::error::AppError;

/// Input for [`CreateSprint::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSprintInput {
    /// Project owning the sprint.
    pub project: Project,
    /// Sprint name.
    pub name: String,
    /// Initial status. Defaults to planned.
    pub status: Option<SprintStatus>,
    /// Actor creating the sprint.
    pub actor: IssueActor,
}

/// Output of [`CreateSprint::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateSprintOutput {
    /// Created sprint.
    pub sprint: Sprint,
}

/// Creates a sprint at `max(order)+1`.
pub struct CreateSprint {
    sprints: Arc<dyn SprintStore>,
    ids: Arc<dyn IdGenerator>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl CreateSprint {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        sprints: Arc<dyn SprintStore>,
        ids: Arc<dyn IdGenerator>,
        clock: Arc<dyn Clock>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            sprints,
            ids,
            clock,
            events,
        }
    }

    /// Executes creation.
    ///
    /// # Errors
    /// [`AppError`] on invariant or store failure.
    pub async fn execute(&self, input: CreateSprintInput) -> Result<CreateSprintOutput, AppError> {
        let rows = self.sprints.list(&input.project.root).await?;
        let next_order = rows.iter().map(|row| row.order.get()).max().unwrap_or(0) + 1;
        let sprint = Sprint::new(
            SprintId::from_uuid(self.ids.new_uuid()),
            SprintOrder::new(next_order).map_err(|err| AppError::Invalid(err.to_string()))?,
            input.name,
            input.status,
            input.actor,
            now(&self.clock),
        )
        .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.sprints.create(&input.project.root, &sprint).await?;
        self.events.publish(DomainEvent::SprintCreated {
            sprint_id: sprint.id,
            order: sprint.order,
        });
        Ok(CreateSprintOutput { sprint })
    }
}

/// Input for [`ListSprints::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListSprintsInput {
    /// Project owning the sprints.
    pub project: Project,
}

/// Sprint list row enriched with ticket count.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SprintListEntry {
    /// Sprint index projection.
    pub sprint: SprintIndexEntry,
    /// Number of tickets assigned to the sprint.
    pub ticket_count: usize,
}

/// Output of [`ListSprints::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListSprintsOutput {
    /// Sprints sorted by order.
    pub sprints: Vec<SprintListEntry>,
}

/// Lists sprints with derived ticket counts.
pub struct ListSprints {
    sprints: Arc<dyn SprintStore>,
    issues: Arc<dyn IssueStore>,
}

impl ListSprints {
    /// Builds the use case.
    #[must_use]
    pub fn new(sprints: Arc<dyn SprintStore>, issues: Arc<dyn IssueStore>) -> Self {
        Self { sprints, issues }
    }

    /// Executes list.
    ///
    /// # Errors
    /// [`AppError`] on store failure.
    pub async fn execute(&self, input: ListSprintsInput) -> Result<ListSprintsOutput, AppError> {
        let rows = self.sprints.list(&input.project.root).await?;
        let mut out = Vec::with_capacity(rows.len());
        for row in rows {
            let ticket_count = self
                .issues
                .list(
                    &input.project.root,
                    IssueListFilter {
                        sprint: Some(row.id),
                        ..IssueListFilter::default()
                    },
                )
                .await?
                .len();
            out.push(SprintListEntry {
                sprint: row,
                ticket_count,
            });
        }
        Ok(ListSprintsOutput { sprints: out })
    }
}

/// Input for [`RenameSprint::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenameSprintInput {
    /// Project owning the sprint.
    pub project: Project,
    /// Sprint id.
    pub sprint_id: SprintId,
    /// Expected optimistic version.
    pub expected_version: SprintVersion,
    /// New name.
    pub name: String,
    /// Actor updating the sprint.
    pub actor: IssueActor,
}

/// Output for sprint mutations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SprintOutput {
    /// Updated sprint.
    pub sprint: Sprint,
}

/// Renames a sprint.
pub struct RenameSprint {
    sprints: Arc<dyn SprintStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl RenameSprint {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        sprints: Arc<dyn SprintStore>,
        clock: Arc<dyn Clock>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            sprints,
            clock,
            events,
        }
    }

    /// Executes rename.
    ///
    /// # Errors
    /// [`AppError`] on store failure or conflict.
    pub async fn execute(&self, input: RenameSprintInput) -> Result<SprintOutput, AppError> {
        let current = self
            .sprints
            .get(&input.project.root, input.sprint_id)
            .await?;
        let updated = current
            .mutate(input.actor, now(&self.clock), |sprint| {
                sprint.name = input.name;
            })
            .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.sprints
            .update(&input.project.root, &updated, input.expected_version)
            .await?;
        self.events.publish(DomainEvent::SprintRenamed {
            sprint_id: updated.id,
            name: updated.name.clone(),
            version: updated.version,
        });
        Ok(SprintOutput { sprint: updated })
    }
}

/// Input for [`ReorderSprints::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReorderSprintsInput {
    /// Project owning the sprints.
    pub project: Project,
    /// Complete ordered list of sprint ids.
    pub ordered_ids: Vec<SprintId>,
    /// Actor updating the sprints.
    pub actor: IssueActor,
}

/// Output of [`ReorderSprints::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReorderSprintsOutput {
    /// Updated sprints sorted by order.
    pub sprints: Vec<Sprint>,
}

/// Reorders all sprints and normalises orders to 1..n.
pub struct ReorderSprints {
    sprints: Arc<dyn SprintStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl ReorderSprints {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        sprints: Arc<dyn SprintStore>,
        clock: Arc<dyn Clock>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            sprints,
            clock,
            events,
        }
    }

    /// Executes reorder.
    ///
    /// # Errors
    /// [`AppError`] when ids are incomplete/duplicated or persistence fails.
    pub async fn execute(
        &self,
        input: ReorderSprintsInput,
    ) -> Result<ReorderSprintsOutput, AppError> {
        let rows = self.sprints.list(&input.project.root).await?;
        let current_ids: HashSet<SprintId> = rows.iter().map(|row| row.id).collect();
        let ordered_ids: HashSet<SprintId> = input.ordered_ids.iter().copied().collect();
        if ordered_ids.len() != input.ordered_ids.len() || ordered_ids != current_ids {
            return Err(AppError::Invalid(
                "ordered sprint ids must contain every sprint exactly once".to_owned(),
            ));
        }

        let mut updated = Vec::with_capacity(input.ordered_ids.len());
        for (idx, sprint_id) in input.ordered_ids.into_iter().enumerate() {
            let current = self.sprints.get(&input.project.root, sprint_id).await?;
            let expected_version = current.version;
            let order = SprintOrder::new((idx as u32) + 1)
                .map_err(|err| AppError::Invalid(err.to_string()))?;
            let next = current
                .mutate(input.actor.clone(), now(&self.clock), |sprint| {
                    sprint.order = order;
                })
                .map_err(|err| AppError::Invalid(err.to_string()))?;
            self.sprints
                .update(&input.project.root, &next, expected_version)
                .await?;
            self.events.publish(DomainEvent::SprintReordered {
                sprint_id: next.id,
                order: next.order,
                version: next.version,
            });
            updated.push(next);
        }
        updated.sort_by_key(|sprint| sprint.order.get());
        Ok(ReorderSprintsOutput { sprints: updated })
    }
}

/// Input for [`DeleteSprint::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteSprintInput {
    /// Project owning the sprint.
    pub project: Project,
    /// Sprint id.
    pub sprint_id: SprintId,
    /// Actor deleting the sprint and unassigning tickets.
    pub actor: IssueActor,
}

/// Deletes a sprint and unassigns its tickets.
pub struct DeleteSprint {
    sprints: Arc<dyn SprintStore>,
    issues: Arc<dyn IssueStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl DeleteSprint {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        sprints: Arc<dyn SprintStore>,
        issues: Arc<dyn IssueStore>,
        clock: Arc<dyn Clock>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            sprints,
            issues,
            clock,
            events,
        }
    }

    /// Executes deletion.
    ///
    /// # Errors
    /// [`AppError`] on store failure or issue conflict.
    pub async fn execute(&self, input: DeleteSprintInput) -> Result<(), AppError> {
        self.sprints
            .get(&input.project.root, input.sprint_id)
            .await?;
        let assigned = self
            .issues
            .list(
                &input.project.root,
                IssueListFilter {
                    sprint: Some(input.sprint_id),
                    ..IssueListFilter::default()
                },
            )
            .await?;
        for row in assigned {
            self.assign_issue(
                &input.project,
                row.issue_ref,
                None,
                input.actor.clone(),
                now(&self.clock),
            )
            .await?;
        }
        self.sprints
            .delete(&input.project.root, input.sprint_id)
            .await?;
        self.events.publish(DomainEvent::SprintDeleted {
            sprint_id: input.sprint_id,
        });
        Ok(())
    }

    async fn assign_issue(
        &self,
        project: &Project,
        issue_ref: IssueRef,
        sprint_id: Option<SprintId>,
        actor: IssueActor,
        now_ms: u64,
    ) -> Result<(), AppError> {
        let current = self.issues.get_by_ref(&project.root, issue_ref).await?;
        let expected_version = current.version;
        let from = current.sprint;
        if from == sprint_id {
            return Ok(());
        }
        let updated = current
            .mutate(actor, now_ms, |issue| issue.sprint = sprint_id)
            .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.issues
            .update(&project.root, &updated, expected_version)
            .await?;
        publish_issue_sprint_changed(&self.events, issue_ref, from, sprint_id, updated.version);
        Ok(())
    }
}

/// Input for [`AssignTicketToSprint::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssignTicketToSprintInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
    /// Target sprint.
    pub sprint_id: SprintId,
    /// Expected issue optimistic version.
    pub expected_version: IssueVersion,
    /// Actor updating the issue.
    pub actor: IssueActor,
}

/// Input for [`UnassignTicketFromSprint::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnassignTicketFromSprintInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
    /// Expected issue optimistic version.
    pub expected_version: IssueVersion,
    /// Actor updating the issue.
    pub actor: IssueActor,
}

/// Output of ticket/sprint assignment use cases.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssignTicketToSprintOutput {
    /// Updated issue.
    pub issue: domain::Issue,
}

/// Assigns a ticket to a sprint via `IssueStore.update`.
pub struct AssignTicketToSprint {
    sprints: Arc<dyn SprintStore>,
    issues: Arc<dyn IssueStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl AssignTicketToSprint {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        sprints: Arc<dyn SprintStore>,
        issues: Arc<dyn IssueStore>,
        clock: Arc<dyn Clock>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            sprints,
            issues,
            clock,
            events,
        }
    }

    /// Executes assignment.
    ///
    /// # Errors
    /// [`AppError`] when the sprint/issue is missing or the issue version conflicts.
    pub async fn execute(
        &self,
        input: AssignTicketToSprintInput,
    ) -> Result<AssignTicketToSprintOutput, AppError> {
        self.sprints
            .get(&input.project.root, input.sprint_id)
            .await?;
        assign_ticket_sprint(
            &self.issues,
            &self.clock,
            &self.events,
            input.project,
            input.issue_ref,
            Some(input.sprint_id),
            input.expected_version,
            input.actor,
        )
        .await
    }
}

/// Unassigns a ticket from its sprint via `IssueStore.update`.
pub struct UnassignTicketFromSprint {
    issues: Arc<dyn IssueStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl UnassignTicketFromSprint {
    /// Builds the use case.
    #[must_use]
    pub fn new(
        issues: Arc<dyn IssueStore>,
        clock: Arc<dyn Clock>,
        events: Arc<dyn EventBus>,
    ) -> Self {
        Self {
            issues,
            clock,
            events,
        }
    }

    /// Executes unassignment.
    ///
    /// # Errors
    /// [`AppError`] when the issue is missing or its version conflicts.
    pub async fn execute(
        &self,
        input: UnassignTicketFromSprintInput,
    ) -> Result<AssignTicketToSprintOutput, AppError> {
        assign_ticket_sprint(
            &self.issues,
            &self.clock,
            &self.events,
            input.project,
            input.issue_ref,
            None,
            input.expected_version,
            input.actor,
        )
        .await
    }
}

async fn assign_ticket_sprint(
    issues: &Arc<dyn IssueStore>,
    clock: &Arc<dyn Clock>,
    events: &Arc<dyn EventBus>,
    project: Project,
    issue_ref: IssueRef,
    sprint_id: Option<SprintId>,
    expected_version: IssueVersion,
    actor: IssueActor,
) -> Result<AssignTicketToSprintOutput, AppError> {
    let current = issues.get_by_ref(&project.root, issue_ref).await?;
    let from = current.sprint;
    let updated = current
        .mutate(actor, now(clock), |issue| issue.sprint = sprint_id)
        .map_err(|err| AppError::Invalid(err.to_string()))?;
    issues
        .update(&project.root, &updated, expected_version)
        .await?;
    if from != sprint_id {
        publish_issue_sprint_changed(events, issue_ref, from, sprint_id, updated.version);
    }
    Ok(AssignTicketToSprintOutput { issue: updated })
}

fn publish_issue_sprint_changed(
    events: &Arc<dyn EventBus>,
    issue_ref: IssueRef,
    from: Option<SprintId>,
    to: Option<SprintId>,
    version: IssueVersion,
) {
    events.publish(DomainEvent::IssueUpdated { issue_ref, version });
    events.publish(DomainEvent::IssueSprintChanged {
        issue_ref,
        from,
        to,
        version,
    });
}

fn now(clock: &Arc<dyn Clock>) -> u64 {
    clock.now_millis().max(0) as u64
}

/// Returns sprints in the requested order with contiguous order values.
///
/// # Errors
/// [`AppError::Invalid`] when the requested ids are not a complete permutation.
pub fn normalized_reorder(
    sprints: &[Sprint],
    ordered_ids: &[SprintId],
    actor: IssueActor,
    now_ms: u64,
) -> Result<Vec<Sprint>, AppError> {
    let by_id: HashMap<SprintId, Sprint> = sprints
        .iter()
        .cloned()
        .map(|sprint| (sprint.id, sprint))
        .collect();
    if by_id.len() != sprints.len() || ordered_ids.len() != sprints.len() {
        return Err(AppError::Invalid(
            "ordered sprint ids must contain every sprint exactly once".to_owned(),
        ));
    }
    let mut seen = HashSet::new();
    let mut out = Vec::with_capacity(ordered_ids.len());
    for (idx, sprint_id) in ordered_ids.iter().copied().enumerate() {
        if !seen.insert(sprint_id) {
            return Err(AppError::Invalid(
                "ordered sprint ids must contain every sprint exactly once".to_owned(),
            ));
        }
        let Some(current) = by_id.get(&sprint_id) else {
            return Err(AppError::Invalid(
                "ordered sprint ids must contain every sprint exactly once".to_owned(),
            ));
        };
        let order =
            SprintOrder::new((idx as u32) + 1).map_err(|err| AppError::Invalid(err.to_string()))?;
        out.push(
            current
                .clone()
                .mutate(actor.clone(), now_ms, |sprint| sprint.order = order)
                .map_err(|err| AppError::Invalid(err.to_string()))?,
        );
    }
    Ok(out)
}
