/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

//! Component interface implementation.
//!
//! This module implements the `nsIKeyValue` XPCOM interfaces that are
//! exposed to C++ and chrome JS callers.

use std::{io, ops::Bound, sync::Arc};

use atomic_refcell::AtomicRefCell;
use crossbeam_utils::atomic::AtomicCell;
use nserror::{nsresult, NS_OK};
use nsstring::{nsACString, nsAString, nsCString, nsString};
use rkv::{
    backend::{SafeMode as RkvSafeMode, SafeModeEnvironment as RkvSafeModeEnvironment},
    Rkv,
};
use storage_variant::{DataType, HashPropertyBag, NsIVariantExt, VariantType};
use thin_vec::ThinVec;
use xpcom::{
    getter_addrefs,
    interfaces::{
        nsIAsyncShutdownClient, nsIAsyncShutdownService, nsIKeyValueDatabaseCallback,
        nsIKeyValueDatabaseImportOptions, nsIKeyValueEnumeratorCallback,
        nsIKeyValueImportSourceSpec, nsIKeyValueImporter, nsIKeyValuePair,
        nsIKeyValueVariantCallback, nsIKeyValueVoidCallback, nsIPropertyBag, nsIVariant,
    },
    xpcom, xpcom_method, RefPtr,
};

use crate::{
    fs::WidePathBuf,
    skv::{
        abort::AbortError,
        coordinator::{Coordinator, CoordinatorClient, CoordinatorError},
        database::{Database, DatabaseError, GetOptions},
        importer::{
            CleanupPolicy, ConflictPolicy, ImporterError, NamedSourceDatabase, RkvImporter,
            SourceDatabases,
        },
        key::Key,
        store::{Store, StoreError, StorePath},
        value::{Value, ValueError},
    },
};

fn value_from_variant(variant: &nsIVariant) -> Result<Option<Value>, InterfaceError> {
    let raw_type = variant.get_data_type();
    let result = || -> Result<Option<Value>, nsresult> {
        Ok(Some(Value::from(match raw_type.try_into()? {
            DataType::Bool => serde_json::Value::from(bool::from_variant(variant)?),
            DataType::Int32 => serde_json::Value::from(i32::from_variant(variant)?),
            DataType::Int64 => serde_json::Value::from(i64::from_variant(variant)?),
            DataType::Double => serde_json::Value::from(f64::from_variant(variant)?),
            DataType::AString | DataType::WCharStr | DataType::WStringSizeIs => {
                serde_json::Value::from(nsString::from_variant(variant)?.to_string())
            }
            DataType::CString
            | DataType::CharStr
            | DataType::StringSizeIs
            | DataType::Utf8String => {
                serde_json::Value::from(nsCString::from_variant(variant)?.to_utf8().into_owned())
            }
            DataType::Void | DataType::EmptyArray | DataType::Empty => return Ok(None),
        })))
    }();
    result.map_err(|code| InterfaceError::FromVariant(raw_type, code))
}

fn value_to_variant(value: &Value) -> Result<RefPtr<nsIVariant>, ValueError> {
    Ok(match value.inner() {
        serde_json::Value::Null => ().into_variant(),
        serde_json::Value::Bool(v) => v.into_variant(),
        serde_json::Value::Number(n) if n.is_i64() => n
            .as_i64()
            .map(i64::into_variant)
            .ok_or(ValueError::ToVariant)?,
        serde_json::Value::Number(n) if n.is_f64() => n
            .as_f64()
            .map(f64::into_variant)
            .ok_or(ValueError::ToVariant)?,
        serde_json::Value::String(s) => nsCString::from(s).into_variant(),
        serde_json::Value::Number(_)
        | serde_json::Value::Array(_)
        | serde_json::Value::Object(_) => Err(ValueError::ToVariant)?,
    })
}

fn store_path_from_wide(path: WidePathBuf) -> Result<StorePath, StoreError> {
    if path == StorePath::IN_MEMORY_DATABASE_NAME {
        Ok(StorePath::for_in_memory())
    } else {
        let dir = path.canonicalize().map_err(StoreError::StorageDir)?;
        Ok(StorePath::for_storage_dir(dir))
    }
}

