use crate::error::{EntityType, StoreError};
use crate::model::{AnnotationSet, Library, LibraryId, Reference, ReferenceId, Snapshot};
use crate::store::Store;
use chrono::{DateTime, TimeZone, Utc};
use git2::{IndexAddOption, Repository, Signature};
use std::path::{Path, PathBuf};

const REFERENCES_DIR: &str = "references";
const LIBRARIES_DIR: &str = "libraries";
const ANNOTATIONS_DIR: &str = "annotations";
const PDFS_DIR: &str = "pdfs";

/// Filesystem + git-backed store. Each entity is a TOML file.
/// Snapshots are git commits; time travel is git checkout.
pub struct FsStore {
    root: PathBuf,
    repo: Repository,
}

impl FsStore {
    /// Create a new Brittle repository at the given path.
    /// Fails if the path already contains a git repository.
    pub fn create(path: &Path) -> Result<Self, StoreError> {
        if path.join(".git").exists() {
            return Err(StoreError::RepoAlreadyExists {
                path: path.to_owned(),
            });
        }

        let repo = Repository::init(path).map_err(StoreError::Git)?;

        // Create subdirectories.
        for dir in [REFERENCES_DIR, LIBRARIES_DIR, ANNOTATIONS_DIR, PDFS_DIR] {
            std::fs::create_dir_all(path.join(dir))?;
        }

        let mut store = Self {
            root: path.to_owned(),
            repo,
        };

        // Create the initial commit so the repo has a HEAD.
        store.commit_all("Initialize Brittle repository")?;

        Ok(store)
    }

    /// Open an existing Brittle repository.
    pub fn open(path: &Path) -> Result<Self, StoreError> {
        let repo = Repository::open(path).map_err(|_| StoreError::RepoNotFound {
            path: path.to_owned(),
        })?;
        Ok(Self {
            root: path.to_owned(),
            repo,
        })
    }

    /// Stage all changes and create a git commit. Returns the commit OID as hex.
    fn commit_all(&mut self, message: &str) -> Result<String, StoreError> {
        let mut index = self.repo.index().map_err(StoreError::Git)?;
        index
            .add_all(["*"].iter(), IndexAddOption::DEFAULT, None)
            .map_err(StoreError::Git)?;
        index.write().map_err(StoreError::Git)?;

        let tree_oid = index.write_tree().map_err(StoreError::Git)?;
        let tree = self.repo.find_tree(tree_oid).map_err(StoreError::Git)?;

        let sig = Signature::now("Brittle", "brittle@local").map_err(StoreError::Git)?;

        let parent_commit = self.repo.head().ok().and_then(|h| h.peel_to_commit().ok());

        let oid = match &parent_commit {
            Some(parent) => self
                .repo
                .commit(Some("HEAD"), &sig, &sig, message, &tree, &[parent])
                .map_err(StoreError::Git)?,
            None => self
                .repo
                .commit(Some("HEAD"), &sig, &sig, message, &tree, &[])
                .map_err(StoreError::Git)?,
        };

        Ok(oid.to_string())
    }

    fn reference_path(&self, id: ReferenceId) -> PathBuf {
        self.root.join(REFERENCES_DIR).join(format!("{id}.toml"))
    }

    fn library_path(&self, id: LibraryId) -> PathBuf {
        self.root.join(LIBRARIES_DIR).join(format!("{id}.toml"))
    }

    fn annotation_path(&self, ref_id: ReferenceId) -> PathBuf {
        self.root
            .join(ANNOTATIONS_DIR)
            .join(format!("{ref_id}.toml"))
    }

    pub fn pdf_dir(&self) -> PathBuf {
        self.root.join(PDFS_DIR)
    }

    /// Returns the repository root directory.
    pub fn root(&self) -> &Path {
        &self.root
    }

    fn write_toml<T: serde::Serialize>(&self, path: &Path, value: &T) -> Result<(), StoreError> {
        let content = toml::to_string(value).map_err(|e| StoreError::Serialization {
            message: e.to_string(),
        })?;
        std::fs::write(path, content)?;
        Ok(())
    }

    fn read_toml<T: serde::de::DeserializeOwned>(&self, path: &Path) -> Result<T, StoreError> {
        let content = std::fs::read_to_string(path)?;
        toml::from_str(&content).map_err(|e| StoreError::Deserialization {
            path: path.to_owned(),
            message: e.to_string(),
        })
    }

    fn ids_from_dir<T, F>(&self, dir: &str, parse: F) -> Result<Vec<T>, StoreError>
    where
        F: Fn(&str) -> Option<T>,
    {
        let dir_path = self.root.join(dir);
        let mut ids = Vec::new();
        for entry in std::fs::read_dir(&dir_path)? {
            let entry = entry?;
            let name = entry.file_name();
            let name = name.to_string_lossy();
            if let Some(stem) = name.strip_suffix(".toml")
                && let Some(id) = parse(stem)
            {
                ids.push(id);
            }
        }
        Ok(ids)
    }
}

