/*
 * stats/top front-end for midi-ble-rtctl.
 *
 * CMake renames the original midi-ble-rtctl.c main() to
 * midi_ble_rtctl_bluez_main().  This file provides the public main(), handles
 * local stats commands, and delegates all BlueZ commands to the original CLI.
 */

#include "mb-slice-ring.h"
#include "mb-stats.h"

#include <glib.h>
#include "mb-timeouts.h"
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

#define STATS_QUEUE_BAR_WIDTH 20u

int midi_ble_rtctl_bluez_main(int argc, char **argv);

static char *control_socket_default_path(void) {
    const char *runtime_dir = g_get_user_runtime_dir();
    if (!runtime_dir || !*runtime_dir)
        runtime_dir = g_get_tmp_dir();

    return g_build_filename(runtime_dir, "midi-ble-rt", "control.sock", NULL);
}

static int control_send_command(const char *command) {
    char *path = control_socket_default_path();

    int fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (fd < 0) {
        g_printerr("Could not create control socket client: %s\n", g_strerror(errno));
        g_free(path);
        return 1;
    }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;

    if (strlen(path) >= sizeof(addr.sun_path)) {
        g_printerr("Control socket path too long: %s\n", path);
        close(fd);
        g_free(path);
        return 1;
    }

    strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);

    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
        g_printerr("Could not connect to midi-ble-rtd control socket: %s\n", path);
        g_printerr("Reason: %s\n", g_strerror(errno));
        g_printerr("Start midi-ble-rtd with config-directory runtime first.\n");
        close(fd);
        g_free(path);
        return 1;
    }

    char *line = g_strdup_printf("%s\n", command);
    size_t len = strlen(line);
    const char *p = line;

    while (len > 0) {
        ssize_t n = write(fd, p, len);
        if (n < 0) {
            if (errno == EINTR)
                continue;
            g_printerr("Could not write control command: %s\n", g_strerror(errno));
            g_free(line);
            close(fd);
            g_free(path);
            return 1;
        }
        p += n;
        len -= (size_t)n;
    }

    g_free(line);

    char buf[1024];
    for (;;) {
        ssize_t n = read(fd, buf, sizeof(buf) - 1);
        if (n < 0) {
            if (errno == EINTR)
                continue;
            g_printerr("Could not read control reply: %s\n", g_strerror(errno));
            close(fd);
            g_free(path);
            return 1;
        }

        if (n == 0)
            break;

        buf[n] = '\0';
        fputs(buf, stdout);
    }

    close(fd);
    g_free(path);
    return 0;
}

static void control_device_usage(const char *argv0, const char *cmd) {
    g_printerr("Usage:\n");
    g_printerr("  %s %s DEVICE\n", argv0, cmd);
    g_printerr("\n");
    g_printerr("DEVICE must match a configured device id or Bluetooth address.\n");
}

static int control_send_device_command(const char *verb, const char *device) {
    if (!verb || !device || !*device)
        return 2;

    char *command = g_strdup_printf("%s %s", verb, device);
    int rc = control_send_command(command);
    g_free(command);
    return rc;
}

typedef struct {
    char *version;
    char **header;
    char **value;
    GPtrArray *rows; /* element type: char ** */
    gsize n_header;
    gsize n_value;
} StatsTable;

static gsize strv_len(char **v) {
    gsize n = 0;
    if (!v)
        return 0;
    while (v[n])
        n++;
    return n;
}

static void stats_table_clear(StatsTable *t) {
    if (!t)
        return;
    g_free(t->version);
    g_strfreev(t->header);
    g_strfreev(t->value);
    if (t->rows)
        g_ptr_array_free(t->rows, TRUE);
    memset(t, 0, sizeof(*t));
}

static bool stats_is_v2(const StatsTable *t) {
    return t && g_strcmp0(t->version, "v2") == 0;
}

static bool stats_is_v3(const StatsTable *t) {
    return t && g_strcmp0(t->version, "v3") == 0;
}