fn key_from_nscstring(s: &nsACString) -> Key {
    Key::from(&*s.to_utf8())
}

fn key_to_nscstring(key: Key) -> nsCString {
    nsCString::from(key.into_string())
}

#[xpcom(implement(nsIKeyValueService), atomic)]
pub struct KeyValueService {
    client: CoordinatorClient<'static>,
}

impl KeyValueService {
    pub fn new() -> RefPtr<Self> {
        let client = Coordinator::get_or_create().client_with_name("skv:KeyValueService");
        KeyValueServiceShutdownBlocker::register(client.clone());
        KeyValueService::allocate(InitKeyValueService { client })
    }

    xpcom_method!(
        get_or_create => GetOrCreate(
            callback: *const nsIKeyValueDatabaseCallback,
            dir: *const nsAString,
            name: *const nsACString
        )
    );
    fn get_or_create(
        &self,
        callback: &nsIKeyValueDatabaseCallback,
        dir: &nsAString,
        name: &nsACString,
    ) -> Result<(), Infallible> {
        let client = self.client.clone();
        let dir = WidePathBuf::new(dir);
        let request =
            moz_task::spawn_blocking("skv:KeyValueService:GetOrCreate:Request", async move {
                Ok((
                    client.child_with_name("skv:KeyValueDatabase")?,
                    store_path_from_wide(dir)?,
                ))
            });

        let name = nsCString::from(name);
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueService:GetOrCreate:Response", async move {
            match request.await {
                Ok((client, path)) => {
                    let db = KeyValueDatabase::new(client, path, name.to_utf8().into());
                    unsafe { callback.Resolve(db.coerce()) }
                }
                Err::<_, InterfaceError>(err) => unsafe {
                    callback.Reject(&*nsCString::from(err.to_string()))
                },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        get_or_create_with_options => GetOrCreateWithOptions(
            callback: *const nsIKeyValueDatabaseCallback,
            path: *const nsAString,
            name: *const nsACString,
            strategy: u8
        )
    );
    fn get_or_create_with_options(
        &self,
        _callback: &nsIKeyValueDatabaseCallback,
        _path: &nsAString,
        _name: &nsACString,
        _strategy: u8,
    ) -> Result<(), nsresult> {
        Err(nserror::NS_ERROR_NOT_IMPLEMENTED)
    }

    xpcom_method!(
        create_importer => CreateImporter(
            type_: *const nsACString,
            dir: *const nsAString
        ) -> *const nsIKeyValueImporter
    );
    fn create_importer(
        &self,
        type_: &nsACString,
        dir: &nsAString,
    ) -> Result<RefPtr<nsIKeyValueImporter>, nsresult> {
        match &*type_.to_utf8() {
            RkvSafeModeKeyValueImporter::TYPE => {
                let client = self
                    .client
                    .child_with_name("skv:RkvSafeModeKeyValueImporter")
                    .map_err(|_| nserror::NS_ERROR_FAILURE)?;
                let importer = RkvSafeModeKeyValueImporter::new(client, WidePathBuf::new(dir));
                Ok(RefPtr::new(importer.coerce()))
            }
            _ => Err(nserror::NS_ERROR_INVALID_ARG),
        }
    }
}

#[xpcom(implement(nsIKeyValueDatabase), atomic)]
pub struct KeyValueDatabase {
    client: CoordinatorClient<'static>,
    path: StorePath,
    name: String,
}

impl KeyValueDatabase {
    fn new(client: CoordinatorClient<'static>, path: StorePath, name: String) -> RefPtr<Self> {
        KeyValueDatabase::allocate(InitKeyValueDatabase { client, path, name })
    }

    fn store(&self) -> Result<Arc<Store>, InterfaceError> {
        Ok(self.client.store_for_path(self.path.clone())?)
    }

