/*
**  API to history routines
**
**  Copyright (c) 2001, Thus plc
**
**  Various bug fixes, code and documentation improvements since then
**  in 2002-2004, 2006, 2009, 2014, 2017, 2021, 2022, 2024, 2026.
**
**  Redistribution and use of the source code in source and binary
**  forms, with or without modification, are permitted provided that
**  the following 3 conditions are met:
**
**  1. Redistributions of the source code must retain the above
**  copyright notice, this list of conditions and the disclaimer
**  set out below.
**
**  2. Redistributions of the source code in binary form must
**  reproduce the above copyright notice, this list of conditions
**  and the disclaimer set out below in the documentation and/or
**  other materials provided with the distribution.
**
**  3. Neither the name of the Thus plc nor the names of its
**  contributors may be used to endorse or promote products
**  derived from this software without specific prior written
**  permission from Thus plc.
**
**  Disclaimer:
**
**  "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
**  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
**  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
**  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE DIRECTORS
**  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
**  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
**  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
**  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
**  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
**  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
**  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
*/

#include "portable/system.h"

#include <errno.h>
#include <syslog.h>

#ifdef HAVE_SYS_TIME_H
#    include <sys/time.h>
#endif
#include <time.h>

#include "inn/history.h"
#include "inn/libinn.h"
#include "inn/messages.h"
#include "inn/storage.h"
#include "inn/timer.h"

#include "hisinterface.h"
#include "hismethods.h"

struct hiscache {
    HASH Hash;  /* Hash value of the message-id using Hash() */
    bool Found; /* Whether this entry is in the dbz file yet */
};

struct history {
    struct hismethod *methods;
    void *sub;
    struct hiscache *cache;
    size_t cachesize;
    const char *error;
    struct histstats stats;
};

enum HISRESULT {
    HIScachehit,
    HIScachemiss,
    HIScachedne
};

static const struct histstats nullhist = {0, 0, 0, 0};

/*
** Put an entry into the history cache
*/
static void
his_cacheadd(struct history *h, HASH MessageID, bool Found)
{
    unsigned int i, loc;

    his_logger("HIScacheadd begin", S_HIScacheadd);
    if (h->cache != NULL) {
        memcpy(&loc, ((char *) &MessageID) + (sizeof(HASH) - sizeof(loc)),
               sizeof(loc));
        i = loc % h->cachesize;
        memcpy((char *) &h->cache[i].Hash, (char *) &MessageID, sizeof(HASH));
        h->cache[i].Found = Found;
    }
    his_logger("HIScacheadd end", S_HIScacheadd);
}

/*
** Lookup an entry in the history cache
*/
static enum HISRESULT
his_cachelookup(struct history *h, HASH MessageID)
{
    unsigned int i, loc;

    if (h->cache == NULL)
        return HIScachedne;
    his_logger("HIScachelookup begin", S_HIScachelookup);
    memcpy(&loc, ((char *) &MessageID) + (sizeof(HASH) - sizeof(loc)),
           sizeof(loc));
    i = loc % h->cachesize;
    if (memcmp((char *) &h->cache[i].Hash, (char *) &MessageID, sizeof(HASH))
        == 0) {
        his_logger("HIScachelookup end", S_HIScachelookup);
        if (h->cache[i].Found) {
            return HIScachehit;
        } else {
            return HIScachemiss;
        }
    } else {
        his_logger("HIScachelookup end", S_HIScachelookup);
        return HIScachedne;
    }
}

/*
**  set error status to that indicated by s; doesn't copy the string,
**  assumes the caller did that for us
*/
void
his_seterror(struct history *h, const char *s)
{
    if (h != NULL) {
        if (h->error)
            free((void *) h->error);
        h->error = s;
    }
    if (s != NULL)
        warn("%s", s);
}

struct history *
HISopen(const char *path, const char *method, int flags)
{
    struct history *h;
    int i;

    for (i = 0; i < NUM_HIS_METHODS; ++i) {
        if (strcmp(method, his_methods[i].name) == 0)
            break;
    }
    if (i == NUM_HIS_METHODS) {
        warn("`%s' isn't a valid history method", method);
        return NULL;
    }

    /* allocate up our structure and start subordinate history
     * manager */
    h = xmalloc(sizeof *h);
    h->methods = &his_methods[i];
    h->cache = NULL;
    h->error = NULL;
    h->cachesize = 0;
    h->stats = nullhist;
    h->sub = (*h->methods->open)(path, flags, h);
    if (h->sub == NULL) {
        free(h);
        h = NULL;
    }
    return h;
}

