#ifndef USER_DB_HPP_
#define USER_DB_HPP_

#include <AtomicDataStore/AtomicDataStore.hpp>
#include <string>
#include <shared_mutex>
#include <unordered_map>
#include <Utilities/Flags.hpp>
#include <Serialize/Serialize.hpp>
#include <UserPermission/UserPermission.hpp>
#include <PasswordHash/PasswordHash.hpp>

/**
 * @brief Class for handling the user database on a server.
 */
class UserDB
{
public:
    typedef Flags<UserPermission, uint64_t> PermissionSet;

    /**
     * @brief Represents the possible states a user can be in.
     */
    enum class UserState : uint8_t
    {
        /**
         * @brief No session ID is stored yet, can log in using password.
         * 
         * The `sid` and `expiryTime` fields are ignored.
         */
        NoSessionID = 0,

        /**
         * @brief A session ID is known (but may be expired), can log in using password too.
         * 
         * The `sid` field is set to the current session ID. `expiryTime` is the UNIX time for
         * the expiry of the session. If we are beyond the expiry time, disregard the `sid`, and
         * only allow login in via passwords. Hence, with an expired session, it is as if we were
         * in the `NoSessionID` state.
         */
        KnownSessionID = 1,

        /**
         * @brief User was banned, ban may be expired.
         * 
         * The `sid` field is ignored. If `expiryTime` is 0, the user has a permanent ban and
         * cannot log in using any method. If `expiryTime` is nonzero, then it is the UNIX time
         * at which the ban expires. If we are past this time, the user can log is using a
         * password; as if we were in the `NoSessionID` state.
         */
        Banned = 2,

        /**
         * @brief The user is in a pending state.
         * 
         * Login is not possible, an admin must approve the user before they can log in. However, the
         * intended password hash is already stored.
         */
        Pending = 3,
    };

    /**
     * @brief Represents a session ID.
     */
    struct SessionID
    {
        uint8_t sid[32];

        void generate();

        bool operator==(SessionID other);
    };

    /**
     * @brief Represents an individual user.
     */
    struct User
    {
        /**
         * @brief The set of granted permissions.
         */
        PermissionSet perms;

        /**
         * @brief UNIX time for the expiry of the current state.
         * 
         * See state description for specific meanings.
         */
        uint64_t expiryTime;

        /**
         * @brief This user's password hash.
         */
        PasswordHash passwordHash;

        /**
         * @brief Session ID if applicable.
         */
        SessionID sid;

        /**
         * @brief The current state.
         */
        UserState state;
    };

private:
    typedef std::unordered_map<std::string, User> UserCache;

    std::string dbpath;
    std::unique_ptr<AtomicDataStore> store;
    std::shared_mutex mutex;
    UserCache users;

    UserCache::iterator tryGetUser(const std::string &username);
    void flushUser(std::string username);
    bool isUsernameValid(std::string username);
    
public:
    /**
     * @brief The result of user creation.
     */
    struct CreateUserResult
    {
        /**
         * @brief Has the account been created?
         */
        bool created;

        /**
         * @brief Informative message about what happened.
         */
        std::string message;
    };

    /**
     * @brief Construct a user database.
     * 
     * @param dbpath The database file.
     */
    UserDB(std::string dbpath);

    /**
     * @brief Open the user database, or crash if it does not exist.
     */
    void open();

    /**
     * @brief Create and open a new user database, or crash if that fails.
     */
    void create();

    /**
     * @brief Create a user with the specified name and password.
     * 
     * @param username The user name.
     * @param password The password.
     * @param createAsAdmin If true, grant all permissions to the new user.
     * @return A `CreateUserResult` filled in according to the results of this operation.
     */
    CreateUserResult createUser(std::string username, std::string password, bool createAsAdmin = false);

    /**
     * @brief Get information about a user.
     * 
     * @param username The name of the user.
     * @param outUser If the user is found, data is output here.
     * @return true If the user was found and `outUser` is filled.
     * @return false If the user was not found, `outUser` is unchanged.
     */
    bool getUserInfo(std::string username, User &outUser);

    /**
     * @brief Try to start a session, and get user state.
     * 
     * This function succeeds if the correct username and password were entered. However, you
     * then have to check if the user state indicates a successful session, or if the user is
     * banned.
     * 
     * @param username The user name.
     * @param password The password.
     * @param outSessionID Outputs the user state if password is correct.
     * @return true If the password is correct (user might still be banned).
     * @return false If the username or password is incorrect.
     */
    bool startSession(std::string username, std::string password, UserDB::User &outUser);

    /**
     * @brief Approve the user with the specified username.
     * 
     * @param userName The user name.
     * @return 
     */
    bool approveUser(std::string username);

    /**
     * @brief Grant the specified permission to the specified user.
     */
    bool grantPermission(std::string username, UserPermission perm);

    /**
     * @brief Revoke the specified permission from the specified user.
     */
    bool revokePermission(std::string username, UserPermission perm);

    /**
     * @brief Ban the specified user; returns an empty string on success, message on error.
     */
    std::string banUser(std::string username);

    /**
     * @brief Un-ban the specified user; returns an empty string on success, message on error.
     */
    std::string unbanUser(std::string username);
};

PLAIN_SERIALIZABLE(UserDB::SessionID);

extern UserDB userDB;

#endif