/*
 * report.c - Generate analysis reports from boot logs.
 *
 * Reads serial.log, trace.bin, qemu.cmdline, and env.txt from a log
 * directory, then writes report.json and report.txt into the same directory.
 *
 * Also derives a structured "analysis" (boot result, failure domain, graphics
 * hints) from the serial console and writes a flat analysis.meta sidecar
 * (KEY=VALUE) so the initramfs can fold derived fields into the canonical
 * session manifest without a JSON parser.
 */

#include "report.h"
#include "trace_parser.h"
#include "serial_parser.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

#define MAX_PATH       4096
#define MAX_CMDLINE    4096
#define MAX_ENV_ENTRIES 64
#define MAX_ENV_KEY     128
#define MAX_ENV_VALUE   512
#define MAX_LINEBUF    (MAX_ENV_KEY + MAX_ENV_VALUE + 64)

typedef struct {
    char key[MAX_ENV_KEY];
    char value[MAX_ENV_VALUE];
} env_entry_t;

/* ── Derived analysis (Redbear telemetry) ───────────────────── */
typedef struct {
    const char *boot_result;        /* "success" | "failure" | "incomplete" | "no_serial" */
    const char *failure_domain;     /* "none" | "firmware" | "bootloader" | "kernel" |
                                     * "hardware" | "userspace" | "unknown" */
    int reached_login;              /* saw login/complete stage */
    int kernel_panic_count;         /* error lines containing "panic" */
    int graphics_drm_seen;          /* any DRM/KMS line observed */
    char graphics_drivers[256];     /* comma-separated unique driver names found */
    char graphics_resolution[64];   /* first WxH token found, or "" */
} analysis_t;

static void trim_trailing_nl(char *s)
{
    size_t len = strlen(s);
    while (len > 0 && (s[len - 1] == '\n' || s[len - 1] == '\r'))
        s[--len] = '\0';
}

static int read_env_file(const char *path, env_entry_t *entries,
                         int max_entries, int *count)
{
    FILE *f = fopen(path, "r");
    if (!f)
        return -1;

    *count = 0;
    char buf[MAX_LINEBUF];

    while (*count < max_entries && fgets(buf, sizeof(buf), f)) {
        char *colon = strchr(buf, ':');
        if (!colon)
            continue;
        *colon = '\0';

        char *value = colon + 1;
        while (*value == ' ' || *value == '\t')
            value++;
        trim_trailing_nl(value);

        if (strlen(buf) == 0)
            continue;

        strncpy(entries[*count].key, buf, MAX_ENV_KEY - 1);
        entries[*count].key[MAX_ENV_KEY - 1] = '\0';
        strncpy(entries[*count].value, value, MAX_ENV_VALUE - 1);
        entries[*count].value[MAX_ENV_VALUE - 1] = '\0';
        (*count)++;
    }

    fclose(f);
    return 0;
}

static int read_cmdline(const char *path, char *cmdline, size_t maxlen)
{
    cmdline[0] = '\0';

    FILE *f = fopen(path, "r");
    if (!f)
        return -1;

    if (!fgets(cmdline, (int)maxlen, f)) {
        fclose(f);
        return -1;
    }

    fclose(f);
    trim_trailing_nl(cmdline);
    return 0;
}

static void json_escape(FILE *out, const char *str)
{
    fputc('"', out);
    while (*str) {
        unsigned char ch = (unsigned char)*str;
        switch (ch) {
        case '"':  fputs("\\\"", out); break;
        case '\\': fputs("\\\\", out); break;
        case '\n': fputs("\\n", out);  break;
        case '\r': fputs("\\r", out);  break;
        case '\t': fputs("\\t", out);  break;
        default:
            if (ch < 0x20)
                fprintf(out, "\\u%04x", ch);
            else
                fputc((int)ch, out);
        }
        str++;
    }
    fputc('"', out);
}

static void build_path(char *dst, size_t dstsz,
                       const char *dir, const char *file)
{
    snprintf(dst, dstsz, "%s/%s", dir, file);
}

