use crate::error::{Result, SyncError};
use crate::sync::scanner::FileEntry;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::SystemTime;

/// Cached file metadata for incremental scanning
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedFile {
	/// File path relative to sync root
	pub path: PathBuf,
	/// File size in bytes
	pub size: u64,
	/// Last modification time
	pub modified: SystemTime,
	/// Whether this is a directory
	pub is_dir: bool,
}

impl CachedFile {
	/// Create from FileEntry
	pub fn from_file_entry(file: &FileEntry) -> Self {
		Self { path: (*file.relative_path).clone(), size: file.size, modified: file.modified, is_dir: file.is_dir }
	}

	/// Convert back to FileEntry
	/// Note: Some fields have default values for simplicity
	pub fn to_file_entry(&self, source_root: &Path) -> FileEntry {
		FileEntry {
			path: Arc::new(source_root.join(&self.path)),
			relative_path: Arc::new(self.path.clone()),
			size: self.size,
			modified: self.modified,
			is_dir: self.is_dir,
			is_symlink: false, // Not cached
			symlink_target: None,
			is_sparse: false,
			allocated_size: self.size,
			xattrs: None,
			inode: None,
			nlink: 1,
			acls: None,
			bsd_flags: None,
		}
	}
}

/// Directory modification time cache for incremental scanning
///
/// Stores the last known mtime of directories and cached file metadata to enable
/// incremental scanning. When a directory's mtime hasn't changed, we can use the
/// cached file list instead of re-scanning, dramatically speeding up re-syncs.
///
/// # Performance Impact
/// - Initial sync: No overhead (cache is empty)
/// - Re-sync with no changes: ~100x faster (skips all directory scans)
/// - Re-sync with changes: Only scans changed directories
///
/// # Cache File Format
/// - Location: `<dest>/.sy-dir-cache.json`
/// - Format: JSON (human-readable, debuggable)
/// - Size: ~200 bytes per file (includes full metadata)
///
/// # Invalidation
/// - Directory mtime changed → re-scan that directory
/// - Parent directory changed → re-scan subtree
/// - Cache file corrupted → full re-scan (safe fallback)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryCache {
	/// Map of directory path (relative to sync root) to last known mtime
	#[serde(rename = "directories")]
	dir_entries: HashMap<PathBuf, SystemTime>,

	/// Cached file metadata keyed by directory path
	/// Maps directory -> list of files in that directory
	#[serde(rename = "files", default)]
	file_entries: HashMap<PathBuf, Vec<CachedFile>>,

	/// Version number for cache format changes
	#[serde(default = "default_version")]
	version: u32,

	/// Timestamp when cache was last updated
	#[serde(default = "SystemTime::now")]
	last_updated: SystemTime,
}

fn default_version() -> u32 {
	1
}

impl DirectoryCache {
	const CURRENT_VERSION: u32 = 2; // Bumped for new cache format
	const CACHE_FILENAME: &'static str = ".sy-dir-cache.json";

	/// Create a new empty cache
	pub fn new() -> Self {
		Self {
			dir_entries: HashMap::new(),
			file_entries: HashMap::new(),
			version: Self::CURRENT_VERSION,
			last_updated: SystemTime::now(),
		}
	}