static bool
his_checknull(struct history *h)
{
    if (h != NULL)
        return false;
    errno = EBADF;
    return true;
}

bool
HISclose(struct history *h)
{
    bool r;

    if (his_checknull(h))
        return false;
    r = (*h->methods->close)(h->sub);
    if (h->cache) {
        free(h->cache);
        h->cache = NULL;
    }
    if (h->error) {
        free((void *) h->error);
        h->error = NULL;
    }
    free(h);
    return r;
}

bool
HISsync(struct history *h)
{
    bool r = false;

    if (his_checknull(h))
        return false;
    TMRstart(TMR_HISSYNC);
    r = (*h->methods->sync)(h->sub);
    TMRstop(TMR_HISSYNC);
    return r;
}

bool
HISlookup(struct history *h, const char *key, time_t *arrived, time_t *posted,
          time_t *expires, TOKEN *token)
{
    bool r;

    if (his_checknull(h))
        return false;
    TMRstart(TMR_HISGREP);
    r = (*h->methods->lookup)(h->sub, key, arrived, posted, expires, token);
    TMRstop(TMR_HISGREP);
    return r;
}

bool
HIScheck(struct history *h, const char *key)
{
    bool r = false;
    HASH hash;

    if (his_checknull(h))
        return false;
    TMRstart(TMR_HISHAVE);
    hash = HashMessageID(key);
    switch (his_cachelookup(h, hash)) {
    case HIScachehit:
        h->stats.hitpos++;
        r = true;
        break;

    case HIScachemiss:
        h->stats.hitneg++;
        r = false;
        break;

    case HIScachedne:
        r = (*h->methods->check)(h->sub, key);
        his_cacheadd(h, hash, r);
        if (r)
            h->stats.misses++;
        else
            h->stats.dne++;
        break;
    }
    TMRstop(TMR_HISHAVE);
    return r;
}

bool
HISwrite(struct history *h, const char *key, time_t arrived, time_t posted,
         time_t expires, const TOKEN *token)
{
    bool r;

    if (his_checknull(h))
        return false;
    TMRstart(TMR_HISWRITE);
    r = (*h->methods->write)(h->sub, key, arrived, posted, expires, token);
    if (r == true) {
        HASH hash;

        /* if we successfully wrote it, add it to the cache */
        hash = HashMessageID(key);
        his_cacheadd(h, hash, true);
    }
    TMRstop(TMR_HISWRITE);

    return r;
}

bool
HISremember(struct history *h, const char *key, time_t arrived, time_t posted)
{
    bool r;

    if (his_checknull(h))
        return false;
    TMRstart(TMR_HISWRITE);
    r = (*h->methods->remember)(h->sub, key, arrived, posted);
    if (r == true) {
        HASH hash;

        /* if we successfully wrote it, add it to the cache */
        hash = HashMessageID(key);
        his_cacheadd(h, hash, true);
    }
    TMRstop(TMR_HISWRITE);

    return r;
}

bool
HISreplace(struct history *h, const char *key, time_t arrived, time_t posted,
           time_t expires, const TOKEN *token)
{
    bool r;

    if (his_checknull(h))
        return false;
    r = (*h->methods->replace)(h->sub, key, arrived, posted, expires, token);
    if (r == true) {
        HASH hash;

        /* if we successfully wrote it, add it to the cache */
        hash = HashMessageID(key);
        his_cacheadd(h, hash, true);
    }
    return r;
}

bool
HISwalk(struct history *h, const char *reason, void *cookie,
        bool (*callback)(void *, const HASH *, time_t, time_t, time_t,
                         const TOKEN *))
{
    bool r;

    if (his_checknull(h))
        return false;
    r = (*h->methods->walk)(h->sub, reason, cookie, callback);
    return r;
}

