/*
 * leaderboard.cpp — submission + browse over HTTP.
 *
 * Hand-rolled JSON build/parse. The wire schema is tiny and fixed-shape
 * (see plan); no point in pulling in a JSON dep just to handle two
 * response types.
 *
 * Threading: callbacks fire from http_pump() (native) or the Emscripten
 * runtime (web) — both single-threaded relative to the game loop, so
 * mutating the global state structs from the callbacks is safe.
 */
// pfile.h (pulled in via score.h via leaderboard.h) uses FILE without
// including stdio.h itself; pull it in first.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "leaderboard.h"
#include "httpclient.h"
#include "prefs.h"
#include "kobo.h"
#include "kobolog.h"

extern prefs_t *prefs;

namespace {

leaderboard_submission_t g_sub  = { LB_IDLE, 0, 0 };
leaderboard_browse_t     g_brw  = { LB_IDLE, 0, 0, {}, "" };

bool enabled()
{
    return prefs && prefs->global_leaderboard
        && prefs->leaderboard_url[0]   != '\0'
        && prefs->leaderboard_board[0] != '\0';
}

// ---- JSON helpers ----------------------------------------------------------
//
// Build: snprintf with manual escaping for string fields.
// Parse: substring-search the known keys. Tolerates whitespace but assumes
// the server emits one occurrence of each key (which our server contract does).

void json_escape(char *out, size_t cap, const char *in)
{
    size_t o = 0;
    for(size_t i = 0; in[i] && o + 2 < cap; ++i)
    {
        char c = in[i];
        if(c == '"' || c == '\\')
        {
            if(o + 3 >= cap) break;
            out[o++] = '\\'; out[o++] = c;
        }
        else if((unsigned char)c < 0x20)
        {
            // Skip control bytes; the in-game name editor only emits ASCII
            // letters anyway. Logging this would be noise.
            continue;
        }
        else out[o++] = c;
    }
    out[o] = '\0';
}

// Find `"key":` and return pointer to first char after the colon, or NULL.
const char *find_key(const char *body, const char *key)
{
    size_t klen = strlen(key);
    for(const char *p = body; (p = strchr(p, '"')) != NULL; ++p)
    {
        ++p;
        if(strncmp(p, key, klen) != 0) continue;
        if(p[klen] != '"') continue;
        const char *q = p + klen + 1;
        while(*q == ' ' || *q == '\t') ++q;
        if(*q != ':') continue;
        ++q;
        while(*q == ' ' || *q == '\t') ++q;
        return q;
    }
    return NULL;
}

bool parse_int(const char *p, int *out)
{
    if(!p) return false;
    char *end = NULL;
    long v = strtol(p, &end, 10);
    if(end == p) return false;
    *out = (int)v;
    return true;
}

// Copies the next "string" into out, returns pointer past the closing quote.
const char *parse_string(const char *p, char *out, size_t cap)
{
    if(!p || *p != '"') { if(cap) out[0] = '\0'; return NULL; }
    ++p;
    size_t o = 0;
    while(*p && *p != '"')
    {
        if(*p == '\\' && p[1])
        {
            if(o + 1 < cap) out[o++] = p[1];
            p += 2;
        }
        else
        {
            if(o + 1 < cap) out[o++] = *p;
            ++p;
        }
    }
    if(cap) out[o] = '\0';
    return *p == '"' ? p + 1 : NULL;
}

// ---- callbacks --------------------------------------------------------------

void on_submit(const http_response_t *resp, void * /*user*/)
{
    if(resp->status == 0)
    {
        g_sub.status = LB_OFFLINE;
        log_printf(WLOG, "leaderboard: submission failed (network)\n");
        return;
    }
    if(resp->status / 100 != 2)
    {
        g_sub.status = LB_FAILED;
        log_printf(WLOG, "leaderboard: submission rejected (HTTP %d)\n",
                   resp->status);
        return;
    }
    int rank = -1, total = -1;
    parse_int(find_key(resp->body, "rank"),  &rank);
    parse_int(find_key(resp->body, "total"), &total);
    g_sub.rank   = rank;
    g_sub.total  = total;
    g_sub.status = LB_DONE;
}

void on_browse(const http_response_t *resp, void * /*user*/)
{
    if(resp->status == 0)        { g_brw.status = LB_OFFLINE; return; }
    if(resp->status / 100 != 2)  { g_brw.status = LB_FAILED;  return; }

    g_brw.count = 0;
    int total = -1;
    parse_int(find_key(resp->body, "total"), &total);
    g_brw.total = total;

    // "scores": [ {name, score, end_scene}, ... ]
    const char *p = find_key(resp->body, "scores");
    if(!p || *p != '[') { g_brw.status = LB_FAILED; return; }
    ++p;
    while(g_brw.count < LEADERBOARD_BROWSE_MAX)
    {
        while(*p && *p != '{' && *p != ']') ++p;
        if(*p != '{') break;
        // Each entry is a small object — scan it locally.
        const char *end = strchr(p, '}');
        if(!end) break;

        leaderboard_entry_t &e = g_brw.entries[g_brw.count];
        e.name[0] = '\0';
        e.score = 0;
        e.end_scene = 0;

        const char *kp = find_key(p, "name");
        if(kp && kp < end) parse_string(kp, e.name, sizeof(e.name));

        kp = find_key(p, "score");
        int sv = 0;
        if(kp && kp < end && parse_int(kp, &sv) && sv >= 0)
            e.score = (unsigned int)sv;

        kp = find_key(p, "end_scene");
        if(kp && kp < end) parse_int(kp, &e.end_scene);

        ++g_brw.count;
        p = end + 1;
    }
    g_brw.status = LB_DONE;
}

} // namespace

