/* Copyright (C) 2016 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR
 *
 * This file is part of simmc-agent
 *
 *     last.c   Re-implementation of the 'last' command, this time
 *          for Linux. Yes I know there is BSD last, but I
 *          just felt like writing this. No thanks :-).
 *          Also, this version gives lots more info (especially with -x)
 *
 *     Author:  Miquel van Smoorenburg, miquels@cistron.nl
 *
 *     Version: @(#)last  2.85  30-Jul-2004  miquels@cistron.nl
 *
 *          This file is part of the sysvinit suite,
 *          Copyright (C) 1991-2004 Miquel van Smoorenburg.
 *
 *          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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */
#include "agent/linux/get_user_count.h"
#include <string>
#include <set>

/* Global variables */
struct utmplist *utmplist = NULL;
char *ufile;        /* Filename of this file */
time_t lastdate;    /* Last date we've seen */
struct tm * timeinfo;
set<string> users;
char yesterday[7], today[7];

/*
 *  Read one utmp entry, return in new format.
 *  Automatically reposition file pointer.
 */
static int uread(FILE *fp, struct utmp *u, int *quit) {
    static int utsize, bpos;
    static char buf[UCHUNKSIZE];
    char tmp[1024];
    static off_t fpos;
    off_t o;

    if (quit == NULL && u != NULL) {
        /*
         *  Normal read.
         */
        return fread(u, sizeof(struct utmp), 1, fp);
    }

    if (u == NULL) {
        /*
         *  Initialize and position.
         */
        utsize =  sizeof(struct utmp);
        fseeko(fp, 0, SEEK_END);
        fpos = ftello(fp);
        if (fpos == 0)
            return 0;
        o = ((fpos - 1) / UCHUNKSIZE) * UCHUNKSIZE;
        if (fseeko(fp, o, SEEK_SET) < 0) {
            fprintf(stderr, "ERROR: seek failed!\n");
            return 0;
        }
        bpos = static_cast<int>(fpos - o);
        if (fread(buf, bpos, 1, fp) != 1) {
            fprintf(stderr, "ERROR: read failed!\n");
            return 0;
        }
        fpos = o;
        return 1;
    }

    /*
     *  Read one struct. From the buffer if possible.
     */
    bpos -= utsize;
    if (bpos >= 0) {
        memcpy(u, buf + bpos, sizeof(struct utmp));
        return 1;
    }

    /*
     *  Oops we went "below" the buffer. We should be able to
     *  seek back UCHUNKSIZE bytes.
     */
    fpos -= UCHUNKSIZE;
    if (fpos < 0)
        return 0;

    /*
     *  Copy whatever is left in the buffer.
     */
    memcpy(tmp + (-bpos), buf, utsize + bpos);
    if (fseeko(fp, fpos, SEEK_SET) < 0) {
        perror("fseek");
        return 0;
    }

    /*
     *  Read another UCHUNKSIZE bytes.
     */
    if (fread(buf, UCHUNKSIZE, 1, fp) != 1) {
        perror("fread");
        return 0;
    }

    /*
     *  The end of the UCHUNKSIZE byte buffer should be the first
     *  few bytes of the current struct utmp.
     */
    memcpy(tmp, buf + UCHUNKSIZE + bpos, -bpos);
    bpos += UCHUNKSIZE;

    memcpy(u, tmp, sizeof(struct utmp));

    return 1;
}

static void list(struct utmp *p, time_t t, int what) {
    time_t tmp;
    struct user_log * u = (struct user_log *)malloc(sizeof(struct user_log));
    char login[25], logout[25], month[3];

    tmp = (time_t)p->ut_time;
    timeinfo = gmtime(&tmp);  // NOLINT(runtime/threadsafe_fn)
    strftime(month, 3, "%m", timeinfo);
    month[3]='\0';

    snprintf(u->user, sizeof(u->user), p->ut_name, 0);
    snprintf(login, sizeof(login), ctime(&tmp), 0);
    u->login[0] = login[22];
    u->login[1] = login[23];
    u->login[2] = month[0];
    u->login[3] = month[1];
    u->login[4] = login[8] == ' '?'0':login[8];
    u->login[5] = login[9];
    u->login[6] = '\0';

    switch (what) {
        case R_NOW:
            snprintf(u->logout, sizeof(u->logout), "999999");
            break;
        case R_PHANTOM:
            snprintf(u->logout, sizeof(u->logout), "000000");
            break;
        case R_NORMAL:
            timeinfo = gmtime(&t);  // NOLINT(runtime/threadsafe_fn)
            strftime(month, 3, "%m", timeinfo);
            month[3]='\0';
            snprintf(logout, sizeof(logout), ctime(&t), 0);
            u->logout[0] = logout[22];
            u->logout[1] = logout[23];
            u->logout[2] = month[0];
            u->logout[3] = month[1];
            u->logout[4] = logout[8] == ' '?'0':logout[8];
            u->logout[5] = logout[9];
            u->logout[6] = '\0';
            break;
    }

        if (!strcmp(u->logout, yesterday)) {
            users.insert(u->user);
        } else if (!strcmp(u->login, yesterday)) {
            users.insert(u->user);
        } else if (( strcmp(u->login, yesterday) &&
            strcmp(u->login, today)) &&  (!strcmp(u->logout, today) ||
            !strcmp(u->logout, "999999"))) {
            users.insert(u->user);
        }
}

