/*
 * $Id$
 *
 * DEBUG: section 00    Client Database
 * AUTHOR: Duane Wessels
 *
 * SQUID Web Proxy Cache          http://www.squid-cache.org/
 * ----------------------------------------------------------
 *
 *  Squid is the result of efforts by numerous individuals from
 *  the Internet community; see the CONTRIBUTORS file for full
 *  details.   Many organizations have provided support for Squid's
 *  development; see the SPONSORS file for full details.  Squid is
 *  Copyrighted (C) 2001 by the Regents of the University of
 *  California; see the COPYRIGHT file for full details.  Squid
 *  incorporates software developed and/or copyrighted by other
 *  sources; see the CREDITS file for full details.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
 *
 */

#include "squid.h"
#include "event.h"
#include "CacheManager.h"
#include "ClientInfo.h"
#include "ip/Address.h"
#include "SquidMath.h"
#include "SquidTime.h"
#include "Store.h"


static hash_table *client_table = NULL;

static ClientInfo *clientdbAdd(const Ip::Address &addr);
static FREE clientdbFreeItem;
static void clientdbStartGC(void);
static void clientdbScheduledGC(void *);

static int max_clients = 32;
static int cleanup_running = 0;
static int cleanup_scheduled = 0;
static int cleanup_removed;

#define CLIENT_DB_HASH_SIZE 467

static ClientInfo *

clientdbAdd(const Ip::Address &addr)
{
    ClientInfo *c;
    char *buf = new char[MAX_IPSTRLEN];
    c = (ClientInfo *)memAllocate(MEM_CLIENT_INFO);
    c->hash.key = addr.NtoA(buf,MAX_IPSTRLEN);
    c->addr = addr;
    hash_join(client_table, &c->hash);
    statCounter.client_http.clients++;

    if ((statCounter.client_http.clients > max_clients) && !cleanup_running && cleanup_scheduled < 2) {
        cleanup_scheduled++;
        eventAdd("client_db garbage collector", clientdbScheduledGC, NULL, 90, 0);
    }

    return c;
}

static void
clientdbRegisterWithCacheManager(void)
{
    CacheManager::GetInstance()->
    registerAction("client_list", "Cache Client List", clientdbDump, 0, 1);
}

void
clientdbInit(void)
{
    clientdbRegisterWithCacheManager();

    if (client_table)
        return;

    client_table = hash_create((HASHCMP *) strcmp, CLIENT_DB_HASH_SIZE, hash_string);

}

void
clientdbUpdate(const Ip::Address &addr, log_type ltype, protocol_t p, size_t size)
{
    char key[MAX_IPSTRLEN];
    ClientInfo *c;

    if (!Config.onoff.client_db)
        return;

    addr.NtoA(key,MAX_IPSTRLEN);

    c = (ClientInfo *) hash_lookup(client_table, key);

    if (c == NULL)
        c = clientdbAdd(addr);

    if (c == NULL)
        debug_trap("clientdbUpdate: Failed to add entry");

    if (p == PROTO_HTTP) {
        c->Http.n_requests++;
        c->Http.result_hist[ltype]++;
        kb_incr(&c->Http.kbytes_out, size);

        if (logTypeIsATcpHit(ltype))
            kb_incr(&c->Http.hit_kbytes_out, size);
    } else if (p == PROTO_ICP) {
        c->Icp.n_requests++;
        c->Icp.result_hist[ltype]++;
        kb_incr(&c->Icp.kbytes_out, size);

        if (LOG_UDP_HIT == ltype)
            kb_incr(&c->Icp.hit_kbytes_out, size);
    }

    c->last_seen = squid_curtime;
}

/**
 * This function tracks the number of currently established connections
 * for a client IP address.  When a connection is accepted, call this
 * with delta = 1.  When the connection is closed, call with delta =
 * -1.  To get the current value, simply call with delta = 0.
 */
int
clientdbEstablished(const Ip::Address &addr, int delta)
{
    char key[MAX_IPSTRLEN];
    ClientInfo *c;

    if (!Config.onoff.client_db)
        return 0;

    addr.NtoA(key,MAX_IPSTRLEN);

    c = (ClientInfo *) hash_lookup(client_table, key);

    if (c == NULL) {
        c = clientdbAdd(addr);
    }

    if (c == NULL)
        debug_trap("clientdbUpdate: Failed to add entry");

    c->n_established += delta;

    return c->n_established;
}

#define CUTOFF_SECONDS 3600
int

