/* SPDX-License-Identifier: GPL-3.0-or-later
 * Copyright © 2016-2020 The TokTok team.
 * Copyright © 2015 Tox project.
 */

/**
 * An implementation of massive text only group chats.
 */

#ifndef C_TOXCORE_TOXCORE_GROUP_MODERATION_H
#define C_TOXCORE_TOXCORE_GROUP_MODERATION_H

#include <stdbool.h>
#include <stdint.h>

#include "DHT.h"
#include "attributes.h"
#include "crypto_core.h"
#include "logger.h"
#include "mem.h"

#ifdef __cplusplus
extern "C" {
#endif

#define MOD_MODERATION_HASH_SIZE CRYPTO_SHA256_SIZE
#define MOD_LIST_ENTRY_SIZE SIG_PUBLIC_KEY_SIZE
#define MOD_SANCTION_HASH_SIZE CRYPTO_SHA256_SIZE

#define TIME_STAMP_SIZE sizeof(uint64_t)

/* The packed size of a Mod_Sanction_Creds */
#define MOD_SANCTIONS_CREDS_SIZE (sizeof(uint32_t) + MOD_SANCTION_HASH_SIZE + sizeof(uint16_t) +\
                                       SIG_PUBLIC_KEY_SIZE + SIGNATURE_SIZE)

/* The packed size of a Mod_Sanction */
#define MOD_SANCTION_PACKED_SIZE (SIG_PUBLIC_KEY_SIZE + TIME_STAMP_SIZE + 1 + ENC_PUBLIC_KEY_SIZE + SIGNATURE_SIZE)

/* The max size of a groupchat packet with 100 bytes reserved for header data */
#define MAX_PACKET_SIZE_NO_HEADERS 49900

/* The maximum possible number of moderators that can be sent in a group packet sequence. */
#define MOD_MAX_NUM_MODERATORS_LIMIT (((MAX_PACKET_SIZE_NO_HEADERS) / (MOD_LIST_ENTRY_SIZE)))

/* The maximum number of moderators that we allow in a group: 100 */
#define MOD_MAX_NUM_MODERATORS       ((MOD_MAX_NUM_MODERATORS_LIMIT / 16) + 3)

/* The maximum number of sanctions that be sent in a group packet sequence. */
#define MOD_MAX_NUM_SANCTIONS_LIMIT  (((MAX_PACKET_SIZE_NO_HEADERS - (MOD_SANCTIONS_CREDS_SIZE)) / (MOD_SANCTION_PACKED_SIZE)))

/* The maximum number of sanctions that we allow in a group: 30 */
#define MOD_MAX_NUM_SANCTIONS        (MOD_MAX_NUM_SANCTIONS_LIMIT / 12)

typedef enum Mod_Sanction_Type {
    SA_OBSERVER = 0x00,
    SA_INVALID  = 0x01,
} Mod_Sanction_Type;

typedef struct Mod_Sanction_Creds {
    uint32_t    version;
    uint8_t     hash[MOD_SANCTION_HASH_SIZE];    // hash of all sanctions list signatures + version
    uint16_t    checksum;  // a sum of the hash
    uint8_t     sig_pk[SIG_PUBLIC_KEY_SIZE];    // Last mod to have modified the sanctions list
    uint8_t     sig[SIGNATURE_SIZE];    // signature of hash, signed by sig_pk
} Mod_Sanction_Creds;

/** Holds data pertaining to a peer who has been sanctioned. */
typedef struct Mod_Sanction {
    uint8_t     setter_public_sig_key[SIG_PUBLIC_KEY_SIZE];

    // TODO(Jfreegman): This timestamp can potentially be used to track a user across
    // different group chats if they're a moderator and set many sanctions across the
    // different groups. This should be addressed in the future.
    uint64_t    time_set;

    uint8_t     type;
    uint8_t     target_public_enc_key[ENC_PUBLIC_KEY_SIZE];

    /* Signature of all above packed data signed by the owner of public_sig_key */
    uint8_t     signature[SIGNATURE_SIZE];
} Mod_Sanction;

