/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <syslog.h>

#include "alloc-util.h"
#include "hashmap.h"
#include "journald-rate-limit.h"
#include "logarithm.h"
#include "time-util.h"

#define POOLS_MAX 5
#define GROUPS_MAX 2047

static const int priority_map[] = {
        [LOG_EMERG]   = 0,
        [LOG_ALERT]   = 0,
        [LOG_CRIT]    = 0,
        [LOG_ERR]     = 1,
        [LOG_WARNING] = 2,
        [LOG_NOTICE]  = 3,
        [LOG_INFO]    = 3,
        [LOG_DEBUG]   = 4,
};

typedef struct JournalRateLimitPool {
        usec_t begin;
        unsigned num;
        unsigned suppressed;
} JournalRateLimitPool;

typedef struct JournalRateLimitGroup {
        OrderedHashmap *groups_by_id;

        char *id;

        /* Interval is stored to keep track of when the group expires */
        usec_t interval;

        JournalRateLimitPool pools[POOLS_MAX];
} JournalRateLimitGroup;

static JournalRateLimitGroup* journal_ratelimit_group_free(JournalRateLimitGroup *g) {
        if (!g)
                return NULL;

        if (g->groups_by_id && g->id)
                /* The group is already removed from the hashmap when this is called from the
                 * destructor of the hashmap. Hence, do not check the return value here. */
                ordered_hashmap_remove_value(g->groups_by_id, g->id, g);

        free(g->id);
        return mfree(g);
}

DEFINE_TRIVIAL_CLEANUP_FUNC(JournalRateLimitGroup*, journal_ratelimit_group_free);

DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
        journal_ratelimit_group_hash_ops,
        char,
        string_hash_func,
        string_compare_func,
        JournalRateLimitGroup,
        journal_ratelimit_group_free);

static bool journal_ratelimit_group_expired(JournalRateLimitGroup *g, usec_t ts) {
        assert(g);

        FOREACH_ELEMENT(p, g->pools)
                if (usec_add(p->begin, g->interval) >= ts)
                        return false;

        return true;
}

static void journal_ratelimit_vacuum(OrderedHashmap *groups_by_id, usec_t ts) {

        /* Makes room for at least one new item, but drop all expired items too. */

        while (ordered_hashmap_size(groups_by_id) >= GROUPS_MAX)
                journal_ratelimit_group_free(ordered_hashmap_first(groups_by_id));

        JournalRateLimitGroup *g;
        while ((g = ordered_hashmap_first(groups_by_id)) && journal_ratelimit_group_expired(g, ts))
                journal_ratelimit_group_free(g);
}

static int journal_ratelimit_group_new(
                OrderedHashmap **groups_by_id,
                const char *id,
                usec_t interval,
                usec_t ts,
                JournalRateLimitGroup **ret) {

        _cleanup_(journal_ratelimit_group_freep) JournalRateLimitGroup *g = NULL;
        int r;

        assert(groups_by_id);
        assert(id);
        assert(ret);

        g = new(JournalRateLimitGroup, 1);
        if (!g)
                return -ENOMEM;

        *g = (JournalRateLimitGroup) {
                .id = strdup(id),
                .interval = interval,
        };
        if (!g->id)
                return -ENOMEM;

        journal_ratelimit_vacuum(*groups_by_id, ts);

        r = ordered_hashmap_ensure_put(groups_by_id, &journal_ratelimit_group_hash_ops, g->id, g);
        if (r < 0)
                return r;
        assert(r > 0);

        g->groups_by_id = *groups_by_id;

        *ret = TAKE_PTR(g);
        return 0;
}

static int journal_ratelimit_group_acquire(
                OrderedHashmap **groups_by_id,
                const char *id,
                usec_t interval,
                usec_t ts,
                JournalRateLimitGroup **ret) {

        JournalRateLimitGroup *g;

        assert(groups_by_id);
        assert(id);
        assert(ret);

        g = ordered_hashmap_get(*groups_by_id, id);
        if (!g)
                return journal_ratelimit_group_new(groups_by_id, id, interval, ts, ret);

        g->interval = interval;

        *ret = g;
        return 0;
}

static unsigned burst_modulate(unsigned burst, uint64_t available) {
        unsigned k;

        /* Modulates the burst rate a bit with the amount of available
         * disk space */

        k = log2u64(available);

        /* 1MB */
        if (k <= 20)
                return burst;

        burst = (burst * (k-16)) / 4;

        /*
         * Example:
         *
         *      <= 1MB = rate * 1
         *        16MB = rate * 2
         *       256MB = rate * 3
         *         4GB = rate * 4
         *        64GB = rate * 5
         *         1TB = rate * 6
         */

        return burst;
}

int journal_ratelimit_test(
                OrderedHashmap **groups_by_id,
                const char *id,
                usec_t rl_interval,
                unsigned rl_burst,
                int priority,
                uint64_t available) {

        JournalRateLimitGroup *g;
        JournalRateLimitPool *p;
        unsigned burst;
        usec_t ts;
        int r;

        assert(groups_by_id);
        assert(id);

        /* Returns:
         *
         * 0     → the log message shall be suppressed,
         * 1 + n → the log message shall be permitted, and n messages were dropped from the peer before
         * < 0   → error
         */

        ts = now(CLOCK_MONOTONIC);

        r = journal_ratelimit_group_acquire(groups_by_id, id, rl_interval, ts, &g);
        if (r < 0)
                return r;

        if (rl_interval == 0 || rl_burst == 0)
                return 1;

        burst = burst_modulate(rl_burst, available);

        p = &g->pools[priority_map[priority]];

        if (p->begin <= 0) {
                p->suppressed = 0;
                p->num = 1;
                p->begin = ts;
                return 1;
        }

        if (usec_add(p->begin, rl_interval) < ts) {
                unsigned s;

                s = p->suppressed;
                p->suppressed = 0;
                p->num = 1;
                p->begin = ts;

                return 1 + s;
        }

        if (p->num < burst) {
                p->num++;
                return 1;
        }

        p->suppressed++;
        return 0;
}
