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