#pragma once

#include <concepts>
#include <memory>
#include <oxen/quic/loop.hpp>
#include <session/clock.hpp>
#include <session/config/namespaces.hpp>
#include <session/crypto/ed25519.hpp>
#include <session/mnemonics.hpp>
#include <session/sodium_array.hpp>
#include <session/sqlite.hpp>
#include <session/types.hpp>

#include "core/callbacks.hpp"
#include "core/configs.hpp"
#include "core/devices.hpp"
#include "core/globals.hpp"
#include "core/pro.hpp"
#include "core/schema/schema_registry.hpp"
#include "session/network/key_types.hpp"
#include "session/network/service_node.hpp"

/// The "Core" class holds a Session account's own state, in an encrypted sqlite database: its keys,
/// its device group, its configs, and the bookkeeping needed to talk to the network on its behalf.
///
/// Core was once meant to be the whole data model -- conversations, contacts and messages included
/// -- and the split into Core and `session::client::Client` divided that job rather than shrinking
/// it.  The two together are what Core alone was originally envisioned to be, so a reader looking
/// for conversations or message history should look up, not deeper.
///
/// Core is the primitive layer, and deliberately stops short of interpretation.  It holds the
/// account's keys, talks to swarms, signs and authenticates requests, encrypts and decrypts, and
/// stores what it must to do those things.  What it does *not* do is decide what any of it means: a
/// message that arrives is handed up as an authenticated sender plus a span of decrypted bytes, and
/// nothing in Core parses those bytes, knows what a conversation is, or can tell an outgoing
/// message from an incoming one.
///
/// That is `session::client::Client`'s work.  The dividing line is worth stating because it is easy
/// to erode one convenience at a time: if a question can be answered without knowing what a message
/// *says*, it belongs here; the moment answering it requires reading the payload, it belongs above.
///
/// The apparent exception proves it — Core does parse its own namespaces, the device group and the
/// account keys, because that state *is* Core's rather than a conversation's.
///
/// Core can drive its own network once one is attached with set_network(), after which it polls the
/// account's swarms on a timer.  Without one it can still be fed inbound messages through
/// receive_messages(), but it cannot send: a send with no network attached reports no_network.
///
/// The typical intended flow for using the Core is to construct it early in the application and
/// store it for the application duration:
///
///     session::core::Core core{
///         std::filesystem::path{"/path/to/libsession.db"},
///         session::sqlite::argon2id_password{"user-supplied password"}
///     };
///
/// (Or keep it in a unique or shared ptr if you have more complex ownership needs).
///
/// The above examples shows a usage where the database is:
/// - encrypted using AEGIS-256
/// - encrypted using a password resulting from argon2id password hashing
/// - is fully encrypted, with the first 16 bytes of the file containing the password salt.
///
/// The above can be modified to pass any of the options supported by session::sqlite::Database, but
/// some of the most common ones are depicted here.
///
/// If you have secure storage of a 32 byte secure random value (for example, 32 bytes generated by
/// libsodium's randombytes_buf) then instead of the argon2id_password argument you can pass a
/// raw_key:
///
///     session::core::Core core{
///         std::filesystem::path{"/path/to/libsession.db"},
///         session::sqlite::raw_key{key}
///     };
///
/// If you have such a raw key that you can keep secure, this will open the database substantially
/// faster than needing to perform an argon2id hash.  DO NOT use this approach with a user-supplied
/// password.
///
/// (Note that the above makes a secure copy when opening the database.  If you cannot avoid making
/// a copy yourself, remember to copy into at least a `session::cleared_b32` or use similar secure
/// clearing after use to ensure the key bytes does not remain in unused process memory).
///
/// If you want password protection instead of secure raw key protection then replace
///
///     session::sqlite::raw_key{key}
///
/// with
///
///     session::sqlite::argon2id_password{user_pass}
///
/// You can optionally crank up the argon2 settings for a more secure (but slower to open) database
/// by adding additional arguments to the argon2id_password constructor; see the session-sqlite
/// documentation for details.
///
/// For iOS, where full encryption causes the OS to kill the process but opens up a magic special
/// snowflake exception if it thinks your file is SQLite, you can pass an extra argument value:
///
///     session::sqlite::plaintext_header
///
/// which will cause the initial 24 bytes of the file to be unencrypted, allowing the file to pass
/// iOS's sniff test to get magic permissions.  Note that if you combine this with argon2id_password
/// you must also create and store a session::sqlite::salt value and pass that every time the
/// database is opened.  The salt should be random bytes generated by a cryptographically secure
/// RNG, but can be stored without encryption once generated (i.e. it is not a sensitive value).
///
/// Example construction:
///
/// ```C++
/// #include <session/core.hpp>
/// #include <session/sqlite.hpp>
///
/// int main() {
///
///     session::core::Core core{
///         std::filesystem::path{"/path/to/libsession.db"},
///         session::sqlite::argon2id_password{"correct horse battery staple"}
///     };
///
///     run_my_app(core);
/// }
///
///
///     // Later on somewhere deep inside `run_my_app` when an updated revocation list is received:
///     try {
///         core.pro.update_revocations(...);
///     } catch (const std::exception& e) {
///         log::warning(cat, "Failed to update Pro revocation list: {}", e.what());
///     }
///
///     // When checking to verify a Pro account proof:
///     if (!core.pro.proof_is_revoked(...)) {
///         // bro is Pro!
///     }
///
/// ```