static bool stats_is_v4(const StatsTable *t) {
    return t && g_strcmp0(t->version, "v4") == 0;
}

static bool stats_is_v5(const StatsTable *t) {
    return t && g_strcmp0(t->version, "v5") == 0;
}

static bool stats_has_window(const StatsTable *t) {
    return stats_is_v2(t) || stats_is_v3(t) || stats_is_v4(t);
}

static const char *stats_get(const StatsTable *t, const char *key) {
    if (!t || !key)
        return "-";
    for (gsize i = 0; i < t->n_header && i < t->n_value; i++) {
        if (g_strcmp0(t->header[i], key) == 0)
            return t->value[i] && *t->value[i] ? t->value[i] : "-";
    }
    return "-";
}

static const char *stats_row_get(const StatsTable *t, char **row, const char *key) {
    if (!t || !row || !key)
        return "-";
    for (gsize i = 0; i < t->n_header && row[i]; i++) {
        if (g_strcmp0(t->header[i], key) == 0)
            return row[i] && *row[i] ? row[i] : "-";
    }
    return "-";
}

static guint64 stats_row_u64(const StatsTable *t, char **row, const char *key) {
    const char *s = stats_row_get(t, row, key);
    if (!s || g_strcmp0(s, "-") == 0)
        return 0;
    return g_ascii_strtoull(s, NULL, 10);
}

static const char *stats_row_rate(const StatsTable *t, char **row, const char *key) {
    const char *s = stats_row_get(t, row, key);
    return s && *s ? s : "0.000";
}

static guint64 stats_u64(const StatsTable *t, const char *key) {
    const char *s = stats_get(t, key);
    if (!s || g_strcmp0(s, "-") == 0)
        return 0;
    return g_ascii_strtoull(s, NULL, 10);
}

static const char *stats_rate(const StatsTable *t, const char *key) {
    const char *s = stats_get(t, key);
    return s && *s ? s : "0.000";
}

static double stats_queue_fill_pct(guint64 depth) {
    if (MB_SLICE_RING_COUNT == 0)
        return 0.0;
    return ((double)depth * 100.0) / (double)MB_SLICE_RING_COUNT;
}

static void stats_queue_bar(guint64 depth, char *out, size_t out_len) {
    if (!out || out_len < STATS_QUEUE_BAR_WIDTH + 3)
        return;

    guint64 clamped = depth;
    if (clamped > MB_SLICE_RING_COUNT)
        clamped = MB_SLICE_RING_COUNT;

    unsigned filled = 0;
    if (MB_SLICE_RING_COUNT > 0) {
        filled = (unsigned)((clamped * STATS_QUEUE_BAR_WIDTH + (MB_SLICE_RING_COUNT / 2)) /
                            MB_SLICE_RING_COUNT);
        if (depth > 0 && filled == 0)
            filled = 1;
        if (filled > STATS_QUEUE_BAR_WIDTH)
            filled = STATS_QUEUE_BAR_WIDTH;
    }

    out[0] = '[';
    for (unsigned i = 0; i < STATS_QUEUE_BAR_WIDTH; i++)
        out[i + 1] = i < filled ? '#' : '.';
    out[STATS_QUEUE_BAR_WIDTH + 1] = ']';
    out[STATS_QUEUE_BAR_WIDTH + 2] = '\0';
}

static guint64 stats_queue_peak(const StatsTable *t, const char *depth_key, const char *peak_key) {
    guint64 depth = stats_u64(t, depth_key);
    guint64 peak = stats_u64(t, peak_key);
    return peak > depth ? peak : depth;
}