static struct user_log * last_modified() {
    FILE *fp;     /* Filepointer of wtmp file */

    struct utmp ut;   /* Current utmp entry */
    struct utmp oldut;    /* Old utmp entry to check for duplicates */
    struct utmplist *p, *next;   /* Pointer into utmplist */

    time_t lastboot = 0;  /* Last boottime */
    time_t lastdown;  /* Last downtime */
    time_t begintime; /* When wtmp begins */

    int c, x;     /* Scratch */
    int skip = 1;
    struct stat st;   /* To stat the [uw]tmp file */
    int quit = 0;     /* Flag */

    struct user_log * u = NULL;
    struct user_log * v = NULL;
    struct user_log * w = NULL;

    /*
     *    Which file do we want to read?
     */
    ufile = (char *)(WTMP_FILE);
    time(&lastdown);

    /*
     *    Fill in 'lastdate'
     */
    lastdate = lastdown;

    /*
     *    Open the utmp file
     */
    if ((fp = fopen(ufile, "r")) == NULL) {
        x = errno;
        if (x == ENOENT)
            // file not found
            throw std::string(ufile) + std::string(": ") +
                  std::string(strerror(errno));
        throw strerror(errno);
    }

    /*
     *    Optimize the buffer size.
     */
    setvbuf(fp, NULL, _IOFBF, UCHUNKSIZE);

    /*
     *    Read first structure to capture the time field
     */
    if (uread(fp, &ut, NULL) == 1) {
        begintime = ut.ut_time;
    } else {
        fstat(fileno(fp), &st);
        begintime = st.st_ctime;
        quit = 1;
    }

    /*
     *    Go to end of file minus one structure
     *    and/or initialize utmp reading code.
     */
    uread(fp, NULL, NULL);

    /*
     *    Read struct after struct backwards from the file.
     */

    while (!quit && skip) {
        if (uread(fp, &ut, &quit) != 1)
            break;

        if (memcmp(&ut, &oldut, sizeof(struct utmp)) == 0) continue;
        memcpy(&oldut, &ut, sizeof(struct utmp));
        lastdate = ut.ut_time;

        /*
         *  Set ut_type to the correct type.
         */
        if (strncmp(ut.ut_line, "~", 1) == 0) {
            if (strncmp(ut.ut_user, "shutdown", 8) == 0)
                ut.ut_type = SHUTDOWN_TIME;
            else if (strncmp(ut.ut_user, "reboot", 6) == 0)
                ut.ut_type = BOOT_TIME;
            else if (strncmp(ut.ut_user, "runlevel", 8) == 0)
                ut.ut_type = RUN_LVL;
        /*def COMPAT*/
        /*
         *  For stupid old applications that don't fill in
         *  ut_type correctly.
         */
        } else {
            if (ut.ut_type != DEAD_PROCESS &&
                ut.ut_name[0] && ut.ut_line[0] &&
                strcmp(ut.ut_name, "LOGIN") != 0)
                ut.ut_type = USER_PROCESS;
            /*
             *  Even worse, applications that write ghost
             *  entries: ut_type set to USER_PROCESS but
             *  empty ut_name...
             */
            if (ut.ut_name[0] == 0)
                ut.ut_type = DEAD_PROCESS;

            /*
             *  Clock changes.
             */
            if (strcmp(ut.ut_name, "date") == 0) {
                if (ut.ut_line[0] == '|') ut.ut_type = OLD_TIME;
                if (ut.ut_line[0] == '{') ut.ut_type = NEW_TIME;
            }
        }
        switch (ut.ut_type) {
            case USER_PROCESS:
                /*
                 *  This was a login - show the first matching
                 *  logout record and delete all records with
                 *  the same ut_line.
                 */
                c = 0;
                skip = 0;
                for (p = utmplist; p; p = next) {
                    next = p->next;
                    if (strncmp(p->ut.ut_line, ut.ut_line,
                        UT_LINESIZE) == 0) {
                        /* Show it */
                        if (c == 0) {
                            quit = 0;
                            list(&ut, p->ut.ut_time, R_NORMAL);
                            c = 1;
                        }
                        if (p->next) p->next->prev = p->prev;
                        if (p->prev)
                            p->prev->next = p->next;
                        else
                            utmplist = p->next;
                        free(p);
                    }
                }
                /*
                 *  Not found? Then crashed, down, still
                 *  logged in, or missing logout record.
                 */
                if (c == 0) {
                    c = R_NOW;

                    /* Is process still alive? */
                    if (ut.ut_pid > 0 &&
                        kill(ut.ut_pid, 0) != 0 &&
                        errno == ESRCH)
                        c = R_PHANTOM;

                    quit = 0;
                    list(&ut, lastboot, c);
                }
                /* FALLTHRU */

            case DEAD_PROCESS:
                /*
                 *  Just store the data if it is
                 *  interesting enough.
                 */
                if (ut.ut_line[0] == 0)
                    break;

                p = (struct utmplist *)malloc(sizeof(struct utmplist));
                if (p == NULL) {
                    throw std::string("ERROR: out of memory");
                }

                memcpy(&p->ut, &ut, sizeof(struct utmp));
                p->next  = utmplist;
                p->prev  = NULL;
                if (utmplist) utmplist->prev = p;
                utmplist = p;
                break;
        }
    }