namespace oxen::quic {
struct Ticker;
}  // namespace oxen::quic

namespace session::pro_backend {
struct ProRevocationItem;
};  // namespace session::pro_backend

namespace session::network {
class Network;
}

namespace session {
class TestHelper;
}

// Forward declared rather than including SessionProtos.pb.h so that consumers of this header do not
// inherit a protobuf include dependency; only the send_dm() Content overload needs the full type.
namespace SessionProtos {
class Content;
}

namespace session::core {

using namespace std::literals;
namespace quic = oxen::quic;

namespace detail {
    class CoreComponent;

    /// Extracts an option of type T from a pack; returns the first match wrapped in optional, or
    /// nullopt if not present.  Mirrors the same helper in session::sqlite::Database.
    ///
    /// Public so that a layer wrapping the Core constructor (such as session::client::Client, which
    /// must find and chain the caller's `callbacks` before forwarding the rest) can pick options
    /// out of the pack without duplicating this.
    template <typename T, typename... Opts>
    constexpr auto maybe_instance(Opts&&... opts) {
        using Ret = std::optional<T>;
        if constexpr (sizeof...(Opts) == 0)
            return Ret{std::nullopt};
        else {
            auto finder = []<typename Opt, typename... More>(
                                  auto&& self, Opt&& o, More&&... more) -> Ret {
                if constexpr (std::same_as<std::remove_cvref_t<Opt>, T>)
                    return std::make_optional<T>(std::forward<Opt>(o));
                else if constexpr (sizeof...(More) > 0)
                    return self(self, std::forward<More>(more)...);
                else
                    return std::nullopt;
            };
            return finder(finder, std::forward<Opts>(opts)...);
        }
    }

    /// Collects every option of type T from a pack, in the order given.  Unlike maybe_instance this
    /// is for options that may meaningfully be repeated.
    template <typename T, typename... Opts>
    std::vector<T> all_instances(Opts&&... opts) {
        std::vector<T> found;
        (
                [&]<typename Opt>(Opt&& o) {
                    if constexpr (std::same_as<std::remove_cvref_t<Opt>, T>)
                        found.push_back(std::forward<Opt>(o));
                }(std::forward<Opts>(opts)),
                ...);
        return found;
    }
}  // namespace detail

/// Wraps a predefined 32-byte account seed to pass to the Core constructor, overriding any seed
/// already stored in the database.  Used when restoring an existing account from a seed.
struct predefined_seed {
    cleared_b32 bytes;
    explicit predefined_seed(std::span<const std::byte, 32> s) {
        std::ranges::copy(s, bytes.begin());
    }
    explicit predefined_seed(std::span<const unsigned char, 32> s) {
        std::ranges::copy(std::as_bytes(s), bytes.begin());
    }