clientdbCutoffDenied(const Ip::Address &addr)
{
    char key[MAX_IPSTRLEN];
    int NR;
    int ND;
    double p;
    ClientInfo *c;

    if (!Config.onoff.client_db)
        return 0;

    addr.NtoA(key,MAX_IPSTRLEN);

    c = (ClientInfo *) hash_lookup(client_table, key);

    if (c == NULL)
        return 0;

    /*
     * If we are in a cutoff window, we don't send a reply
     */
    if (squid_curtime - c->cutoff.time < CUTOFF_SECONDS)
        return 1;

    /*
     * Calculate the percent of DENIED replies since the last
     * cutoff time.
     */
    NR = c->Icp.n_requests - c->cutoff.n_req;

    if (NR < 150)
        NR = 150;

    ND = c->Icp.result_hist[LOG_UDP_DENIED] - c->cutoff.n_denied;

    p = 100.0 * ND / NR;

    if (p < 95.0)
        return 0;

    debugs(1, 0, "WARNING: Probable misconfigured neighbor at " << key);

    debugs(1, 0, "WARNING: " << ND << " of the last " << NR <<
           " ICP replies are DENIED");

    debugs(1, 0, "WARNING: No replies will be sent for the next " <<
           CUTOFF_SECONDS << " seconds");

    c->cutoff.time = squid_curtime;

    c->cutoff.n_req = c->Icp.n_requests;

    c->cutoff.n_denied = c->Icp.result_hist[LOG_UDP_DENIED];

    return 1;
}

log_type &operator++ (log_type &aLogType)
{
    int tmp = (int)aLogType;
    aLogType = (log_type)(++tmp);
    return aLogType;
}

void
clientdbDump(StoreEntry * sentry)
{
    const char *name;
    ClientInfo *c;
    log_type l;
    int icp_total = 0;
    int icp_hits = 0;
    int http_total = 0;
    int http_hits = 0;
    storeAppendPrintf(sentry, "Cache Clients:\n");
    hash_first(client_table);

    while ((c = (ClientInfo *) hash_next(client_table))) {
        storeAppendPrintf(sentry, "Address: %s\n", hashKeyStr(&c->hash));
        if ( (name = fqdncache_gethostbyaddr(c->addr, 0)) ) {
            storeAppendPrintf(sentry, "Name:    %s\n", name);
        }
        storeAppendPrintf(sentry, "Currently established connections: %d\n",
                          c->n_established);
        storeAppendPrintf(sentry, "    ICP  Requests %d\n",
                          c->Icp.n_requests);

        for (l = LOG_TAG_NONE; l < LOG_TYPE_MAX; ++l) {
            if (c->Icp.result_hist[l] == 0)
                continue;

            icp_total += c->Icp.result_hist[l];

            if (LOG_UDP_HIT == l)
                icp_hits += c->Icp.result_hist[l];

            storeAppendPrintf(sentry, "        %-20.20s %7d %3d%%\n",log_tags[l], c->Icp.result_hist[l], Math::intPercent(c->Icp.result_hist[l], c->Icp.n_requests));
        }

        storeAppendPrintf(sentry, "    HTTP Requests %d\n", c->Http.n_requests);

        for (l = LOG_TAG_NONE; l < LOG_TYPE_MAX; ++l) {
            if (c->Http.result_hist[l] == 0)
                continue;

            http_total += c->Http.result_hist[l];

            if (logTypeIsATcpHit(l))
                http_hits += c->Http.result_hist[l];

            storeAppendPrintf(sentry,
                              "        %-20.20s %7d %3d%%\n",
                              log_tags[l],
                              c->Http.result_hist[l],
                              Math::intPercent(c->Http.result_hist[l], c->Http.n_requests));
        }

        storeAppendPrintf(sentry, "\n");
    }

    storeAppendPrintf(sentry, "TOTALS\n");
    storeAppendPrintf(sentry, "ICP : %d Queries, %d Hits (%3d%%)\n",
                      icp_total, icp_hits, Math::intPercent(icp_hits, icp_total));
    storeAppendPrintf(sentry, "HTTP: %d Requests, %d Hits (%3d%%)\n",
                      http_total, http_hits, Math::intPercent(http_hits, http_total));
}

static void
clientdbFreeItem(void *data)
{
    ClientInfo *c = (ClientInfo *)data;
    safe_free(c->hash.key);
    memFree(c, MEM_CLIENT_INFO);
}

void
clientdbFreeMemory(void)
{
    hashFreeItems(client_table, clientdbFreeItem);
    hashFreeMemory(client_table);
    client_table = NULL;
}

static void
clientdbScheduledGC(void *unused)
{
    cleanup_scheduled = 0;
    clientdbStartGC();
}