/* ── Analysis derivation ────────────────────────────────────── */

static const char *stage_to_domain(boot_stage_t s)
{
    switch (s) {
    case STAGE_FIRMWARE:      return "firmware";
    case STAGE_BOOTLOADER:    return "bootloader";
    case STAGE_KERNEL_INIT:   return "kernel";
    case STAGE_HARDWARE_INIT: return "hardware";
    case STAGE_USERSPACE:     return "userspace";
    default:                  return "unknown";
    }
}

/* Scan a single text line for a WxH resolution token (e.g. "1024x768").
 * Returns 1 and fills out[] on success, 0 if none found. */
static int extract_resolution(const char *s, char *out, size_t outsz)
{
    for (const char *p = s; *p; p++) {
        if (*p < '0' || *p > '9')
            continue;
        const char *wstart = p;
        while (*p >= '0' && *p <= '9')
            p++;
        int wlen = (int)(p - wstart);
        if (wlen < 3 || wlen > 4)
            continue;
        if (*p != 'x' && *p != 'X')
            continue;
        p++;
        const char *hstart = p;
        while (*p >= '0' && *p <= '9')
            p++;
        int hlen = (int)(p - hstart);
        if (hlen < 3 || hlen > 4)
            continue;
        snprintf(out, outsz, "%.*sx%.*s", wlen, wstart, hlen, hstart);
        return 1;
    }
    return 0;
}

/* GPU driver names we can detect from serial console text.  These are
 * matched as substrings; only names literally present are reported. */
static const char *known_gpu_drivers[] = {
    "i915", "amdgpu", "radeon", "nouveau", "nvidia",
    "virtio_gpu", "virtio-gpu", "cirrus", "vmwgfx",
    "mgag200", "simpledrm", "ast", "panfrost", NULL
};

static void scan_graphics(const serial_summary_t *s, analysis_t *a)
{
    a->graphics_drivers[0] = '\0';
    a->graphics_resolution[0] = '\0';
    a->graphics_drm_seen = 0;

    int first = 1;
    for (serial_line_t *sl = s->lines; sl; sl = sl->next) {
        const char *t = sl->text;
        if (!t || !*t)
            continue;

        if (strstr(t, "[drm]") || strstr(t, "[DRM]") ||
            strstr(t, "drm_kms_helper") || strstr(t, "DRM:") ||
            strstr(t, "fb:"))
            a->graphics_drm_seen = 1;

        for (int i = 0; known_gpu_drivers[i]; i++) {
            if (strstr(t, known_gpu_drivers[i]) &&
                !strstr(a->graphics_drivers, known_gpu_drivers[i])) {
                if (!first)
                    strncat(a->graphics_drivers, ",",
                            sizeof(a->graphics_drivers) -
                            strlen(a->graphics_drivers) - 1);
                strncat(a->graphics_drivers, known_gpu_drivers[i],
                        sizeof(a->graphics_drivers) -
                        strlen(a->graphics_drivers) - 1);
                first = 0;
            }
        }

        if (a->graphics_resolution[0] == '\0')
            extract_resolution(t, a->graphics_resolution,
                               sizeof(a->graphics_resolution));
    }
}

