use std::path::Path;

use crate::schema::EntityKind;

use super::WriteBatch;
use super::batch::BatchOp;
use super::engine::StorageEngine;
use super::error::StorageError;
use super::iterator::PrefixIterator;

/// A storage backend wrapping a single fjall keyspace.
///
/// Implements `StorageEngine` by delegating to fjall's `insert`, `get`,
/// `remove`, and `prefix` operations.
///
/// `Clone` is cheap: `fjall::Keyspace` is reference-counted internally.
#[derive(Clone)]
pub struct FjallBackend {
    keyspace: fjall::Keyspace,
}

impl FjallBackend {
    /// Wrap an existing fjall keyspace.
    pub(crate) const fn new(keyspace: fjall::Keyspace) -> Self {
        Self { keyspace }
    }

    /// Access the underlying fjall keyspace.
    ///
    /// Used by `FjallAtomicBatch` for cross-keyspace operations.
    #[must_use]
    pub const fn keyspace(&self) -> &fjall::Keyspace {
        &self.keyspace
    }
}

impl std::fmt::Debug for FjallBackend {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FjallBackend").finish_non_exhaustive()
    }
}

/// Map a fjall error to our `StorageError`.
fn map_fjall_err(e: &fjall::Error) -> StorageError {
    StorageError::Corruption {
        message: e.to_string(),
    }
}

impl StorageEngine for FjallBackend {
    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StorageError> {
        Ok(self
            .keyspace
            .get(key)
            .map_err(|e| map_fjall_err(&e))?
            .map(|value| value.to_vec()))
    }

    fn put(&self, key: &[u8], value: &[u8]) -> Result<(), StorageError> {
        self.keyspace
            .insert(key, value)
            .map_err(|e| map_fjall_err(&e))
    }

    fn delete(&self, key: &[u8]) -> Result<(), StorageError> {
        self.keyspace.remove(key).map_err(|e| map_fjall_err(&e))
    }

    fn scan_prefix(&self, prefix: &[u8]) -> PrefixIterator<'_> {
        // Collect into Vec to avoid holding fjall snapshot across iteration boundary.
        let entries: Vec<_> = self
            .keyspace
            .prefix(prefix)
            .map(|guard| {
                let (k, v) = guard.into_inner().map_err(|e| map_fjall_err(&e))?;
                Ok((k.to_vec(), v.to_vec()))
            })
            .collect();

        Box::new(entries.into_iter())
    }

    fn write_batch(&self, batch: WriteBatch) -> Result<(), StorageError> {
        for op in &batch.ops {
            match op {
                BatchOp::Put { key, value } => {
                    self.keyspace
                        .insert(key.as_slice(), value.as_slice())
                        .map_err(|e| map_fjall_err(&e))?;
                }
                BatchOp::Delete { key } => {
                    self.keyspace
                        .remove(key.as_slice())
                        .map_err(|e| map_fjall_err(&e))?;
                }
            }
        }
        Ok(())
    }

    fn flush(&self) -> Result<(), StorageError> {
        self.keyspace
            .rotate_memtable_and_wait()
            .map_err(|e| map_fjall_err(&e))
    }
}

/// Owns a fjall `Database` and provides three `FjallBackend` instances
/// for the three entity kinds (items, users, creators).
pub struct FjallStorage {
    db: fjall::Database,
    items: FjallBackend,
    users: FjallBackend,
    creators: FjallBackend,
}

impl FjallStorage {
    /// Open (or create) a `FjallStorage` at the given path.
    ///
    /// Creates three keyspaces: "items", "users", "creators".
    ///
    /// # Errors
    ///
    /// Returns `StorageError` if the underlying fjall database cannot be opened.
    pub fn open(path: impl AsRef<Path>) -> Result<Self, StorageError> {
        let db = fjall::Database::builder(path)
            .open()
            .map_err(|e| StorageError::Corruption {
                message: format!("failed to open fjall database: {e}"),
            })?;

        let items = FjallBackend::new(
            db.keyspace("items", fjall::KeyspaceCreateOptions::default)
                .map_err(|e| StorageError::Corruption {
                    message: format!("failed to open items keyspace: {e}"),
                })?,
        );

        let users = FjallBackend::new(
            db.keyspace("users", fjall::KeyspaceCreateOptions::default)
                .map_err(|e| StorageError::Corruption {
                    message: format!("failed to open users keyspace: {e}"),
                })?,
        );

        let creators = FjallBackend::new(
            db.keyspace("creators", fjall::KeyspaceCreateOptions::default)
                .map_err(|e| StorageError::Corruption {
                    message: format!("failed to open creators keyspace: {e}"),
                })?,
        );

        Ok(Self {
            db,
            items,
            users,
            creators,
        })
    }

