/*
 * 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_system.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_partition.h"
#include "esp_ota_ops.h"
#include "esp_app_desc.h"
#include "esp_idf_version.h"
#include "esp_pm.h"
#include "esp_sleep.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_core_dump.h"
#include "nvs_flash.h"
#include "linenoise/linenoise.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "git_versions.h" // Generated during the build process.
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <SystemK.h>

static void print_usage(void)
{
    printf(
        "Usage:\n"
        "  system chip-info      - Show chip and system information\n"
        "  system coredump       - Check coredump status\n"
        "  system factory-reset  - Clear NVS and restart\n"
        "  system flash          - Show flash information\n"
        "  system heap-caps      - Show heap by capability\n"
        "  system hostname [name]- Show/set hostname\n"
        "  system mac            - Show MAC addresses\n"
        "  system partition      - List partition table\n"
        "  system RAM            - Display heap memory usage\n"
        "  system reboot         - Reboot the device\n"
        "  system reset-reason   - Show last reset reason\n"
        "  system stats          - Combined system statistics\n"
        "  system tasks          - List FreeRTOS tasks\n"
        "  system uptime         - Display system uptime\n"
        "  system version        - Show firmware version\n");
}

static int system_cmd_chip_info(void)
{
    esp_chip_info_t chip_info;
    esp_chip_info(&chip_info);

    printf("Chip Information:\n");
    printf("  Model: %s\n", CONFIG_IDF_TARGET);
    printf("  Cores: %d\n", chip_info.cores);
    printf("  Revision: %d\n", chip_info.revision);
    printf("  Features: %s%s%s%s\n",
           (chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi " : "",
           (chip_info.features & CHIP_FEATURE_BT) ? "BT " : "",
           (chip_info.features & CHIP_FEATURE_BLE) ? "BLE " : "",
           (chip_info.features & CHIP_FEATURE_IEEE802154) ? "802.15.4 " : "");

    uint32_t flash_size;
    if (esp_flash_get_size(NULL, &flash_size) == ESP_OK)
    {
        printf("  Flash: %lu MB %s\n",
               flash_size / (1024 * 1024),
               (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "(embedded)" : "(external)");
    }

    return 0;
}

static int system_cmd_uptime(void)
{
    int64_t uptime_us = esp_timer_get_time();
    int64_t uptime_s = uptime_us / 1000000;

    int days = uptime_s / 86400;
    int hours = (uptime_s % 86400) / 3600;
    int minutes = (uptime_s % 3600) / 60;
    int seconds = uptime_s % 60;

    printf("Uptime: %d days, %02d:%02d:%02d\n", days, hours, minutes, seconds);
    printf("        (%lld seconds)\n", uptime_s);

    return 0;
}

static int system_cmd_tasks(void)
{
#if (configUSE_TRACE_FACILITY == 1)
    uint32_t task_count = uxTaskGetNumberOfTasks();
    TaskStatus_t *task_array = malloc(task_count * sizeof(TaskStatus_t));

    if (task_array == NULL)
    {
        printf("Failed to allocate memory for task list\n");
        return 1;
    }

    uint32_t total_runtime;
    task_count = uxTaskGetSystemState(task_array, task_count, &total_runtime);

    printf("Tasks (%lu total):\n", task_count);
    printf("%-16s %5s %6s %11s %8s", "Name", "State", "Core", "Priority", "Stack");
#if (configGENERATE_RUN_TIME_STATS == 1)
    printf(" %7s", "CPU%");
#endif
    printf("\n");
    printf("----------------------------------------------------------------\n");

    for (uint32_t i = 0; i < task_count; i++)
    {
        const char *state_str;
        switch (task_array[i].eCurrentState)
        {
        case eRunning:
            state_str = "RUN  ";
            break;
        case eReady:
            state_str = "READY";
            break;
        case eBlocked:
            state_str = "BLOCK";
            break;
        case eSuspended:
            state_str = "SUSP ";
            break;
        case eDeleted:
            state_str = "DEL  ";
            break;
        default:
            state_str = "?    ";
            break;
        }

        uint32_t stack_remaining = task_array[i].usStackHighWaterMark;

        BaseType_t affinity = xTaskGetCoreID(task_array[i].xHandle);

        printf("%-16s %5s ", task_array[i].pcTaskName, state_str);

        if (affinity == tskNO_AFFINITY)
        {
            printf("%6s", "ANY");
        }
        else
        {
            printf("%6d", (int)affinity);
        }

        printf(" %11u %8lu", task_array[i].uxCurrentPriority, stack_remaining);

#if (configGENERATE_RUN_TIME_STATS == 1)
        float cpu_percent = 0.0;
        if (total_runtime > 0)
        {
            cpu_percent = (100.0 * task_array[i].ulRunTimeCounter) / total_runtime;
        }
        printf(" %6.1f%%", cpu_percent);
#endif
        printf("\n");
    }

    free(task_array);
#else
    printf("Task listing not available (configUSE_TRACE_FACILITY not enabled)\n");
    printf("Enable in menuconfig: Component config -> FreeRTOS -> Kernel\n");
#endif
    return 0;
}

static int system_cmd_reset_reason(void)
{
    esp_reset_reason_t reason = esp_reset_reason();

    printf("Reset reason: ");
    switch (reason)
    {
    case ESP_RST_UNKNOWN:
        printf("Unknown\n");
        break;
    case ESP_RST_POWERON:
        printf("Power-on reset\n");
        break;
    case ESP_RST_EXT:
        printf("External pin reset\n");
        break;
    case ESP_RST_SW:
        printf("Software reset\n");
        break;
    case ESP_RST_PANIC:
        printf("Exception/panic\n");
        break;
    case ESP_RST_INT_WDT:
        printf("Interrupt watchdog\n");
        break;
    case ESP_RST_TASK_WDT:
        printf("Task watchdog\n");
        break;
    case ESP_RST_WDT:
        printf("Other watchdog\n");
        break;
    case ESP_RST_DEEPSLEEP:
        printf("Deep sleep reset\n");
        break;
    case ESP_RST_BROWNOUT:
        printf("Brownout reset\n");
        break;
    case ESP_RST_SDIO:
        printf("SDIO reset\n");
        break;
    default:
        printf("Code %d\n", reason);
        break;
    }

    return 0;
}

static int system_cmd_ram(void)
{
    size_t free_heap = esp_get_free_heap_size();
    size_t min_free_heap = esp_get_minimum_free_heap_size();
    size_t largest_block = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);

    printf("Heap Memory:\n");
    printf("  Free: %u bytes (%.2f KB)\n", free_heap, free_heap / 1024.0);
    printf("  Minimum free: %u bytes (%.2f KB)\n", min_free_heap, min_free_heap / 1024.0);
    printf("  Largest free block: %u bytes (%.2f KB)\n", largest_block, largest_block / 1024.0);

    return 0;
}

static int system_cmd_flash(void)
{
    uint32_t flash_size;
    if (esp_flash_get_size(NULL, &flash_size) != ESP_OK)
    {
        printf("Failed to get flash size\n");
        return 1;
    }

    printf("Flash Information:\n");
    printf("  Total size: %lu bytes (%.2f MB)\n", flash_size, flash_size / (1024.0 * 1024.0));

    const esp_partition_t *running = esp_ota_get_running_partition();
    if (running)
    {
        printf("  Running partition: %s (offset 0x%lx, size %lu KB)\n",
               running->label, running->address, running->size / 1024);
    }

    return 0;
}

static int system_cmd_partition(void)
{
    printf("Partition Table:\n");
    printf("%-16s %-10s %-10s %10s %10s\n", "Label", "Type", "SubType", "Offset", "Size");
    printf("------------------------------------------------------------------------\n");

    esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);

    while (it != NULL)
    {
        const esp_partition_t *part = esp_partition_get(it);

        const char *type_str = "?";
        if (part->type == ESP_PARTITION_TYPE_APP)
            type_str = "app";
        else if (part->type == ESP_PARTITION_TYPE_DATA)
            type_str = "data";

        printf("%-16s %-10s 0x%-8x 0x%08lx %8lu KB\n",
               part->label,
               type_str,
               part->subtype,
               part->address,
               part->size / 1024);

        it = esp_partition_next(it);
    }

    esp_partition_iterator_release(it);
    return 0;
}

static int system_cmd_heap_caps(void)
{
    printf("Heap by Capability:\n");

    size_t internal_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
    size_t internal_total = heap_caps_get_total_size(MALLOC_CAP_INTERNAL);
    printf("  Internal: %u / %u bytes free (%.1f%%)\n",
           internal_free, internal_total,
           internal_total > 0 ? 100.0 * internal_free / internal_total : 0);

    size_t spiram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
    size_t spiram_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM);
    if (spiram_total > 0)
    {
        printf("  SPIRAM: %u / %u bytes free (%.1f%%)\n",
               spiram_free, spiram_total, 100.0 * spiram_free / spiram_total);
    }

    size_t dma_free = heap_caps_get_free_size(MALLOC_CAP_DMA);
    size_t dma_total = heap_caps_get_total_size(MALLOC_CAP_DMA);
    printf("  DMA capable: %u / %u bytes free (%.1f%%)\n",
           dma_free, dma_total,
           dma_total > 0 ? 100.0 * dma_free / dma_total : 0);

    size_t exec_free = heap_caps_get_free_size(MALLOC_CAP_EXEC);
    size_t exec_total = heap_caps_get_total_size(MALLOC_CAP_EXEC);
    printf("  Executable: %u / %u bytes free (%.1f%%)\n",
           exec_free, exec_total,
           exec_total > 0 ? 100.0 * exec_free / exec_total : 0);

    return 0;
}

static int system_cmd_stats(void)
{
    printf("=== System Statistics ===\n\n");

    system_cmd_uptime();
    printf("\n");
    system_cmd_ram();
    printf("\n");

    uint32_t task_count = uxTaskGetNumberOfTasks();
    printf("Active tasks: %lu\n\n", task_count);

    system_cmd_reset_reason();

    return 0;
}

static int system_cmd_coredump(void)
{
    esp_core_dump_summary_t summary;
    esp_err_t err = esp_core_dump_get_summary(&summary);

    if (err == ESP_ERR_NOT_FOUND)
    {
        printf("No coredump found in flash\n");
        return 0;
    }
    else if (err != ESP_OK)
    {
        printf("Failed to read coredump: %s\n", esp_err_to_name(err));
        return 1;
    }

    printf("Coredump found:\n");
    printf("  Program counter: 0x%08lx\n", summary.exc_pc);
    printf("  Exception cause: %lu\n", summary.ex_info.exc_cause);
    printf("  Exception vaddr: 0x%08lx\n", summary.ex_info.exc_vaddr);

    return 0;
}

static int system_cmd_mac(void)
{
    uint8_t mac[6];

    printf("MAC Addresses:\n");

    if (esp_read_mac(mac, ESP_MAC_WIFI_STA) == ESP_OK)
    {
        printf("  WiFi STA: %02x:%02x:%02x:%02x:%02x:%02x\n",
               mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    if (esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP) == ESP_OK)
    {
        printf("  WiFi AP:  %02x:%02x:%02x:%02x:%02x:%02x\n",
               mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    if (esp_read_mac(mac, ESP_MAC_BT) == ESP_OK)
    {
        printf("  BT:       %02x:%02x:%02x:%02x:%02x:%02x\n",
               mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    if (esp_read_mac(mac, ESP_MAC_ETH) == ESP_OK)
    {
        printf("  Ethernet: %02x:%02x:%02x:%02x:%02x:%02x\n",
               mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    }

    return 0;
}
typedef struct
{
    const char **hostname_out;
    bool success;
} hostname_get_ctx_t;

typedef struct
{
    const char *hostname_in;
    esp_err_t result;
} hostname_set_ctx_t;

static esp_err_t get_hostname_tcpip(void *ctx)
{
    hostname_get_ctx_t *context = (hostname_get_ctx_t *)ctx;
    esp_netif_t *netif = esp_netif_next_unsafe(NULL);

    if (netif)
    {
        esp_netif_get_hostname(netif, context->hostname_out);
        context->success = true;
    }
    else
    {
        context->success = false;
    }

    return ESP_OK;
}

static esp_err_t set_hostname_tcpip(void *ctx)
{
    hostname_set_ctx_t *context = (hostname_set_ctx_t *)ctx;
    esp_netif_t *netif = esp_netif_next_unsafe(NULL);

    if (netif)
    {
        context->result = esp_netif_set_hostname(netif, context->hostname_in);
    }
    else
    {
        context->result = ESP_ERR_NOT_FOUND;
    }

    return ESP_OK;
}

static int system_cmd_hostname(int argc, char **argv)
{
    if (argc < 3)
    {
        const char *hostname = NULL;
        hostname_get_ctx_t ctx = {
            .hostname_out = &hostname,
            .success = false};

        esp_err_t err = esp_netif_tcpip_exec(get_hostname_tcpip, &ctx);

        if (err != ESP_OK)
        {
            printf("Failed to access network interface: %s\n", esp_err_to_name(err));
            return 1;
        }

        if (ctx.success)
        {
            if (hostname)
            {
                printf("Hostname: %s\n", hostname);
            }
            else
            {
                printf("No hostname set\n");
            }
        }
        else
        {
            printf("No network interface available\n");
        }
        return 0;
    }

    hostname_set_ctx_t ctx = {
        .hostname_in = argv[2],
        .result = ESP_FAIL};

    esp_err_t err = esp_netif_tcpip_exec(set_hostname_tcpip, &ctx);

    if (err != ESP_OK)
    {
        printf("Failed to access network interface: %s\n", esp_err_to_name(err));
        return 1;
    }

    if (ctx.result == ESP_ERR_NOT_FOUND)
    {
        printf("No network interface available\n");
    }
    else if (ctx.result == ESP_OK)
    {
        printf("Hostname set to: %s\n", argv[2]);
    }
    else
    {
        printf("Failed to set hostname: %s\n", esp_err_to_name(ctx.result));
    }

    return 0;
}

static int system_cmd_version(void)
{
    const esp_app_desc_t *app_desc = esp_app_get_description();

    printf("Firmware Information:\n");
    printf("  Version: %s\n", app_desc->version);
    printf("  Project: %s\n", app_desc->project_name);
    printf("  IDF version: %s\n", app_desc->idf_ver);
    printf("  SystemK version: %s (%s)\n", SYSTEMK_VERSION_STRING, SYSTEMK_GIT_COMMIT_HASH_SHORT);
    printf("  Git commit: %s\n", GIT_COMMIT_HASH_LONG);
    printf("  Compile time: %s %s\n", app_desc->date, app_desc->time);

    return 0;
}

static int system_cmd_factory_reset(void)
{
    printf("WARNING: This will erase all NVS data!\n");
    printf("Type 'yes' to confirm: ");

    char confirm[10];
    if (fgets(confirm, sizeof(confirm), stdin) == NULL)
    {
        printf("\nCancelled\n");
        return 0;
    }

    // Remove newline
    confirm[strcspn(confirm, "\n")] = 0;

    if (strcmp(confirm, "yes") != 0)
    {
        printf("Factory reset cancelled.\n");
        return 0;
    }

    printf("Erasing NVS...\n");
    esp_err_t err = nvs_flash_erase();
    if (err != ESP_OK)
    {
        printf("Failed to erase NVS: %s\n", esp_err_to_name(err));
        return 1;
    }

    printf("Factory reset complete. Rebooting...\n");
    vTaskDelay(pdMS_TO_TICKS(1000));
    esp_restart();

    return 0;
}

static int system_cmd_reboot(void)
{
    printf("Rebooting...\n");
    vTaskDelay(pdMS_TO_TICKS(100));
    esp_restart();
    return 0;
}

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

    if (!strcmp(argv[1], "chip-info"))
        return system_cmd_chip_info();
    if (!strcmp(argv[1], "coredump"))
        return system_cmd_coredump();
    if (!strcmp(argv[1], "factory-reset"))
        return system_cmd_factory_reset();
    if (!strcmp(argv[1], "flash"))
        return system_cmd_flash();
    if (!strcmp(argv[1], "heap-caps"))
        return system_cmd_heap_caps();
    if (!strcmp(argv[1], "hostname"))
        return system_cmd_hostname(argc, argv);
    if (!strcmp(argv[1], "mac"))
        return system_cmd_mac();
    if (!strcmp(argv[1], "partition"))
        return system_cmd_partition();
    if (!strcmp(argv[1], "RAM"))
        return system_cmd_ram();
    if (!strcmp(argv[1], "reboot"))
        return system_cmd_reboot();
    if (!strcmp(argv[1], "reset-reason"))
        return system_cmd_reset_reason();
    if (!strcmp(argv[1], "stats"))
        return system_cmd_stats();
    if (!strcmp(argv[1], "tasks"))
        return system_cmd_tasks();
    if (!strcmp(argv[1], "uptime"))
        return system_cmd_uptime();
    if (!strcmp(argv[1], "version"))
        return system_cmd_version();

    printf("Unknown command\n");
    print_usage();
    return 0;
}

static void system_completion(const char *buf, linenoiseCompletions *lc)
{
    if (!strncmp(buf, "system", 6))
    {
        linenoiseAddCompletion(lc, "system chip-info");
        linenoiseAddCompletion(lc, "system coredump");
        linenoiseAddCompletion(lc, "system factory-reset");
        linenoiseAddCompletion(lc, "system flash");
        linenoiseAddCompletion(lc, "system heap-caps");
        linenoiseAddCompletion(lc, "system hostname");
        linenoiseAddCompletion(lc, "system mac");
        linenoiseAddCompletion(lc, "system partition");
        linenoiseAddCompletion(lc, "system RAM");
        linenoiseAddCompletion(lc, "system reboot");
        linenoiseAddCompletion(lc, "system reset-reason");
        linenoiseAddCompletion(lc, "system stats");
        linenoiseAddCompletion(lc, "system tasks");
        linenoiseAddCompletion(lc, "system uptime");
        linenoiseAddCompletion(lc, "system version");
    }
}

void Register_System_Command(void)
{
    linenoiseSetCompletionCallback(system_completion);

    const esp_console_cmd_t cmd = {
        .command = "system",
        .help = "System management and diagnostic commands",
        .func = &cmd_system,
    };

    ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}