static void print_queue_visual_line(const StatsTable *t,
                                    const char *dir,
                                    const char *depth_key,
                                    const char *peak_key) {
    guint64 depth = stats_u64(t, depth_key);
    guint64 peak = stats_queue_peak(t, depth_key, peak_key);
    char now_bar[STATS_QUEUE_BAR_WIDTH + 3];
    char peak_bar[STATS_QUEUE_BAR_WIDTH + 3];

    stats_queue_bar(depth, now_bar, sizeof(now_bar));
    stats_queue_bar(peak, peak_bar, sizeof(peak_bar));

    g_print("%-8s %5" G_GUINT64_FORMAT "/%-5u %7.1f%%  %s\n",
            dir,
            depth,
            (unsigned)MB_SLICE_RING_COUNT,
            stats_queue_fill_pct(depth),
            now_bar);
    g_print("%-8s %5" G_GUINT64_FORMAT "/%-5u %7.1f%%  %s\n",
            "  peak",
            peak,
            (unsigned)MB_SLICE_RING_COUNT,
            stats_queue_fill_pct(peak),
            peak_bar);
}

static void print_queue_visual(const StatsTable *t) {
    g_print("Buffer fill:\n");
    g_print("%-8s %11s %8s  %s\n", "DIR", "DEPTH/CAP", "FILL", "BAR");
    print_queue_visual_line(t, "RX", "rx_queue_depth", "rx_queue_high_watermark");
    print_queue_visual_line(t, "TX", "tx_queue_depth", "tx_queue_high_watermark");
}

static bool read_stats_table(const char *path, StatsTable *out) {
    char *content = NULL;
    GError *error = NULL;

    memset(out, 0, sizeof(*out));

    if (!g_file_get_contents(path, &content, NULL, &error)) {
        g_printerr("Could not read stats file: %s\n", path);
        g_printerr("Reason: %s\n", error ? error->message : "unknown error");
        g_printerr("Start midi-ble-rtd with stats enabled first.\n");
        g_clear_error(&error);
        return false;
    }

    char **lines = g_strsplit(content, "\n", 0);
    g_free(content);

    /*
     * v5 is a multi-row format:
     *
     *   v5
     *   header...
     *   device RX row...
     *   device TX row...
     *
     * Handle it before the legacy single-row validator, because the old
     * validator expects exactly the v1-v4 shape and may reject v5 as incomplete.
     */
    if (lines && lines[0] && g_strcmp0(lines[0], "v5") == 0) {
        out->version = g_strdup(lines[0]);

        if (!lines[1] || !*lines[1]) {
            g_printerr("Unsupported or incomplete stats file: %s\n", path);
            g_printerr("Stats version found: %s\n", lines[0]);
            g_strfreev(lines);
            return false;
        }

        out->header = g_strsplit(lines[1], "\t", 0);
        out->n_header = strv_len(out->header);
        out->rows = g_ptr_array_new_with_free_func((GDestroyNotify)g_strfreev);

        for (gsize i = 2; lines[i]; i++) {
            if (!*lines[i])
                continue;
            g_ptr_array_add(out->rows, g_strsplit(lines[i], "\t", 0));
        }

        if (out->rows->len > 0) {
            out->value = g_strdupv((char **)g_ptr_array_index(out->rows, 0));
            out->n_value = strv_len(out->value);
        }

        g_strfreev(lines);

        if (out->n_header == 0) {
            g_printerr("Empty stats table: %s\n", path);
            stats_table_clear(out);
            return false;
        }

        return true;
    }

    bool supported = lines &&
                     (g_strcmp0(lines[0], "v1") == 0 ||
                      g_strcmp0(lines[0], "v2") == 0 ||
                      g_strcmp0(lines[0], "v3") == 0 ||
                      g_strcmp0(lines[0], "v4") == 0) &&
                     lines[1] && lines[2];
    if (!supported) {
        g_printerr("Unsupported or incomplete stats file: %s\n", path);
        if (lines && lines[0])
            g_printerr("Stats version found: %s\n", lines[0]);
        g_strfreev(lines);
        return false;
    }

    out->version = g_strdup(lines[0]);
    out->header = g_strsplit(lines[1], "\t", 0);
    out->n_header = strv_len(out->header);

    if (g_strcmp0(out->version, "v5") == 0) {
        out->rows = g_ptr_array_new_with_free_func((GDestroyNotify)g_strfreev);
        for (gsize i = 2; lines[i]; i++) {
            if (!*lines[i])
                continue;
            g_ptr_array_add(out->rows, g_strsplit(lines[i], "\t", 0));
        }

        if (out->rows->len > 0) {
            out->value = g_strdupv(g_ptr_array_index(out->rows, 0));
            out->n_value = strv_len(out->value);
        }
    } else {
        out->value = g_strsplit(lines[2], "\t", 0);
        out->n_value = strv_len(out->value);
    }

    g_strfreev(lines);

    if (out->n_header == 0 || out->n_value == 0) {
        g_printerr("Empty stats table: %s\n", path);
        stats_table_clear(out);
        return false;
    }

    return true;
}