    /// Get the backend for a specific entity kind.
    #[must_use]
    pub const fn backend(&self, kind: EntityKind) -> &FjallBackend {
        match kind {
            EntityKind::Item => &self.items,
            EntityKind::User => &self.users,
            EntityKind::Creator => &self.creators,
        }
    }

    /// Flush all three keyspaces and persist the database to stable storage.
    ///
    /// # Errors
    ///
    /// Returns `StorageError` if any flush or persist fails.
    pub fn flush_all(&self) -> Result<(), StorageError> {
        self.items.flush()?;
        self.users.flush()?;
        self.creators.flush()?;
        self.db
            .persist(fjall::PersistMode::SyncAll)
            .map_err(|e| map_fjall_err(&e))
    }

    /// Access the underlying fjall database.
    ///
    /// Used by `FjallAtomicBatch` for cross-keyspace atomic writes.
    #[must_use]
    pub const fn db(&self) -> &fjall::Database {
        &self.db
    }
}

impl std::fmt::Debug for FjallStorage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FjallStorage").finish_non_exhaustive()
    }
}

/// Cross-keyspace atomic batch for fjall.
///
/// Wraps `fjall::OwnedWriteBatch` to provide atomic writes across multiple
/// keyspaces (entity kinds). This is a fjall-specific API not
/// exposed through the `StorageEngine` trait.
pub struct FjallAtomicBatch {
    batch: fjall::OwnedWriteBatch,
}

impl FjallAtomicBatch {
    /// Create a new atomic batch from a `FjallStorage`.
    #[must_use]
    pub fn new(storage: &FjallStorage) -> Self {
        Self {
            batch: storage.db().batch(),
        }
    }

    /// Add a put operation for a specific keyspace.
    pub fn put(&mut self, backend: &FjallBackend, key: &[u8], value: &[u8]) {
        self.batch.insert(backend.keyspace(), key, value);
    }

    /// Add a remove operation for a specific keyspace.
    pub fn remove(&mut self, backend: &FjallBackend, key: &[u8]) {
        self.batch.remove(backend.keyspace(), key);
    }

    /// Commit the batch atomically.
    ///
    /// # Errors
    ///
    /// Returns `StorageError` if the commit fails.
    pub fn commit(self) -> Result<(), StorageError> {
        self.batch.commit().map_err(|e| map_fjall_err(&e))
    }
}