    xpcom_method!(
        is_empty => IsEmpty(
            callback: *const nsIKeyValueVariantCallback
        )
    );
    fn is_empty(&self, callback: &nsIKeyValueVariantCallback) -> Result<(), Infallible> {
        let store = self.store();
        let name = self.name.clone();
        let request =
            moz_task::spawn_blocking("skv:KeyValueDatabase:IsEmpty:Request", async move {
                let store = store?;
                let db = Database::new(&store, &name);
                Ok(db.is_empty()?)
            });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:IsEmpty:Response", async move {
            match signal.aborting(request).await {
                Ok(b) => unsafe { callback.Resolve(b.into_variant().coerce()) },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("isEmpty: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        count => Count(
            callback: *const nsIKeyValueVariantCallback
        )
    );
    fn count(&self, callback: &nsIKeyValueVariantCallback) -> Result<(), Infallible> {
        let store = self.store();
        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Count:Request", async move {
            let store = store?;
            let db = Database::new(&store, &name);
            Ok(db.count()?)
        });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Count:Response", async move {
            match signal.aborting(request).await {
                Ok(n) => unsafe { callback.Resolve(n.into_variant().coerce()) },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("count: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        size => Size(
            callback: *const nsIKeyValueVariantCallback
        )
    );
    fn size(&self, callback: &nsIKeyValueVariantCallback) -> Result<(), Infallible> {
        let store = self.store();
        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Size:Request", async move {
            let store = store?;
            let db = Database::new(&store, &name);
            Ok(db.size()?)
        });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Size:Response", async move {
            match signal.aborting(request).await {
                Ok(n) => unsafe { callback.Resolve(n.into_variant().coerce()) },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("size: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        put => Put(
            callback: *const nsIKeyValueVoidCallback,
            key: *const nsACString,
            value: *const nsIVariant
        )
    );
    fn put(
        &self,
        callback: &nsIKeyValueVoidCallback,
        key: &nsACString,
        value: &nsIVariant,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let key = key_from_nscstring(key);
            let value = value_from_variant(value)?;
            Ok((store, key, value))
        }();

        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Put:Request", async move {
            let (store, key, value) = inputs?;
            let db = Database::new(&store, &name);
            Ok(db.put(&[(key, value)])?)
        });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Put:Response", async move {
            match signal.aborting(request).await {
                Ok(()) => unsafe { callback.Resolve() },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("put: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        write_many => WriteMany(
            callback: *const nsIKeyValueVoidCallback,
            pairs: *const ThinVec<Option<RefPtr<nsIKeyValuePair>>>
        )
    );
    fn write_many(
        &self,
        callback: &nsIKeyValueVoidCallback,
        pairs: &ThinVec<Option<RefPtr<nsIKeyValuePair>>>,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let pairs = pairs
                .into_iter()
                .flatten()
                .map(|pair| {
                    let mut key = nsCString::new();
                    unsafe { pair.GetKey(&mut *key) }.to_result()?;
                    let value = getter_addrefs(|p| unsafe { pair.GetValue(p) })?;
                    Ok((key_from_nscstring(&key), value_from_variant(value.coerce())?))
                })
                .collect::<Result<Vec<_>, InterfaceError>>()?;
            Ok((store, pairs))
        }();

        let name = self.name.clone();
        let request =
            moz_task::spawn_blocking("skv:KeyValueDatabase:WriteMany:Request", async move {
                let (store, pairs) = inputs?;
                let db = Database::new(&store, &name);
                Ok(db.put(pairs.as_slice())?)
            });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:WriteMany:Response", async move {
            match signal.aborting(request).await {
                Ok(()) => unsafe { callback.Resolve() },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("writeMany: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        get => Get(
            callback: *const nsIKeyValueVariantCallback,
            key: *const nsACString,
            default_value: *const nsIVariant
        )
    );
    fn get(
        &self,
        callback: &nsIKeyValueVariantCallback,
        key: &nsACString,
        default_value: &nsIVariant,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let key = key_from_nscstring(key);
            Ok((store, key))
        }();

        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Get:Request", async move {
            let (store, key) = inputs?;
            let db = Database::new(&store, &name);
            Ok(db.get(&key, &GetOptions::new())?)
        });

        let signal = self.client.signal();
        let default_value = RefPtr::new(default_value);
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Get:Response", async move {
            let result = signal.aborting(request).await.and_then(|value| {
                Ok(match value {
                    Some(value) => value_to_variant(&value)?,
                    None => default_value,
                })
            });
            match result {
                Ok(variant) => unsafe { callback.Resolve(variant.coerce()) },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("get: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        has => Has(callback: *const nsIKeyValueVariantCallback, key: *const nsACString)
    );
    fn has(
        &self,
        callback: &nsIKeyValueVariantCallback,
        key: &nsACString,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let key = key_from_nscstring(key);
            Ok((store, key))
        }();

        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Has:Request", async move {
            let (store, key) = inputs?;
            let db = Database::new(&store, &name);
            Ok(db.has(&key, &GetOptions::new())?)
        });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Has:Response", async move {
            match signal.aborting(request).await {
                Ok(b) => unsafe { callback.Resolve(b.into_variant().coerce()) },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("has: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        delete => Delete(callback: *const nsIKeyValueVoidCallback, key: *const nsACString)
    );
    fn delete(
        &self,
        callback: &nsIKeyValueVoidCallback,
        key: &nsACString,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let key = key_from_nscstring(key);
            Ok((store, key))
        }();

        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Delete:Request", async move {
            let (store, key) = inputs?;
            let db = Database::new(&store, &name);
            Ok(db.delete(&key)?)
        });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Delete:Response", async move {
            match signal.aborting(request).await {
                Ok(()) => unsafe { callback.Resolve() },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("delete: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        delete_range => DeleteRange(
            callback: *const nsIKeyValueVoidCallback,
            from_key: *const nsACString,
            to_key: *const nsACString
        )
    );
    fn delete_range(
        &self,
        callback: &nsIKeyValueVoidCallback,
        from_key: &nsACString,
        to_key: &nsACString,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let from_key = match from_key.is_empty() {
                true => Bound::Unbounded,
                false => Bound::Included(key_from_nscstring(from_key)),
            };
            let to_key = match to_key.is_empty() {
                true => Bound::Unbounded,
                false => Bound::Excluded(key_from_nscstring(to_key)),
            };
            Ok((store, from_key, to_key))
        }();

        let name = self.name.clone();
        let request =
            moz_task::spawn_blocking("skv:KeyValueDatabase:DeleteRange:Request", async move {
                let (store, from_key, to_key) = inputs?;
                let db = Database::new(&store, &name);
                Ok(db.delete_range((from_key, to_key))?)
            });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:DeleteRange:Response", async move {
            match signal.aborting(request).await {
                Ok(()) => unsafe { callback.Resolve() },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("deleteRange: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        clear => Clear(callback: *const nsIKeyValueVoidCallback)
    );
    fn clear(&self, callback: &nsIKeyValueVoidCallback) -> Result<(), Infallible> {
        let store = self.store();
        let name = self.name.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Clear:Request", async move {
            let store = store?;
            let db = Database::new(&store, &name);
            Ok(db.clear()?)
        });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Clear:Response", async move {
            match signal.aborting(request).await {
                Ok(()) => unsafe { callback.Resolve() },
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("clear: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        enumerate => Enumerate(
            callback: *const nsIKeyValueEnumeratorCallback,
            from_key: *const nsACString,
            to_key: *const nsACString
        )
    );
    fn enumerate(
        &self,
        callback: &nsIKeyValueEnumeratorCallback,
        from_key: &nsACString,
        to_key: &nsACString,
    ) -> Result<(), Infallible> {
        let inputs = || -> Result<_, InterfaceError> {
            let store = self.store()?;
            let from_key = match from_key.is_empty() {
                true => Bound::Unbounded,
                false => Bound::Included(key_from_nscstring(from_key)),
            };
            let to_key = match to_key.is_empty() {
                true => Bound::Unbounded,
                false => Bound::Excluded(key_from_nscstring(to_key)),
            };
            Ok((store, from_key, to_key))
        }();

        let name = self.name.clone();
        let request =
            moz_task::spawn_blocking("skv:KeyValueDatabase:Enumerate:Request", async move {
                let (store, from_key, to_key) = inputs?;
                let db = Database::new(&store, &name);
                Ok(db.enumerate((from_key, to_key), GetOptions::new().concurrent(true))?)
            });

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Enumerate:Response", async move {
            match signal.aborting(request).await {
                Ok(pairs) => {
                    let enumerator = KeyValueEnumerator::new(pairs);
                    unsafe { callback.Resolve(enumerator.coerce()) }
                }
                Err(InterfaceError::Abort(_)) => unsafe {
                    callback.Reject(&*nsCString::from("enumerate: aborted"))
                },
                Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
            }
        })
        .detach();

        Ok(())
    }

    xpcom_method!(
        close => Close(
            callback: *const nsIKeyValueVoidCallback
        )
    );
    fn close(&self, callback: &nsIKeyValueVoidCallback) -> Result<(), Infallible> {
        let client = self.client.clone();
        let request = moz_task::spawn_blocking("skv:KeyValueDatabase:Close:Request", async move {
            client.invalidate()
        });

        let callback = RefPtr::new(callback);
        moz_task::spawn_local("skv:KeyValueDatabase:Close:Response", async move {
            request.await;
            unsafe { callback.Resolve() };
        })
        .detach();

        Ok(())
    }
}