static void derive_analysis(const serial_summary_t *s, int has_serial,
                            analysis_t *a)
{
    memset(a, 0, sizeof(*a));
    a->boot_result   = "no_serial";
    a->failure_domain = "unknown";
    a->kernel_panic_count = 0;

    if (!has_serial || !s)
        return;

    for (int i = 0; i < s->error_count; i++) {
        if (strstr(s->error_messages[i], "panic") ||
            strstr(s->error_messages[i], "Panic"))
            a->kernel_panic_count++;
    }

    a->reached_login = (s->lines_per_stage[STAGE_LOGIN] > 0 ||
                        s->lines_per_stage[STAGE_COMPLETE] > 0);

    /* Highest stage with observed lines (excluding error/unknown). */
    boot_stage_t progressed = STAGE_UNKNOWN;
    for (int i = STAGE_FIRMWARE; i < STAGE_ERROR; i++) {
        if (s->lines_per_stage[i] > 0)
            progressed = (boot_stage_t)i;
    }

    if (a->kernel_panic_count > 0) {
        a->boot_result   = "failure";
        a->failure_domain = (progressed != STAGE_UNKNOWN)
                              ? stage_to_domain(progressed) : "unknown";
    } else if (a->reached_login) {
        a->boot_result   = (s->error_count > 0) ? "success_with_warnings" : "success";
        a->failure_domain = "none";
    } else if (s->error_count > 0) {
        a->boot_result   = "failure";
        a->failure_domain = (progressed != STAGE_UNKNOWN)
                              ? stage_to_domain(progressed) : "unknown";
    } else {
        a->boot_result   = "incomplete";
        a->failure_domain = (progressed != STAGE_UNKNOWN)
                              ? stage_to_domain(progressed) : "unknown";
    }

    scan_graphics(s, a);
}

/* ── analysis.meta flat sidecar (consumed by initramfs init) ── */

static int write_analysis_meta(const char *path, const analysis_t *a,
                               const serial_summary_t *serial, int has_serial,
                               const trace_summary_t *trace, int has_trace)
{
    FILE *out = fopen(path, "w");
    if (!out)
        return -1;

    const char *stage = "unknown";
    if (has_serial && serial->final_stage >= 0 &&
        serial->final_stage < STAGE_COUNT)
        stage = boot_stage_names[serial->final_stage];

    /* stages_reached: comma-separated list of stages that had activity */
    char stages_reached[384] = "";
    if (has_serial) {
        int first = 1;
        for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) {
            if (serial->lines_per_stage[i] > 0) {
                if (!first) {
                    strncat(stages_reached, ",",
                            sizeof(stages_reached) - strlen(stages_reached) - 1);
                }
                strncat(stages_reached, boot_stage_names[i],
                        sizeof(stages_reached) - strlen(stages_reached) - 1);
                first = 0;
            }
        }
    }

    /* trace_duration_ms: from trace timestamps, already parsed */
    uint64_t trace_duration_ms = 0;
    if (has_trace && trace->last_timestamp > trace->first_timestamp)
        trace_duration_ms = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL;

    fprintf(out, "BOOT_RESULT=%s\n", a->boot_result);
    fprintf(out, "FAILURE_DOMAIN=%s\n", a->failure_domain);
    fprintf(out, "BOOT_STAGE_FINAL=%s\n", stage);
    fprintf(out, "STAGES_REACHED=%s\n", stages_reached);
    fprintf(out, "REACHED_LOGIN=%d\n", a->reached_login ? 1 : 0);
    fprintf(out, "KERNEL_PANIC_COUNT=%d\n", a->kernel_panic_count);
    fprintf(out, "ERROR_COUNT=%d\n", has_serial ? serial->error_count : 0);
    fprintf(out, "HAS_SERIAL=%d\n", has_serial ? 1 : 0);
    fprintf(out, "HAS_TRACE=%d\n", has_trace ? 1 : 0);
    fprintf(out, "BOOT_DURATION_MS=%" PRIu64 "\n",
            has_serial ? serial->boot_duration_ms : (uint64_t)0);
    fprintf(out, "TRACE_DURATION_MS=%" PRIu64 "\n", trace_duration_ms);
    fprintf(out, "GRAPHICS_DRIVERS=%s\n", a->graphics_drivers);
    fprintf(out, "GRAPHICS_RESOLUTION=%s\n", a->graphics_resolution);
    fprintf(out, "GRAPHICS_DRM_SEEN=%d\n", a->graphics_drm_seen ? 1 : 0);

    fclose(out);
    return 0;
}

/* ── report.json ────────────────────────────────────────────── */

