//! Issue use cases.
//!
//! The domain/code term is `Issue` throughout this layer.

use std::collections::HashSet;
use std::sync::Arc;

use domain::{AgentContextStore, Clock, EventBus, IdGenerator};
use domain::{
    AgentId, AgentIssueRef, AgentIssueRole, DomainEvent, Issue, IssueActor, IssueCarnet, IssueId,
    IssueIndexEntry, IssueLink, IssueLinkKind, IssueListFilter, IssueNumberAllocator,
    IssuePriority, IssueRef, IssueStatus, IssueStore, IssueVersion, MarkdownDoc, Project,
};

use crate::error::AppError;

/// Input for [`CreateIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateIssueInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue title.
    pub title: String,
    /// Issue description Markdown.
    pub description: String,
    /// Initial priority.
    pub priority: IssuePriority,
    /// Initial status.
    pub status: IssueStatus,
    /// Initial issue links.
    pub links: Vec<IssueLink>,
    /// Initially assigned agents.
    pub assigned_agent_ids: Vec<AgentId>,
    /// Actor creating the issue.
    pub actor: IssueActor,
}

/// Output of [`CreateIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateIssueOutput {
    /// Created issue.
    pub issue: Issue,
}

/// Creates an issue, allocating a non-reused project-local number.
pub struct CreateIssue {
    issues: Arc<dyn IssueStore>,
    allocator: Arc<dyn IssueNumberAllocator>,
    contexts: Arc<dyn AgentContextStore>,
    ids: Arc<dyn IdGenerator>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

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

    /// Executes creation.
    ///
    /// # Errors
    /// [`AppError`] on invariant, manifest validation or store failure.
    pub async fn execute(&self, input: CreateIssueInput) -> Result<CreateIssueOutput, AppError> {
        validate_agents(&self.contexts, &input.project, &input.assigned_agent_ids).await?;
        let number = self.allocator.allocate_next(&input.project.root).await?;
        let issue_ref = IssueRef::from(number);
        for link in &input.links {
            if link.target != issue_ref {
                self.issues
                    .get_by_ref(&input.project.root, link.target)
                    .await?;
            }
        }
        let agent_refs = input
            .assigned_agent_ids
            .into_iter()
            .map(|agent_id| AgentIssueRef {
                agent_id,
                role: AgentIssueRole::Assigned,
            })
            .collect();
        let issue = Issue::new(
            IssueId::from_uuid(self.ids.new_uuid()),
            number,
            input.title,
            MarkdownDoc::new(input.description),
            input.status,
            input.priority,
            MarkdownDoc::default(),
            input.links,
            agent_refs,
            input.actor,
            now(&self.clock),
        )
        .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.issues.create(&input.project.root, &issue).await?;
        self.events.publish(DomainEvent::IssueCreated {
            issue_id: issue.id,
            issue_ref: issue.reference(),
        });
        Ok(CreateIssueOutput { issue })
    }
}

/// Input for [`ReadIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadIssueInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
}

/// Output of [`ReadIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadIssueOutput {
    /// Issue.
    pub issue: Issue,
}

/// Reads an issue.
pub struct ReadIssue {
    issues: Arc<dyn IssueStore>,
}

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

    /// Executes read.
    ///
    /// # Errors
    /// [`AppError`] on store failure.
    pub async fn execute(&self, input: ReadIssueInput) -> Result<ReadIssueOutput, AppError> {
        Ok(ReadIssueOutput {
            issue: self
                .issues
                .get_by_ref(&input.project.root, input.issue_ref)
                .await?,
        })
    }
}

/// Input for [`DeleteIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteIssueInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
}

/// Output of [`DeleteIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeleteIssueOutput {
    /// Deleted issue reference.
    pub issue_ref: IssueRef,
}

/// Deletes an issue.
pub struct DeleteIssue {
    issues: Arc<dyn IssueStore>,
    events: Arc<dyn EventBus>,
}

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

    /// Executes deletion.
    ///
    /// # Errors
    /// [`AppError`] on missing issue or store failure.
    pub async fn execute(&self, input: DeleteIssueInput) -> Result<DeleteIssueOutput, AppError> {
        let issue = self
            .issues
            .get_by_ref(&input.project.root, input.issue_ref)
            .await?;
        let freed_sprint = issue.sprint;
        self.issues
            .delete(&input.project.root, input.issue_ref)
            .await?;
        self.events.publish(DomainEvent::IssueDeleted {
            project_id: input.project.id,
            issue_ref: input.issue_ref,
            freed_sprint,
        });
        Ok(DeleteIssueOutput {
            issue_ref: input.issue_ref,
        })
    }
}