typedef struct Moderation {
    const       Memory *_Nonnull mem;
    const       Logger *_Nonnull log;

    Mod_Sanction *_Nullable sanctions;
    uint16_t    num_sanctions;

    Mod_Sanction_Creds sanctions_creds;

    uint8_t     *_Nullable *_Nullable mod_list;  // array of public signature keys of all the mods
    uint16_t    num_mods;

    // copies from parent/sibling chat/shared state objects
    uint8_t     founder_public_sig_key[CRYPTO_PUBLIC_KEY_SIZE];
    uint8_t     self_public_sig_key[SIG_PUBLIC_KEY_SIZE];
    uint8_t     self_secret_sig_key[SIG_SECRET_KEY_SIZE];
    uint32_t    shared_state_version;
} Moderation;

/** @brief Returns the size in bytes of the packed moderation list. */
uint16_t mod_list_packed_size(const Moderation *_Nonnull moderation);

/** @brief Unpacks data into the moderator list.
 *
 * @param data should contain num_mods entries of size MOD_LIST_ENTRY_SIZE.
 *
 * Returns length of unpacked data on success.
 * Returns -1 on failure.
 */
int mod_list_unpack(Moderation *_Nonnull moderation, const uint8_t *_Nonnull data, uint16_t length, uint16_t num_mods);

/** @brief Packs moderator list into data.
 * @param data must have room for the number of bytes returned by `mod_list_packed_size`.
 */
void mod_list_pack(const Moderation *_Nonnull moderation, uint8_t *_Nonnull data);

/** @brief Creates a new moderator list hash and puts it in `hash`.
 *
 * @param hash must have room for at least MOD_MODERATION_HASH_SIZE bytes.
 *
 * If num_mods is 0 the hash is zeroed.
 *
 * Returns true on sucess.
 */
bool mod_list_make_hash(const Moderation *_Nonnull moderation, uint8_t *_Nonnull hash);

/** @brief Puts a sha256 hash of `packed_mod_list` of `length` bytes in `hash`.
 *
 * @param hash must have room for at least MOD_MODERATION_HASH_SIZE bytes.
 */
void mod_list_get_data_hash(uint8_t *_Nonnull hash, const uint8_t *_Nonnull packed_mod_list, uint16_t length);

/** @brief Removes moderator at index-th position in the moderator list.
 *
 * Returns true on success.
 */
bool mod_list_remove_index(Moderation *_Nonnull moderation, uint16_t index);

/** @brief Removes public_sig_key from the moderator list.
 *
 * Returns true on success.
 */
bool mod_list_remove_entry(Moderation *_Nonnull moderation, const uint8_t *_Nonnull public_sig_key);

/** @brief Adds a mod to the moderator list.
 *
 * @param mod_data must be MOD_LIST_ENTRY_SIZE bytes.
 *
 * Returns true on success.
 */
bool mod_list_add_entry(Moderation *_Nonnull moderation, const uint8_t *_Nonnull mod_data);

/** @return true if the public signature key belongs to a moderator or the founder */
bool mod_list_verify_sig_pk(const Moderation *_Nonnull moderation, const uint8_t *_Nonnull sig_pk);

/** @brief Frees all memory associated with the moderator list and sets num_mods to 0. */
void mod_list_cleanup(Moderation *_Nullable moderation);
/** @brief Returns the size in bytes of num_sanctions packed sanctions. */
uint16_t sanctions_list_packed_size(uint16_t num_sanctions);

/** @brief Packs sanctions into data. Additionally packs the sanctions credentials into creds.
 *
 * @param data The byte array being packed. Must have room for the number of bytes returned
 *   by `sanctions_list_packed_size`.
 * @param length The size of the byte array.
 * @param sanctions The sanctions list.
 * @param num_sanctions The number of sanctions in the sanctions list. This value must be the same
 *   value used when calling `sanctions_list_packed_size`.
 * @param creds The credentials object to fill.
 *
 * @retval The length of packed data on success.
 * @retval -1 on failure.
 */
int sanctions_list_pack(uint8_t *_Nonnull data, uint16_t length, const Mod_Sanction *_Nullable sanctions, uint16_t num_sanctions,
                        const Mod_Sanction_Creds *_Nullable creds);