static void print_stats_v5(const char *path, const StatsTable *t) {
    g_print("Stats file: %s\n", path);
    g_print("Format:     %s\n", t->version ? t->version : "-");

    if (!t->rows || t->rows->len == 0) {
        g_print("No device stats rows.\n");
        return;
    }

    char **first = g_ptr_array_index(t->rows, 0);
    g_print("Session:    config-dir\n");
    g_print("Uptime:     %s ms\n", stats_row_get(t, first, "uptime_ms"));
    g_print("Window:     %s ms\n", stats_row_get(t, first, "window_ms"));
    g_print("\n");

    g_print("%-18s %-2s %-10s %-8s %10s %10s %10s %10s %10s %10s %8s %8s %7s  %s\n",
            "DEVICE", "D", "STATE", "ALSA", "PKT_WIN", "B_WIN", "DROP_WIN",
            "PKT/s", "B/s", "DROP/s", "QDEPTH", "PEAK", "FILL", "BAR");

    for (guint i = 0; i < t->rows->len; i++) {
        char **row = g_ptr_array_index(t->rows, i);
        guint64 depth = stats_row_u64(t, row, "queue_depth");
        guint64 peak = stats_row_u64(t, row, "queue_high_watermark");
        if (depth > peak)
            peak = depth;

        char bar[STATS_QUEUE_BAR_WIDTH + 3];
        stats_queue_bar(peak, bar, sizeof(bar));

        char *alsa = g_strdup_printf("%s:%s",
                                     stats_row_get(t, row, "alsa_client_id"),
                                     stats_row_get(t, row, "alsa_port_id"));

        const char *device = stats_row_get(t, row, "id");
        if (g_strcmp0(device, "-") == 0)
            device = stats_row_get(t, row, "address");

        g_print("%-18.18s %-2.2s %-10.10s %-8.8s %10" G_GUINT64_FORMAT " %10" G_GUINT64_FORMAT " %10" G_GUINT64_FORMAT " %10s %10s %10s %8" G_GUINT64_FORMAT " %8" G_GUINT64_FORMAT " %6.1f%%  %s\n",
                device,
                stats_row_get(t, row, "dir"),
                stats_row_get(t, row, "state"),
                alsa,
                stats_row_u64(t, row, "packets"),
                stats_row_u64(t, row, "bytes"),
                stats_row_u64(t, row, "drops"),
                stats_row_rate(t, row, "packets_per_sec"),
                stats_row_rate(t, row, "bytes_per_sec"),
                stats_row_rate(t, row, "drops_per_sec"),
                depth,
                peak,
                stats_queue_fill_pct(peak),
                bar);

        g_free(alsa);
    }

    g_print("\n");
    g_print("%-18s %-2s %12s %12s %12s\n",
            "DEVICE", "D", "LAST_MS", "GAP_AVG_MS", "GAP_MAX_MS");

    for (guint i = 0; i < t->rows->len; i++) {
        char **row = g_ptr_array_index(t->rows, i);
        const char *device = stats_row_get(t, row, "id");
        if (g_strcmp0(device, "-") == 0)
            device = stats_row_get(t, row, "address");

        g_print("%-18.18s %-2.2s %12s %12s %12s\n",
                device,
                stats_row_get(t, row, "dir"),
                stats_row_get(t, row, "last_ms"),
                stats_row_get(t, row, "gap_avg_ms"),
                stats_row_get(t, row, "gap_max_ms"));
    }
}