static int write_report_json(const char *path,
                             const env_entry_t *env, int env_count,
                             const char *cmdline,
                             const serial_summary_t *serial, int has_serial,
                             const trace_summary_t *trace, int has_trace,
                             const analysis_t *analysis)
{
    FILE *out = fopen(path, "w");
    if (!out)
        return -1;

    fprintf(out, "{\n");

    fprintf(out, "  \"environment\": {");
    for (int i = 0; i < env_count; i++) {
        if (i > 0)
            fprintf(out, ",");
        fprintf(out, "\n    ");
        json_escape(out, env[i].key);
        fprintf(out, ": ");
        json_escape(out, env[i].value);
    }
    fprintf(out, env_count > 0 ? "\n  },\n" : "},\n");

    fprintf(out, "  \"qemu_cmdline\": ");
    json_escape(out, cmdline);
    fprintf(out, ",\n");

    const char *stage = "unknown";
    if (has_serial) {
        if (serial->final_stage >= 0 && serial->final_stage < STAGE_COUNT)
            stage = boot_stage_names[serial->final_stage];
    }
    fprintf(out, "  \"boot_stage\": ");
    json_escape(out, stage);
    fprintf(out, ",\n");

    fprintf(out, "  \"boot_duration_ms\": %" PRIu64 ",\n",
            has_serial ? serial->boot_duration_ms : (uint64_t)0);

    fprintf(out, "  \"errors\": [");
    for (int i = 0; has_serial && i < serial->error_count; i++) {
        if (i > 0)
            fprintf(out, ", ");
        json_escape(out, serial->error_messages[i]);
    }
    fprintf(out, "],\n");

    if (has_serial) {
        fprintf(out, "  \"serial_summary\": {\n");
        fprintf(out, "    \"total_lines\": %zu,\n", serial->total_lines);
        for (int i = 0; i < STAGE_COUNT; i++) {
            fprintf(out, "    \"lines_%s\": %zu%s\n",
                    boot_stage_names[i],
                    serial->lines_per_stage[i],
                    i < STAGE_COUNT - 1 ? "," : "");
        }
        fprintf(out, "  },\n");
    } else {
        fprintf(out, "  \"serial_summary\": null,\n");
    }

    if (has_trace) {
        uint64_t trace_dur_ms = 0;
        if (trace->last_timestamp > trace->first_timestamp)
            trace_dur_ms = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL;

        fprintf(out, "  \"trace_summary\": {\n");
        fprintf(out, "    \"total_events\": %" PRIu64 ",\n", trace->total_events);
        fprintf(out, "    \"disk_reads\": %" PRIu64 ",\n", trace->total_disk_reads);
        fprintf(out, "    \"disk_writes\": %" PRIu64 ",\n", trace->total_disk_writes);
        fprintf(out, "    \"serial_writes\": %" PRIu64 ",\n", trace->total_serial_writes);
        fprintf(out, "    \"first_timestamp_ns\": %" PRIu64 ",\n", trace->first_timestamp);
        fprintf(out, "    \"last_timestamp_ns\": %" PRIu64 ",\n", trace->last_timestamp);
        fprintf(out, "    \"duration_ms\": %" PRIu64 "\n", trace_dur_ms);
        fprintf(out, "  },\n");
    } else {
        fprintf(out, "  \"trace_summary\": null,\n");
    }

    /* Derived analysis block (additive, always present). */
    fprintf(out, "  \"analysis\": {\n");
    fprintf(out, "    \"boot_result\": ");
    json_escape(out, analysis->boot_result);
    fprintf(out, ",\n");
    fprintf(out, "    \"failure_domain\": ");
    json_escape(out, analysis->failure_domain);
    fprintf(out, ",\n");
    fprintf(out, "    \"boot_stage_final\": ");
    json_escape(out, stage);
    fprintf(out, ",\n");
    fprintf(out, "    \"stages_reached\": [");
    if (has_serial) {
        int first_stg = 1;
        for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) {
            if (serial->lines_per_stage[i] > 0) {
                if (!first_stg)
                    fprintf(out, ", ");
                json_escape(out, boot_stage_names[i]);
                first_stg = 0;
            }
        }
    }
    fprintf(out, "],\n");
    fprintf(out, "    \"reached_login\": %s,\n",
            analysis->reached_login ? "true" : "false");
    fprintf(out, "    \"kernel_panic_count\": %d,\n",
            analysis->kernel_panic_count);
    fprintf(out, "    \"error_count\": %d,\n",
            has_serial ? serial->error_count : 0);
    fprintf(out, "    \"has_serial\": %s,\n", has_serial ? "true" : "false");
    fprintf(out, "    \"has_trace\": %s,\n", has_trace ? "true" : "false");
    fprintf(out, "    \"boot_duration_ms\": %" PRIu64 ",\n",
            has_serial ? serial->boot_duration_ms : (uint64_t)0);
    {
        uint64_t tdm = 0;
        if (has_trace && trace->last_timestamp > trace->first_timestamp)
            tdm = (trace->last_timestamp - trace->first_timestamp) / 1000000ULL;
        fprintf(out, "    \"trace_duration_ms\": %" PRIu64 ",\n", tdm);
    }
    fprintf(out, "    \"graphics\": {\n");
    fprintf(out, "      \"drm_seen\": %s,\n",
            analysis->graphics_drm_seen ? "true" : "false");
    fprintf(out, "      \"drivers\": [");
    {
        const char *p = analysis->graphics_drivers;
        int first_drv = 1;
        while (*p) {
            const char *comma = strchr(p, ',');
            size_t len = comma ? (size_t)(comma - p) : strlen(p);
            if (len > 0) {
                if (!first_drv)
                    fprintf(out, ", ");
                fprintf(out, "\"%.*s\"", (int)len, p);
                first_drv = 0;
            }
            if (!comma)
                break;
            p = comma + 1;
        }
    }
    fprintf(out, "],\n");
    fprintf(out, "      \"resolution\": ");
    json_escape(out, analysis->graphics_resolution);
    fprintf(out, "\n");
    fprintf(out, "    }\n");
    fprintf(out, "  }\n");

    fprintf(out, "}\n");
    fclose(out);
    return 0;
}