/** @brief Unpacks sanctions and new sanctions credentials.
 *
 * @param sanctions The sanctions array the sanctions data is unpacked into.
 * @param creds The creds object the creds data is unpacked into.
 * @param max_sanctions The maximum number of sanctions that the sanctions array can hold.
 * @param data The packed data array.
 * @param length The size of the packed data.
 * @param processed_data_len If non-null, will contain the number of processed bytes on success.
 *
 * @retval The number of unpacked entries on success.
 * @retval -1 on failure.
 */
int sanctions_list_unpack(Mod_Sanction *_Nonnull sanctions, Mod_Sanction_Creds *_Nonnull creds, uint16_t max_sanctions,
                          const uint8_t *_Nonnull data, uint16_t length, uint16_t *_Nullable processed_data_len);
/** @brief Packs sanction list credentials into data.
 *
 * @param data must have room for MOD_SANCTIONS_CREDS_SIZE bytes.
 *
 * Returns length of packed data.
 */
uint16_t sanctions_creds_pack(const Mod_Sanction_Creds *_Nonnull creds, uint8_t *_Nonnull data);

/** @brief Unpacks sanctions credentials into creds from data.
 *
 * @param data must have room for MOD_SANCTIONS_CREDS_SIZE bytes.
 *
 * Returns the length of the data processed.
 */
uint16_t sanctions_creds_unpack(Mod_Sanction_Creds *_Nonnull creds, const uint8_t *_Nonnull data);

/** @brief Updates sanction list credentials.
 *
 * Increment version, replace sig_pk with your own, update hash to reflect new
 * sanction list, and sign new hash signature.
 *
 * Returns true on success.
 */
bool sanctions_list_make_creds(Moderation *_Nonnull moderation);

/** @brief Validates all sanctions list entries as well as the list itself.
 *
 * Returns true if all entries are valid.
 * Returns false if one or more entries are invalid.
 */
bool sanctions_list_check_integrity(const Moderation *_Nonnull moderation, const Mod_Sanction_Creds *_Nonnull creds, const Mod_Sanction *_Nonnull sanctions, uint16_t num_sanctions);

/** @brief Adds an entry to the sanctions list.
 *
 * The entry is first validated and the resulting new sanction list is
 * compared against the new credentials.
 *
 * Entries must be unique.
 *
 * Returns true on success.
 */
bool sanctions_list_add_entry(Moderation *_Nonnull moderation, const Mod_Sanction *_Nonnull sanction, const Mod_Sanction_Creds *_Nullable creds);
/** @brief Creates a new sanction entry for `public_key` where type is one of Mod_Sanction_Type.
 *
 * New entry is signed and placed in the sanctions list.
 *
 * Returns true on success.
 */
bool sanctions_list_make_entry(Moderation *_Nonnull moderation, const uint8_t *_Nonnull public_key, Mod_Sanction *_Nonnull sanction, uint8_t type);

/** @return true if public key is in the observer list. */
bool sanctions_list_is_observer(const Moderation *_Nonnull moderation, const uint8_t *_Nonnull public_key);

/** @return true if sanction already exists in the sanctions list. */
bool sanctions_list_entry_exists(const Moderation *_Nonnull moderation, const Mod_Sanction *_Nonnull sanction);

/** @brief Removes observer entry for public key from sanction list.
 *
 * If creds is NULL we make new credentials (this should only be done by a moderator or founder)
 *
 * Returns false on failure or if entry was not found.
 */
bool sanctions_list_remove_observer(Moderation *_Nonnull moderation, const uint8_t *_Nonnull public_key,
                                    const Mod_Sanction_Creds *_Nullable creds);
/** @brief Replaces all sanctions list signatures made by public_sig_key with the caller's.
 *
 * This is called whenever the founder demotes a moderator.
 *
 * Returns the number of entries re-signed.
 */
uint16_t sanctions_list_replace_sig(Moderation *_Nonnull moderation, const uint8_t *_Nonnull public_sig_key);

void sanctions_list_cleanup(Moderation *_Nonnull moderation);

#ifdef __cplusplus
} /* extern "C" */
#endif

#endif /* C_TOXCORE_TOXCORE_GROUP_MODERATION_H */
