//! Issue domain model.
//!
//! Code uses `Issue` terminology exclusively to avoid colliding with the
//! inter-agent delegation model.

use std::str::FromStr;

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

use crate::ids::{AgentId, IssueId, SprintId};
use crate::markdown::MarkdownDoc;

/// Sequential per-project issue number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct IssueNumber(u64);

impl IssueNumber {
    /// Builds an issue number. Values start at 1.
    ///
    /// # Errors
    /// [`IssueError::InvalidNumber`] when `value == 0`.
    pub fn new(value: u64) -> Result<Self, IssueError> {
        if value == 0 {
            return Err(IssueError::InvalidNumber);
        }
        Ok(Self(value))
    }

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

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

/// Human-friendly issue reference, formatted as `#<number>`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IssueRef(IssueNumber);

impl IssueRef {
    /// Builds a reference from a validated number.
    #[must_use]
    pub const fn new(number: IssueNumber) -> Self {
        Self(number)
    }

    /// Returns the referenced issue number.
    #[must_use]
    pub const fn number(self) -> IssueNumber {
        self.0
    }
}

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

impl From<IssueNumber> for IssueRef {
    fn from(number: IssueNumber) -> Self {
        Self::new(number)
    }
}

impl FromStr for IssueRef {
    type Err = IssueError;

    fn from_str(raw: &str) -> Result<Self, Self::Err> {
        let digits = raw
            .strip_prefix('#')
            .ok_or_else(|| IssueError::InvalidRef(raw.to_owned()))?;
        if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
            return Err(IssueError::InvalidRef(raw.to_owned()));
        }
        let number = digits
            .parse::<u64>()
            .map_err(|_| IssueError::InvalidRef(raw.to_owned()))?;
        Ok(Self(IssueNumber::new(number)?))
    }
}

impl Serialize for IssueRef {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> Deserialize<'de> for IssueRef {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let raw = String::deserialize(deserializer)?;
        raw.parse().map_err(serde::de::Error::custom)
    }
}

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

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

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

    /// Returns the raw numeric value.
    #[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 IssueVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Issue lifecycle status.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IssueStatus {
    /// Open and not started.
    Open,
    /// Being worked.
    InProgress,
    /// Ready for QA.
    Qa,
    /// Closed.
    Closed,
}

/// Issue priority.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IssuePriority {
    /// Low priority.
    Low,
    /// Default priority.
    Medium,
    /// High priority.
    High,
    /// Critical priority.
    Critical,
}

/// Relationship kind between two issues.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum IssueLinkKind {
    /// Informational relation.
    RelatesTo,
    /// This issue blocks the target.
    Blocks,
    /// This issue is blocked by the target.
    BlockedBy,
    /// This issue duplicates the target.
    Duplicates,
    /// This issue depends on the target.
    DependsOn,
}

/// Link from one issue to another.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueLink {
    /// Target issue.
    pub target: IssueRef,
    /// Link kind.
    pub kind: IssueLinkKind,
}

/// Role an agent has on an issue.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AgentIssueRole {
    /// Assigned implementer.
    Assigned,
    /// Mentioned participant.
    Mentioned,
    /// Reviewer.
    Reviewer,
    /// Owner.
    Owner,
}

/// Agent reference on an issue.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentIssueRef {
    /// Agent id.
    pub agent_id: AgentId,
    /// Agent role.
    pub role: AgentIssueRole,
}

/// Actor responsible for an issue mutation.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum IssueActor {
    /// Human user.
    User,
    /// Agent actor.
    Agent {
        /// Agent id.
        agent_id: AgentId,
    },
    /// System actor.
    System,
}

/// Issue aggregate.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Issue {
    /// Stable UUID.
    pub id: IssueId,
    /// Sequential per-project number.
    pub number: IssueNumber,
    /// Human-readable title.
    pub title: String,
    /// Markdown description.
    pub description: MarkdownDoc,
    /// Lifecycle status.
    pub status: IssueStatus,
    /// Priority.
    pub priority: IssuePriority,
    /// Sprint membership. `None` means backlog / no sprint.
    pub sprint: Option<SprintId>,
    /// Editable issue-local knowledge.
    pub carnet: MarkdownDoc,
    /// Links to other issues.
    pub links: Vec<IssueLink>,
    /// Agent references.
    pub agent_refs: Vec<AgentIssueRef>,
    /// 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: IssueVersion,
}