    /// Constructs a predefined_seed from a mnemonic word list.
    ///
    /// Accepts 12 or 13 words (128-bit seed; the upper 16 bytes are set to zero), or 24 or 25
    /// words (256-bit seed).  13- and 25-word inputs include a checksum word which is validated.
    ///
    /// @throws std::invalid_argument if the word count is not 12, 13, 24, or 25.
    /// @throws mnemonics::unknown_word_error if a word is not found in the language dictionary.
    /// @throws mnemonics::checksum_error if the checksum word (if present) does not match.
    explicit predefined_seed(
            std::span<const std::string_view> words, const mnemonics::Mnemonics& lang);
    explicit predefined_seed(
            std::span<const std::string_view> words, std::string_view lang_name = "English");
};

/// Opens the account without inventing an identity for it.
///
/// Ordinarily a Core with no stored seed and no predefined_seed generates one, which gives an
/// application no way to *ask* whether a database already holds an account: finding out has already
/// answered it.  With this option that database opens with no account, globals.have_account()
/// reports which case it is, and the application resolves it with globals.create_account() or
/// globals.restore_account().
///
/// Between construction and that call the account has no identity, so anything needing one --
/// session_id(), account_seed(), send_dm(), attaching a network -- throws no_account.  Nothing
/// else in Core needs it that early: components initialise fine without it and polling does not
/// start until a network is attached.
struct defer_account {};

/// Additional database migrations to apply during Core construction, on behalf of a layer built on
/// top of Core (such as a Client holding conversations and message history in the same database).
///
/// May be passed more than once, and each set is applied in the order given, after all of Core's
/// own migrations.  Two consequences of that ordering:
///
/// - Extension migrations may depend on Core's tables; Core's may never depend on an extension's.
/// - A Core migration added later runs *after* an extension's on an already-populated database,
///   but before it on a fresh one.  Only the rule above makes both orders safe.
///
/// Extension migrations run from apply_migrations(), i.e. before any Core component's init() has
/// run: the Core they are handed is constructed but not yet initialised, so (for example) the
/// account seed is not loaded and globals::session_id() is unavailable.  The layer that supplied
/// them does not exist at all yet.
struct schema_extension {
    /// Distinguishes these migrations from Core's and from other extensions' in the
    /// migrations_applied table.  Must be non-empty, must not contain ':', and must never change
    /// once released: it forms part of the recorded identity of an applied migration, so changing
    /// it re-runs the entire set.
    std::string_view owner;

    /// The migrations themselves.  Not copied, but only referenced while the Core constructor
    /// runs, so this need only outlive construction.
    std::span<const schema::Migration> migrations;

    /// Optional: the schema with every one of the above applied, as generated into the registry's
    /// FULL_SCHEMA.  When a database has none of this owner's migrations applied it is built from
    /// this in one step and they are all recorded without running, which is both faster than
    /// replaying the chain and the reason a readable current schema can exist at all.
    ///
    /// Anything a migration does beyond DDL — seeding rows, say — must therefore also be in here,
    /// since on a fresh database that migration never runs.
    std::string_view full_schema;
};

/// Concept satisfied by any type usable as a Core constructor option: a sqlite database option
/// (encryption, behaviour), or a Core-specific option tag (predefined_seed, callbacks,
/// schema_extension).  All options can be passed in any order after the db_path positional
/// argument.
template <typename T>
concept CoreOption = sqlite::DatabaseOption<std::remove_cvref_t<T>> ||
                     std::same_as<std::remove_cvref_t<T>, predefined_seed> ||
                     std::same_as<std::remove_cvref_t<T>, callbacks> ||
                     std::same_as<std::remove_cvref_t<T>, defer_account> ||
                     std::same_as<std::remove_cvref_t<T>, schema_extension>;

class Core {
    friend class session::TestHelper;  // for unit tests

