/*
    Copyright (C) 2011  ABRT team
    Copyright (C) 2011  RedHat Inc

    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 "libabrt.h"

/* We remember N worst files and their sizes, not just one:
 * if DIR is deep, scanning it takes *a few seconds* even if it's in cache.
 * If we rescan it after each single file deletion, it can be VERY slow.
 * (I observed ~20 min long case).
 */
#define MAX_VICTIM_LIST_SIZE 128

struct name_and_size {
    off_t size;
    double weighted_size_and_age;
    char name[1];
};

/* Updates a list of files sorted by increasing weighted_size_and_age.
 */
static GList *insert_name_and_sizes(GList *list, const char *name, double wsa, off_t sz)
{
    struct name_and_size *ns;
    unsigned list_len = 0;
    GList *cur = list;

    while (cur)
    {
        ns = cur->data;
        if (ns->weighted_size_and_age >= wsa)
            break;
        list_len++;
        cur = cur->next;
    }
    list_len += g_list_length(cur);

    if (cur != list || list_len < MAX_VICTIM_LIST_SIZE)
    {
        ns = g_malloc(sizeof(*ns) + strlen(name));
        ns->weighted_size_and_age = wsa;
        ns->size = sz;
        strcpy(ns->name, name);
        list = g_list_insert_before(list, cur, ns);
        list_len++;
        if (list_len > MAX_VICTIM_LIST_SIZE)
        {
            free(list->data);
            list = g_list_delete_link(list, list);
        }
    }

    return list;
}

static double get_dir_size(const char *dirname,
                GList **pp_worst_file_list,
                GList *preserve_files_list
) {
    DIR *dp = opendir(dirname);
    if (!dp)
        return 0;

    /* "now" is used only if caller wants to know worst_file */
    time_t now = pp_worst_file_list ? time(NULL) : 0;
    struct dirent *dent;
    double size = 0;
    while ((dent = readdir(dp)) != NULL)
    {
        if (libreport_dot_or_dotdot(dent->d_name))
            continue;

        g_autofree char *fullname = g_build_filename(dirname, dent->d_name, NULL);
        struct stat stats;
        if (lstat(fullname, &stats) != 0)
            goto next;

        if (S_ISDIR(stats.st_mode))
        {
            double sz = get_dir_size(fullname, pp_worst_file_list, preserve_files_list);
            size += sz;
        }
        else if (S_ISREG(stats.st_mode) || S_ISLNK(stats.st_mode))
        {
            double sz = stats.st_size;
            /* Account for filename and inode storage (approximately).
             * This also makes even zero-length files to have nonzero cost.
             */
            sz += strlen(dent->d_name) + sizeof(stats);
            size += sz;

            if (pp_worst_file_list)
            {
                GList *cur = preserve_files_list;
                while (cur)
                {
                    //log_warning("'%s' ? '%s'", fullname, *pp);
                    if (strcmp(fullname, (char*)cur->data) == 0)
                        goto next;
                    cur = cur->next;
                }

                /* Calculate "weighted" size and age
                 * w = sz_kbytes * age_mins */
                sz /= 1024;
                long age = (now - stats.st_mtime) / 60;
                if (age > 1)
                    sz *= age;

                *pp_worst_file_list = insert_name_and_sizes(*pp_worst_file_list, fullname, sz, stats.st_size);
            }
        }
 next:
        continue;
    }
    closedir(dp);

    return size;
}

static const char *parse_size_pfx(double *size, const char *str)
{
    errno = (isdigit(str[0]) ? 0 : ERANGE);
    char *end;
    *size = strtoull(str, &end, 10);

    if (end != str)
    {
        char c = (*end | 0x20); /* lowercasing */
        if (c == 'k')
            end++, *size *= 1024;
        else if (c == 'm')
            end++, *size *= 1024*1024;
        else if (c == 'g')
            end++, *size *= 1024*1024*1024;
        else if (c == 't')
            end++, *size *= 1024.0*1024*1024*1024;
    }

    if (errno || end == str || *end != ':')
        perror_msg_and_die("Bad size prefix in '%s'", str);

    return end + 1;
}

static void delete_dirs(gpointer data, gpointer exclude_path)
{
    double cap_size;
    const char *dir = parse_size_pfx(&cap_size, data);

    abrt_trim_problem_dirs(dir, cap_size, exclude_path);
}

