use crate::{Result, paths::Dirs};
use fs2::FileExt;
use std::{fs::File, io::Write};

use super::model::Instance;

#[derive(Debug, Clone)]
pub struct Registry {
    dirs: Dirs,
}

impl Registry {
    pub fn new() -> Result<Self> {
        let dirs = Dirs::for_app()?;
        Self::new_in(dirs)
    }

    pub fn new_in(dirs: Dirs) -> Result<Self> {
        dirs.ensure()?;
        Ok(Self { dirs })
    }

    pub fn dirs(&self) -> &Dirs {
        &self.dirs
    }

    pub fn write(&self, inst: &Instance) -> Result<()> {
        let path = self.dirs.instance_file(&inst.id);
        let lock_path = self.dirs.base.join(".registry.lock");

        let lock = File::create(lock_path)?;

        // prevent concurrent writes
        lock.lock_exclusive()?;

        let json = serde_json::to_vec_pretty(inst)?;
        let mut f = File::create(path)?;
        f.write_all(&json)?;
        f.write_all(b"\n")?;

        fs2::FileExt::unlock(&lock)?;
        Ok(())
    }

    pub fn read(&self, id: &str) -> Result<Instance> {
        let path = self.dirs.instance_file(id);
        let bytes = std::fs::read(path)?;
        Ok(serde_json::from_slice(&bytes)?)
    }

    pub fn remove_metadata(&self, id: &str) -> Result<()> {
        let path = self.dirs.instance_file(id);
        if path.exists() {
            std::fs::remove_file(path)?;
        }
        Ok(())
    }

    pub fn list(&self) -> Result<Vec<Instance>> {
        let mut out = Vec::new();
        for entry in std::fs::read_dir(&self.dirs.instances)? {
            let entry = entry?;
            let p = entry.path();
            if p.extension().and_then(|s| s.to_str()) != Some("json") {
                continue;
            }
            let bytes = std::fs::read(&p)?;
            match serde_json::from_slice::<Instance>(&bytes) {
                Ok(i) => out.push(i),
                Err(_) => {
                    // Ignore corrupted entries for now (can add warnings later)
                    continue;
                }
            }
        }
        out.sort_by_key(|i| std::cmp::Reverse(i.created_at));
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::Registry;
    use crate::engine::Engine;
    use crate::instance::{Backend, ConnectionInfo, Instance, SqliteInfo};
    use crate::paths::Dirs;
    use std::path::PathBuf;
    use time::OffsetDateTime;

    fn test_dirs() -> Dirs {
        let base = std::env::temp_dir().join(format!(
            "dbnest-registry-test-{}",
            uuid::Uuid::new_v4().simple()
        ));
        Dirs {
            instances: base.join("instances"),
            sqlite: base.join("sqlite"),
            base,
        }
    }

    fn instance(id: &str, created_at: i64) -> Instance {
        let path = PathBuf::from(format!("/tmp/{id}.sqlite"));
        Instance {
            id: id.into(),
            engine: Engine::Sqlite,
            backend: Backend::Embedded,
            created_at: OffsetDateTime::from_unix_timestamp(created_at).unwrap(),
            connection: ConnectionInfo {
                database_url: format!("sqlite:///{}", path.display()),
                host: None,
                port: None,
                database: None,
                user: None,
            },
            sqlite: Some(SqliteInfo {
                path,
                managed: false,
            }),
            container: None,
        }
    }

    #[test]
    fn registry_writes_reads_lists_and_removes_instances() {
        let dirs = test_dirs();
        let base = dirs.base.clone();
        let registry = Registry::new_in(dirs).unwrap();

        let older = instance("older", 10);
        let newer = instance("newer", 20);
        registry.write(&older).unwrap();
        registry.write(&newer).unwrap();

        assert_eq!(registry.read("older").unwrap().id, "older");

        let list = registry.list().unwrap();
        assert_eq!(
            list.iter().map(|i| i.id.as_str()).collect::<Vec<_>>(),
            ["newer", "older"]
        );

        registry.remove_metadata("older").unwrap();
        assert!(registry.read("older").is_err());

        let _ = std::fs::remove_dir_all(base);
    }

    #[test]
    fn registry_ignores_corrupted_entries_when_listing() {
        let dirs = test_dirs();
        let base = dirs.base.clone();
        let registry = Registry::new_in(dirs).unwrap();

        registry.write(&instance("valid", 10)).unwrap();
        std::fs::write(registry.dirs().instance_file("corrupt"), b"not json").unwrap();

        let list = registry.list().unwrap();
        assert_eq!(list.len(), 1);
        assert_eq!(list[0].id, "valid");

        let _ = std::fs::remove_dir_all(base);
    }
}
