#pragma once

#include <memory>
#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/devices.hpp"
#include "core/globals.hpp"
#include "core/pro.hpp"
#include "session/network/key_types.hpp"

/// The "Core" class is intended to be used by a Session account instance to hold account state.  It
/// manages an encrypted sqlite database for storing account keys, messages, and other account
/// state.
///
/// This class is currently in an early state with only partial database, but this class is intended
/// to eventually grow to the point where it contains the entire data model of a Session account
/// (i.e.  conversations, contacts, messages, etc.) and manages that data (e.g. by fetching data
/// from swarms).
///
/// Currently Core does not yet have its own network stack and so the integrating application must
/// update the Core instance when it receives the appropriate data (such as pro revocations) from
/// the 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 {
class Loop;
struct Ticker;
}  // namespace oxen::quic

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

namespace session::network {
class Network;
}

namespace session {
class TestHelper;
}

namespace session::core {

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

namespace detail {
    class CoreComponent;
}

/// 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");
};

/// 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).  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>;

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

    // Constructed first (in init()), destroyed last: must outlive all components that use it.
    // Custom deleter allows quic::Loop to remain an incomplete type in this header.
    struct LoopDeleter {
        void operator()(quic::Loop*) const;
    };
    std::unique_ptr<quic::Loop, LoopDeleter> _loop;

    std::shared_ptr<network::Network> _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();

    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();
    void _handle_poll_response(
            const network::ed25519_pubkey& sn_pubkey,
            std::span<const config::Namespace> namespaces,
            std::string body);

    // 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);

    // 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);

    // Dispatches a fully-encoded payload to a swarm for storage.  Tries the send_to_swarm
    // callback first; falls back to the attached network object; fires on_complete with the
    // outcome.  Throws std::logic_error if neither delivery path is available.
    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)> on_complete);

    // 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.
    template <typename T, typename... Opts>
    static 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)...);
        }
    }

    // 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{_maybe_instance<core::callbacks>(std::forward<Opts>(opts)...)
                              .value_or(core::callbacks{})},
            db{_make_db(std::move(db_path), std::forward<Opts>(opts)...)} {
        if (auto s = _maybe_instance<predefined_seed>(std::forward<Opts>(opts)...))
            globals._predefined_seed = std::move(s->bytes);
        init();
    }

    /// Set an optional network interface that can be used to make network requests to swarm members
    void set_network(std::shared_ptr<network::Network> network);

    /// 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);

    /// Returns the optional network interface, if set.
    const std::shared_ptr<network::Network>& network() const { return _network; }

    // 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};

    // 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