static void
clientdbGC(void *unused)
{
    static int bucket = 0;
    hash_link *link_next;

    link_next = hash_get_bucket(client_table, bucket++);

    while (link_next != NULL) {
        ClientInfo *c = (ClientInfo *)link_next;
        int age = squid_curtime - c->last_seen;
        link_next = link_next->next;

        if (c->n_established)
            continue;

        if (age < 24 * 3600 && c->Http.n_requests > 100)
            continue;

        if (age < 4 * 3600 && (c->Http.n_requests > 10 || c->Icp.n_requests > 10))
            continue;

        if (age < 5 * 60 && (c->Http.n_requests > 1 || c->Icp.n_requests > 1))
            continue;

        if (age < 60)
            continue;

        hash_remove_link(client_table, &c->hash);

        clientdbFreeItem(c);

        statCounter.client_http.clients--;

        cleanup_removed++;
    }

    if (bucket < CLIENT_DB_HASH_SIZE)
        eventAdd("client_db garbage collector", clientdbGC, NULL, 0.15, 0);
    else {
        bucket = 0;
        cleanup_running = 0;
        max_clients = statCounter.client_http.clients * 3 / 2;

        if (!cleanup_scheduled) {
            cleanup_scheduled = 1;
            eventAdd("client_db garbage collector", clientdbScheduledGC, NULL, 6 * 3600, 0);
        }

        debugs(49, 2, "clientdbGC: Removed " << cleanup_removed << " entries");
    }
}

static void
clientdbStartGC(void)
{
    max_clients = statCounter.client_http.clients;
    cleanup_running = 1;
    cleanup_removed = 0;
    clientdbGC(NULL);
}

#if SQUID_SNMP

Ip::Address *
client_entry(Ip::Address *current)
{
    ClientInfo *c = NULL;
    char key[MAX_IPSTRLEN];

    if (current) {
        current->NtoA(key,MAX_IPSTRLEN);
        hash_first(client_table);
        while ((c = (ClientInfo *) hash_next(client_table))) {
            if (!strcmp(key, hashKeyStr(&c->hash)))
                break;
        }

        c = (ClientInfo *) hash_next(client_table);
    } else {
        hash_first(client_table);
        c = (ClientInfo *) hash_next(client_table);
    }

    hash_last(client_table);

    if (c)
        return (&c->addr);
    else
        return (NULL);

}

variable_list *
snmp_meshCtblFn(variable_list * Var, snint * ErrP)
{
    char key[MAX_IPSTRLEN];
    ClientInfo *c = NULL;
    Ip::Address keyIp;

    *ErrP = SNMP_ERR_NOERROR;
    MemBuf tmp;
    debugs(49, 6, HERE << "Current : length=" << Var->name_length << ": " << snmpDebugOid(Var->name, Var->name_length, tmp));
    if (Var->name_length == 16) {
        oid2addr(&(Var->name[12]), keyIp, 4);
    } else if (Var->name_length == 28) {
        oid2addr(&(Var->name[12]), keyIp, 16);
    } else {
        *ErrP = SNMP_ERR_NOSUCHNAME;
        return NULL;
    }

    keyIp.NtoA(key, sizeof(key));
    debugs(49, 5, HERE << "[" << key << "] requested!");
    c = (ClientInfo *) hash_lookup(client_table, key);

    if (c == NULL) {
        debugs(49, 5, HERE << "not found.");
        *ErrP = SNMP_ERR_NOSUCHNAME;
        return NULL;
    }

    variable_list *Answer = NULL;
    int aggr = 0;
    log_type l;

    switch (Var->name[LEN_SQ_NET + 2]) {

    case MESH_CTBL_ADDR_TYPE: {
        int ival;
        ival = c->addr.IsIPv4() ? INETADDRESSTYPE_IPV4 : INETADDRESSTYPE_IPV6 ;
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      ival, SMI_INTEGER);
    }
    break;

    case MESH_CTBL_ADDR: {
        Answer = snmp_var_new(Var->name, Var->name_length);
        // InetAddress doesn't have its own ASN.1 type,
        // like IpAddr does (SMI_IPADDRESS)
        // See: rfc4001.txt
        Answer->type = ASN_OCTET_STR;
        char client[MAX_IPSTRLEN];
        c->addr.NtoA(client,MAX_IPSTRLEN);
        Answer->val_len = strlen(client);
        Answer->val.string =  (u_char *) xstrdup(client);
    }
    break;
    case MESH_CTBL_HTBYTES:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) c->Http.kbytes_out.kb,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_HTREQ:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) c->Http.n_requests,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_HTHITS:
        aggr = 0;

        for (l = LOG_TAG_NONE; l < LOG_TYPE_MAX; ++l) {
            if (logTypeIsATcpHit(l))
                aggr += c->Http.result_hist[l];
        }

        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) aggr,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_HTHITBYTES:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) c->Http.hit_kbytes_out.kb,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_ICPBYTES:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) c->Icp.kbytes_out.kb,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_ICPREQ:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) c->Icp.n_requests,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_ICPHITS:
        aggr = c->Icp.result_hist[LOG_UDP_HIT];
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) aggr,
                                      SMI_COUNTER32);
        break;

    case MESH_CTBL_ICPHITBYTES:
        Answer = snmp_var_new_integer(Var->name, Var->name_length,
                                      (snint) c->Icp.hit_kbytes_out.kb,
                                      SMI_COUNTER32);
        break;

    default:
        *ErrP = SNMP_ERR_NOSUCHNAME;
        debugs(49, 5, "snmp_meshCtblFn: illegal column.");
        break;
    }

    return Answer;
}

#endif /*SQUID_SNMP */