/// Input for [`ReadIssueCarnet::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadIssueCarnetInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
}

/// Output of [`ReadIssueCarnet::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadIssueCarnetOutput {
    /// Carnet projection.
    pub carnet: IssueCarnet,
}

/// Reads an issue carnet.
pub struct ReadIssueCarnet {
    issues: Arc<dyn IssueStore>,
}

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

    /// Executes carnet read.
    ///
    /// # Errors
    /// [`AppError`] on store failure.
    pub async fn execute(
        &self,
        input: ReadIssueCarnetInput,
    ) -> Result<ReadIssueCarnetOutput, AppError> {
        Ok(ReadIssueCarnetOutput {
            carnet: self
                .issues
                .read_carnet(&input.project.root, input.issue_ref)
                .await?,
        })
    }
}

/// Input for [`ListIssues::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListIssuesInput {
    /// Project owning the issues.
    pub project: Project,
    /// Filter.
    pub filter: IssueListFilter,
}

/// Output of [`ListIssues::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListIssuesOutput {
    /// Matching issue index entries.
    pub issues: Vec<IssueIndexEntry>,
}

/// Lists issues.
pub struct ListIssues {
    issues: Arc<dyn IssueStore>,
}

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

    /// Executes list.
    ///
    /// # Errors
    /// [`AppError`] on store failure.
    pub async fn execute(&self, input: ListIssuesInput) -> Result<ListIssuesOutput, AppError> {
        Ok(ListIssuesOutput {
            issues: self.issues.list(&input.project.root, input.filter).await?,
        })
    }
}

/// Input for [`UpdateIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateIssueInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
    /// Expected optimistic version.
    pub expected_version: IssueVersion,
    /// Optional title replacement.
    pub title: Option<String>,
    /// Optional description replacement.
    pub description: Option<String>,
    /// Optional status replacement.
    pub status: Option<IssueStatus>,
    /// Optional priority replacement.
    pub priority: Option<IssuePriority>,
    /// Optional full assigned-agent replacement.
    pub assigned_agent_ids: Option<Vec<AgentId>>,
    /// Actor updating the issue.
    pub actor: IssueActor,
}

/// Output of [`UpdateIssue::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateIssueOutput {
    /// Updated issue.
    pub issue: Issue,
}

/// Updates issue fields with optimistic concurrency.
pub struct UpdateIssue {
    issues: Arc<dyn IssueStore>,
    contexts: Arc<dyn AgentContextStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

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

    /// Executes update.
    ///
    /// # Errors
    /// [`AppError`] on invariant, manifest validation or store failure.
    pub async fn execute(&self, input: UpdateIssueInput) -> Result<UpdateIssueOutput, AppError> {
        if let Some(agent_ids) = &input.assigned_agent_ids {
            validate_agents(&self.contexts, &input.project, agent_ids).await?;
        }
        let current = self
            .issues
            .get_by_ref(&input.project.root, input.issue_ref)
            .await?;
        let old_status = current.status;
        let old_priority = current.priority;
        let old_assigned = assigned_set(&current);
        let assigned_agent_ids = input.assigned_agent_ids.clone();
        let updated = current
            .mutate(input.actor, now(&self.clock), |issue| {
                if let Some(title) = input.title {
                    issue.title = title;
                }
                if let Some(description) = input.description {
                    issue.description = MarkdownDoc::new(description);
                }
                if let Some(status) = input.status {
                    issue.status = status;
                }
                if let Some(priority) = input.priority {
                    issue.priority = priority;
                }
                if let Some(agent_ids) = assigned_agent_ids {
                    replace_assigned_agents(issue, agent_ids);
                }
            })
            .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.issues
            .update(&input.project.root, &updated, input.expected_version)
            .await?;
        publish_update_events(
            &self.events,
            &updated,
            old_status,
            old_priority,
            old_assigned,
        );
        Ok(UpdateIssueOutput { issue: updated })
    }
}

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

/// Output of [`UpdateIssueCarnet::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UpdateIssueCarnetOutput {
    /// Carnet projection.
    pub carnet: IssueCarnet,
}