#[xpcom(implement(nsIKeyValueEnumerator), atomic)]
pub struct KeyValueEnumerator {
    iter: AtomicRefCell<std::vec::IntoIter<(Key, Value)>>,
}

impl KeyValueEnumerator {
    fn new(pairs: Vec<(Key, Value)>) -> RefPtr<Self> {
        KeyValueEnumerator::allocate(InitKeyValueEnumerator {
            iter: AtomicRefCell::new(pairs.into_iter()),
        })
    }

    xpcom_method!(has_more_elements => HasMoreElements() -> bool);
    fn has_more_elements(&self) -> Result<bool, Infallible> {
        Ok(!self.iter.borrow().as_slice().is_empty())
    }

    xpcom_method!(get_next => GetNext() -> *const nsIKeyValuePair);
    fn get_next(&self) -> Result<RefPtr<nsIKeyValuePair>, nsresult> {
        let mut iter = self.iter.borrow_mut();
        let (key, value) = iter.next().ok_or(nserror::NS_ERROR_FAILURE)?;
        let pair = KeyValuePair::new(key, value);
        Ok(RefPtr::new(pair.coerce()))
    }
}

#[xpcom(implement(nsIKeyValuePair), atomic)]
pub struct KeyValuePair {
    key: Key,
    value: Value,
}