	/// Load cache from destination directory
	///
	/// Returns empty cache if file doesn't exist or is corrupted.
	pub fn load(dest_root: &Path) -> Self {
		let cache_path = dest_root.join(Self::CACHE_FILENAME);

		match std::fs::read_to_string(&cache_path) {
			Ok(content) => match serde_json::from_str::<Self>(&content) {
				Ok(mut cache) => {
					// Check version compatibility
					if cache.version != Self::CURRENT_VERSION {
						tracing::warn!("Directory cache version mismatch (found {}, expected {}). Using empty cache.", cache.version, Self::CURRENT_VERSION);
						return Self::new();
					}

					cache.last_updated = SystemTime::now();
					tracing::debug!(
						"Loaded directory cache: {} directories, {} file entries",
						cache.dir_entries.len(),
						cache.file_entries.values().map(|v| v.len()).sum::<usize>()
					);
					cache
				}
				Err(e) => {
					tracing::warn!("Failed to parse directory cache: {}. Using empty cache.", e);
					Self::new()
				}
			},
			Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
				tracing::debug!("No existing directory cache found. Will create new cache.");
				Self::new()
			}
			Err(e) => {
				tracing::warn!("Failed to read directory cache: {}. Using empty cache.", e);
				Self::new()
			}
		}
	}

	/// Save cache to destination directory
	pub fn save(&self, dest_root: &Path) -> Result<()> {
		let cache_path = dest_root.join(Self::CACHE_FILENAME);

		let content =
			serde_json::to_string_pretty(self).map_err(|e| SyncError::Io(std::io::Error::other(format!("Failed to serialize directory cache: {}", e))))?;

		std::fs::write(&cache_path, content)
			.map_err(|e| SyncError::Io(std::io::Error::other(format!("Failed to write directory cache to {}: {}", cache_path.display(), e))))?;

		tracing::debug!(
			"Saved directory cache: {} directories, {} files to {}",
			self.dir_entries.len(),
			self.file_entries.values().map(|v| v.len()).sum::<usize>(),
			cache_path.display()
		);

		Ok(())
	}

	/// Delete cache file from destination directory
	pub fn delete(dest_root: &Path) -> Result<()> {
		let cache_path = dest_root.join(Self::CACHE_FILENAME);

		if cache_path.exists() {
			std::fs::remove_file(&cache_path).map_err(|e| SyncError::Io(std::io::Error::other(format!("Failed to delete directory cache: {}", e))))?;
			tracing::debug!("Deleted directory cache");
		}

		Ok(())
	}

	/// Check if a directory needs to be re-scanned
	///
	/// Returns true if:
	/// - Directory not in cache (first scan)
	/// - Directory mtime has changed (was modified)
	/// - Parent directory was modified (might affect this directory)
	///
	/// Returns false if directory mtime matches cache (can skip scan)
	pub fn needs_rescan(&self, dir_path: &Path, current_mtime: SystemTime) -> bool {
		match self.dir_entries.get(dir_path) {
			Some(&cached_mtime) => {
				// Compare mtimes (with 1-second tolerance for filesystem granularity)
				match current_mtime.duration_since(cached_mtime) {
					Ok(duration) => duration.as_secs() > 1,
					Err(e) => e.duration().as_secs() > 1,
				}
			}
			None => {
				// Not in cache - need to scan
				true
			}
		}
	}

	/// Update cache entry for a directory
	pub fn update(&mut self, dir_path: PathBuf, mtime: SystemTime) {
		self.dir_entries.insert(dir_path, mtime);
	}

	/// Get cached files for a directory
	/// Returns None if directory not in cache or needs rescan
	pub fn get_cached_files(&self, dir_path: &Path) -> Option<&Vec<CachedFile>> {
		self.file_entries.get(dir_path)
	}

	/// Store cached files for a directory
	pub fn cache_files(&mut self, dir_path: PathBuf, files: Vec<CachedFile>) {
		self.file_entries.insert(dir_path, files);
	}

	/// Remove a directory from cache (e.g., after deletion)
	#[allow(dead_code)] // Used in tests
	pub fn remove(&mut self, dir_path: &Path) -> bool {
		let dir_removed = self.dir_entries.remove(dir_path).is_some();
		let files_removed = self.file_entries.remove(dir_path).is_some();
		dir_removed || files_removed
	}

	/// Clear all cache entries
	#[allow(dead_code)] // Used in tests
	pub fn clear(&mut self) {
		self.dir_entries.clear();
		self.file_entries.clear();
		self.last_updated = SystemTime::now();
	}

	/// Get number of cached directories
	pub fn len(&self) -> usize {
		self.dir_entries.len()
	}

	/// Check if cache is empty
	#[allow(dead_code)] // Used in tests
	pub fn is_empty(&self) -> bool {
		self.dir_entries.is_empty() && self.file_entries.is_empty()
	}

	/// Get cache file path for a destination
	#[allow(dead_code)] // Will be used for incremental scanning
	pub fn cache_path(dest_root: &Path) -> PathBuf {
		dest_root.join(Self::CACHE_FILENAME)
	}
}