/// Updates an issue carnet.
pub struct UpdateIssueCarnet {
    issues: Arc<dyn IssueStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl UpdateIssueCarnet {
    /// 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 carnet update.
    ///
    /// # Errors
    /// [`AppError`] on store failure or conflict.
    pub async fn execute(
        &self,
        input: UpdateIssueCarnetInput,
    ) -> Result<UpdateIssueCarnetOutput, AppError> {
        let carnet = self
            .issues
            .write_carnet(
                &input.project.root,
                input.issue_ref,
                MarkdownDoc::new(input.carnet),
                input.actor,
                now(&self.clock),
                input.expected_version,
            )
            .await?;
        self.events.publish(DomainEvent::IssueCarnetUpdated {
            issue_ref: input.issue_ref,
            version: carnet.version,
        });
        Ok(UpdateIssueCarnetOutput { carnet })
    }
}

/// Input for [`LinkIssues::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkIssuesInput {
    /// Project owning the issues.
    pub project: Project,
    /// Source issue.
    pub issue_ref: IssueRef,
    /// Target issue.
    pub target: IssueRef,
    /// Link kind.
    pub kind: IssueLinkKind,
    /// Expected optimistic version.
    pub expected_version: IssueVersion,
    /// Actor updating the issue.
    pub actor: IssueActor,
}

/// Input for [`UnlinkIssues::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnlinkIssuesInput {
    /// Project owning the issues.
    pub project: Project,
    /// Source issue.
    pub issue_ref: IssueRef,
    /// Target issue.
    pub target: IssueRef,
    /// Optional link kind. `None` removes every link from `issue_ref` to `target`.
    pub kind: Option<IssueLinkKind>,
    /// Expected optimistic version.
    pub expected_version: IssueVersion,
    /// Actor updating the issue.
    pub actor: IssueActor,
}

/// Output for link mutations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkIssuesOutput {
    /// Updated issue.
    pub issue: Issue,
}

/// Adds an issue link.
pub struct LinkIssues {
    issues: Arc<dyn IssueStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl LinkIssues {
    /// 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 link creation.
    ///
    /// # Errors
    /// [`AppError`] on invariant or store failure.
    pub async fn execute(&self, input: LinkIssuesInput) -> Result<LinkIssuesOutput, AppError> {
        // Ensure target exists and source self-link is caught before persisting.
        self.issues
            .get_by_ref(&input.project.root, input.target)
            .await?;
        let current = self
            .issues
            .get_by_ref(&input.project.root, input.issue_ref)
            .await?;
        let updated = current
            .mutate(input.actor, now(&self.clock), |issue| {
                let link = IssueLink {
                    target: input.target,
                    kind: input.kind,
                };
                if !issue.links.contains(&link) {
                    issue.links.push(link);
                }
            })
            .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.issues
            .update(&input.project.root, &updated, input.expected_version)
            .await?;
        self.events.publish(DomainEvent::IssueLinked {
            issue_ref: input.issue_ref,
            target: input.target,
            kind: input.kind,
            version: updated.version,
        });
        Ok(LinkIssuesOutput { issue: updated })
    }
}

/// Removes an issue link.
pub struct UnlinkIssues {
    issues: Arc<dyn IssueStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

impl UnlinkIssues {
    /// 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 link removal.
    ///
    /// # Errors
    /// [`AppError`] on store failure.
    pub async fn execute(&self, input: UnlinkIssuesInput) -> Result<LinkIssuesOutput, AppError> {
        let current = self
            .issues
            .get_by_ref(&input.project.root, input.issue_ref)
            .await?;
        let removed: Vec<IssueLinkKind> = current
            .links
            .iter()
            .filter(|link| {
                link.target == input.target && input.kind.map_or(true, |kind| link.kind == kind)
            })
            .map(|link| link.kind)
            .collect();
        let updated = current
            .mutate(input.actor, now(&self.clock), |issue| {
                issue.links.retain(|link| {
                    !(link.target == input.target
                        && input.kind.map_or(true, |kind| link.kind == kind))
                });
            })
            .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.issues
            .update(&input.project.root, &updated, input.expected_version)
            .await?;
        for kind in removed {
            self.events.publish(DomainEvent::IssueUnlinked {
                issue_ref: input.issue_ref,
                target: input.target,
                kind,
                version: updated.version,
            });
        }
        Ok(LinkIssuesOutput { issue: updated })
    }
}

/// Input for [`AssignIssueAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssignIssueAgentInput {
    /// Project owning the issue.
    pub project: Project,
    /// Issue reference.
    pub issue_ref: IssueRef,
    /// Agent to assign or unassign.
    pub agent_id: AgentId,
    /// Whether the agent must be assigned.
    pub assigned: bool,
    /// Expected optimistic version.
    pub expected_version: IssueVersion,
    /// Actor updating the issue.
    pub actor: IssueActor,
}

/// Output of [`AssignIssueAgent::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssignIssueAgentOutput {
    /// Updated issue.
    pub issue: Issue,
}

