//! Project types for the Dirigent system.
//!
//! WASM-compatible shared types for the Projects module. These types are
//! used by both server and client (web UI) code.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;

/// A project in the Dirigent system.
///
/// Projects organize work across repositories, sessions, and connectors.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Project {
    /// Unique project identifier (UUID v7)
    pub id: Uuid,
    /// Human-readable project name
    pub name: String,
    /// Project description (empty by default)
    #[serde(default)]
    pub description: String,
    /// Optional icon (emoji or abbreviation)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icon: Option<String>,
    /// Owner user ID
    pub owner: Uuid,
    /// Member user IDs
    #[serde(default)]
    pub members: Vec<Uuid>,
    /// Categorization tags
    #[serde(default)]
    pub tags: Vec<String>,
    /// Programming languages used
    #[serde(default)]
    pub languages: Vec<String>,
    /// Linked project IDs (for multi-project setups)
    #[serde(default)]
    pub linked_projects: Vec<Uuid>,
    /// Arbitrary metadata
    #[serde(default = "default_metadata")]
    pub metadata: serde_json::Value,
    /// When this project was created
    pub created_at: DateTime<Utc>,
    /// When this project was last updated
    pub updated_at: DateTime<Utc>,
}

fn default_metadata() -> serde_json::Value {
    serde_json::Value::Object(serde_json::Map::new())
}

/// A local git repository associated with a project.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProjectRepository {
    /// Unique repository identifier (UUID v7)
    pub id: Uuid,
    /// Project this repository belongs to
    pub project_id: Uuid,
    /// Local filesystem path
    pub path: PathBuf,
    /// Whether this is the primary repository
    #[serde(default)]
    pub is_primary: bool,
    /// Optional human-readable label
    #[serde(skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// Access mode
    #[serde(default)]
    pub access: AccessMode,
    /// When this repository was added
    pub created_at: DateTime<Utc>,
    /// When this repository was last updated
    pub updated_at: DateTime<Utc>,
}

/// Repository access mode.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum AccessMode {
    /// Read-only access
    Read,
    /// Read and write access
    #[default]
    ReadWrite,
}

/// A git worktree linked to a repository.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Worktree {
    /// Unique worktree identifier (UUID v7)
    pub id: Uuid,
    /// Repository this worktree belongs to
    pub repository_id: Uuid,
    /// Local filesystem path
    pub path: PathBuf,
    /// Branch name
    pub branch: String,
    /// Optional work branch name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub work_branch: Option<String>,
    /// Optional naming strategy
    #[serde(skip_serializing_if = "Option::is_none")]
    pub naming_strategy: Option<String>,
    /// When this worktree was created
    pub created_at: DateTime<Utc>,
}

/// Binding between a project and a connector/session.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ProjectBinding {
    /// Unique binding identifier (UUID v7)
    pub id: Uuid,
    /// Project this binding belongs to
    pub project_id: Uuid,
    /// Optional connector ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub connector_id: Option<String>,
    /// Optional session ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub session_id: Option<Uuid>,
    /// Optional working directory override
    #[serde(skip_serializing_if = "Option::is_none")]
    pub working_dir: Option<PathBuf>,
}

/// Runtime git state (not persisted, computed on demand).
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct GitState {
    /// Current branch name
    pub branch: String,
    /// Whether there are uncommitted changes
    #[serde(default)]
    pub is_dirty: bool,
    /// Commits ahead of remote
    #[serde(default)]
    pub ahead: u32,
    /// Commits behind remote
    #[serde(default)]
    pub behind: u32,
    /// Remote names
    #[serde(default)]
    pub remotes: Vec<String>,
    /// Active worktrees
    #[serde(default)]
    pub worktrees: Vec<WorktreeInfo>,
    /// Unexpected conditions
    #[serde(default)]
    pub unexpected: Vec<GitWarning>,
}

/// Information about an active worktree.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct WorktreeInfo {
    /// Worktree filesystem path
    pub path: PathBuf,
    /// Branch checked out (None if detached)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
    /// Whether HEAD is detached
    #[serde(default)]
    pub is_detached: bool,
}

/// A warning about unexpected git state.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GitWarning {
    /// Warning code for programmatic handling
    pub code: String,
    /// Human-readable warning message
    pub message: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_project_serialization_roundtrip() {
        let now = Utc::now();
        let project = Project {
            id: Uuid::now_v7(),
            name: "Test Project".to_string(),
            description: "A test project".to_string(),
            icon: Some("🚀".to_string()),
            owner: Uuid::now_v7(),
            members: vec![],
            tags: vec!["rust".to_string()],
            languages: vec!["Rust".to_string()],
            linked_projects: vec![],
            metadata: serde_json::json!({"key": "value"}),
            created_at: now,
            updated_at: now,
        };

        let json = serde_json::to_string(&project).expect("serialize");
        let deser: Project = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(deser.id, project.id);
        assert_eq!(deser.name, project.name);
        assert_eq!(deser.icon, project.icon);
    }

    #[test]
    fn test_project_defaults() {
        let json = r#"{
            "id": "019504a0-0000-7000-8000-000000000001",
            "name": "Minimal",
            "owner": "019504a0-0000-7000-8000-000000000002",
            "created_at": "2026-01-01T00:00:00Z",
            "updated_at": "2026-01-01T00:00:00Z"
        }"#;

        let project: Project = serde_json::from_str(json).expect("deserialize");
        assert_eq!(project.description, "");
        assert!(project.tags.is_empty());
        assert!(project.members.is_empty());
        assert!(project.metadata.is_object());
    }

    #[test]
    fn test_access_mode_default() {
        assert_eq!(AccessMode::default(), AccessMode::ReadWrite);
    }

    #[test]
    fn test_project_repository_roundtrip() {
        let now = Utc::now();
        let repo = ProjectRepository {
            id: Uuid::now_v7(),
            project_id: Uuid::now_v7(),
            path: PathBuf::from("/home/user/project"),
            is_primary: true,
            label: Some("main".to_string()),
            access: AccessMode::ReadWrite,
            created_at: now,
            updated_at: now,
        };

        let json = serde_json::to_string(&repo).expect("serialize");
        let deser: ProjectRepository = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(deser.id, repo.id);
        assert!(deser.is_primary);
    }

    #[test]
    fn test_git_state_default() {
        let state = GitState::default();
        assert_eq!(state.branch, "");
        assert!(!state.is_dirty);
        assert_eq!(state.ahead, 0);
    }

    #[test]
    fn test_binding_roundtrip() {
        let binding = ProjectBinding {
            id: Uuid::now_v7(),
            project_id: Uuid::now_v7(),
            connector_id: Some("conn-1".to_string()),
            session_id: None,
            working_dir: None,
        };

        let json = serde_json::to_string(&binding).expect("serialize");
        let deser: ProjectBinding = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(deser.id, binding.id);
        assert!(deser.session_id.is_none());
    }
}
