use anyhow::{Context, Result, bail};
use fs2::FileExt;
use std::path::{Path, PathBuf};

#[allow(dead_code)]
pub trait StorageProvider: Send + Sync {
    fn write_file(&self, rel_path: &str, data: &[u8]) -> Result<()>;
    fn read_file(&self, rel_path: &str) -> Result<Vec<u8>>;
    fn exists(&self, rel_path: &str) -> bool;
    fn list_files(&self, prefix: &str) -> Result<Vec<String>>;
    fn sync_up(&self) -> Result<()>;
    fn sync_down(&self) -> Result<()>;
    fn root(&self) -> &Path;
}

pub struct GitStorage {
    repo_path: PathBuf,
}

impl GitStorage {
    pub fn new(repo_path: PathBuf) -> Self {
        Self { repo_path }
    }

    pub fn lock(&self) -> Result<std::fs::File> {
        self.lock_inner(true)
    }

    pub fn try_lock(&self) -> Result<std::fs::File> {
        self.lock_inner(false)
    }

    fn lock_inner(&self, blocking: bool) -> Result<std::fs::File> {
        let lock_path = self.repo_path.join(".clync.lock");
        let file = std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(&lock_path)
            .context("could not create lock file")?;
        if blocking {
            file.lock_exclusive()
                .context("another clync process is running (locked)")?;
        } else {
            file.try_lock_exclusive()
                .context("sync in progress, try again shortly")?;
        }
        Ok(file)
    }

    pub fn init_repo(repo_path: &Path) -> Result<Self> {
        std::fs::create_dir_all(repo_path)?;
        if !repo_path.join(".git").exists() {
            let status = std::process::Command::new("git")
                .args(["init", "-b", "main"])
                .current_dir(repo_path)
                .status()
                .context("git init failed")?;
            if !status.success() {
                bail!("git init failed");
            }
            std::fs::write(repo_path.join(".gitignore"), ".clync.lock\n")?;
        }
        Ok(Self {
            repo_path: repo_path.to_path_buf(),
        })
    }

    pub fn clone_repo(url: &str, dest: &Path) -> Result<Self> {
        let status = std::process::Command::new("git")
            .args(["clone", url, &dest.to_string_lossy()])
            .status()
            .context("git clone failed")?;
        if !status.success() {
            bail!("git clone failed");
        }
        Ok(Self {
            repo_path: dest.to_path_buf(),
        })
    }

    pub fn has_remote(&self) -> bool {
        std::process::Command::new("git")
            .args(["remote"])
            .current_dir(&self.repo_path)
            .output()
            .map(|o| !o.stdout.is_empty())
            .unwrap_or(false)
    }

    pub fn get_remote_url(&self) -> Option<String> {
        std::process::Command::new("git")
            .args(["remote", "get-url", "origin"])
            .current_dir(&self.repo_path)
            .output()
            .ok()
            .filter(|o| o.status.success())
            .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
    }

    pub fn add_remote(&self, url: &str) -> Result<()> {
        self.run_git(&["remote", "add", "origin", url])
    }

    pub fn checkout_first_branch(&self) -> Result<()> {
        let branches = std::process::Command::new("git")
            .args(["branch", "-r"])
            .current_dir(&self.repo_path)
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
            .unwrap_or_default();
        for line in branches.lines() {
            let branch = line.trim().trim_start_matches("origin/");
            if branch == "HEAD" || branch.contains("->") {
                continue;
            }
            let _ = std::process::Command::new("git")
                .args(["checkout", branch])
                .current_dir(&self.repo_path)
                .status();
            break;
        }
        Ok(())
    }

    fn run_git(&self, args: &[&str]) -> Result<()> {
        let status = std::process::Command::new("git")
            .args(args)
            .current_dir(&self.repo_path)
            .status()
            .with_context(|| format!("git {} failed", args.join(" ")))?;
        if !status.success() {
            bail!("git {} exited with {}", args.join(" "), status);
        }
        Ok(())
    }

    pub fn commit(&self, message: &str) -> Result<()> {
        self.run_git(&["add", "-A"])?;

        let status = std::process::Command::new("git")
            .args(["diff", "--cached", "--quiet"])
            .current_dir(&self.repo_path)
            .status()?;
        if status.success() {
            return Ok(());
        }

        self.run_git(&["commit", "-m", message])
    }

    pub fn push_remote(&self) -> Result<()> {
        if !self.has_remote() {
            return Ok(());
        }
        let result = self.run_git(&["push"]);
        if result.is_err() {
            let branch = std::process::Command::new("git")
                .args(["branch", "--show-current"])
                .current_dir(&self.repo_path)
                .output()
                .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
                .unwrap_or_else(|_| "main".into());
            self.run_git(&["push", "--set-upstream", "origin", &branch])?;
        }
        Ok(())
    }

    pub fn pull_remote(&self) -> Result<()> {
        if !self.has_remote() {
            return Ok(());
        }
        let output = std::process::Command::new("git")
            .args(["pull", "--ff-only"])
            .current_dir(&self.repo_path)
            .output()
            .context("git pull failed")?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            if stderr.contains("Not possible to fast-forward") || stderr.contains("divergent") {
                bail!(
                    "git pull failed: repos have diverged. \
                     Try: cd {} && git pull --rebase",
                    self.repo_path.display()
                );
            }
            bail!("git pull --ff-only failed: {}", stderr.trim());
        }
        Ok(())
    }
}