    // Declared first, so it is constructed first and destroyed last: it must outlive every
    // component that uses it, and the poll ticker below.
    quic::Loop _loop;

    // Singly owned: a Network must not be kept alive by anything else, least of all by a callback
    // it hands out, so that its destructor never runs on its own loop thread.
    //
    // Custom deleter allows network::Network to remain an incomplete type in this header.
    struct NetworkDeleter {
        void operator()(network::Network*) const;
    };
    std::unique_ptr<network::Network, NetworkDeleter> _network;

    sqlite::Database db;
    friend class detail::CoreComponent;

    core::callbacks callbacks;

    // Called during the constructor: the database is opened and all members are constructed, but
    // `init()` hasn't called called yet.
    void apply_migrations();

    // Extra migration sets supplied via schema_extension options, in the order given.  Cleared once
    // applied, so that the spans they hold are not retained beyond construction.
    std::vector<schema_extension> _schema_extensions;

    std::list<detail::CoreComponent*> _comp_init;
    void register_comp_init(detail::CoreComponent* c);

    // Performs the non-templated part of Core construction: this executes any needed database
    // migrations, and then calls init() on each sub-component.
    void init();

    // Polling-related members and methods
    std::chrono::milliseconds _poll_interval = 20s;
    std::shared_ptr<oxen::quic::Ticker> _poll_ticker;
    void _update_polling();
    void _poll();

    // Sends one round of retrieves to `node` for `namespaces`.  A retrieve is capped by the storage
    // server, so one round may not exhaust a namespace; `round` counts continuations and bounds
    // them.  Every round goes to the same node: the retrieve cursor is stored per (namespace, node),
    // so continuing against a different swarm member would resume from that member's position.
    void _send_poll(
            network::Network* net,
            network::service_node node,
            std::vector<config::Namespace> namespaces,
            int round);
    void _handle_poll_response(
            network::service_node node,
            std::vector<config::Namespace> namespaces,
            std::string body,
            int round);

    // Decrypts and dispatches one-to-one messages from Namespace::Default.
    void _handle_direct_messages(std::span<const SwarmMessage> messages);

    // Handles a PFS fetch response
    void _handle_pfs_response(std::span<const std::byte, 33> sid, std::string body);

    // Stores PFS keys for a remote session_id in the pfs_key_cache.  Returns true if the keys
    // differ from the previously cached entry (or no entry existed).
    bool _store_pfs_keys(
            std::span<const std::byte, 33> session_id,
            std::span<const std::byte, 32> x25519_pub,
            std::span<const std::byte, 1184> mlkem768_pub);

    // Stores a NAK (no keys found) for a remote session_id in the pfs_key_cache.
    void _store_pfs_nak(std::span<const std::byte, 33> session_id);

    // Monotonic message ID counter for send_dm().
    int64_t _next_message_id{1};

    // Queued sends waiting for a PFS key fetch to complete.
    struct PendingSend {
        int64_t id;
        std::array<std::byte, 33> recipient;
        std::vector<std::byte> content;
        sys_ms sent_timestamp;
        std::optional<cleared_b64> pro_privkey;
        std::chrono::milliseconds ttl;
        bool force_v2;
    };
    std::vector<PendingSend> _pending_sends;

    // Drains pending sends whose PFS key fetch has completed.
    void _flush_pending_sends(std::span<const std::byte, 33> session_id);

    // Called on every terminal outcome of a PFS key fetch: notifies the application and then
    // releases any sends that were queued waiting on those keys.  All fetch completion paths must
    // go through here rather than firing the callback directly, or queued sends for that recipient
    // are never dispatched.
    void _pfs_fetch_done(std::span<const std::byte, 33> session_id, PfsKeyFetch result);