impl KeyValuePair {
    fn new(key: Key, value: Value) -> RefPtr<Self> {
        KeyValuePair::allocate(InitKeyValuePair { key, value })
    }

    xpcom_method!(get_key => GetKey() -> nsACString);
    fn get_key(&self) -> Result<nsCString, Infallible> {
        Ok(key_to_nscstring(self.key.clone()))
    }

    xpcom_method!(get_value => GetValue() -> *const nsIVariant);
    fn get_value(&self) -> Result<RefPtr<nsIVariant>, nsresult> {
        value_to_variant(&self.value).map_err(|_| nserror::NS_ERROR_FAILURE)
    }
}

#[xpcom(implement(nsIAsyncShutdownBlocker), nonatomic)]
pub struct KeyValueServiceShutdownBlocker {
    client: CoordinatorClient<'static>,
}

impl KeyValueServiceShutdownBlocker {
    fn new(client: CoordinatorClient<'static>) -> RefPtr<Self> {
        assert!(moz_task::is_main_thread());
        KeyValueServiceShutdownBlocker::allocate(InitKeyValueServiceShutdownBlocker { client })
    }

    /// Registers a blocker to close all open databases on shutdown.
    ///
    /// This function may be called on any thread, but the blocker itself is
    /// registered on the main thread, because `nsIAsyncShutdownService` is
    /// main-thread-only.
    fn register(client: CoordinatorClient<'static>) {
        let Ok(main_thread) = moz_task::get_main_thread() else {
            return;
        };
        moz_task::spawn_onto(
            "skv:KeyValueServiceShutdownBlocker:Register",
            main_thread.coerce(),
            async move {
                let _ = || -> Result<(), InterfaceError> {
                    let service =
                        xpcom::components::AsyncShutdown::service::<nsIAsyncShutdownService>()?;
                    let barrier = getter_addrefs(|p| unsafe { service.GetProfileBeforeChange(p) })?;
                    let blocker = Self::new(client);
                    unsafe {
                        barrier.AddBlocker(
                            blocker.coerce(),
                            &*nsString::from(file!()),
                            line!() as i32,
                            &*nsString::new(),
                        )
                    }
                    .to_result()?;
                    Ok(())
                }();
            },
        )
        .detach();
    }