impl std::fmt::Debug for FjallAtomicBatch {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("FjallAtomicBatch").finish_non_exhaustive()
    }
}

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

    fn temp_storage() -> (tempfile::TempDir, FjallStorage) {
        let dir = tempfile::tempdir().expect("create temp dir");
        let storage = FjallStorage::open(dir.path()).expect("open storage");
        (dir, storage)
    }

    #[test]
    fn open_and_write() {
        let (_dir, storage) = temp_storage();
        let items = storage.backend(EntityKind::Item);
        items.put(b"key1", b"value1").unwrap();
        let val = items.get(b"key1").unwrap();
        assert_eq!(val.as_deref(), Some(b"value1".as_slice()));
    }

    #[test]
    fn delete_key() {
        let (_dir, storage) = temp_storage();
        let items = storage.backend(EntityKind::Item);
        items.put(b"key1", b"value1").unwrap();
        items.delete(b"key1").unwrap();
        assert_eq!(items.get(b"key1").unwrap(), None);
    }

    #[test]
    fn scan_prefix() {
        let (_dir, storage) = temp_storage();
        let users = storage.backend(EntityKind::User);
        users.put(b"pre_a", b"1").unwrap();
        users.put(b"pre_b", b"2").unwrap();
        users.put(b"other", b"3").unwrap();

        let results: Vec<_> = users
            .scan_prefix(b"pre_")
            .collect::<Result<Vec<_>, _>>()
            .unwrap();

        assert_eq!(results.len(), 2);
        assert!(results.iter().all(|(k, _)| k.starts_with(b"pre_")));
    }

    #[test]
    fn entity_kind_isolation() {
        let (_dir, storage) = temp_storage();
        storage
            .backend(EntityKind::Item)
            .put(b"key", b"item")
            .unwrap();
        storage
            .backend(EntityKind::User)
            .put(b"key", b"user")
            .unwrap();

        assert_eq!(
            storage
                .backend(EntityKind::Item)
                .get(b"key")
                .unwrap()
                .as_deref(),
            Some(b"item".as_slice())
        );
        assert_eq!(
            storage
                .backend(EntityKind::User)
                .get(b"key")
                .unwrap()
                .as_deref(),
            Some(b"user".as_slice())
        );
        assert_eq!(
            storage.backend(EntityKind::Creator).get(b"key").unwrap(),
            None
        );
    }

    #[test]
    fn persistence_across_reopen() {
        let dir = tempfile::tempdir().expect("create temp dir");

        // Write data and flush
        {
            let storage = FjallStorage::open(dir.path()).unwrap();
            storage
                .backend(EntityKind::Item)
                .put(b"persistent", b"data")
                .unwrap();
            storage.flush_all().unwrap();
        }

        // Reopen and verify data survives
        {
            let storage = FjallStorage::open(dir.path()).unwrap();
            let val = storage
                .backend(EntityKind::Item)
                .get(b"persistent")
                .unwrap();
            assert_eq!(val.as_deref(), Some(b"data".as_slice()));
        }
    }

    #[test]
    fn flush_all_succeeds() {
        let (_dir, storage) = temp_storage();
        storage.backend(EntityKind::Item).put(b"k", b"v").unwrap();
        storage.flush_all().unwrap();
    }

    #[test]
    fn write_batch() {
        let (_dir, storage) = temp_storage();
        let items = storage.backend(EntityKind::Item);

        items.put(b"existing", b"old").unwrap();

        let mut batch = WriteBatch::new();
        batch.put(b"new1".to_vec(), b"val1".to_vec());
        batch.put(b"new2".to_vec(), b"val2".to_vec());
        batch.delete(b"existing".to_vec());

        items.write_batch(batch).unwrap();

        assert_eq!(
            items.get(b"new1").unwrap().as_deref(),
            Some(b"val1".as_slice())
        );
        assert_eq!(
            items.get(b"new2").unwrap().as_deref(),
            Some(b"val2".as_slice())
        );
        assert_eq!(items.get(b"existing").unwrap(), None);
    }

    #[test]
    fn atomic_batch_cross_keyspace() {
        let (_dir, storage) = temp_storage();
        let mut batch = FjallAtomicBatch::new(&storage);

        batch.put(storage.backend(EntityKind::Item), b"item_key", b"item_val");
        batch.put(storage.backend(EntityKind::User), b"user_key", b"user_val");

        batch.commit().unwrap();

        assert_eq!(
            storage
                .backend(EntityKind::Item)
                .get(b"item_key")
                .unwrap()
                .as_deref(),
            Some(b"item_val".as_slice())
        );
        assert_eq!(
            storage
                .backend(EntityKind::User)
                .get(b"user_key")
                .unwrap()
                .as_deref(),
            Some(b"user_val".as_slice())
        );
    }

    #[test]
    fn overwrite_value() {
        let (_dir, storage) = temp_storage();
        let items = storage.backend(EntityKind::Item);
        items.put(b"key", b"v1").unwrap();
        items.put(b"key", b"v2").unwrap();
        assert_eq!(
            items.get(b"key").unwrap().as_deref(),
            Some(b"v2".as_slice())
        );
    }

    #[test]
    fn delete_nonexistent_is_ok() {
        let (_dir, storage) = temp_storage();
        let items = storage.backend(EntityKind::Item);
        items.delete(b"nonexistent").unwrap();
    }

    #[test]
    fn get_missing_returns_none() {
        let (_dir, storage) = temp_storage();
        let items = storage.backend(EntityKind::Item);
        assert_eq!(items.get(b"missing").unwrap(), None);
    }
}