    // Encrypts, envelopes, and dispatches a single DM.  Called from send_dm() and from
    // _flush_pending_sends() when a queued send is ready.  Fires the message_send_status
    // callback on completion.
    void _do_send_dm(
            int64_t message_id,
            std::span<const std::byte, 33> recipient,
            std::span<const std::byte> content,
            sys_ms sent_timestamp,
            const ed25519::OptionalPrivKeySpan& pro_privkey,
            std::chrono::milliseconds ttl,
            bool force_v2);

  public:
    /// Deletes messages from *our own* swarm by hash, and forgets the cursors that named them.
    ///
    /// Only our own: a delete is signed by the account that owns the swarm, so this can reach the
    /// copy of a message we sent that lives in our swarm for our other devices, and the copy of a
    /// message we received that was delivered to us.  It cannot touch the copy sitting in someone
    /// else's swarm — asking them to remove that is what an unsend request is for.
    ///
    /// Best effort, and `on_complete` says only whether the request was accepted.  A storage server
    /// that no longer holds a hash reports that rather than failing, which is the same outcome we
    /// wanted; treating it as an error would make a second delete look broken.
    ///
    /// The hashes are removed from the cursor history as part of this, which is the whole reason
    /// that history exists: the next retrieve from a node then measures from the newest hash we
    /// still hold rather than from one the node has just been told to forget.
    ///
    /// Does nothing and reports success for an empty list.  Throws std::logic_error if no network
    /// is attached.
    void delete_from_swarm(
            std::vector<std::string> hashes, std::function<void(bool success)> on_complete);

  private:
    // Dispatches a fully-encoded payload to a swarm for storage via the attached network object,
    // firing on_complete with the outcome and, where the storage server reported one, the hash it
    // assigned the message.  Throws std::logic_error if no network is attached.
    void _send_to_swarm(
            std::span<const std::byte, 33> dest_pubkey,
            config::Namespace ns,
            std::vector<std::byte> payload,
            std::chrono::milliseconds ttl,
            std::function<void(bool success, std::optional<std::string_view> swarm_hash)>
                    on_complete);

    // Constructs a sqlite::Database from the subset of opts that satisfy sqlite::DatabaseOption.
    template <CoreOption... Opts>
    static sqlite::Database _make_db(std::filesystem::path path, Opts&&... opts) {
        return std::apply(
                [&]<sqlite::DatabaseOption... DBOpts>(DBOpts&&... db_opts) {
                    return sqlite::Database{std::move(path), std::forward<DBOpts>(db_opts)...};
                },
                std::tuple_cat([]<typename T>(T&& o) {
                    if constexpr (sqlite::DatabaseOption<std::remove_cvref_t<T>>)
                        return std::tuple<std::remove_cvref_t<T>>{std::forward<T>(o)};
                    else
                        return std::tuple<>{};
                }(std::forward<Opts>(opts))...));
    }

  public:
    // Constructor taking a db path and any mix of Core and database options in any order.
    // - callbacks (optional, defaults to empty): event callbacks for the application
    // - predefined_seed (optional): overrides any seed stored in the database
    // - database options: see sqlite::DatabaseOption (encryption, behaviour flags, etc.)
    template <CoreOption... Opts>
    Core(std::filesystem::path db_path, Opts&&... opts) :
            callbacks{detail::maybe_instance<core::callbacks>(std::forward<Opts>(opts)...)
                              .value_or(core::callbacks{})},
            db{_make_db(std::move(db_path), std::forward<Opts>(opts)...)} {
        _schema_extensions = detail::all_instances<schema_extension>(std::forward<Opts>(opts)...);
        if (auto s = detail::maybe_instance<predefined_seed>(std::forward<Opts>(opts)...))
            globals._predefined_seed = std::move(s->bytes);
        globals._defer_account =
                detail::maybe_instance<defer_account>(std::forward<Opts>(opts)...).has_value();
        init();
    }

    /// Set an optional network interface that can be used to make network requests to swarm
    /// members.  Ownership is taken: nothing else may hold on to the Network.
    void set_network(std::unique_ptr<network::Network> network);

