use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use serde::{Deserialize, Serialize};

use crate::config::Config;
use crate::error::{Error, Result};
use crate::indexer::FileIndexer;

/// Serializable file entry for state persistence.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileEntry {
    /// Relative path from the index root.
    pub path: PathBuf,

    /// File size in bytes.
    pub size: u64,

    /// Last modification time (as duration since UNIX_EPOCH).
    pub modified_secs: u64,

    /// Content hash (XXH3).
    pub hash: Option<u64>,
}

/// Persisted index state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexState {
    /// Version for format compatibility.
    pub version: u32,

    /// Root path this index was created from.
    pub root: PathBuf,

    /// Timestamp when this state was saved.
    pub saved_at_secs: u64,

    /// Indexed files.
    pub files: Vec<FileEntry>,
}

impl IndexState {
    /// Current state format version.
    pub const VERSION: u32 = 1;

    /// Create a new state from a file indexer.
    pub fn from_indexer(indexer: &FileIndexer) -> Self {
        let files: Vec<FileEntry> = indexer
            .files()
            .map(|f| {
                let relative_path = f
                    .path
                    .strip_prefix(indexer.root())
                    .unwrap_or(&f.path)
                    .to_path_buf();

                let modified_secs = f
                    .metadata
                    .modified
                    .duration_since(SystemTime::UNIX_EPOCH)
                    .map(|d| d.as_secs())
                    .unwrap_or(0);

                FileEntry {
                    path: relative_path,
                    size: f.metadata.size,
                    modified_secs,
                    hash: f.hash,
                }
            })
            .collect();

        let saved_at_secs = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map(|d| d.as_secs())
            .unwrap_or(0);

        Self {
            version: Self::VERSION,
            root: indexer.root().to_path_buf(),
            saved_at_secs,
            files,
        }
    }

    /// Save state to a file.
    pub fn save(&self, path: &Path) -> Result<()> {
        let file = File::create(path).map_err(|e| Error::Io {
            path: path.to_path_buf(),
            source: e,
        })?;
        let writer = BufWriter::new(file);
        serde_json::to_writer(writer, self).map_err(|e| Error::Serialize(e.to_string()))?;
        Ok(())
    }

    /// Load state from a file.
    pub fn load(path: &Path) -> Result<Self> {
        let file = File::open(path).map_err(|e| Error::Io {
            path: path.to_path_buf(),
            source: e,
        })?;
        let reader = BufReader::new(file);
        let state: Self =
            serde_json::from_reader(reader).map_err(|e| Error::Deserialize(e.to_string()))?;
        Ok(state)
    }

    /// Build a hash map for quick lookups.
    pub fn as_map(&self) -> HashMap<PathBuf, &FileEntry> {
        self.files.iter().map(|f| (f.path.clone(), f)).collect()
    }

    /// Get the number of files in this state.
    pub fn len(&self) -> usize {
        self.files.len()
    }

    /// Check if the state is empty.
    pub fn is_empty(&self) -> bool {
        self.files.is_empty()
    }
}

/// Represents changes between two index states.
#[derive(Debug, Clone, Default)]
pub struct FileDiff {
    /// Files that are new (not in the old state).
    pub added: Vec<PathBuf>,

    /// Files that were removed (not in the new state).
    pub removed: Vec<PathBuf>,

    /// Files that have different hashes.
    pub modified: Vec<PathBuf>,

    /// Files that are unchanged.
    pub unchanged: Vec<PathBuf>,
}

impl FileDiff {
    /// Check if there are any changes.
    pub fn has_changes(&self) -> bool {
        !self.added.is_empty() || !self.removed.is_empty() || !self.modified.is_empty()
    }

    /// Get total number of changed files.
    pub fn changed_count(&self) -> usize {
        self.added.len() + self.removed.len() + self.modified.len()
    }

    /// Compute diff between two states.
    pub fn between(old: &IndexState, new: &IndexState) -> Self {
        let old_map = old.as_map();
        let new_map = new.as_map();

        let mut diff = FileDiff::default();

        // Check for added and modified files
        for (path, new_entry) in &new_map {
            match old_map.get(path) {
                None => diff.added.push(path.clone()),
                Some(old_entry) => {
                    if old_entry.hash != new_entry.hash {
                        diff.modified.push(path.clone());
                    } else {
                        diff.unchanged.push(path.clone());
                    }
                }
            }
        }

        // Check for removed files
        for path in old_map.keys() {
            if !new_map.contains_key(path) {
                diff.removed.push(path.clone());
            }
        }

        diff
    }
}

impl FileIndexer {
    /// Save the current index state to a file.
    ///
    /// The state includes file paths, sizes, modification times, and content hashes.
    /// This allows for incremental re-indexing by comparing states.
    pub fn save_state(&self, path: &Path) -> Result<()> {
        let state = IndexState::from_indexer(self);
        state.save(path)
    }

    /// Load a previously saved index state.
    pub fn load_state(path: &Path) -> Result<IndexState> {
        IndexState::load(path)
    }

    /// Compare this indexer's current state with a saved state.
    ///
    /// Returns a diff showing added, removed, and modified files.
    pub fn diff_with_state(&self, old_state: &IndexState) -> FileDiff {
        let current_state = IndexState::from_indexer(self);
        FileDiff::between(old_state, &current_state)
    }

    /// Compare this indexer with another indexer.
    pub fn diff(&self, other: &FileIndexer) -> FileDiff {
        let self_state = IndexState::from_indexer(self);
        let other_state = IndexState::from_indexer(other);
        FileDiff::between(&self_state, &other_state)
    }

    /// Create an indexer from a saved state, useful for targeted re-scanning.
    ///
    /// Note: This creates an indexer pointed at the original root path.
    /// The state is used for comparison, not for restoration.
    pub fn from_state(state: &IndexState, config: Config) -> Self {
        Self::with_config(&state.root, config)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::TempDir;

    #[test]
    fn test_save_load_state() {
        let dir = TempDir::new().unwrap();
        let file_path = dir.path().join("test.txt");
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "hello world").unwrap();

        let indexer = FileIndexer::new(dir.path());
        let state_path = dir.path().join(".fsindex-state.json");

        indexer.save_state(&state_path).unwrap();

        let loaded = FileIndexer::load_state(&state_path).unwrap();
        assert_eq!(loaded.files.len(), 1);
        assert_eq!(loaded.root, dir.path());
    }

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

        // Create initial file
        let file_path = dir.path().join("test.txt");
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "hello").unwrap();
        drop(file);

        let indexer1 = FileIndexer::new(dir.path());
        let state1 = IndexState::from_indexer(&indexer1);

        // Modify the file
        let mut file = File::create(&file_path).unwrap();
        writeln!(file, "world").unwrap();
        drop(file);

        // Add new file
        let file2_path = dir.path().join("test2.txt");
        File::create(&file2_path).unwrap();

        let indexer2 = FileIndexer::new(dir.path());
        let state2 = IndexState::from_indexer(&indexer2);

        let diff = FileDiff::between(&state1, &state2);

        assert_eq!(diff.added.len(), 1);
        assert_eq!(diff.modified.len(), 1);
        assert!(diff.removed.is_empty());
    }
}