bool
HISexpire(struct history *h, const char *path, const char *reason,
          bool writing, void *cookie, time_t threshold,
          bool (*exists)(void *, time_t, time_t, time_t, TOKEN *))
{
    bool r;

    if (his_checknull(h))
        return false;
    r = (*h->methods->expire)(h->sub, path, reason, writing, cookie, threshold,
                              exists);
    return r;
}

void
HISsetcache(struct history *h, size_t size)
{
    if (h == NULL)
        return;
    if (h->cache) {
        free(h->cache);
        h->cache = NULL;
    }
    h->cachesize = size / sizeof(struct hiscache);
    if (h->cachesize != 0)
        h->cache = xcalloc(h->cachesize, sizeof(struct hiscache));
    h->stats = nullhist;
}


/*
**  return current history cache stats and zero the counters
*/
struct histstats
HISstats(struct history *h)
{
    struct histstats r;

    if (h == NULL)
        return nullhist;
    r = h->stats;
    h->stats = nullhist;
    return r;
}


/*
**  return current error status to caller
*/
const char *
HISerror(struct history *h)
{
    if (h == NULL)
        return NULL;
    return h->error;
}


/*
**  control interface to underlying method
*/
bool
HISctl(struct history *h, int selector, void *val)
{
    bool r;

    if (his_checknull(h))
        return false;
    r = (*h->methods->ctl)(h->sub, selector, val);
    return r;
}


/*
**  This code doesn't fit well with the generic history API, it really
**  needs migrating to use the new nested timers
*/

static FILE *HISfdlog = NULL; /* filehandle for history logging purpose */

static struct timeval HISstat_start[S_HIS_MAX];
static struct timeval HISstat_total[S_HIS_MAX];
static unsigned long HISstat_count[S_HIS_MAX];

void
HISlogclose(void)
{
    if (HISfdlog != NULL)
        Fclose(HISfdlog);
    HISfdlog = NULL;
}

void
HISlogto(const char *s)
{
    int i;

    HISlogclose();
    if ((HISfdlog = Fopen(s, "a", INND_HISLOG)) == NULL)
        syswarn("cant open %s", s);
    /* initialize our counters */
    for (i = 0; i < S_HIS_MAX; i++) {
        HISstat_start[i].tv_sec = 0;
        HISstat_start[i].tv_usec = 0;
        HISstat_total[i].tv_sec = 0;
        HISstat_total[i].tv_usec = 0;
        HISstat_count[i] = 0;
    }
}

void
his_logger(const char *s, int code)
{
    struct timeval tv;
    struct tm *tm;
    time_t t;

    if (HISfdlog == NULL) /* do nothing unless HISlogto() has been called */
        return;

    gettimeofday(&tv, NULL);
    t = tv.tv_sec;
    tm = localtime(&t);
    if (HISstat_start[code].tv_sec != 0) {
        fprintf(HISfdlog, "%d/%d/%d %02d:%02d:%02d.%06d: [%d] %s (%.6f)\n",
                tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
                tm->tm_min, tm->tm_sec, (int) tv.tv_usec, code, s,
                (double) tv.tv_sec + (double) tv.tv_usec / 1000000
                    - (double) HISstat_start[code].tv_sec
                    - (double) HISstat_start[code].tv_usec / 1000000);
        if (tv.tv_usec < HISstat_start[code].tv_usec) {
            HISstat_total[code].tv_sec++;
            HISstat_total[code].tv_usec +=
                tv.tv_usec - HISstat_start[code].tv_usec + 1000000;
        } else
            HISstat_total[code].tv_usec +=
                tv.tv_usec - HISstat_start[code].tv_usec;
        HISstat_total[code].tv_sec += tv.tv_sec - HISstat_start[code].tv_sec;
        HISstat_count[code]++;
        HISstat_start[code].tv_sec = 0;
        HISstat_start[code].tv_usec = 0;
    } else {
        fprintf(HISfdlog, "%d/%d/%d %02d:%02d:%02d.%06d: [%d] %s\n",
                tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour,
                tm->tm_min, tm->tm_sec, (int) tv.tv_usec, code, s);
        HISstat_start[code].tv_sec = tv.tv_sec;
        HISstat_start[code].tv_usec = tv.tv_usec;
    }
}