    /// Constructs the network in place and attaches it, forwarding the arguments to its
    /// constructor.  Returns a reference to it, valid until it is replaced or this Core is
    /// destroyed.
    ///
    ///     core.make_network(session::network::config::Config{});
    ///
    /// The usual way to call `set_network`: the object exists only to be owned here, so building it
    /// and handing it over in one step spares the caller a `make_unique` — and spares it the chance
    /// to keep a copy of what it has just given away, which the ownership rule above forbids.
    ///
    /// `N` names a subclass when there is one, which in practice means a test double:
    ///
    ///     auto& net = core.make_network<MockNetwork>();
    ///
    /// This is a template, so `N` has to be complete where it is called — which it is, since the
    /// caller is naming its constructor. `session::network::Network` is only forward declared here.
    template <std::derived_from<network::Network> N = network::Network, typename... Args>
    N& make_network(Args&&... args) {
        auto net = std::make_unique<N>(std::forward<Args>(args)...);
        auto& ref = *net;
        set_network(std::move(net));
        return ref;
    }

    /// How long a cached PFS key is considered fresh (no re-fetch needed).
    static constexpr auto PFS_KEY_FRESH_DURATION = 24h;
    /// How long a cached PFS key is usable as a fallback before it expires entirely.
    static constexpr auto PFS_KEY_EXPIRY_DURATION = 48h;
    /// How long a NAK (successful fetch that returned no keys) suppresses re-fetching.
    static constexpr auto PFS_KEY_NAK_DURATION = 1h;

    /// Checks and/or initiates a background fetch of the X25519 and ML-KEM-768 account public keys
    /// for the given remote session_id (33-byte 0x05-prefixed X25519 pubkey), caching the result
    /// in the pfs_key_cache table.
    ///
    /// Returns a PfsKeyStatus value describing the current cache state:
    /// - fresh   -- cached key is less than PFS_KEY_FRESH_DURATION (24h) old; no fetch initiated.
    /// - stale   -- cached key is 24–48h old; a background re-fetch has been initiated and the
    ///              pfs_keys_fetched callback will fire when it completes.
    /// - fetching -- no usable key is cached; a background fetch has been initiated and the
    ///              pfs_keys_fetched callback will fire when it completes.
    /// - nak     -- a recent fetch returned no keys; re-fetching is suppressed for
    ///              PFS_KEY_NAK_DURATION (1h).
    PfsKeyStatus prefetch_pfs_keys(std::span<const std::byte, 33> session_id);

    /// Sets the polling interval used when a network object is attached.  The default is 20s.
    /// Takes effect by replacing the active ticker (if any); safe to call at any time.
    void set_poll_interval(std::chrono::milliseconds interval);

    /// Encrypt and send a direct message to the given recipient.
    ///
    /// Returns a unique message_id that will later be reported via the message_send_status
    /// callback.
    ///
    /// Version selection:
    /// - If fresh/stale PFS+PQ account keys are cached for the recipient, a v2 PFS message is
    ///   sent.
    /// - If no keys are cached (NAK) and force_v2 is false, falls back to a v1 message.
    /// - If no keys are cached and force_v2 is true, sends a v2 non-PFS message.
    /// - If a key fetch is in progress, the send is queued and dispatched when the fetch
    ///   completes, using the above rules.
    ///
    /// Parameters:
    /// - recipient_session_id -- 33-byte 0x05-prefixed session ID of the recipient
    /// - content -- serialised SessionProtos::Content protobuf (the inner plaintext)
    /// - sent_timestamp -- the message timestamp as a time point (should match sigTimestamp
    ///   inside the Content protobuf); used in the v1 Envelope when falling back to v1
    /// - pro_privkey -- optional Session Pro rotating Ed25519 private key (32-byte seed or
    ///   64-byte libsodium key).  When provided, a Pro signature is attached to the message.
    /// - ttl -- time-to-live for the stored message; defaults to 14 days
    /// - force_v2 -- when true, never fall back to v1; use v2 non-PFS if no PFS keys are
    ///   available
    int64_t send_dm(
            std::span<const std::byte, 33> recipient_session_id,
            std::span<const std::byte> content,
            sys_ms sent_timestamp,
            const ed25519::OptionalPrivKeySpan& pro_privkey = std::nullopt,
            std::chrono::milliseconds ttl = 14 * 24h,
            bool force_v2 = false);