namespace leaderboard {

void submit_score(const s_hiscore_t *hi)
{
    g_sub.status = LB_IDLE;
    g_sub.rank   = -1;
    g_sub.total  = -1;
    if(!enabled() || !hi) return;

    char name_esc[LEADERBOARD_NAME_MAX * 2 + 1];
    char board_esc[LEADERBOARD_NAME_MAX * 2 + 1];
    char id_esc[128];
    const char *player_name = (hi->profile && hi->profile->name[0])
                                ? hi->profile->name : "ANON";
    json_escape(name_esc,  sizeof(name_esc),  player_name);
    json_escape(board_esc, sizeof(board_esc), prefs->leaderboard_board);
    json_escape(id_esc,    sizeof(id_esc),    prefs->client_id);

    char body[1024];
    int n = snprintf(body, sizeof(body),
        "{\"board\":\"%s\",\"name\":\"%s\",\"client_id\":\"%s\","
        "\"client\":\"kobodl/0.5.1\","
        "\"score\":%u,\"skill\":%d,\"gametype\":%d,"
        "\"end_scene\":%d,\"end_lives\":%d,\"end_health\":%d,"
        "\"playtime\":%u,\"start_date\":%u,\"end_date\":%u}",
        board_esc, name_esc, id_esc,
        hi->score, hi->skill, hi->gametype,
        hi->end_scene, hi->end_lives, hi->end_health,
        hi->playtime, hi->start_date, hi->end_date);
    if(n <= 0 || n >= (int)sizeof(body)) return;

    char url[512];
    snprintf(url, sizeof(url), "%s/api/scores", prefs->leaderboard_url);

    g_sub.status = LB_INFLIGHT;
    http_post_json(url, body, on_submit, NULL);
}

void fetch_board()
{
    g_brw.status = LB_IDLE;
    g_brw.count  = 0;
    g_brw.total  = 0;
    if(!enabled()) return;

    strncpy(g_brw.board, prefs->leaderboard_board, sizeof(g_brw.board) - 1);
    g_brw.board[sizeof(g_brw.board) - 1] = '\0';

    char board_esc[LEADERBOARD_NAME_MAX * 2 + 1];
    json_escape(board_esc, sizeof(board_esc), prefs->leaderboard_board);

    char url[768];
    snprintf(url, sizeof(url),
             "%s/api/scores?board=%s&limit=%d",
             prefs->leaderboard_url, board_esc, LEADERBOARD_BROWSE_MAX);

    g_brw.status = LB_INFLIGHT;
    http_get(url, on_browse, NULL);
}

const leaderboard_submission_t *submission() { return &g_sub; }
const leaderboard_browse_t     *browse()     { return &g_brw; }

void reset_submission()
{
    g_sub.status = LB_IDLE;
    g_sub.rank   = -1;
    g_sub.total  = -1;
}

} // namespace leaderboard