static void print_stats_aligned(const char *path, const StatsTable *t) {
    if (stats_is_v5(t)) {
        print_stats_v5(path, t);
        return;
    }

    g_print("Stats file: %s\n", path);
    g_print("Format:     %s\n", t->version ? t->version : "-");
    g_print("Session:    %s\n", stats_get(t, "label"));
    g_print("Address:    %s\n", stats_get(t, "address"));
    g_print("State:      %s\n", stats_get(t, "state"));

    if (stats_is_v3(t) || stats_is_v4(t)) {
        g_print("ALSA RX:    %s:%s\n",
                stats_get(t, "alsa_rx_client_id"),
                stats_get(t, "alsa_rx_port_id"));
        g_print("ALSA TX:    %s:%s\n",
                stats_get(t, "alsa_tx_client_id"),
                stats_get(t, "alsa_tx_port_id"));
    } else if (stats_is_v2(t)) {
        g_print("ALSA:       %s:%s\n",
                stats_get(t, "alsa_client_id"),
                stats_get(t, "alsa_port_id"));
    }

    g_print("Uptime:     %s ms\n", stats_get(t, "uptime_ms"));
    if (stats_has_window(t))
        g_print("Window:     %s ms\n", stats_get(t, "window_ms"));
    g_print("\n");

    if (stats_has_window(t)) {
        g_print("%-8s %12s %12s %12s %12s %12s %12s %12s\n",
                "DIR", "PKT_WIN", "B_WIN", "DROP_WIN", "PKT/s", "B/s", "DROP/s", "QDEPTH");
        g_print("%-8s %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12s %12s %12s %12s\n",
                "RX",
                stats_u64(t, "rx_packets"),
                stats_u64(t, "rx_bytes"),
                stats_u64(t, "rx_drops"),
                stats_rate(t, "rx_packets_per_sec"),
                stats_rate(t, "rx_bytes_per_sec"),
                stats_rate(t, "rx_drops_per_sec"),
                stats_get(t, "rx_queue_depth"));
        g_print("%-8s %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12s %12s %12s %12s\n",
                "TX",
                stats_u64(t, "tx_packets"),
                stats_u64(t, "tx_bytes"),
                stats_u64(t, "tx_drops"),
                stats_rate(t, "tx_packets_per_sec"),
                stats_rate(t, "tx_bytes_per_sec"),
                stats_rate(t, "tx_drops_per_sec"),
                stats_get(t, "tx_queue_depth"));
    } else {
        g_print("%-8s %12s %12s %12s %12s %12s\n",
                "DIR", "PACKETS", "BYTES", "DROPS", "LAST_MS", "QDEPTH");
        g_print("%-8s %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12s %12s\n",
                "RX",
                stats_u64(t, "rx_packets"),
                stats_u64(t, "rx_bytes"),
                stats_u64(t, "rx_drops"),
                stats_get(t, "last_rx_ms"),
                stats_get(t, "rx_queue_depth"));
        g_print("%-8s %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12" G_GUINT64_FORMAT " %12s %12s\n",
                "TX",
                stats_u64(t, "tx_packets"),
                stats_u64(t, "tx_bytes"),
                stats_u64(t, "tx_drops"),
                stats_get(t, "last_tx_ms"),
                stats_get(t, "tx_queue_depth"));
    }

    g_print("\n");
    print_queue_visual(t);
    g_print("\n");

    g_print("%-8s %12s %12s %12s\n", "DIR", "LAST_MS", "GAP_AVG_MS", "GAP_MAX_MS");
    g_print("%-8s %12s %12s %12s\n",
            "RX",
            stats_get(t, "last_rx_ms"),
            stats_get(t, "rx_gap_avg_ms"),
            stats_get(t, "rx_gap_max_ms"));
    g_print("%-8s %12s %12s %12s\n",
            "TX",
            stats_get(t, "last_tx_ms"),
            stats_get(t, "tx_gap_avg_ms"),
            stats_get(t, "tx_gap_max_ms"));
}