impl Issue {
    /// Builds a new issue with version 1.
    ///
    /// # Errors
    /// [`IssueError`] when invariants are violated.
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        id: IssueId,
        number: IssueNumber,
        title: impl Into<String>,
        description: MarkdownDoc,
        status: IssueStatus,
        priority: IssuePriority,
        carnet: MarkdownDoc,
        links: Vec<IssueLink>,
        agent_refs: Vec<AgentIssueRef>,
        actor: IssueActor,
        now_ms: u64,
    ) -> Result<Self, IssueError> {
        let issue = Self {
            id,
            number,
            title: title.into(),
            description,
            status,
            priority,
            sprint: None,
            carnet,
            links,
            agent_refs,
            created_by: actor.clone(),
            updated_by: actor,
            created_at: now_ms,
            updated_at: now_ms,
            version: IssueVersion::INITIAL,
        };
        issue.validate()?;
        Ok(issue)
    }

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

    /// Returns this issue as `#<number>`.
    #[must_use]
    pub const fn reference(&self) -> IssueRef {
        IssueRef::new(self.number)
    }

    /// Validates invariants.
    ///
    /// # Errors
    /// [`IssueError`] when an invariant is violated.
    pub fn validate(&self) -> Result<(), IssueError> {
        IssueNumber::new(self.number.get())?;
        IssueVersion::new(self.version.get())?;
        if self.title.trim().is_empty() {
            return Err(IssueError::EmptyTitle);
        }
        let own = self.reference();
        if self.links.iter().any(|link| link.target == own) {
            return Err(IssueError::SelfLink { reference: own });
        }
        Ok(())
    }

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

/// Carnet projection with its optimistic version.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IssueCarnet {
    /// Issue reference.
    pub issue_ref: IssueRef,
    /// Carnet Markdown body.
    pub carnet: MarkdownDoc,
    /// Current issue version.
    pub version: IssueVersion,
    /// Actor that last updated the carnet.
    pub updated_by: IssueActor,
    /// Last carnet update time, epoch milliseconds.
    pub updated_at: u64,
}

/// Index row used by stores and list use cases.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueIndexEntry {
    /// Issue reference.
    pub issue_ref: IssueRef,
    /// Relative path to the issue folder.
    pub path: String,
    /// Title.
    pub title: String,
    /// Status.
    pub status: IssueStatus,
    /// Priority.
    pub priority: IssuePriority,
    /// Sprint membership.
    pub sprint: Option<SprintId>,
    /// Assigned agent ids.
    pub assigned_agent_ids: Vec<AgentId>,
    /// Last update time.
    pub updated_at: u64,
}

impl From<&Issue> for IssueIndexEntry {
    fn from(issue: &Issue) -> Self {
        Self {
            issue_ref: issue.reference(),
            path: issue.number.get().to_string(),
            title: issue.title.clone(),
            status: issue.status,
            priority: issue.priority,
            sprint: issue.sprint,
            assigned_agent_ids: issue
                .agent_refs
                .iter()
                .filter(|r| r.role == AgentIssueRole::Assigned)
                .map(|r| r.agent_id)
                .collect(),
            updated_at: issue.updated_at,
        }
    }
}

/// Store-side list filter.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IssueListFilter {
    /// Allowed statuses. Empty means every status.
    pub statuses: Vec<IssueStatus>,
    /// Allowed priorities. Empty means every priority.
    pub priorities: Vec<IssuePriority>,
    /// Optional assigned agent filter.
    pub assigned_agent_id: Option<AgentId>,
    /// Optional sprint membership filter.
    pub sprint: Option<SprintId>,
    /// Optional case-insensitive text query.
    pub text: Option<String>,
}

/// Domain errors for issue invariants and value objects.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum IssueError {
    /// Issue numbers are strictly positive.
    #[error("issue number must be greater than zero")]
    InvalidNumber,
    /// Issue versions are strictly positive.
    #[error("issue version must be greater than zero")]
    InvalidVersion,
    /// Issue title cannot be empty.
    #[error("issue title cannot be empty")]
    EmptyTitle,
    /// Issue reference must be exactly `#<positive integer>`.
    #[error("invalid issue reference: {0}")]
    InvalidRef(String),
    /// A link targets the same issue.
    #[error("issue {reference} cannot link to itself")]
    SelfLink {
        /// Self reference.
        reference: IssueRef,
    },
}
