/*
 * This program source code file is part of the KTag project, a DIY laser tag
 * game with customizable features and wide interoperability.
 *
 * 🛡 <https://ktag.clubk.club> 🃞
 *
 * Copyright © 2026 Joseph P. Kearney and the KTag developers.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, either version 3 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 Affero General Public License for more
 * details.
 *
 * There should be a copy of the GNU Affero General Public License in the LICENSE
 * file in the root of this repository.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "esp_console.h"
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "linenoise/linenoise.h"
#include "esp_vfs_dev.h"
#include <string.h>
#include <stdio.h>
#include <ctype.h>

#define LOG_NVS_NS "logcfg"
#define LOGCFG_VERSION 1
#define MAX_TAGS 16
#define MAX_TAGLEN 16

typedef struct
{
    char tag[MAX_TAGLEN];
    uint8_t level;
} tag_entry_t;

static esp_log_level_t Saved_Global_Level = ESP_LOG_INFO;
static tag_entry_t Tag_Table[MAX_TAGS] = {0};
static size_t Tag_Count = 0;

static bool parse_level(const char *s, esp_log_level_t *out)
{
    if (!strcmp(s, "none"))
        *out = ESP_LOG_NONE;
    else if (!strcmp(s, "error"))
        *out = ESP_LOG_ERROR;
    else if (!strcmp(s, "warn"))
        *out = ESP_LOG_WARN;
    else if (!strcmp(s, "info"))
        *out = ESP_LOG_INFO;
    else if (!strcmp(s, "debug"))
        *out = ESP_LOG_DEBUG;
    else if (!strcmp(s, "verbose"))
        *out = ESP_LOG_VERBOSE;
    else
        return false;
    return true;
}

static const char *level_str(esp_log_level_t lvl)
{
    switch (lvl)
    {
    case ESP_LOG_NONE:
        return "none";
    case ESP_LOG_ERROR:
        return "error";
    case ESP_LOG_WARN:
        return "warn";
    case ESP_LOG_INFO:
        return "info";
    case ESP_LOG_DEBUG:
        return "debug";
    case ESP_LOG_VERBOSE:
        return "verbose";
    default:
        return "?";
    }
}

static void nvs_save(void)
{
    nvs_handle_t h;
    esp_err_t err = nvs_open(LOG_NVS_NS, NVS_READWRITE, &h);
    if (err != ESP_OK)
        return;

    nvs_set_u8(h, "ver", LOGCFG_VERSION);
    nvs_set_u8(h, "global", esp_log_get_default_level());
    nvs_set_u8(h, "tagcnt", Tag_Count);
    nvs_set_blob(h, "tags", Tag_Table, Tag_Count * sizeof(tag_entry_t));

    nvs_commit(h);
    nvs_close(h);
}

static void nvs_clear_tags(void)
{
    nvs_handle_t h;
    if (nvs_open(LOG_NVS_NS, NVS_READWRITE, &h) != ESP_OK)
        return;
    nvs_erase_key(h, "tagcnt");
    nvs_erase_key(h, "tags");
    nvs_commit(h);
    nvs_close(h);
}

static void log_config_load(void)
{
    nvs_handle_t h;
    if (nvs_open(LOG_NVS_NS, NVS_READONLY, &h) != ESP_OK)
        return;

    uint8_t ver;
    if (nvs_get_u8(h, "ver", &ver) != ESP_OK || ver != LOGCFG_VERSION)
    {
        nvs_close(h);
        nvs_clear_tags();
        return;
    }

    uint8_t lvl;
    if (nvs_get_u8(h, "global", &lvl) == ESP_OK)
    {
        esp_log_level_set("*", lvl);
        Saved_Global_Level = lvl;
    }

    uint8_t cnt;
    if (nvs_get_u8(h, "tagcnt", &cnt) == ESP_OK && cnt <= MAX_TAGS)
    {
        size_t sz = cnt * sizeof(tag_entry_t);
        if (nvs_get_blob(h, "tags", Tag_Table, &sz) == ESP_OK)
        {
            Tag_Count = cnt;
            for (size_t i = 0; i < Tag_Count; i++)
            {
                esp_log_level_set(Tag_Table[i].tag, Tag_Table[i].level);
            }
        }
    }

    nvs_close(h);
}

static void clear_tag_levels(void)
{
    esp_log_level_t def = esp_log_get_default_level();
    for (size_t i = 0; i < Tag_Count; i++)
    {
        esp_log_level_set(Tag_Table[i].tag, def);
    }
    Tag_Count = 0;
}

static bool set_tag_level(const char *tag, esp_log_level_t level)
{
    for (size_t i = 0; i < Tag_Count; i++)
    {
        if (Tag_Table[i].tag[0] != '\0' && !strcmp(Tag_Table[i].tag, tag))
        {
            Tag_Table[i].level = level;
            return true;
        }
    }

    if (Tag_Count >= MAX_TAGS)
        return false;

    strncpy(Tag_Table[Tag_Count].tag, tag, MAX_TAGLEN - 1);
    Tag_Table[Tag_Count].tag[MAX_TAGLEN - 1] = 0;
    Tag_Table[Tag_Count].level = level;
    Tag_Count++;
    return true;
}

static void print_usage(void)
{
    printf(
        "Usage:\n"
        "  log show\n"
        "  log reset\n"
        "  log off | on\n"
        "  log <level>\n"
        "  log tag <tag> [level|off]\n"
        "Levels: none error warn info debug verbose\n");
}

static int log_cmd_show(void)
{
    printf("Global level: %s\n",
           level_str(esp_log_get_default_level()));

    if (Tag_Count == 0)
    {
        printf("No tag overrides\n");
    }
    else
    {
        printf("Tag overrides:\n");
        for (size_t i = 0; i < Tag_Count; i++)
        {
            printf("  %-10s : %s\n",
                   Tag_Table[i].tag,
                   level_str(Tag_Table[i].level));
        }
    }
    return 0;
}

static int log_cmd_reset(void)
{
    esp_log_level_set("*", ESP_LOG_INFO);
    Saved_Global_Level = ESP_LOG_INFO;
    clear_tag_levels();
    nvs_clear_tags();
    nvs_save();
    printf("Logging reset to defaults\n");
    return 0;
}

static int log_cmd_off(void)
{
    Saved_Global_Level = esp_log_get_default_level();
    esp_log_level_set("*", ESP_LOG_NONE);
    nvs_save();
    printf("Global logging disabled\n");
    return 0;
}

static int log_cmd_on(void)
{
    esp_log_level_set("*", Saved_Global_Level);
    nvs_save();
    printf("Global logging restored\n");
    return 0;
}

static int log_cmd_tag(int argc, char **argv)
{
    if (argc < 3)
    {
        printf("Usage: log tag <tag> [level|off]\n");
        return 0;
    }

    // Ensure tag is null-terminated and within bounds
    char tag[MAX_TAGLEN];
    strncpy(tag, argv[2], MAX_TAGLEN - 1);
    tag[MAX_TAGLEN - 1] = '\0';

    if (argc == 3)
    {
        printf("Tag '%s' is %s\n",
               tag,
               level_str(esp_log_level_get(tag)));
        return 0;
    }

    esp_log_level_t level;
    if (!strcmp(argv[3], "off"))
    {
        level = ESP_LOG_NONE;
    }
    else if (!parse_level(argv[3], &level))
    {
        printf("Invalid level\n");
        return 0;
    }

    if (!set_tag_level(tag, level))
    {
        printf("Tag table full (%d max)\n", MAX_TAGS);
        return 0;
    }

    esp_log_level_set(tag, level);
    nvs_save();
    printf("Tag '%s' set to %s\n", tag, argv[3]);
    return 0;
}

static int log_cmd_set_global(const char *lvl)
{
    esp_log_level_t level;
    if (!parse_level(lvl, &level))
    {
        print_usage();
        return 0;
    }

    esp_log_level_set("*", level);
    Saved_Global_Level = level;
    nvs_save();
    printf("Global level set to %s\n", lvl);
    return 0;
}

static int cmd_log(int argc, char **argv)
{
    if (argc < 2)
    {
        print_usage();
        return 0;
    }

    if (!strcmp(argv[1], "show"))
        return log_cmd_show();
    if (!strcmp(argv[1], "reset"))
        return log_cmd_reset();
    if (!strcmp(argv[1], "off"))
        return log_cmd_off();
    if (!strcmp(argv[1], "on"))
        return log_cmd_on();
    if (!strcmp(argv[1], "tag"))
        return log_cmd_tag(argc, argv);

    return log_cmd_set_global(argv[1]);
}

static void log_completion(const char *buf, linenoiseCompletions *lc)
{
    if (!strncmp(buf, "log", 3))
    {
        linenoiseAddCompletion(lc, "log show");
        linenoiseAddCompletion(lc, "log clear");
        linenoiseAddCompletion(lc, "log reset");
        linenoiseAddCompletion(lc, "log off");
        linenoiseAddCompletion(lc, "log on");
        linenoiseAddCompletion(lc, "log info");
        linenoiseAddCompletion(lc, "log debug");
        linenoiseAddCompletion(lc, "log warn");
    }
}

void Register_Log_Command(void)
{
    log_config_load();
    
    linenoiseSetCompletionCallback(log_completion);

    const esp_console_cmd_t cmd = {
        .command = "log",
        .help = "Control logging levels",
        .func = &cmd_log,
    };

    ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}