    xpcom_method!(
        block_shutdown => BlockShutdown(
            barrier: *const nsIAsyncShutdownClient
        )
    );
    fn block_shutdown(&self, barrier: &nsIAsyncShutdownClient) -> Result<(), Infallible> {
        assert!(moz_task::is_main_thread());

        let client = self.client.clone();
        let request = moz_task::spawn_blocking(
            "skv:KeyValueServiceShutdownBlocker:BlockShutdown:Request",
            async move { client.invalidate() },
        );

        let barrier = RefPtr::new(barrier);
        let blocker = RefPtr::new(self);
        moz_task::spawn_local(
            "skv:KeyValueServiceShutdownBlocker:BlockShutdown:Response",
            async move {
                request.await;
                unsafe { barrier.RemoveBlocker(blocker.coerce()) };
            },
        )
        .detach();

        Ok(())
    }

    xpcom_method!(
        get_name => GetName() -> nsAString
    );
    fn get_name(&self) -> Result<nsString, Infallible> {
        Ok("KeyValueService: shutdown".into())
    }

    xpcom_method!(
        get_state => GetState() -> *const nsIPropertyBag
    );
    fn get_state(&self) -> Result<RefPtr<nsIPropertyBag>, Infallible> {
        let state = HashPropertyBag::new();
        Ok(RefPtr::new(state.bag().coerce()))
    }
}

#[xpcom(implement(nsIKeyValueImporter), atomic)]
pub struct RkvSafeModeKeyValueImporter {
    client: CoordinatorClient<'static>,
    specs: AtomicRefCell<Vec<RefPtr<KeyValueImportSourceSpec>>>,
}

impl RkvSafeModeKeyValueImporter {
    const TYPE: &'static str = "rkv-safe-mode";

    pub fn new(client: CoordinatorClient<'static>, dir: WidePathBuf) -> RefPtr<Self> {
        RkvSafeModeKeyValueImporter::allocate(InitRkvSafeModeKeyValueImporter {
            client,
            // The first spec is always the destination storage directory.
            specs: vec![KeyValueImportSourceSpec::new(dir)].into(),
        })
    }

    xpcom_method!(get_type => GetType() -> nsACString);
    fn get_type(&self) -> Result<nsCString, Infallible> {
        Ok(Self::TYPE.into())
    }

    xpcom_method!(get_path => GetPath() -> nsAString);
    fn get_path(&self) -> Result<nsString, Infallible> {
        self.specs.borrow()[0].get_path()
    }

    xpcom_method!(add_path => AddPath(dir: *const nsAString) -> *const nsIKeyValueImportSourceSpec);
    fn add_path(&self, dir: &nsAString) -> Result<RefPtr<nsIKeyValueImportSourceSpec>, Infallible> {
        let spec = KeyValueImportSourceSpec::new(WidePathBuf::new(dir));
        self.specs.borrow_mut().push(spec.clone());
        Ok(RefPtr::new(spec.coerce()))
    }

    xpcom_method!(
        add_database => AddDatabase(
            name: *const nsACString
        ) -> *const nsIKeyValueDatabaseImportOptions
    );
    fn add_database(
        &self,
        name: &nsACString,
    ) -> Result<RefPtr<nsIKeyValueDatabaseImportOptions>, nsresult> {
        self.specs.borrow()[0].add_database(name)
    }

    xpcom_method!(add_all_databases => AddAllDatabases() -> *const nsIKeyValueDatabaseImportOptions);
    fn add_all_databases(&self) -> Result<RefPtr<nsIKeyValueDatabaseImportOptions>, nsresult> {
        self.specs.borrow()[0].add_all_databases()
    }

