mockforge_core/workspace/
sync.rs

1//! Synchronization functionality
2//!
3//! This module provides synchronization capabilities for workspaces,
4//! including conflict resolution, merge strategies, and sync status tracking.
5
6use crate::workspace::core::{EntityId, Workspace};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json;
10use serde_yaml;
11use std::collections::HashMap;
12use std::path::Path;
13use tokio::fs;
14
15/// Synchronization configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SyncConfig {
18    /// Whether synchronization is enabled
19    pub enabled: bool,
20    /// Synchronization provider (git, cloud, etc.)
21    pub provider: SyncProvider,
22    /// Synchronization interval in seconds
23    pub interval_seconds: u64,
24    /// Conflict resolution strategy
25    pub conflict_strategy: ConflictResolutionStrategy,
26    /// Whether to auto-commit changes
27    pub auto_commit: bool,
28    /// Whether to push changes automatically
29    pub auto_push: bool,
30    /// Directory structure preference
31    pub directory_structure: SyncDirectoryStructure,
32    /// Sync direction preference
33    pub sync_direction: SyncDirection,
34}
35
36/// Synchronization provider
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub enum SyncProvider {
39    /// Git-based synchronization
40    Git {
41        /// Repository URL
42        repo_url: String,
43        /// Branch name
44        branch: String,
45        /// Authentication token (optional)
46        auth_token: Option<String>,
47    },
48    /// Cloud-based synchronization
49    Cloud {
50        /// Service URL
51        service_url: String,
52        /// API key
53        api_key: String,
54        /// Project ID
55        project_id: String,
56    },
57    /// Local file system synchronization
58    Local {
59        /// Directory path
60        directory_path: String,
61        /// Watch for changes
62        watch_changes: bool,
63    },
64}
65
66/// Conflict resolution strategy
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum ConflictResolutionStrategy {
69    /// Always use local version
70    LocalWins,
71    /// Always use remote version
72    RemoteWins,
73    /// Manual resolution required
74    Manual,
75    /// Use last modified timestamp
76    LastModified,
77}
78
79/// Directory structure for synchronization
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum SyncDirectoryStructure {
82    /// Single directory with all workspaces
83    SingleDirectory,
84    /// Separate directory per workspace
85    PerWorkspace,
86    /// Hierarchical structure based on folders
87    Hierarchical,
88}
89
90/// Synchronization direction
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum SyncDirection {
93    /// Bidirectional sync
94    Bidirectional,
95    /// Local to remote only
96    LocalToRemote,
97    /// Remote to local only
98    RemoteToLocal,
99}
100
101/// Synchronization status
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SyncStatus {
104    /// Last sync timestamp
105    pub last_sync: Option<DateTime<Utc>>,
106    /// Current sync state
107    pub state: SyncState,
108    /// Number of pending changes
109    pub pending_changes: usize,
110    /// Number of conflicts
111    pub conflicts: usize,
112    /// Last error message (if any)
113    pub last_error: Option<String>,
114}
115
116/// Current synchronization state
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub enum SyncState {
119    /// Not synchronized
120    NotSynced,
121    /// Currently syncing
122    Syncing,
123    /// Synchronized successfully
124    Synced,
125    /// Sync failed
126    SyncFailed,
127    /// Has conflicts
128    HasConflicts,
129}
130
131/// Synchronization result
132#[derive(Debug, Clone)]
133pub struct SyncResult {
134    /// Whether sync was successful
135    pub success: bool,
136    /// Number of files changed
137    pub changes_count: usize,
138    /// Conflicts that occurred
139    pub conflicts: Vec<SyncConflict>,
140    /// Error message (if failed)
141    pub error: Option<String>,
142}
143
144/// Conflict during synchronization
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SyncConflict {
147    /// Entity ID that has conflict
148    pub entity_id: EntityId,
149    /// Entity type (workspace, request, etc.)
150    pub entity_type: String,
151    /// Local version
152    pub local_version: serde_json::Value,
153    /// Remote version
154    pub remote_version: serde_json::Value,
155    /// Resolution strategy used
156    pub resolution: ConflictResolution,
157}
158
159/// Conflict resolution choice
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum ConflictResolution {
162    /// Use local version
163    Local,
164    /// Use remote version
165    Remote,
166    /// Manual resolution required
167    Manual,
168}
169
170/// Workspace synchronization manager
171#[derive(Debug, Clone)]
172pub struct WorkspaceSyncManager {
173    /// Synchronization configuration
174    config: SyncConfig,
175    /// Current sync status
176    status: SyncStatus,
177    /// Pending conflicts
178    conflicts: Vec<SyncConflict>,
179    /// Total number of sync operations performed
180    total_syncs: usize,
181    /// Number of successful syncs
182    successful_syncs: usize,
183    /// Number of failed syncs
184    failed_syncs: usize,
185    /// Total number of resolved conflicts
186    resolved_conflicts: usize,
187    /// Duration of last sync in milliseconds
188    last_sync_duration_ms: Option<u64>,
189}
190
191/// Synchronization event
192#[derive(Debug, Clone)]
193pub enum SyncEvent {
194    /// Sync started
195    Started,
196    /// Sync progress update
197    Progress { current: usize, total: usize },
198    /// Sync completed successfully
199    Completed(SyncResult),
200    /// Sync failed
201    Failed(String),
202    /// Conflict detected
203    ConflictDetected(SyncConflict),
204}
205
206impl WorkspaceSyncManager {
207    /// Create a new sync manager
208    pub fn new(config: SyncConfig) -> Self {
209        let status = SyncStatus {
210            last_sync: None,
211            state: SyncState::NotSynced,
212            pending_changes: 0,
213            conflicts: 0,
214            last_error: None,
215        };
216
217        Self {
218            config,
219            status,
220            conflicts: Vec::new(),
221            total_syncs: 0,
222            successful_syncs: 0,
223            failed_syncs: 0,
224            resolved_conflicts: 0,
225            last_sync_duration_ms: None,
226        }
227    }
228
229    /// Get the current sync configuration
230    pub fn get_config(&self) -> &SyncConfig {
231        &self.config
232    }
233
234    /// Update sync configuration
235    pub fn update_config(&mut self, config: SyncConfig) {
236        self.config = config;
237    }
238
239    /// Get current sync status
240    pub fn get_status(&self) -> &SyncStatus {
241        &self.status
242    }
243
244    /// Get pending conflicts
245    pub fn get_conflicts(&self) -> &[SyncConflict] {
246        &self.conflicts
247    }
248
249    /// Check if sync is enabled
250    pub fn is_enabled(&self) -> bool {
251        self.config.enabled
252    }
253
254    /// Sync a workspace
255    pub async fn sync_workspace(
256        &mut self,
257        workspace: &mut Workspace,
258    ) -> Result<SyncResult, String> {
259        if !self.config.enabled {
260            return Err("Synchronization is disabled".to_string());
261        }
262
263        // Track sync count
264        self.total_syncs += 1;
265
266        // Start timing
267        let start_time = std::time::Instant::now();
268
269        self.status.state = SyncState::Syncing;
270        self.status.last_error = None;
271
272        let result = match &self.config.provider {
273            SyncProvider::Git {
274                repo_url,
275                branch,
276                auth_token,
277            } => self.sync_with_git(workspace, repo_url, branch, auth_token.as_deref()).await,
278            SyncProvider::Cloud {
279                service_url,
280                api_key,
281                project_id,
282            } => self.sync_with_cloud(workspace, service_url, api_key, project_id).await,
283            SyncProvider::Local {
284                directory_path,
285                watch_changes,
286            } => self.sync_with_local(workspace, directory_path, *watch_changes).await,
287        };
288
289        // Calculate duration
290        let duration = start_time.elapsed();
291        let duration_ms = duration.as_millis() as u64;
292        self.last_sync_duration_ms = Some(duration_ms);
293
294        match &result {
295            Ok(sync_result) => {
296                if sync_result.success {
297                    self.successful_syncs += 1;
298                    self.status.state = SyncState::Synced;
299                    self.status.last_sync = Some(Utc::now());
300                    self.status.pending_changes = 0;
301                    self.status.conflicts = sync_result.conflicts.len();
302                } else {
303                    self.failed_syncs += 1;
304                    self.status.state = SyncState::SyncFailed;
305                    self.status.last_error = sync_result.error.clone();
306                }
307            }
308            Err(error) => {
309                self.failed_syncs += 1;
310                self.status.state = SyncState::SyncFailed;
311                self.status.last_error = Some(error.clone());
312            }
313        }
314
315        result
316    }
317
318    /// Sync with Git provider
319    async fn sync_with_git(
320        &self,
321        workspace: &mut Workspace,
322        repo_url: &str,
323        branch: &str,
324        auth_token: Option<&str>,
325    ) -> Result<SyncResult, String> {
326        // Create a temporary directory for the Git repository
327        let temp_dir =
328            tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {}", e))?;
329
330        let repo_path = temp_dir.path().join("repo");
331
332        match self.config.sync_direction {
333            SyncDirection::LocalToRemote => {
334                self.sync_local_to_git(workspace, repo_url, branch, auth_token, &repo_path)
335                    .await
336            }
337            SyncDirection::RemoteToLocal => {
338                self.sync_git_to_local(workspace, repo_url, branch, auth_token, &repo_path)
339                    .await
340            }
341            SyncDirection::Bidirectional => {
342                self.sync_bidirectional_git(workspace, repo_url, branch, auth_token, &repo_path)
343                    .await
344            }
345        }
346    }
347
348    /// Sync local workspace to Git repository
349    async fn sync_local_to_git(
350        &self,
351        workspace: &Workspace,
352        repo_url: &str,
353        branch: &str,
354        auth_token: Option<&str>,
355        repo_path: &std::path::Path,
356    ) -> Result<SyncResult, String> {
357        // Clone or ensure repository exists
358        self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
359
360        // Serialize workspace to YAML file
361        let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
362        let workspace_yaml = serde_yaml::to_string(workspace)
363            .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
364
365        tokio::fs::write(&workspace_file, &workspace_yaml)
366            .await
367            .map_err(|e| format!("Failed to write workspace file: {}", e))?;
368
369        // Add, commit, and push changes
370        self.git_add_commit_push(repo_path, &workspace_file, auth_token).await?;
371
372        Ok(SyncResult {
373            success: true,
374            changes_count: 1,
375            conflicts: vec![],
376            error: None,
377        })
378    }
379
380    /// Sync Git repository to local workspace
381    async fn sync_git_to_local(
382        &self,
383        workspace: &mut Workspace,
384        repo_url: &str,
385        branch: &str,
386        auth_token: Option<&str>,
387        repo_path: &std::path::Path,
388    ) -> Result<SyncResult, String> {
389        // Clone or pull repository
390        self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
391
392        // Read workspace from Git repository
393        let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
394
395        if !workspace_file.exists() {
396            return Ok(SyncResult {
397                success: true,
398                changes_count: 0,
399                conflicts: vec![],
400                error: None,
401            });
402        }
403
404        let workspace_yaml = tokio::fs::read_to_string(&workspace_file)
405            .await
406            .map_err(|e| format!("Failed to read workspace file: {}", e))?;
407
408        let remote_workspace: Workspace = serde_yaml::from_str(&workspace_yaml)
409            .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
410
411        // Check for conflicts
412        let conflicts = self.detect_conflicts(workspace, &remote_workspace);
413
414        if conflicts.is_empty() {
415            // No conflicts, update local workspace
416            *workspace = remote_workspace;
417            Ok(SyncResult {
418                success: true,
419                changes_count: 1,
420                conflicts: vec![],
421                error: None,
422            })
423        } else {
424            // Conflicts exist
425            Ok(SyncResult {
426                success: true,
427                changes_count: 0,
428                conflicts,
429                error: None,
430            })
431        }
432    }
433
434    /// Bidirectional sync with Git repository
435    async fn sync_bidirectional_git(
436        &self,
437        workspace: &mut Workspace,
438        repo_url: &str,
439        branch: &str,
440        auth_token: Option<&str>,
441        repo_path: &std::path::Path,
442    ) -> Result<SyncResult, String> {
443        // First sync from Git to local
444        let pull_result = self
445            .sync_git_to_local(workspace, repo_url, branch, auth_token, repo_path)
446            .await?;
447
448        if !pull_result.conflicts.is_empty() {
449            // Conflicts detected, return them
450            return Ok(pull_result);
451        }
452
453        // No conflicts, sync local to Git
454        self.sync_local_to_git(workspace, repo_url, branch, auth_token, repo_path).await
455    }
456
457    /// Ensure Git repository exists and is up to date
458    async fn ensure_git_repo(
459        &self,
460        repo_url: &str,
461        branch: &str,
462        auth_token: Option<&str>,
463        repo_path: &std::path::Path,
464    ) -> Result<(), String> {
465        use std::process::Command;
466
467        // Convert path to string using to_string_lossy for cross-platform compatibility
468        let repo_path_str = repo_path.to_string_lossy();
469
470        if repo_path.exists() {
471            // Repository exists, pull latest changes
472            let output = Command::new("git")
473                .args(["-C", repo_path_str.as_ref(), "pull", "origin", branch])
474                .output()
475                .map_err(|e| format!("Failed to pull repository: {}", e))?;
476
477            if !output.status.success() {
478                let stderr = String::from_utf8_lossy(&output.stderr);
479                return Err(format!("Git pull failed: {}", stderr));
480            }
481        } else {
482            // Clone repository
483            // If auth token provided, modify URL for authentication
484            let clone_url = if let Some(token) = auth_token {
485                self.inject_auth_token_into_url(repo_url, token)
486            } else {
487                repo_url.to_string()
488            };
489
490            let output = Command::new("git")
491                .args([
492                    "clone",
493                    "--branch",
494                    branch,
495                    &clone_url,
496                    repo_path_str.as_ref(),
497                ])
498                .output()
499                .map_err(|e| format!("Failed to clone repository: {}", e))?;
500
501            if !output.status.success() {
502                let stderr = String::from_utf8_lossy(&output.stderr);
503                return Err(format!("Git clone failed: {}", stderr));
504            }
505        }
506
507        Ok(())
508    }
509
510    /// Add, commit, and push changes to Git repository
511    async fn git_add_commit_push(
512        &self,
513        repo_path: &std::path::Path,
514        workspace_file: &std::path::Path,
515        _auth_token: Option<&str>,
516    ) -> Result<(), String> {
517        use std::process::Command;
518
519        // Use to_string_lossy for safe cross-platform path conversion
520        let repo_path_str = repo_path.to_string_lossy();
521
522        // Calculate relative path for git commands (works across platforms)
523        let file_path_str = workspace_file
524            .strip_prefix(repo_path)
525            .unwrap_or(workspace_file)
526            .to_string_lossy();
527
528        // Add file
529        let output = Command::new("git")
530            .args(["-C", repo_path_str.as_ref(), "add", file_path_str.as_ref()])
531            .output()
532            .map_err(|e| format!("Failed to add file to git: {}", e))?;
533
534        if !output.status.success() {
535            let stderr = String::from_utf8_lossy(&output.stderr);
536            return Err(format!("Git add failed: {}", stderr));
537        }
538
539        // Check if there are changes to commit
540        let status_output = Command::new("git")
541            .args(["-C", repo_path_str.as_ref(), "status", "--porcelain"])
542            .output()
543            .map_err(|e| format!("Failed to check git status: {}", e))?;
544
545        if status_output.stdout.is_empty() {
546            // No changes to commit
547            return Ok(());
548        }
549
550        // Commit changes
551        let output = Command::new("git")
552            .args([
553                "-C",
554                repo_path_str.as_ref(),
555                "commit",
556                "-m",
557                "Update workspace",
558            ])
559            .output()
560            .map_err(|e| format!("Failed to commit changes: {}", e))?;
561
562        if !output.status.success() {
563            let stderr = String::from_utf8_lossy(&output.stderr);
564            return Err(format!("Git commit failed: {}", stderr));
565        }
566
567        // Push changes
568        let output = Command::new("git")
569            .args(["-C", repo_path_str.as_ref(), "push", "origin", "HEAD"])
570            .output()
571            .map_err(|e| format!("Failed to push changes: {}", e))?;
572
573        if !output.status.success() {
574            let stderr = String::from_utf8_lossy(&output.stderr);
575            return Err(format!("Git push failed: {}", stderr));
576        }
577
578        Ok(())
579    }
580
581    /// Inject authentication token into Git URL
582    fn inject_auth_token_into_url(&self, url: &str, token: &str) -> String {
583        if let Some(https_pos) = url.find("https://") {
584            let rest = &url[https_pos + "https://".len()..];
585            format!("https://oauth2:{}@{}", token, rest)
586        } else {
587            // For SSH URLs or other formats, return as-is
588            url.to_string()
589        }
590    }
591
592    /// Sync with cloud provider
593    async fn sync_with_cloud(
594        &self,
595        workspace: &mut Workspace,
596        service_url: &str,
597        api_key: &str,
598        project_id: &str,
599    ) -> Result<SyncResult, String> {
600        // Create HTTP client
601        let client = reqwest::Client::new();
602
603        // Build API URLs
604        let base_url = service_url.trim_end_matches('/');
605        let workspace_url =
606            format!("{}/api/v1/projects/{}/workspaces/{}", base_url, project_id, workspace.id);
607
608        match self.config.sync_direction {
609            SyncDirection::LocalToRemote => {
610                // Only upload local workspace to cloud
611                self.upload_workspace_to_cloud(&client, &workspace_url, api_key, workspace)
612                    .await
613            }
614            SyncDirection::RemoteToLocal => {
615                // Only download remote workspace and update local
616                self.download_workspace_from_cloud(&client, &workspace_url, api_key, workspace)
617                    .await
618            }
619            SyncDirection::Bidirectional => {
620                // Fetch remote, compare, handle conflicts, then upload if needed
621                self.bidirectional_sync(&client, &workspace_url, api_key, workspace).await
622            }
623        }
624    }
625
626    /// Upload workspace to cloud service
627    async fn upload_workspace_to_cloud(
628        &self,
629        client: &reqwest::Client,
630        workspace_url: &str,
631        api_key: &str,
632        workspace: &Workspace,
633    ) -> Result<SyncResult, String> {
634        // Serialize workspace to JSON
635        let workspace_json = serde_json::to_string(workspace)
636            .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
637
638        // Upload to cloud
639        let response = client
640            .put(workspace_url)
641            .header("Authorization", format!("Bearer {}", api_key))
642            .header("Content-Type", "application/json")
643            .body(workspace_json)
644            .send()
645            .await
646            .map_err(|e| format!("Failed to upload workspace: {}", e))?;
647
648        if !response.status().is_success() {
649            let status = response.status();
650            let error_text = response.text().await.unwrap_or_default();
651            return Err(format!("Cloud upload failed with status {}: {}", status, error_text));
652        }
653
654        Ok(SyncResult {
655            success: true,
656            changes_count: 1,
657            conflicts: vec![],
658            error: None,
659        })
660    }
661
662    /// Download workspace from cloud service
663    async fn download_workspace_from_cloud(
664        &self,
665        client: &reqwest::Client,
666        workspace_url: &str,
667        api_key: &str,
668        workspace: &mut Workspace,
669    ) -> Result<SyncResult, String> {
670        // Download from cloud
671        let response = client
672            .get(workspace_url)
673            .header("Authorization", format!("Bearer {}", api_key))
674            .send()
675            .await
676            .map_err(|e| format!("Failed to download workspace: {}", e))?;
677
678        if response.status() == reqwest::StatusCode::NOT_FOUND {
679            // Workspace doesn't exist in cloud, nothing to sync
680            return Ok(SyncResult {
681                success: true,
682                changes_count: 0,
683                conflicts: vec![],
684                error: None,
685            });
686        }
687
688        if !response.status().is_success() {
689            let status = response.status();
690            let error_text = response.text().await.unwrap_or_default();
691            return Err(format!("Cloud download failed with status {}: {}", status, error_text));
692        }
693
694        let remote_json: serde_json::Value = response
695            .json()
696            .await
697            .map_err(|e| format!("Failed to parse remote workspace: {}", e))?;
698
699        // Deserialize remote workspace
700        let remote_workspace: Workspace = serde_json::from_value(remote_json.clone())
701            .map_err(|e| format!("Failed to deserialize remote workspace: {}", e))?;
702
703        // Check for conflicts based on timestamps
704        let conflicts = self.detect_conflicts(workspace, &remote_workspace);
705
706        // Apply conflict resolution
707        if conflicts.is_empty() {
708            // No conflicts, update local workspace with remote
709            *workspace = remote_workspace;
710            Ok(SyncResult {
711                success: true,
712                changes_count: 1,
713                conflicts: vec![],
714                error: None,
715            })
716        } else {
717            // Conflicts exist, return them for manual resolution
718            Ok(SyncResult {
719                success: true,
720                changes_count: 0,
721                conflicts,
722                error: None,
723            })
724        }
725    }
726
727    /// Perform bidirectional synchronization
728    async fn bidirectional_sync(
729        &self,
730        client: &reqwest::Client,
731        workspace_url: &str,
732        api_key: &str,
733        workspace: &mut Workspace,
734    ) -> Result<SyncResult, String> {
735        // First try to download remote workspace
736        let download_result = self
737            .download_workspace_from_cloud(client, workspace_url, api_key, workspace)
738            .await?;
739
740        if !download_result.conflicts.is_empty() {
741            // Conflicts detected, return them
742            return Ok(download_result);
743        }
744
745        // No conflicts, upload local workspace
746        self.upload_workspace_to_cloud(client, workspace_url, api_key, workspace).await
747    }
748
749    /// Detect conflicts between local and remote workspaces
750    fn detect_conflicts(&self, local: &Workspace, remote: &Workspace) -> Vec<SyncConflict> {
751        let mut conflicts = vec![];
752
753        // Simple conflict detection based on updated_at timestamps
754        if local.updated_at > remote.updated_at {
755            // Local is newer, potential conflict
756            let local_json = serde_json::to_value(local).unwrap_or_default();
757            let remote_json = serde_json::to_value(remote).unwrap_or_default();
758
759            if local_json != remote_json {
760                conflicts.push(SyncConflict {
761                    entity_id: local.id.clone(),
762                    entity_type: "workspace".to_string(),
763                    local_version: local_json,
764                    remote_version: remote_json,
765                    resolution: ConflictResolution::Manual,
766                });
767            }
768        }
769
770        conflicts
771    }
772
773    /// Sync with local filesystem
774    async fn sync_with_local(
775        &self,
776        workspace: &mut Workspace,
777        directory_path: &str,
778        _watch_changes: bool,
779    ) -> Result<SyncResult, String> {
780        let dir_path = Path::new(directory_path);
781
782        // Ensure directory exists
783        if !dir_path.exists() {
784            fs::create_dir_all(dir_path)
785                .await
786                .map_err(|e| format!("Failed to create directory {}: {}", directory_path, e))?;
787        }
788
789        match self.config.sync_direction {
790            SyncDirection::LocalToRemote => {
791                // Write workspace to file
792                let file_path = dir_path.join(format!("{}.yaml", workspace.id));
793                let content = serde_yaml::to_string(workspace)
794                    .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
795
796                fs::write(&file_path, content)
797                    .await
798                    .map_err(|e| format!("Failed to write workspace file: {}", e))?;
799
800                Ok(SyncResult {
801                    success: true,
802                    changes_count: 1,
803                    conflicts: vec![],
804                    error: None,
805                })
806            }
807            SyncDirection::RemoteToLocal => {
808                // Load workspace from file
809                let file_path = dir_path.join(format!("{}.yaml", workspace.id));
810
811                if !file_path.exists() {
812                    return Err(format!("Workspace file not found: {:?}", file_path));
813                }
814
815                let content = fs::read_to_string(&file_path)
816                    .await
817                    .map_err(|e| format!("Failed to read workspace file: {}", e))?;
818
819                let remote_workspace: Workspace = serde_yaml::from_str(&content)
820                    .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
821
822                // Compare workspaces and detect conflicts
823                let conflicts = {
824                    let mut conflicts = vec![];
825
826                    // Check for conflicts
827                    if workspace.updated_at > remote_workspace.updated_at {
828                        // Local is newer, this is a conflict
829                        let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
830                        let remote_json =
831                            serde_json::to_value(&remote_workspace).unwrap_or_default();
832                        conflicts.push(SyncConflict {
833                            entity_id: workspace.id.clone(),
834                            entity_type: "workspace".to_string(),
835                            local_version: local_json,
836                            remote_version: remote_json,
837                            resolution: ConflictResolution::Manual,
838                        });
839                    } else if workspace.updated_at == remote_workspace.updated_at {
840                        // Same timestamp, check if content differs
841                        let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
842                        let remote_json =
843                            serde_json::to_value(&remote_workspace).unwrap_or_default();
844                        if local_json != remote_json {
845                            // Content differs but timestamps are same, conflict
846                            conflicts.push(SyncConflict {
847                                entity_id: workspace.id.clone(),
848                                entity_type: "workspace".to_string(),
849                                local_version: local_json,
850                                remote_version: remote_json,
851                                resolution: ConflictResolution::Manual,
852                            });
853                        }
854                    }
855
856                    conflicts
857                };
858
859                // If no conflicts and remote is newer or equal, update local workspace with remote
860                if conflicts.is_empty() && remote_workspace.updated_at >= workspace.updated_at {
861                    *workspace = remote_workspace;
862                    Ok(SyncResult {
863                        success: true,
864                        changes_count: 1,
865                        conflicts: vec![],
866                        error: None,
867                    })
868                } else {
869                    Ok(SyncResult {
870                        success: true,
871                        changes_count: 0,
872                        conflicts,
873                        error: None,
874                    })
875                }
876            }
877            SyncDirection::Bidirectional => {
878                // For bidirectional, first try to load remote, then write local
879                let file_path = dir_path.join(format!("{}.yaml", workspace.id));
880
881                let mut conflicts = vec![];
882
883                if file_path.exists() {
884                    let content = fs::read_to_string(&file_path)
885                        .await
886                        .map_err(|e| format!("Failed to read workspace file: {}", e))?;
887
888                    let remote_workspace: Workspace = serde_yaml::from_str(&content)
889                        .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
890
891                    // Simple conflict detection based on updated_at
892                    if remote_workspace.updated_at > workspace.updated_at {
893                        // Remote is newer, this would be a conflict
894                        let remote_version =
895                            serde_json::to_value(&remote_workspace).unwrap_or_default();
896                        conflicts.push(SyncConflict {
897                            entity_id: workspace.id.clone(),
898                            entity_type: "workspace".to_string(),
899                            local_version: serde_json::to_value(&*workspace).unwrap_or_default(),
900                            remote_version,
901                            resolution: ConflictResolution::Manual,
902                        });
903                    }
904                }
905
906                // Write local workspace
907                let content = serde_yaml::to_string(workspace)
908                    .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
909
910                fs::write(&file_path, content)
911                    .await
912                    .map_err(|e| format!("Failed to write workspace file: {}", e))?;
913
914                Ok(SyncResult {
915                    success: true,
916                    changes_count: 1,
917                    conflicts,
918                    error: None,
919                })
920            }
921        }
922    }
923
924    /// Resolve conflicts
925    pub fn resolve_conflicts(
926        &mut self,
927        resolutions: HashMap<EntityId, ConflictResolution>,
928    ) -> Result<usize, String> {
929        let mut resolved_count = 0;
930
931        for conflict in &self.conflicts.clone() {
932            if let Some(resolution) = resolutions.get(&conflict.entity_id) {
933                match resolution {
934                    ConflictResolution::Local => {
935                        // Apply local version
936                        resolved_count += 1;
937                    }
938                    ConflictResolution::Remote => {
939                        // Apply remote version
940                        resolved_count += 1;
941                    }
942                    ConflictResolution::Manual => {
943                        // Mark for manual resolution
944                        continue;
945                    }
946                }
947            }
948        }
949
950        // Track resolved conflicts
951        self.resolved_conflicts += resolved_count;
952
953        // Remove resolved conflicts
954        self.conflicts.retain(|conflict| {
955            !resolutions.contains_key(&conflict.entity_id)
956                || matches!(resolutions.get(&conflict.entity_id), Some(ConflictResolution::Manual))
957        });
958
959        self.status.conflicts = self.conflicts.len();
960        if self.conflicts.is_empty() {
961            self.status.state = SyncState::Synced;
962        } else {
963            self.status.state = SyncState::HasConflicts;
964        }
965
966        Ok(resolved_count)
967    }
968
969    /// Get sync statistics
970    pub fn get_sync_stats(&self) -> SyncStats {
971        SyncStats {
972            total_syncs: self.total_syncs,
973            successful_syncs: self.successful_syncs,
974            failed_syncs: self.failed_syncs,
975            total_conflicts: self.conflicts.len(),
976            resolved_conflicts: self.resolved_conflicts,
977            last_sync_duration_ms: self.last_sync_duration_ms,
978        }
979    }
980
981    /// Export sync configuration
982    pub fn export_config(&self) -> Result<String, String> {
983        serde_json::to_string_pretty(&self.config)
984            .map_err(|e| format!("Failed to serialize sync config: {}", e))
985    }
986
987    /// Import sync configuration
988    pub fn import_config(&mut self, json_data: &str) -> Result<(), String> {
989        let config: SyncConfig = serde_json::from_str(json_data)
990            .map_err(|e| format!("Failed to deserialize sync config: {}", e))?;
991
992        self.config = config;
993        Ok(())
994    }
995
996    /// Check if there are pending changes
997    pub fn has_pending_changes(&self) -> bool {
998        self.status.pending_changes > 0
999    }
1000
1001    /// Get conflicts that need manual resolution
1002    pub fn get_manual_conflicts(&self) -> Vec<&SyncConflict> {
1003        self.conflicts
1004            .iter()
1005            .filter(|_conflict| {
1006                // This would need to be determined based on the conflict resolution strategy
1007                true
1008            })
1009            .collect()
1010    }
1011}
1012
1013/// Synchronization statistics
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1015pub struct SyncStats {
1016    /// Total number of sync operations
1017    pub total_syncs: usize,
1018    /// Number of successful syncs
1019    pub successful_syncs: usize,
1020    /// Number of failed syncs
1021    pub failed_syncs: usize,
1022    /// Total number of conflicts encountered
1023    pub total_conflicts: usize,
1024    /// Number of resolved conflicts
1025    pub resolved_conflicts: usize,
1026    /// Duration of last sync in milliseconds
1027    pub last_sync_duration_ms: Option<u64>,
1028}
1029
1030impl Default for SyncConfig {
1031    fn default() -> Self {
1032        Self {
1033            enabled: false,
1034            provider: SyncProvider::Local {
1035                directory_path: "./workspaces".to_string(),
1036                watch_changes: true,
1037            },
1038            interval_seconds: 300,
1039            conflict_strategy: ConflictResolutionStrategy::LocalWins,
1040            auto_commit: true,
1041            auto_push: false,
1042            directory_structure: SyncDirectoryStructure::PerWorkspace,
1043            sync_direction: SyncDirection::Bidirectional,
1044        }
1045    }
1046}
1047
1048impl Default for WorkspaceSyncManager {
1049    fn default() -> Self {
1050        Self::new(SyncConfig::default())
1051    }
1052}