    while (!quit) {
        if (uread(fp, &ut, &quit) != 1)
            break;

        if (memcmp(&ut, &oldut, sizeof(struct utmp)) == 0) continue;
        memcpy(&oldut, &ut, sizeof(struct utmp));
        lastdate = ut.ut_time;

        /*
         *  Set ut_type to the correct type.
         */
        if (strncmp(ut.ut_line, "~", 1) == 0) {
            if (strncmp(ut.ut_user, "shutdown", 8) == 0)
                ut.ut_type = SHUTDOWN_TIME;
            else if (strncmp(ut.ut_user, "reboot", 6) == 0)
                ut.ut_type = BOOT_TIME;
            else if (strncmp(ut.ut_user, "runlevel", 8) == 0)
                ut.ut_type = RUN_LVL;
        /*def COMPAT*/
        /*
         *  For stupid old applications that don't fill in
         *  ut_type correctly.
         */
        } else {
            if (ut.ut_type != DEAD_PROCESS &&
                ut.ut_name[0] && ut.ut_line[0] &&
                strcmp(ut.ut_name, "LOGIN") != 0)
                ut.ut_type = USER_PROCESS;
            /*
             *  Even worse, applications that write ghost
             *  entries: ut_type set to USER_PROCESS but
             *  empty ut_name...
             */
            if (ut.ut_name[0] == 0)
                ut.ut_type = DEAD_PROCESS;

            /*
             *  Clock changes.
             */
            if (strcmp(ut.ut_name, "date") == 0) {
                if (ut.ut_line[0] == '|') ut.ut_type = OLD_TIME;
                if (ut.ut_line[0] == '{') ut.ut_type = NEW_TIME;
            }
        }

        switch (ut.ut_type) {
            case USER_PROCESS:
                /*
                 *  This was a login - show the first matching
                 *  logout record and delete all records with
                 *  the same ut_line.
                 */
                c = 0;
                for (p = utmplist; p; p = next) {
                    next = p->next;
                    if (strncmp(p->ut.ut_line, ut.ut_line,
                        UT_LINESIZE) == 0) {
                        /* Show it */
                        if (c == 0) {
                            quit = 0;
                            list(&ut, p->ut.ut_time, R_NORMAL);
                        }
                        if (p->next) p->next->prev = p->prev;
                        if (p->prev)
                            p->prev->next = p->next;
                        else
                            utmplist = p->next;
                        free(p);
                    }
                }
                /*
                 *  Not found? Then crashed, down, still
                 *  logged in, or missing logout record.
                 */
                if (c == 0) {
                    c = R_NOW;

                    /* Is process still alive? */
                    if (ut.ut_pid > 0 &&
                        kill(ut.ut_pid, 0) != 0 &&
                        errno == ESRCH)
                        c = R_PHANTOM;

                    quit = 0;
                    list(&ut, lastboot, c);
                }
                /* FALLTHRU */

            case DEAD_PROCESS:
                /*
                 *  Just store the data if it is
                 *  interesting enough.
                 */
                if (ut.ut_line[0] == 0)
                    break;
                p = (struct utmplist *)malloc(sizeof(struct utmplist));
                if (p == NULL) {
                    throw std::string("ERROR: out of memory");
                }
                memcpy(&p->ut, &ut, sizeof(struct utmp));
                p->next  = utmplist;
                p->prev  = NULL;
                if (utmplist) utmplist->prev = p;
                utmplist = p;
                break;
        }
    }

    fclose(fp);
}

int get_user_count() {

    /* The agent execution depends on the file '/var/log/wtmp', which is not
     * present on some distros, such as OpenSUSE docker image. When this happens,
     * this file is created and it's permissions setted.
     */

    std::string wtmp_path = "/var/log/wtmp";
    struct stat buffer;
    if (stat(wtmp_path.c_str(), &buffer) != 0) { //file not exists
        ofstream wtmp_file;
        wtmp_file.open(wtmp_path.c_str());
        wtmp_file.close();
        chown(wtmp_path.c_str(), getpwnam("root")->pw_uid, getgrnam("tty")->gr_gid);
        chmod(wtmp_path.c_str(), 664);
    }

    time_t rawtime;

    time(&rawtime);
    timeinfo = localtime(&rawtime);  // NOLINT(runtime/threadsafe_fn)
    strftime(today, 7, "%y%m%d", timeinfo);
    today[6]='\0';
    timeinfo->tm_mday--;
    mktime(timeinfo);

    strftime(yesterday, 7, "%y%m%d", timeinfo);
    yesterday[6] = '\0';

    last_modified();

    return users.size();
}