    xpcom_method!(
        import => Import(
            callback: *const nsIKeyValueVoidCallback
        )
    );
    fn import(&self, callback: &nsIKeyValueVoidCallback) -> Result<(), Infallible> {
        let client = self.client.clone();
        let specs = self.specs.borrow().clone();
        let request = moz_task::spawn_blocking(
            "skv:RkvSafeModeKeyValueImporter:Import:Request",
            async move {
                let mut manager = rkv::Manager::<RkvSafeModeEnvironment>::singleton()
                    .write()
                    .unwrap();
                let store =
                    client.store_for_path(store_path_from_wide(specs[0].dir.clone())?)?;

                // An Rkv environment is unique to a storage directory;
                // if we're importing from multiple directories, we need to
                // open an environment for each directory.
                let rkvs = {
                    let mut rkvs = Vec::with_capacity(specs.len());
                    for spec in specs {
                        let Some(dbs) = spec.to_source_databases() else {
                            continue;
                        };
                        let dir = spec.dir.canonicalize()?;
                        let builder = Rkv::environment_builder::<RkvSafeMode>();
                        let rkv = manager.get_or_create_from_builder(
                            dir.as_path(),
                            builder,
                            Rkv::from_builder::<RkvSafeMode>,
                        )?;
                        rkvs.push((rkv, dbs));
                    }
                    rkvs
                };

                // Import all databases from each directory, in order.
                for (rkv, dbs) in rkvs {
                    let env = rkv.read().unwrap();
                    let importer = RkvImporter::new(&*env, &store, dbs)?;
                    importer.import()?;
                    importer.cleanup();
                }

                Ok(())
            },
        );

        let signal = self.client.signal();
        let callback = RefPtr::new(callback);
        moz_task::spawn_local(
            "skv:RkvSafeModeKeyValueImporter:Import:Response",
            async move {
                match signal.aborting(request).await {
                    Ok(()) => unsafe { callback.Resolve() },
                    Err(InterfaceError::Abort(_)) => unsafe {
                        callback.Reject(&*nsCString::from("import: aborted"))
                    },
                    Err(err) => unsafe { callback.Reject(&*nsCString::from(err.to_string())) },
                }
            },
        )
        .detach();

        Ok(())
    }
}

#[derive(Clone)]
enum KeyValueImportSource {
    Databases(Vec<(String, RefPtr<KeyValueDatabaseImportOptions>)>),
    AllDatabases(RefPtr<KeyValueDatabaseImportOptions>),
}

#[xpcom(implement(nsIKeyValueImportSourceSpec), atomic)]
pub struct KeyValueImportSourceSpec {
    dir: WidePathBuf,
    source: AtomicRefCell<Option<KeyValueImportSource>>,
}

impl KeyValueImportSourceSpec {
    fn new(dir: WidePathBuf) -> RefPtr<Self> {
        KeyValueImportSourceSpec::allocate(InitKeyValueImportSourceSpec {
            dir,
            source: AtomicRefCell::default(),
        })
    }

    fn to_source_databases(&self) -> Option<SourceDatabases> {
        self.source.borrow().as_ref().map(|source| match source {
            KeyValueImportSource::Databases(dbs) => SourceDatabases::Named(
                dbs.iter()
                    .map(|(name, options)| NamedSourceDatabase {
                        name: name.clone().into(),
                        conflict_policy: options.conflict_policy.load(),
                        cleanup_policy: options.cleanup_policy.load(),
                    })
                    .collect(),
            ),
            KeyValueImportSource::AllDatabases(options) => SourceDatabases::All {
                conflict_policy: options.conflict_policy.load(),
                cleanup_policy: options.cleanup_policy.load(),
            },
        })
    }

    xpcom_method!(get_path => GetPath() -> nsAString);
    fn get_path(&self) -> Result<nsString, Infallible> {
        Ok(self.dir.as_wide().into())
    }

    xpcom_method!(
        add_database => AddDatabase(
            dir: *const nsACString
        ) -> *const nsIKeyValueDatabaseImportOptions
    );
    fn add_database(
        &self,
        name: &nsACString,
    ) -> Result<RefPtr<nsIKeyValueDatabaseImportOptions>, nsresult> {
        let mut guard = self.source.borrow_mut();
        let source = guard.get_or_insert_with(|| KeyValueImportSource::Databases(Vec::new()));
        let options = match source {
            KeyValueImportSource::Databases(dbs) => {
                let options = KeyValueDatabaseImportOptions::new();
                dbs.push((name.to_utf8().into_owned(), options.clone()));
                options
            }
            KeyValueImportSource::AllDatabases(_) => {
                return Err(nserror::NS_ERROR_ALREADY_INITIALIZED)
            }
        };
        Ok(RefPtr::new(options.coerce()))
    }