static void delete_files(gpointer data, gpointer void_preserve_list)
{
    double cap_size;
    const char *dir = parse_size_pfx(&cap_size, data);
    GList *preserve_files_list = void_preserve_list;

    unsigned count = 100;
    while (--count != 0)
    {
        GList *worst_file_list = NULL;
        double cur_size = get_dir_size(dir, &worst_file_list, preserve_files_list);

        if (cur_size <= cap_size || !worst_file_list)
        {
            g_list_free_full(g_steal_pointer(&worst_file_list), free);
            log_info("cur_size:%.0f cap_size:%.0f, no (more) trimming", cur_size, cap_size);
            break;
        }

        /* Invert the list, so that largest/oldest file is first */
        worst_file_list = g_list_reverse(worst_file_list);
        /* And delete (some of) them */
        while (worst_file_list && cur_size > cap_size)
        {
            struct name_and_size *ns = worst_file_list->data;
            log_notice("%s is %.0f bytes (more than %.0f MB), deleting '%s' (%llu bytes)",
                    dir, cur_size, cap_size / (1024*1024), ns->name, (long long)ns->size);
            if (unlink(ns->name) != 0)
                perror_msg("Can't unlink '%s'", ns->name);
            else
                cur_size -= ns->size;
            free(ns);
            worst_file_list = g_list_delete_link(worst_file_list, worst_file_list);
        }
    }
}

int main(int argc, char **argv)
{
    /* I18n */
    setlocale(LC_ALL, "");
#if ENABLE_NLS
    bindtextdomain(PACKAGE, LOCALEDIR);
    textdomain(PACKAGE);
#endif

    abrt_init(argv);

    GList *dir_list = NULL;
    GList *file_list = NULL;
    char *preserve = NULL;

    /* Can't keep these strings/structs static: _() doesn't support that */
    const char *program_usage_string = _(
        "& [-v] [-d SIZE:DIR]... [-f SIZE:DIR]... [-p DIR] [FILE]...\n"
        "\n"
        "Deletes problem dirs (-d) or files (-f) in DIRs until they are smaller than SIZE.\n"
        "FILEs are preserved (never deleted)."
    );
    enum {
        OPT_v = 1 << 0,
        OPT_d = 1 << 1,
        OPT_f = 1 << 2,
        OPT_p = 1 << 3,
    };
    /* Keep enum above and order of options below in sync! */
    struct options program_options[] = {
        OPT__VERBOSE(&libreport_g_verbose),
        OPT_LIST('d'  , NULL, &dir_list , "SIZE:DIR", _("Delete whole problem directories")),
        OPT_LIST('f'  , NULL, &file_list, "SIZE:DIR", _("Delete files inside this directory")),
        OPT_STRING('p', NULL, &preserve,  "DIR"     , _("Preserve this directory")),
        OPT_END()
    };
    /*unsigned opts =*/ libreport_parse_opts(argc, argv, program_options, program_usage_string);
    argv += optind;
    if ((argv[0] && !file_list)
     || !(dir_list || file_list)
    ) {
        libreport_show_usage_and_die(program_usage_string, program_options);
    }

    /* We don't have children, so this is not needed: */
    //libreport_export_abrt_envvars(/*set_pfx:*/ 0);

    /* Preserve not only files specified on command line, but,
     * if they are symlinks, preserve also the real files they point to:
     */
    GList *preserve_files_list = NULL;
    while (*argv)
    {
        char *name = *argv++;
        /* Since we don't bother freeing preserve_files_list on exit,
         * we take a shortcut and insert name instead of g_strdup(name)
         * in the next line:
         */
        preserve_files_list = g_list_prepend(preserve_files_list, name);

        g_autofree char *rp = realpath(name, NULL);
        if (rp)
        {
            if (strcmp(rp, name) != 0)
                preserve_files_list = g_list_prepend(preserve_files_list, rp);
        }
    }
    /* Not really necessary, but helps to reduce confusion when debugging */
    preserve_files_list = g_list_reverse(preserve_files_list);

    g_list_foreach(dir_list, delete_dirs, preserve);
    g_list_foreach(file_list, delete_files, preserve_files_list);

    return 0;
}
