
/*
 * Beat Rat -- logging
 *
 * Writes every message to both stderr and a log file, so a run's full
 * history is captured even if the terminal scrollback is gone (or, with
 * -s/--silence, is captured ONLY in the file -- see log_set_quiet()).
 * The log lives at ~/.local/state/beatrat/ (respecting $XDG_STATE_HOME),
 * the same "small self-contained module, own directory-resolution and
 * mkdir_p, deliberately not shared with settings.c/keybinds.c's copies"
 * shape those use for their own config directory. It used to be opened
 * relative to the current working directory, which meant launching from
 * a different folder silently started (or appended to) a completely
 * different log file -- fixed by resolving a real, stable path instead.
 */

#include "log.h"

#include <errno.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>

static FILE *g_logfile = NULL;
static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER;
static int g_quiet = 0;

/* Rotation: once the log reaches this size, log_init() shuffles
 * beatrat.log -> beatrat.log.1 -> beatrat.log.2 -> ... (dropping
 * whatever falls off the end past LOG_MAX_BACKUPS) and starts a fresh
 * file, instead of growing forever. Checked once per run at startup --
 * not mid-run -- which is plenty for how often this actually gets
 * launched, and keeps log_write() itself simple and fast. */
#define LOG_MAX_BYTES ((long)(2 * 1024 * 1024))
#define LOG_MAX_BACKUPS 3

static const char *level_str(LogLevel level) {
    switch (level) {
        case LOG_LEVEL_INFO:  return "INFO ";
        case LOG_LEVEL_WARN:  return "WARN ";
        case LOG_LEVEL_ERROR: return "ERROR";
        default:              return "?????";
    }
}

/* ~/.local/state/beatrat (or $XDG_STATE_HOME/beatrat) -- the XDG
 * "state" location is the right one for logs specifically: it's for
 * data a program wants to keep across runs but that isn't user config
 * (~/.config) or user data (~/.local/share, e.g. saved projects). */
static void state_dir_path(char *buf, size_t buf_size) {
    const char *xdg = getenv("XDG_STATE_HOME");
    if (xdg && *xdg) {
        snprintf(buf, buf_size, "%s/beatrat", xdg);
        return;
    }
    const char *home = getenv("HOME");
    if (!home || !*home)
        home = ".";
    snprintf(buf, buf_size, "%s/.local/state/beatrat", home);
}

static void mkdir_p_local(const char *path) {
    char tmp[768];
    snprintf(tmp, sizeof(tmp), "%s", path);
    size_t len = strlen(tmp);
    if (len > 0 && tmp[len - 1] == '/')
        tmp[len - 1] = '\0';
    for (char *p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = '\0';
            if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
                fprintf(stderr, "beatrat: could not create '%s'\n", tmp);
            *p = '/';
        }
    }
    if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
        fprintf(stderr, "beatrat: could not create '%s'\n", tmp);
}

/* Shuffles path -> path.1 -> path.2 -> ... -> path.LOG_MAX_BACKUPS
 * (the oldest backup, if any, is simply overwritten -- effectively
 * dropped). Only called when `path` is already at/over LOG_MAX_BYTES. */
static void rotate_log(const char *path) {
    /* Sized a bit past the largest `path` log_init() can hand us
     * (state_dir_path()'s buf + '/' + filename, well under 900) plus
     * room for the ".<N>" suffix -- GCC's -Wformat-truncation doesn't
     * know that bound, only that %s could theoretically be up to
     * INT_MAX, hence the (correct but overly conservative) warning
     * this used to produce at the old 800-byte size. */
    char oldest[960], newer[960];
    snprintf(oldest, sizeof(oldest), "%s.%d", path, LOG_MAX_BACKUPS);
    remove(oldest); /* ignore failure -- fine if it never existed */
    for (int i = LOG_MAX_BACKUPS - 1; i >= 1; i--) {
        snprintf(oldest, sizeof(oldest), "%s.%d", path, i + 1);
        snprintf(newer, sizeof(newer), "%s.%d", path, i);
        rename(newer, oldest); /* ignore failure -- fine if newer doesn't exist yet */
    }
    snprintf(oldest, sizeof(oldest), "%s.1", path);
    rename(path, oldest);
}

int log_init(const char *filename) {
    char dir[768];
    state_dir_path(dir, sizeof(dir));
    mkdir_p_local(dir);

    char path[900];
    snprintf(path, sizeof(path), "%s/%s", dir, filename);

    struct stat st;
    if (stat(path, &st) == 0 && st.st_size >= LOG_MAX_BYTES)
        rotate_log(path);

    g_logfile = fopen(path, "a");
    if (!g_logfile) {
        /* Not fatal -- we still have stderr (unless -s/--silence is
         * also active, in which case this one line still gets through
         * regardless, since there's nowhere else left to say it). */
        fprintf(stderr, "beatrat: could not open log file '%s' (continuing with stderr only)\n", path);
        return -1;
    }

    time_t now = time(NULL);
    char timebuf[64];
    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", localtime(&now));
    fprintf(g_logfile, "\n==== Beat Rat run started %s ====\n", timebuf);
    fflush(g_logfile);
    return 0;
}

void log_close(void) {
    pthread_mutex_lock(&g_log_mutex);
    if (g_logfile) {
        fflush(g_logfile);
        fclose(g_logfile);
        g_logfile = NULL;
    }
    pthread_mutex_unlock(&g_log_mutex);
}

void log_set_quiet(int quiet) {
    g_quiet = quiet;
}

void log_write(LogLevel level, const char *func, const char *fmt, ...) {
    char timebuf[16];
    time_t now = time(NULL);
    strftime(timebuf, sizeof(timebuf), "%H:%M:%S", localtime(&now));

    pthread_mutex_lock(&g_log_mutex);

    va_list args;

    if (!g_quiet) {
        fprintf(stderr, "[%s] [%s] (%s): ", timebuf, level_str(level), func);
        va_start(args, fmt);
        vfprintf(stderr, fmt, args);
        va_end(args);
        fprintf(stderr, "\n");
        fflush(stderr);
    }

    if (g_logfile) {
        fprintf(g_logfile, "[%s] [%s] (%s): ", timebuf, level_str(level), func);
        va_start(args, fmt);
        vfprintf(g_logfile, fmt, args);
        va_end(args);
        fprintf(g_logfile, "\n");
        fflush(g_logfile); /* flush every line -- if we crash, we want the log intact */
    }

    pthread_mutex_unlock(&g_log_mutex);
}