impl Default for DirectoryCache {
	fn default() -> Self {
		Self::new()
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use std::time::Duration;
	use tempfile::TempDir;

	#[test]
	fn test_new_cache() {
		let cache = DirectoryCache::new();
		assert!(cache.is_empty());
		assert_eq!(cache.version, DirectoryCache::CURRENT_VERSION);
	}

	#[test]
	fn test_update_and_check() {
		let mut cache = DirectoryCache::new();
		let dir = PathBuf::from("test/dir");
		let mtime = SystemTime::now();

		// First check - should need rescan (not in cache)
		assert!(cache.needs_rescan(&dir, mtime));

		// Update cache
		cache.update(dir.clone(), mtime);
		assert_eq!(cache.len(), 1);

		// Second check - should not need rescan (mtime matches)
		assert!(!cache.needs_rescan(&dir, mtime));

		// Check with mtime 2 seconds in the future (beyond 1-second tolerance)
		let new_mtime = mtime + Duration::from_secs(2);

		// Should need rescan (mtime changed beyond tolerance)
		assert!(cache.needs_rescan(&dir, new_mtime));
	}

	#[test]
	fn test_save_and_load() {
		let temp = TempDir::new().unwrap();
		let mut cache = DirectoryCache::new();

		// Add some entries
		cache.update(PathBuf::from("dir1"), SystemTime::now());
		cache.update(PathBuf::from("dir2"), SystemTime::now());
		cache.update(PathBuf::from("dir3/subdir"), SystemTime::now());

		// Save
		cache.save(temp.path()).unwrap();

		// Verify file exists
		let cache_path = DirectoryCache::cache_path(temp.path());
		assert!(cache_path.exists());

		// Load
		let loaded = DirectoryCache::load(temp.path());
		assert_eq!(loaded.len(), 3);
		assert_eq!(loaded.version, DirectoryCache::CURRENT_VERSION);

		// Verify entries
		assert!(loaded.dir_entries.contains_key(&PathBuf::from("dir1")));
		assert!(loaded.dir_entries.contains_key(&PathBuf::from("dir2")));
		assert!(loaded.dir_entries.contains_key(&PathBuf::from("dir3/subdir")));
	}

	#[test]
	fn test_load_nonexistent() {
		let temp = TempDir::new().unwrap();

		// Load from empty directory
		let cache = DirectoryCache::load(temp.path());
		assert!(cache.is_empty());
	}

	#[test]
	fn test_load_corrupted() {
		let temp = TempDir::new().unwrap();
		let cache_path = DirectoryCache::cache_path(temp.path());

		// Write invalid JSON
		std::fs::write(&cache_path, "not valid json").unwrap();

		// Should return empty cache (safe fallback)
		let cache = DirectoryCache::load(temp.path());
		assert!(cache.is_empty());
	}

	#[test]
	fn test_delete_cache() {
		let temp = TempDir::new().unwrap();
		let mut cache = DirectoryCache::new();

		cache.update(PathBuf::from("dir1"), SystemTime::now());
		cache.save(temp.path()).unwrap();

		let cache_path = DirectoryCache::cache_path(temp.path());
		assert!(cache_path.exists());

		// Delete
		DirectoryCache::delete(temp.path()).unwrap();
		assert!(!cache_path.exists());

		// Deleting non-existent cache should succeed
		DirectoryCache::delete(temp.path()).unwrap();
	}

	#[test]
	fn test_remove_entry() {
		let mut cache = DirectoryCache::new();
		let dir = PathBuf::from("test/dir");

		cache.update(dir.clone(), SystemTime::now());
		assert_eq!(cache.len(), 1);

		// Remove existing entry
		assert!(cache.remove(&dir));
		assert_eq!(cache.len(), 0);

		// Remove non-existent entry
		assert!(!cache.remove(&dir));
	}

	#[test]
	fn test_clear() {
		let mut cache = DirectoryCache::new();

		cache.update(PathBuf::from("dir1"), SystemTime::now());
		cache.update(PathBuf::from("dir2"), SystemTime::now());
		assert_eq!(cache.len(), 2);

		cache.clear();
		assert!(cache.is_empty());
	}

	#[test]
	fn test_mtime_tolerance() {
		let mut cache = DirectoryCache::new();
		let dir = PathBuf::from("test/dir");
		let mtime = SystemTime::now();

		cache.update(dir.clone(), mtime);

		// Check with same mtime - should not need rescan
		assert!(!cache.needs_rescan(&dir, mtime));

		// Check with mtime 500ms later - should not need rescan (within tolerance)
		let mtime_close = mtime + Duration::from_millis(500);
		assert!(!cache.needs_rescan(&dir, mtime_close));

		// Check with mtime 2 seconds later - should need rescan (outside tolerance)
		let mtime_far = mtime + Duration::from_secs(2);
		assert!(cache.needs_rescan(&dir, mtime_far));
	}

	#[test]
	fn test_cache_file_path() {
		let temp = TempDir::new().unwrap();
		let cache_path = DirectoryCache::cache_path(temp.path());

		assert_eq!(cache_path, temp.path().join(DirectoryCache::CACHE_FILENAME));
	}
}