/* ── report.txt ─────────────────────────────────────────────── */

static int write_report_txt(const char *path,
                            const env_entry_t *env, int env_count,
                            const char *cmdline,
                            const serial_summary_t *serial, int has_serial,
                            const trace_summary_t *trace, int has_trace,
                            const analysis_t *analysis)
{
    FILE *out = fopen(path, "w");
    if (!out)
        return -1;

    fprintf(out, "========================================\n");
    fprintf(out, "     hiperiso Boot Analysis Report\n");
    fprintf(out, "========================================\n\n");

    fprintf(out, "--- Environment ---\n");
    for (int i = 0; i < env_count; i++)
        fprintf(out, "  %-20s %s\n", env[i].key, env[i].value);
    if (env_count == 0)
        fprintf(out, "  (not available)\n");
    fprintf(out, "\n");

    fprintf(out, "--- QEMU Command Line ---\n");
    if (cmdline[0])
        fprintf(out, "  %s\n", cmdline);
    else
        fprintf(out, "  (not available)\n");
    fprintf(out, "\n");

    if (has_serial) {
        const char *stage = (serial->final_stage >= 0 &&
                             serial->final_stage < STAGE_COUNT)
                            ? boot_stage_names[serial->final_stage]
                            : "unknown";

        fprintf(out, "--- Serial Console Summary ---\n");
        fprintf(out, "  Total lines:       %zu\n", serial->total_lines);
        fprintf(out, "  Final boot stage:  %s\n", stage);
        fprintf(out, "  Boot duration:     %" PRIu64 " ms\n",
                serial->boot_duration_ms);
        fprintf(out, "\n  Lines per stage:\n");
        for (int i = 0; i < STAGE_COUNT; i++)
            fprintf(out, "    %-20s %zu\n",
                    boot_stage_names[i], serial->lines_per_stage[i]);

        if (serial->error_count > 0) {
            fprintf(out, "\n  Errors (%d):\n", serial->error_count);
            for (int i = 0; i < serial->error_count; i++)
                fprintf(out, "    %s\n", serial->error_messages[i]);
        }
        fprintf(out, "\n");
    } else {
        fprintf(out, "--- Serial Console: NOT AVAILABLE ---\n\n");
    }

    if (has_trace) {
        fprintf(out, "--- Trace Summary ---\n");
        trace_print_text(trace, out);
    } else {
        fprintf(out, "--- Trace: NOT AVAILABLE ---\n");
    }

    fprintf(out, "\n--- Derived Analysis ---\n");
    fprintf(out, "  Boot result:       %s\n", analysis->boot_result);
    fprintf(out, "  Failure domain:    %s\n", analysis->failure_domain);
    if (has_serial) {
        fprintf(out, "  Stage progression: ");
        int first_stg = 1;
        for (int i = STAGE_FIRMWARE; i <= STAGE_COMPLETE; i++) {
            if (serial->lines_per_stage[i] > 0) {
                fprintf(out, "%s%s",
                        first_stg ? "" : " \xe2\x86\x92 ",
                        boot_stage_names[i]);
                first_stg = 0;
            }
        }
        fprintf(out, "\n");
    }
    fprintf(out, "  Reached login:     %s\n",
            analysis->reached_login ? "yes" : "no");
    fprintf(out, "  Kernel panics:     %d\n", analysis->kernel_panic_count);
    fprintf(out, "  Error lines:       %d\n",
            has_serial ? serial->error_count : 0);
    fprintf(out, "  Graphics DRM seen: %s\n",
            analysis->graphics_drm_seen ? "yes" : "no");
    if (analysis->graphics_drivers[0])
        fprintf(out, "  Graphics drivers:  %s\n", analysis->graphics_drivers);
    if (analysis->graphics_resolution[0])
        fprintf(out, "  Graphics res:      %s\n", analysis->graphics_resolution);

    fprintf(out, "\n========================================\n");
    fprintf(out, "          End of Report\n");
    fprintf(out, "========================================\n");

    fclose(out);
    return 0;
}