/// Assigns or unassigns an agent on an issue.
pub struct AssignIssueAgent {
    issues: Arc<dyn IssueStore>,
    contexts: Arc<dyn AgentContextStore>,
    clock: Arc<dyn Clock>,
    events: Arc<dyn EventBus>,
}

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

    /// Executes assignment mutation.
    ///
    /// # Errors
    /// [`AppError`] on manifest validation or store failure.
    pub async fn execute(
        &self,
        input: AssignIssueAgentInput,
    ) -> Result<AssignIssueAgentOutput, AppError> {
        validate_agents(&self.contexts, &input.project, &[input.agent_id]).await?;
        let current = self
            .issues
            .get_by_ref(&input.project.root, input.issue_ref)
            .await?;
        let updated =
            current
                .mutate(input.actor, now(&self.clock), |issue| {
                    if input.assigned {
                        if !issue.agent_refs.iter().any(|r| {
                            r.agent_id == input.agent_id && r.role == AgentIssueRole::Assigned
                        }) {
                            issue.agent_refs.push(AgentIssueRef {
                                agent_id: input.agent_id,
                                role: AgentIssueRole::Assigned,
                            });
                        }
                    } else {
                        issue.agent_refs.retain(|r| {
                            !(r.agent_id == input.agent_id && r.role == AgentIssueRole::Assigned)
                        });
                    }
                })
                .map_err(|err| AppError::Invalid(err.to_string()))?;
        self.issues
            .update(&input.project.root, &updated, input.expected_version)
            .await?;
        if input.assigned {
            self.events.publish(DomainEvent::IssueAgentAssigned {
                issue_ref: input.issue_ref,
                agent_id: input.agent_id,
                version: updated.version,
            });
        } else {
            self.events.publish(DomainEvent::IssueAgentUnassigned {
                issue_ref: input.issue_ref,
                agent_id: input.agent_id,
                version: updated.version,
            });
        }
        Ok(AssignIssueAgentOutput { issue: updated })
    }
}

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

async fn validate_agents(
    contexts: &Arc<dyn AgentContextStore>,
    project: &Project,
    agent_ids: &[AgentId],
) -> Result<(), AppError> {
    if agent_ids.is_empty() {
        return Ok(());
    }
    let manifest = contexts.load_manifest(project).await?;
    let known: HashSet<AgentId> = manifest
        .entries
        .into_iter()
        .map(|entry| entry.agent_id)
        .collect();
    for agent_id in agent_ids {
        if !known.contains(agent_id) {
            return Err(AppError::NotFound(format!("agent {agent_id}")));
        }
    }
    Ok(())
}

fn assigned_set(issue: &Issue) -> HashSet<AgentId> {
    issue
        .agent_refs
        .iter()
        .filter(|reference| reference.role == AgentIssueRole::Assigned)
        .map(|reference| reference.agent_id)
        .collect()
}

fn replace_assigned_agents(issue: &mut Issue, agent_ids: Vec<AgentId>) {
    issue
        .agent_refs
        .retain(|reference| reference.role != AgentIssueRole::Assigned);
    for agent_id in agent_ids {
        issue.agent_refs.push(AgentIssueRef {
            agent_id,
            role: AgentIssueRole::Assigned,
        });
    }
}

fn publish_update_events(
    events: &Arc<dyn EventBus>,
    issue: &Issue,
    old_status: IssueStatus,
    old_priority: IssuePriority,
    old_assigned: HashSet<AgentId>,
) {
    let issue_ref = issue.reference();
    events.publish(DomainEvent::IssueUpdated {
        issue_ref,
        version: issue.version,
    });
    if issue.status != old_status {
        events.publish(DomainEvent::IssueStatusChanged {
            issue_ref,
            status: issue.status,
            version: issue.version,
        });
    }
    if issue.priority != old_priority {
        events.publish(DomainEvent::IssuePriorityChanged {
            issue_ref,
            priority: issue.priority,
            version: issue.version,
        });
    }
    let new_assigned = assigned_set(issue);
    for agent_id in new_assigned.difference(&old_assigned) {
        events.publish(DomainEvent::IssueAgentAssigned {
            issue_ref,
            agent_id: *agent_id,
            version: issue.version,
        });
    }
    for agent_id in old_assigned.difference(&new_assigned) {
        events.publish(DomainEvent::IssueAgentUnassigned {
            issue_ref,
            agent_id: *agent_id,
            version: issue.version,
        });
    }
}