    /// Overload of send_dm() taking an unserialised Content protobuf, which is serialised and
    /// forwarded to the span version above.  Callers using this overload need to include
    /// <SessionProtos.pb.h> and link libsession::protos themselves.
    ///
    /// This overload additionally maintains the invariant that the span version only documents:
    /// the Content's `sigTimestamp` and `sent_timestamp` must agree, because the v1 fallback path
    /// puts `sent_timestamp` in the (unauthenticated) Envelope while the signature covers the
    /// `sigTimestamp` inside the Content.  If `sigTimestamp` is unset it is filled in from
    /// `sent_timestamp`; if it is set to a different value that is a caller bug and throws.
    ///
    /// @throws std::invalid_argument if content.sigTimestamp() is set and disagrees with
    /// sent_timestamp.
    int64_t send_dm(
            std::span<const std::byte, 33> recipient_session_id,
            const SessionProtos::Content& content,
            sys_ms sent_timestamp,
            const ed25519::OptionalPrivKeySpan& pro_privkey = std::nullopt,
            std::chrono::milliseconds ttl = 14 * 24h,
            bool force_v2 = false);

    /// Returns the optional network interface, or nullptr if none is set.  Non-owning: a caller
    /// must not keep this beyond the point where the network could be replaced or dropped.
    network::Network* network() const { return _network.get(); }

    /// The event loop this account's work runs on.
    ///
    /// Everything Core does off the caller's thread — polling, send completion, and therefore every
    /// callback it fires — happens here.  A layer above Core dispatches its own database work onto
    /// it with `loop().call(...)` so that all access is serialised onto one thread, rather than
    /// relying on the database being safe to touch from several.
    ///
    /// `call()` runs the job inline when the caller is already on this thread, so a single-threaded
    /// application pays nothing for the indirection.
    quic::Loop& loop();

    /// The account database, for a layer built on top of Core that keeps its own tables alongside
    /// Core's — the same layer that supplies a schema_extension to create them.
    ///
    /// A layer above Core may create and use its own tables here.  It must not write Core's:
    /// `namespace_sync`, `devices`, `globals` and the rest are Core's to maintain, and nothing
    /// defends them against a well-meaning update from outside.
    ///
    /// `database().conn()` hands back the connection the calling thread already holds, if any, so a
    /// write made from inside a Core callback joins the transaction Core has open rather than
    /// deadlocking against it.  That sharing is the whole reason this is exposed: opening a second
    /// Database on the same file would not have that property.
    ///
    /// This grants no access to Core's own tables' invariants.  Reading Core's tables is fine;
    /// writing them behind Core's back is not, and Core does not defend against it.
    sqlite::Database& database() { return db; }

    // Global value storage.  This are used by some components, but can also be used by the
    // application to persist settings.
    Globals globals{*this};

    // Session Pro-related capabilities
    Pro pro{*this};

    // Device groups for handling shared encryption key among account devices
    Devices devices{*this};

    // The account's synced configuration, shared with its other devices
    Configs configs{*this};

    // Passes a batch of messages retrieved from the swarm to the appropriate handler based on the
    // namespace they were retrieved from.  `is_final` should be true if this batch represents the
    // complete current contents of the namespace (i.e. there are no more messages pending
    // retrieval), and false if more messages may follow.  Note that if there turn out to be no more
    // messages after a non-final call, the caller should still call this with an empty span and
    // is_final=true to flush any actions that are deferred until the end of a fetch.
    void receive_messages(
            std::span<const SwarmMessage> messages, config::Namespace ns, bool is_final);
};

}  // namespace session::core