impl StorageProvider for GitStorage {
    fn write_file(&self, rel_path: &str, data: &[u8]) -> Result<()> {
        let full_path = self.repo_path.join(rel_path);
        if let Some(parent) = full_path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&full_path, data)?;
        Ok(())
    }

    fn read_file(&self, rel_path: &str) -> Result<Vec<u8>> {
        let full_path = self.repo_path.join(rel_path);
        std::fs::read(&full_path).with_context(|| format!("could not read {}", full_path.display()))
    }

    fn exists(&self, rel_path: &str) -> bool {
        self.repo_path.join(rel_path).exists()
    }

    fn list_files(&self, prefix: &str) -> Result<Vec<String>> {
        let dir = self.repo_path.join(prefix);
        if !dir.exists() {
            return Ok(Vec::new());
        }
        let mut files = Vec::new();
        for entry in std::fs::read_dir(&dir)? {
            let entry = entry?;
            if entry.file_type()?.is_file()
                && let Some(name) = entry.file_name().to_str()
            {
                files.push(format!("{prefix}/{name}"));
            }
        }
        Ok(files)
    }

    fn sync_up(&self) -> Result<()> {
        self.commit("clync sync")?;
        self.push_remote()
    }

    fn sync_down(&self) -> Result<()> {
        self.pull_remote()
    }

    fn root(&self) -> &Path {
        &self.repo_path
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn temp_dir(name: &str) -> PathBuf {
        let dir = std::env::temp_dir()
            .join("clync-storage-test")
            .join(name)
            .join(format!("{}", std::process::id()));
        if dir.exists() {
            std::fs::remove_dir_all(&dir).ok();
        }
        std::fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    fn init_repo_creates_git_dir() {
        let dir = temp_dir("init_repo");
        let repo_path = dir.join("repo");
        let storage = GitStorage::init_repo(&repo_path).unwrap();
        assert!(repo_path.join(".git").exists());
        assert!(repo_path.join(".gitignore").exists());
        assert_eq!(storage.root(), repo_path);
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn init_repo_idempotent() {
        let dir = temp_dir("init_idem");
        let repo_path = dir.join("repo");
        GitStorage::init_repo(&repo_path).unwrap();
        GitStorage::init_repo(&repo_path).unwrap();
        assert!(repo_path.join(".git").exists());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn write_read_exists() {
        let dir = temp_dir("write_read");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        storage.write_file("test.txt", b"hello").unwrap();
        assert!(storage.exists("test.txt"));
        assert!(!storage.exists("missing.txt"));
        let data = storage.read_file("test.txt").unwrap();
        assert_eq!(data, b"hello");
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn write_creates_subdirs() {
        let dir = temp_dir("subdirs");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        storage.write_file("a/b/c.txt", b"nested").unwrap();
        assert!(storage.exists("a/b/c.txt"));
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn list_files_works() {
        let dir = temp_dir("list_files");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        storage.write_file("sessions/a.txt", b"a").unwrap();
        storage.write_file("sessions/b.txt", b"b").unwrap();
        let files = storage.list_files("sessions").unwrap();
        assert_eq!(files.len(), 2);
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn list_files_empty() {
        let dir = temp_dir("list_empty");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        let files = storage.list_files("nonexistent").unwrap();
        assert!(files.is_empty());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn has_remote_false_for_new_repo() {
        let dir = temp_dir("no_remote");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        assert!(!storage.has_remote());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn get_remote_url_none_for_new_repo() {
        let dir = temp_dir("no_remote_url");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        assert!(storage.get_remote_url().is_none());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn add_remote_and_check() {
        let dir = temp_dir("add_remote");
        let bare = dir.join("bare.git");
        std::process::Command::new("git")
            .args(["init", "--bare", "-b", "main"])
            .arg(&bare)
            .output()
            .unwrap();

        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        storage.add_remote(bare.to_str().unwrap()).unwrap();
        assert!(storage.has_remote());
        assert!(storage.get_remote_url().is_some());
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn lock_and_try_lock() {
        let dir = temp_dir("lock");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        let _lock = storage.lock().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    fn set_git_test_author(repo: &Path) {
        std::process::Command::new("git")
            .args(["config", "user.email", "test@clync"])
            .current_dir(repo)
            .output()
            .unwrap();
        std::process::Command::new("git")
            .args(["config", "user.name", "test"])
            .current_dir(repo)
            .output()
            .unwrap();
    }

    #[test]
    fn commit_with_changes() {
        let dir = temp_dir("commit");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        set_git_test_author(storage.root());
        storage.write_file("file.txt", b"content").unwrap();
        storage.commit("test commit").unwrap();

        let log = std::process::Command::new("git")
            .args(["log", "--oneline"])
            .current_dir(storage.root())
            .output()
            .unwrap();
        let stdout = String::from_utf8_lossy(&log.stdout);
        assert!(stdout.contains("test commit"), "log: {stdout}");
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn commit_no_changes_is_noop() {
        let dir = temp_dir("commit_noop");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        set_git_test_author(storage.root());
        storage.write_file("file.txt", b"content").unwrap();
        storage.commit("first").unwrap();
        storage.commit("second should be noop").unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn pull_remote_no_remote_is_ok() {
        let dir = temp_dir("pull_no_remote");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        storage.pull_remote().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }

    #[test]
    fn push_remote_no_remote_is_ok() {
        let dir = temp_dir("push_no_remote");
        let storage = GitStorage::init_repo(&dir.join("repo")).unwrap();
        storage.push_remote().unwrap();
        std::fs::remove_dir_all(&dir).ok();
    }
}