impl Store for FsStore {
    fn save_reference(&mut self, reference: &Reference) -> Result<(), StoreError> {
        self.write_toml(&self.reference_path(reference.id), reference)
    }

    fn load_reference(&self, id: ReferenceId) -> Result<Reference, StoreError> {
        let path = self.reference_path(id);
        if !path.exists() {
            return Err(StoreError::NotFound {
                entity_type: EntityType::Reference,
                id: id.to_string(),
            });
        }
        self.read_toml(&path)
    }

    fn delete_reference(&mut self, id: ReferenceId) -> Result<(), StoreError> {
        let path = self.reference_path(id);
        if !path.exists() {
            return Err(StoreError::NotFound {
                entity_type: EntityType::Reference,
                id: id.to_string(),
            });
        }
        std::fs::remove_file(path)?;
        Ok(())
    }

    fn list_reference_ids(&self) -> Result<Vec<ReferenceId>, StoreError> {
        self.ids_from_dir(REFERENCES_DIR, |s| {
            s.parse::<uuid::Uuid>().ok().map(ReferenceId::from)
        })
    }

    fn save_library(&mut self, library: &Library) -> Result<(), StoreError> {
        self.write_toml(&self.library_path(library.id), library)
    }

    fn load_library(&self, id: LibraryId) -> Result<Library, StoreError> {
        let path = self.library_path(id);
        if !path.exists() {
            return Err(StoreError::NotFound {
                entity_type: EntityType::Library,
                id: id.to_string(),
            });
        }
        self.read_toml(&path)
    }

    fn delete_library(&mut self, id: LibraryId) -> Result<(), StoreError> {
        let path = self.library_path(id);
        if !path.exists() {
            return Err(StoreError::NotFound {
                entity_type: EntityType::Library,
                id: id.to_string(),
            });
        }
        std::fs::remove_file(path)?;
        Ok(())
    }

    fn list_library_ids(&self) -> Result<Vec<LibraryId>, StoreError> {
        self.ids_from_dir(LIBRARIES_DIR, |s| {
            s.parse::<uuid::Uuid>().ok().map(LibraryId::from)
        })
    }

    fn load_annotations(&self, ref_id: ReferenceId) -> Result<AnnotationSet, StoreError> {
        let path = self.annotation_path(ref_id);
        if !path.exists() {
            return Ok(AnnotationSet::new(ref_id));
        }
        self.read_toml(&path)
    }

    fn save_annotations(&mut self, set: &AnnotationSet) -> Result<(), StoreError> {
        self.write_toml(&self.annotation_path(set.reference_id), set)
    }

    fn delete_annotations(&mut self, ref_id: ReferenceId) -> Result<(), StoreError> {
        let path = self.annotation_path(ref_id);
        if path.exists() {
            std::fs::remove_file(path)?;
        }
        Ok(())
    }

    fn create_snapshot(&mut self, message: &str) -> Result<Snapshot, StoreError> {
        let oid = self.commit_all(message)?;
        let commit = self
            .repo
            .find_commit(git2::Oid::from_str(&oid).map_err(StoreError::Git)?)
            .map_err(StoreError::Git)?;
        let timestamp = commit_timestamp(&commit)?;

        Ok(Snapshot {
            id: oid,
            message: message.to_owned(),
            timestamp,
        })
    }

    fn list_snapshots(&self) -> Result<Vec<Snapshot>, StoreError> {
        let mut revwalk = self.repo.revwalk().map_err(StoreError::Git)?;
        revwalk.push_head().map_err(StoreError::Git)?;
        revwalk
            .set_sorting(git2::Sort::TIME)
            .map_err(StoreError::Git)?;

        let mut snapshots = Vec::new();
        for oid in revwalk {
            let oid = oid.map_err(StoreError::Git)?;
            let commit = self.repo.find_commit(oid).map_err(StoreError::Git)?;
            let message = commit.message().unwrap_or("").to_owned();
            let timestamp = commit_timestamp(&commit)?;
            snapshots.push(Snapshot {
                id: oid.to_string(),
                message,
                timestamp,
            });
        }

        Ok(snapshots)
    }

    fn restore_snapshot(&mut self, snapshot_id: &str) -> Result<(), StoreError> {
        let oid = git2::Oid::from_str(snapshot_id).map_err(|_| StoreError::NotFound {
            entity_type: EntityType::Snapshot,
            id: snapshot_id.to_owned(),
        })?;

        let commit = self
            .repo
            .find_commit(oid)
            .map_err(|_| StoreError::NotFound {
                entity_type: EntityType::Snapshot,
                id: snapshot_id.to_owned(),
            })?;

        let tree = commit.tree().map_err(StoreError::Git)?;

        // Checkout the tree, updating both the index and the working directory.
        // `force` overwrites modified tracked files; `remove_untracked` removes
        // files that were written since the last snapshot but never committed.
        let mut checkout_opts = git2::build::CheckoutBuilder::new();
        checkout_opts.force().remove_untracked(true);
        self.repo
            .checkout_tree(tree.as_object(), Some(&mut checkout_opts))
            .map_err(StoreError::Git)?;

        // Move HEAD to point at the restored commit.
        self.repo.set_head_detached(oid).map_err(StoreError::Git)?;

        Ok(())
    }