    xpcom_method!(add_all_databases => AddAllDatabases() -> *const nsIKeyValueDatabaseImportOptions);
    fn add_all_databases(&self) -> Result<RefPtr<nsIKeyValueDatabaseImportOptions>, nsresult> {
        let mut guard = self.source.borrow_mut();
        let options = match &mut *guard {
            Some(_) => return Err(nserror::NS_ERROR_ALREADY_INITIALIZED),
            None => {
                let options = KeyValueDatabaseImportOptions::new();
                *guard = Some(KeyValueImportSource::AllDatabases(options.clone()));
                options
            }
        };
        Ok(RefPtr::new(options.coerce()))
    }
}

#[xpcom(implement(nsIKeyValueDatabaseImportOptions), atomic)]
pub struct KeyValueDatabaseImportOptions {
    conflict_policy: AtomicCell<ConflictPolicy>,
    cleanup_policy: AtomicCell<CleanupPolicy>,
}

impl KeyValueDatabaseImportOptions {
    fn new() -> RefPtr<Self> {
        KeyValueDatabaseImportOptions::allocate(InitKeyValueDatabaseImportOptions {
            conflict_policy: AtomicCell::new(ConflictPolicy::Error),
            cleanup_policy: AtomicCell::new(CleanupPolicy::Keep),
        })
    }

    xpcom_method!(
        set_conflict_policy => SetConflictPolicy(
            policy: u8
        ) -> *const nsIKeyValueDatabaseImportOptions
    );
    fn set_conflict_policy(
        &self,
        policy: u8,
    ) -> Result<RefPtr<nsIKeyValueDatabaseImportOptions>, nsresult> {
        let policy = ConflictPolicy::try_from(policy).map_err(|_| nserror::NS_ERROR_INVALID_ARG)?;
        self.conflict_policy.store(policy);
        Ok(RefPtr::new(self.coerce()))
    }

    xpcom_method!(
        set_cleanup_policy => SetCleanupPolicy(
            policy: u8
        ) -> *const nsIKeyValueDatabaseImportOptions
    );
    fn set_cleanup_policy(
        &self,
        policy: u8,
    ) -> Result<RefPtr<nsIKeyValueDatabaseImportOptions>, nsresult> {
        let policy = CleanupPolicy::try_from(policy).map_err(|_| nserror::NS_ERROR_INVALID_ARG)?;
        self.cleanup_policy.store(policy);
        Ok(RefPtr::new(self.coerce()))
    }
}

/// The error type for interface methods that never return an error.
///
/// This is equivalent to [`std::convert::Infallible`], but implements
/// `Into<nsresult>`, which we can't implement on [`std::convert::Infallible`]
/// because of the orphan rule.
enum Infallible {}

impl From<Infallible> for nsresult {
    fn from(_: Infallible) -> Self {
        nserror::NS_ERROR_FAILURE
    }
}

#[derive(Debug, thiserror::Error)]
pub enum InterfaceError {
    #[error("coordinator: {0}")]
    Coordinator(#[from] CoordinatorError),
    #[error(transparent)]
    Abort(#[from] AbortError),
    #[error("database: {0}")]
    Database(#[from] DatabaseError),
    #[error("store: {0}")]
    Store(#[from] StoreError),
    #[error("value: {0}")]
    Value(#[from] ValueError),
    #[error("from variant of type {0}: {1}")]
    FromVariant(u16, nsresult),
    #[error("rkv store: {0}")]
    RkvStore(#[from] rkv::StoreError),
    #[error("importer: {0}")]
    Importer(#[from] ImporterError),
    #[error("I/O: {0}")]
    Io(#[from] io::Error),
    #[error("error code: {0}")]
    Nsresult(#[from] nsresult),
}