int report_generate(const char *log_dir)
{
    if (!log_dir)
        return -1;

    char path[MAX_PATH];
    env_entry_t env[MAX_ENV_ENTRIES];
    int env_count = 0;
    char cmdline[MAX_CMDLINE];
    cmdline[0] = '\0';

    build_path(path, sizeof(path), log_dir, "env.txt");
    read_env_file(path, env, MAX_ENV_ENTRIES, &env_count);

    build_path(path, sizeof(path), log_dir, "qemu.cmdline");
    read_cmdline(path, cmdline, sizeof(cmdline));

    serial_summary_t serial;
    memset(&serial, 0, sizeof(serial));
    build_path(path, sizeof(path), log_dir, "serial.log");
    int has_serial = (serial_parse_file(path, &serial) == 0);

    trace_summary_t trace;
    memset(&trace, 0, sizeof(trace));
    build_path(path, sizeof(path), log_dir, "trace.bin");
    int has_trace = (trace_parse_file(path, &trace) == 0);

    analysis_t analysis;
    derive_analysis(has_serial ? &serial : NULL, has_serial, &analysis);

    int rc = 0;

    build_path(path, sizeof(path), log_dir, "report.json");
    if (write_report_json(path, env, env_count, cmdline,
                          &serial, has_serial, &trace, has_trace,
                          &analysis) != 0)
        rc = -1;

    build_path(path, sizeof(path), log_dir, "report.txt");
    if (write_report_txt(path, env, env_count, cmdline,
                         &serial, has_serial, &trace, has_trace,
                         &analysis) != 0)
        rc = -1;

    /* Flat sidecar for the initramfs to fold into the session manifest. */
    build_path(path, sizeof(path), log_dir, "analysis.meta");
    if (write_analysis_meta(path, &analysis, &serial, has_serial,
                            &trace, has_trace) != 0)
        rc = -1;

    if (has_serial)
        serial_free_summary(&serial);
    if (has_trace)
        trace_free_summary(&trace);

    return rc;
}