    fn has_uncommitted_changes(&self) -> Result<bool, StoreError> {
        let statuses = self
            .repo
            .statuses(Some(
                git2::StatusOptions::new()
                    .include_untracked(true)
                    .recurse_untracked_dirs(true),
            ))
            .map_err(StoreError::Git)?;
        Ok(!statuses.is_empty())
    }
}

fn commit_timestamp(commit: &git2::Commit<'_>) -> Result<DateTime<Utc>, StoreError> {
    let time = commit.time();
    Utc.timestamp_opt(time.seconds(), 0)
        .single()
        .ok_or_else(|| StoreError::Serialization {
            message: "invalid commit timestamp".into(),
        })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{EntryType, Library, Reference};

    fn make_store(dir: &Path) -> FsStore {
        FsStore::create(dir).expect("create store")
    }

    #[test]
    fn create_and_open() {
        let tmp = tempfile::tempdir().unwrap();
        let store = make_store(tmp.path());
        drop(store);
        FsStore::open(tmp.path()).expect("re-open store");
    }

    #[test]
    fn create_fails_if_repo_exists() {
        let tmp = tempfile::tempdir().unwrap();
        make_store(tmp.path());
        assert!(FsStore::create(tmp.path()).is_err());
    }

    #[test]
    fn save_load_delete_reference() {
        let tmp = tempfile::tempdir().unwrap();
        let mut store = make_store(tmp.path());
        let r = Reference::new("test2024", EntryType::Article);
        let id = r.id;

        store.save_reference(&r).unwrap();
        let loaded = store.load_reference(id).unwrap();
        assert_eq!(loaded.cite_key, "test2024");

        store.delete_reference(id).unwrap();
        assert!(store.load_reference(id).is_err());
    }

    #[test]
    fn list_reference_ids() {
        let tmp = tempfile::tempdir().unwrap();
        let mut store = make_store(tmp.path());
        let r1 = Reference::new("a2024", EntryType::Article);
        let r2 = Reference::new("b2024", EntryType::Book);
        store.save_reference(&r1).unwrap();
        store.save_reference(&r2).unwrap();

        let ids = store.list_reference_ids().unwrap();
        assert_eq!(ids.len(), 2);
    }

    #[test]
    fn save_load_library() {
        let tmp = tempfile::tempdir().unwrap();
        let mut store = make_store(tmp.path());
        let lib = Library::new("ML Papers", None);
        let id = lib.id;

        store.save_library(&lib).unwrap();
        let loaded = store.load_library(id).unwrap();
        assert_eq!(loaded.name, "ML Papers");
    }

    #[test]
    fn annotations_missing_returns_empty_set() {
        let tmp = tempfile::tempdir().unwrap();
        let store = make_store(tmp.path());
        let ref_id = ReferenceId::new();
        let set = store.load_annotations(ref_id).unwrap();
        assert!(set.annotations.is_empty());
    }

    #[test]
    fn create_and_list_snapshot() {
        let tmp = tempfile::tempdir().unwrap();
        let mut store = make_store(tmp.path());

        // Save something so there's content to commit beyond the initial commit.
        let r = Reference::new("snap2024", EntryType::Misc);
        store.save_reference(&r).unwrap();
        let snap = store.create_snapshot("my first snapshot").unwrap();

        let snapshots = store.list_snapshots().unwrap();
        assert!(snapshots.iter().any(|s| s.id == snap.id));
        assert!(snapshots.iter().any(|s| s.message == "my first snapshot"));
    }

    #[test]
    fn restore_snapshot_reverts_state() {
        let tmp = tempfile::tempdir().unwrap();
        let mut store = make_store(tmp.path());

        let r = Reference::new("before2024", EntryType::Article);
        let ref_id = r.id;
        store.save_reference(&r).unwrap();
        let snap = store.create_snapshot("baseline").unwrap();

        // Modify state: add another reference.
        let r2 = Reference::new("after2024", EntryType::Book);
        store.save_reference(&r2).unwrap();
        assert_eq!(store.list_reference_ids().unwrap().len(), 2);

        // Restore to baseline — should have only 1 reference.
        store.restore_snapshot(&snap.id).unwrap();

        let store2 = FsStore::open(tmp.path()).unwrap();
        assert_eq!(store2.list_reference_ids().unwrap().len(), 1);
        assert!(store2.load_reference(ref_id).is_ok());
    }

    #[test]
    fn has_uncommitted_changes_detects_new_files() {
        let tmp = tempfile::tempdir().unwrap();
        let mut store = make_store(tmp.path());

        assert!(!store.has_uncommitted_changes().unwrap());

        let r = Reference::new("new2024", EntryType::Misc);
        store.save_reference(&r).unwrap();

        assert!(store.has_uncommitted_changes().unwrap());
    }
}