static int cmd_stats_print(const char *path) {
    StatsTable t;
    if (!read_stats_table(path, &t))
        return 1;
    print_stats_aligned(path, &t);
    stats_table_clear(&t);
    return 0;
}

static void stats_usage(const char *argv0, const char *cmd) {
    g_printerr(
        "Usage:\n"
        "  %s %s [--path PATH]\n"
        "  %s top [--path PATH] [--interval MS]\n",
        argv0, cmd, argv0);
}

static int cmd_stats_like(int argc, char **argv, bool watch) {
    char *path = mb_stats_default_path();
    unsigned interval_ms = MB_STATS_CTL_DEFAULT_INTERVAL_MS;

    for (int i = 2; i < argc; i++) {
        if (g_strcmp0(argv[i], "--path") == 0 && i + 1 < argc) {
            g_free(path);
            path = g_strdup(argv[++i]);
        } else if (watch && g_strcmp0(argv[i], "--interval") == 0 && i + 1 < argc) {
            interval_ms = (unsigned)atoi(argv[++i]);
            if (interval_ms < MB_STATS_CTL_MIN_INTERVAL_MS)
                interval_ms = MB_STATS_CTL_MIN_INTERVAL_MS;
            if (interval_ms > MB_STATS_CTL_MAX_INTERVAL_MS)
                interval_ms = MB_STATS_CTL_MAX_INTERVAL_MS;
        } else {
            stats_usage(argv[0], watch ? "top" : "stats");
            g_free(path);
            return 2;
        }
    }

    if (!watch) {
        int rc = cmd_stats_print(path);
        g_free(path);
        return rc;
    }

    for (;;) {
        g_print("\033[H\033[J");
        (void)cmd_stats_print(path);
        g_usleep((gulong)interval_ms * 1000UL);
    }
}

int main(int argc, char **argv) {
    if (argc >= 2 && g_strcmp0(argv[1], "stats") == 0)
        return cmd_stats_like(argc, argv, false);
    if (argc >= 2 && g_strcmp0(argv[1], "top") == 0)
        return cmd_stats_like(argc, argv, true);

    if (argc == 2 && g_strcmp0(argv[1], "daemon-ping") == 0)
        return control_send_command("PING");
    if (argc == 2 && g_strcmp0(argv[1], "daemon-status") == 0)
        return control_send_command("STATUS");
    if (argc == 2 && g_strcmp0(argv[1], "daemon-list") == 0)
        return control_send_command("LIST");

    if (argc >= 2 && g_strcmp0(argv[1], "daemon-connect") == 0) {
        if (argc != 3) {
            control_device_usage(argv[0], argv[1]);
            return 2;
        }
        return control_send_device_command("CONNECT", argv[2]);
    }

    if (argc >= 2 && g_strcmp0(argv[1], "daemon-disconnect") == 0) {
        if (argc != 3) {
            control_device_usage(argv[0], argv[1]);
            return 2;
        }
        return control_send_device_command("DISCONNECT", argv[2]);
    }

    if (argc >= 2 && g_strcmp0(argv[1], "daemon-recheck") == 0) {
        if (argc != 3) {
            control_device_usage(argv[0], argv[1]);
            return 2;
        }
        return control_send_device_command("RECHECK", argv[2]);
    }

    return midi_ble_rtctl_bluez_main(argc, argv);
}
