Skip to main content

metis_docs_tui/services/
document.rs

1use anyhow::Result;
2use metis_core::{
3    application::services::document::{
4        creation::DocumentCreationConfig, DeletionService, DocumentCreationService,
5    },
6    dal::Database,
7    domain::documents::types::DocumentType,
8};
9use std::path::PathBuf;
10use std::str::FromStr;
11
12/// Service for document operations
13pub struct DocumentService {
14    workspace_dir: PathBuf,
15}
16
17impl DocumentService {
18    pub fn new(workspace_dir: PathBuf) -> Self {
19        Self { workspace_dir }
20    }
21
22    pub async fn create_document(
23        &self,
24        document_type: DocumentType,
25        title: String,
26        description: Option<String>,
27        parent_id: Option<String>,
28    ) -> Result<String> {
29        let creation_service = DocumentCreationService::new(&self.workspace_dir);
30
31        let config = DocumentCreationConfig {
32            title,
33            description,
34            parent_id: parent_id
35                .as_ref()
36                .map(|id| metis_core::domain::documents::types::DocumentId::from(id.clone())),
37            tags: vec![],
38            phase: None,
39            complexity: None,
40            risk_level: None,
41        };
42
43        let result = match document_type {
44            DocumentType::Vision => creation_service.create_vision(config).await?,
45            DocumentType::Strategy => creation_service.create_strategy(config).await?,
46            DocumentType::Initiative => {
47                if let Some(parent_id) = &parent_id {
48                    creation_service
49                        .create_initiative(config, parent_id)
50                        .await?
51                } else {
52                    // Create initiative without parent (streamlined configuration)
53                    // Use "NULL" as strategy_id which the core service handles correctly
54                    creation_service
55                        .create_initiative_with_config(
56                            config,
57                            "NULL",
58                            &metis_core::domain::configuration::FlightLevelConfig::streamlined(),
59                        )
60                        .await?
61                }
62            }
63            DocumentType::Task => {
64                if let Some(initiative_id) = &parent_id {
65                    // For tasks with parent, we need both strategy and initiative IDs
66                    // Try to create the task and let the creation service handle validation
67                    match creation_service
68                        .create_task(config, initiative_id, initiative_id)
69                        .await
70                    {
71                        Ok(result) => result,
72                        Err(e) => return Err(anyhow::anyhow!("Failed to create task: {}", e)),
73                    }
74                } else {
75                    // For tasks without parent, create as backlog item
76                    creation_service.create_backlog_item(config).await?
77                }
78            }
79            DocumentType::Adr => creation_service.create_adr(config).await?,
80        };
81
82        Ok(result.document_id.to_string())
83    }
84
85    pub async fn delete_document(&self, file_path: &str) -> Result<()> {
86        let deletion_service = DeletionService::new();
87        deletion_service
88            .delete_document_recursive(file_path)
89            .await?;
90        Ok(())
91    }
92
93    pub async fn load_documents_from_database(
94        &self,
95    ) -> Result<Vec<crate::models::DatabaseDocument>> {
96        let db_path = self.workspace_dir.join("metis.db");
97        let db = Database::new(&db_path.to_string_lossy())
98            .map_err(|e| anyhow::anyhow!("Database error: {}", e))?;
99
100        let mut repository = db.into_repository();
101        let mut documents = Vec::new();
102
103        // Collect all document types
104        for doc_type in ["vision", "strategy", "initiative", "task", "adr"] {
105            if let Ok(mut docs) = repository.find_by_type(doc_type) {
106                documents.append(&mut docs);
107            }
108        }
109
110        Ok(documents
111            .into_iter()
112            .filter(|doc| !doc.archived) // Filter out archived documents
113            .map(|doc| crate::models::DatabaseDocument {
114                id: doc.id,
115                title: doc.title,
116                document_type: DocumentType::from_str(&doc.document_type).unwrap(),
117                filepath: doc.filepath,
118                archived: doc.archived,
119            })
120            .collect())
121    }
122
123    pub async fn save_document_content(&self, file_path: &str, new_content: &str) -> Result<()> {
124        use metis_core::{Adr, Document, Initiative, Strategy, Task};
125
126        // Load the document from file, update its content, then save it back
127        let path = std::path::Path::new(file_path);
128
129        // Try to determine document type from file path or load and inspect
130        if file_path.contains("/strategies/") && file_path.ends_with("/strategy.md") {
131            let mut strategy = Strategy::from_file(path)
132                .await
133                .map_err(|e| anyhow::anyhow!("Failed to load strategy: {}", e))?;
134            strategy
135                .update_content_body(new_content.to_string())
136                .map_err(|e| anyhow::anyhow!("Failed to update strategy content: {}", e))?;
137            strategy
138                .to_file(path)
139                .await
140                .map_err(|e| anyhow::anyhow!("Failed to save strategy: {}", e))?;
141        } else if file_path.contains("/initiatives/") && file_path.ends_with("/initiative.md") {
142            let mut initiative = Initiative::from_file(path)
143                .await
144                .map_err(|e| anyhow::anyhow!("Failed to load initiative: {}", e))?;
145            initiative
146                .update_content_body(new_content.to_string())
147                .map_err(|e| anyhow::anyhow!("Failed to update initiative content: {}", e))?;
148            initiative
149                .to_file(path)
150                .await
151                .map_err(|e| anyhow::anyhow!("Failed to save initiative: {}", e))?;
152        } else if file_path.contains("/tasks/") {
153            let mut task = Task::from_file(path)
154                .await
155                .map_err(|e| anyhow::anyhow!("Failed to load task: {}", e))?;
156            task.update_content_body(new_content.to_string())
157                .map_err(|e| anyhow::anyhow!("Failed to update task content: {}", e))?;
158            task.to_file(path)
159                .await
160                .map_err(|e| anyhow::anyhow!("Failed to save task: {}", e))?;
161        } else if file_path.contains("/adrs/") {
162            let mut adr = Adr::from_file(path)
163                .await
164                .map_err(|e| anyhow::anyhow!("Failed to load adr: {}", e))?;
165            adr.update_content_body(new_content.to_string())
166                .map_err(|e| anyhow::anyhow!("Failed to update adr content: {}", e))?;
167            adr.to_file(path)
168                .await
169                .map_err(|e| anyhow::anyhow!("Failed to save adr: {}", e))?;
170        } else {
171            return Err(anyhow::anyhow!(
172                "Unable to determine document type from path: {}",
173                file_path
174            ));
175        }
176
177        Ok(())
178    }
179
180    pub async fn create_child_task(
181        &self,
182        title: String,
183        strategy_id: String,
184        initiative_id: String,
185    ) -> Result<String> {
186        let creation_service = DocumentCreationService::new(&self.workspace_dir);
187
188        let config = DocumentCreationConfig {
189            title,
190            description: None,
191            parent_id: Some(metis_core::domain::documents::types::DocumentId::from(
192                initiative_id.clone(),
193            )),
194            tags: vec![],
195            phase: None,
196            complexity: None,
197            risk_level: None,
198        };
199
200        // Determine flight configuration based on strategy_id
201        let flight_config = if strategy_id == "NULL" {
202            metis_core::domain::configuration::FlightLevelConfig::streamlined()
203        } else {
204            metis_core::domain::configuration::FlightLevelConfig::full()
205        };
206
207        let result = creation_service
208            .create_task_with_config(config, &strategy_id, &initiative_id, &flight_config)
209            .await?;
210        Ok(result.document_id.to_string())
211    }
212
213    pub async fn create_adr(&self, title: String, context: Option<String>) -> Result<PathBuf> {
214        let creation_service = DocumentCreationService::new(&self.workspace_dir);
215
216        let config = DocumentCreationConfig {
217            title,
218            description: context,
219            parent_id: None, // ADRs are top-level documents
220            tags: vec![],
221            phase: None, // Will default to draft
222            complexity: None,
223            risk_level: None,
224        };
225
226        let result = creation_service.create_adr(config).await?;
227        Ok(result.file_path)
228    }
229}