#include "ui.h"
#include "../third_party/cJSON.h"

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

typedef struct {
    int loaded;
    char download_dir[512];
    long speed_down;
    int speed_down_enabled;
    long speed_up;
    int speed_up_enabled;
    int alt_enabled;
    long alt_down;
    long alt_up;
    int alt_time_enabled;
    int alt_time_begin;
    int alt_time_end;
    double seed_ratio_limit;
    int seed_ratio_enabled;
} SessionInfo;

static SessionInfo g_sess;
static int g_cursor;
static int g_attempted;  /* we tried session-get at least once since opening */
static int g_connected;  /* whether that attempt (or the latest one) succeeded */

enum {
    /* Daemon connection - these live in Config, not the RPC session, and
     * are saved to config.ini (CLI flags still override them at startup). */
    F_HOST,
    F_PORT,
    F_USERNAME,
    F_PASSWORD,
    F_SHOW_SPLASH,
    /* Everything below is read/written via Transmission's session-get/set. */
    F_DOWNLOAD_DIR,
    F_SPEED_DOWN_ENABLED,
    F_SPEED_DOWN,
    F_SPEED_UP_ENABLED,
    F_SPEED_UP,
    F_ALT_ENABLED,
    F_ALT_DOWN,
    F_ALT_UP,
    F_ALT_TIME_ENABLED,
    F_ALT_TIME_BEGIN,
    F_ALT_TIME_END,
    F_SEED_RATIO_ENABLED,
    F_SEED_RATIO,
    F_COUNT,
};
#define F_DAEMON_COUNT (F_DOWNLOAD_DIR)

static long ji(const cJSON *o, const char *k, long def)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, k);
    return (v && cJSON_IsNumber(v)) ? (long)v->valuedouble : def;
}
static double jd(const cJSON *o, const char *k, double def)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, k);
    return (v && cJSON_IsNumber(v)) ? v->valuedouble : def;
}
static int jb(const cJSON *o, const char *k, int def)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, k);
    return v ? cJSON_IsTrue(v) : def;
}
static void js(const cJSON *o, const char *k, char *buf, size_t n)
{
    const cJSON *v = cJSON_GetObjectItemCaseSensitive(o, k);
    if (v && cJSON_IsString(v))
        snprintf(buf, n, "%s", v->valuestring);
}

static int fetch_session(AppState *st, char *err, size_t errlen)
{
    cJSON *args = NULL;
    if (rpc_call(st->rpc, "session-get", NULL, &args, err, errlen) != 0)
        return -1;

    memset(&g_sess, 0, sizeof(g_sess));
    js(args, "download-dir", g_sess.download_dir, sizeof(g_sess.download_dir));
    g_sess.speed_down = ji(args, "speed-limit-down", 0);
    g_sess.speed_down_enabled = jb(args, "speed-limit-down-enabled", 0);
    g_sess.speed_up = ji(args, "speed-limit-up", 0);
    g_sess.speed_up_enabled = jb(args, "speed-limit-up-enabled", 0);
    g_sess.alt_enabled = jb(args, "alt-speed-enabled", 0);
    g_sess.alt_down = ji(args, "alt-speed-down", 0);
    g_sess.alt_up = ji(args, "alt-speed-up", 0);
    g_sess.alt_time_enabled = jb(args, "alt-speed-time-enabled", 0);
    g_sess.alt_time_begin = (int)ji(args, "alt-speed-time-begin", 0);
    g_sess.alt_time_end = (int)ji(args, "alt-speed-time-end", 0);
    g_sess.seed_ratio_limit = jd(args, "seedRatioLimit", 0.0);
    g_sess.seed_ratio_enabled = jb(args, "seedRatioLimited", 0);
    g_sess.loaded = 1;

    cJSON_Delete(args);
    return 0;
}

static int push_session(AppState *st, cJSON *args, char *err, size_t errlen)
{
    return rpc_call(st->rpc, "session-set", args, NULL, err, errlen);
}

static void fmt_hhmm(int minutes, char *buf, size_t n)
{
    snprintf(buf, n, "%02d:%02d", (minutes / 60) % 24, minutes % 60);
}

static int parse_hhmm(const char *s)
{
    int h = 0, m = 0;
    if (sscanf(s, "%d:%d", &h, &m) != 2)
        return -1;
    if (h < 0 || h > 23 || m < 0 || m > 59)
        return -1;
    return h * 60 + m;
}

/* If the daemon's current session settings don't match what the user last
 * explicitly set via this screen (cfg->cache_*), push the cached values
 * back - transmission-daemon only writes settings.json on a clean
 * shutdown, so an unclean restart (crash, power loss, kill -9) silently
 * reverts RPC-set session values to whatever was last saved. This keeps
 * the user's intent durable across that. Only runs once cache_set has ever
 * been populated (i.e. the user has changed a session field at least once
 * via transtui), and intentionally wins over changes made by other RPC
 * clients since transtui was last opened - that tradeoff is what the user
 * asked for. */
static void reconcile_cache(AppState *st)
{
    Config *cfg = st->cfg;
    if (!cfg->session_cache_set)
        return;

    cJSON *args = cJSON_CreateObject();
    int changed = 0;

    if (strcmp(cfg->cache_download_dir, g_sess.download_dir) != 0) {
        cJSON_AddStringToObject(args, "download-dir", cfg->cache_download_dir);
        changed++;
    }
    if (cfg->cache_speed_down_enabled != g_sess.speed_down_enabled) {
        cJSON_AddBoolToObject(args, "speed-limit-down-enabled", cfg->cache_speed_down_enabled);
        changed++;
    }
    if (cfg->cache_speed_down != g_sess.speed_down) {
        cJSON_AddNumberToObject(args, "speed-limit-down", (double)cfg->cache_speed_down);
        changed++;
    }
    if (cfg->cache_speed_up_enabled != g_sess.speed_up_enabled) {
        cJSON_AddBoolToObject(args, "speed-limit-up-enabled", cfg->cache_speed_up_enabled);
        changed++;
    }
    if (cfg->cache_speed_up != g_sess.speed_up) {
        cJSON_AddNumberToObject(args, "speed-limit-up", (double)cfg->cache_speed_up);
        changed++;
    }
    if (cfg->cache_alt_enabled != g_sess.alt_enabled) {
        cJSON_AddBoolToObject(args, "alt-speed-enabled", cfg->cache_alt_enabled);
        changed++;
    }
    if (cfg->cache_alt_down != g_sess.alt_down) {
        cJSON_AddNumberToObject(args, "alt-speed-down", (double)cfg->cache_alt_down);
        changed++;
    }
    if (cfg->cache_alt_up != g_sess.alt_up) {
        cJSON_AddNumberToObject(args, "alt-speed-up", (double)cfg->cache_alt_up);
        changed++;
    }
    if (cfg->cache_alt_time_enabled != g_sess.alt_time_enabled) {
        cJSON_AddBoolToObject(args, "alt-speed-time-enabled", cfg->cache_alt_time_enabled);
        changed++;
    }
    if (cfg->cache_alt_time_begin != g_sess.alt_time_begin) {
        cJSON_AddNumberToObject(args, "alt-speed-time-begin", cfg->cache_alt_time_begin);
        changed++;
    }
    if (cfg->cache_alt_time_end != g_sess.alt_time_end) {
        cJSON_AddNumberToObject(args, "alt-speed-time-end", cfg->cache_alt_time_end);
        changed++;
    }
    if (cfg->cache_seed_ratio_enabled != g_sess.seed_ratio_enabled) {
        cJSON_AddBoolToObject(args, "seedRatioLimited", cfg->cache_seed_ratio_enabled);
        changed++;
    }
    double ratio_diff = cfg->cache_seed_ratio_limit - g_sess.seed_ratio_limit;
    if (ratio_diff < 0)
        ratio_diff = -ratio_diff;
    if (ratio_diff > 0.001) {
        cJSON_AddNumberToObject(args, "seedRatioLimit", cfg->cache_seed_ratio_limit);
        changed++;
    }

    if (changed == 0) {
        cJSON_Delete(args);
        return;
    }

    char err[256];
    if (push_session(st, args, err, sizeof(err)) == 0) {
        snprintf(g_sess.download_dir, sizeof(g_sess.download_dir), "%s", cfg->cache_download_dir);
        g_sess.speed_down = cfg->cache_speed_down;
        g_sess.speed_down_enabled = cfg->cache_speed_down_enabled;
        g_sess.speed_up = cfg->cache_speed_up;
        g_sess.speed_up_enabled = cfg->cache_speed_up_enabled;
        g_sess.alt_enabled = cfg->cache_alt_enabled;
        g_sess.alt_down = cfg->cache_alt_down;
        g_sess.alt_up = cfg->cache_alt_up;
        g_sess.alt_time_enabled = cfg->cache_alt_time_enabled;
        g_sess.alt_time_begin = cfg->cache_alt_time_begin;
        g_sess.alt_time_end = cfg->cache_alt_time_end;
        g_sess.seed_ratio_limit = cfg->cache_seed_ratio_limit;
        g_sess.seed_ratio_enabled = cfg->cache_seed_ratio_enabled;
        ui_set_status(st, "Restored %d setting(s) the daemon had reverted", changed);
    }
    /* On failure just leave the daemon's current values shown - don't block
     * opening settings or nag about it every time. */
}

/* Popup geometry, shared between render and the row->y mapping used by input. */
static int g_popup_h, g_popup_w;

static int field_row(int field)
{
    int row = field;
    if (field >= F_DOWNLOAD_DIR)
        row += 1; /* blank separator line between daemon and session fields */
    return row;
}

void ui_settings_reconcile_now(AppState *st)
{
    char err[256];
    if (fetch_session(st, err, sizeof(err)) == 0)
        reconcile_cache(st);
}

void ui_settings_render(AppState *st)
{
    /* Always open the popup, even if the daemon is unreachable - that's
     * often exactly when the user needs it, to fix the host/port. Only the
     * session-derived fields (download dir, speed limits, ...) depend on a
     * live connection; the daemon fields below always work. */
    if (!g_attempted) {
        char err[256];
        g_connected = (fetch_session(st, err, sizeof(err)) == 0);
        if (!g_connected) {
            memset(&g_sess, 0, sizeof(g_sess));
            ui_set_status(st, "Not connected: %s", err);
        } else {
            reconcile_cache(st);
        }
        g_attempted = 1;
        g_cursor = 0;
    }

    /* Full-screen view rather than a popup layered over the list backdrop -
     * matches ui_help_render()'s pattern. An overlapping popup+backdrop here
     * turned out unreliable in practice (state changed correctly on 'g' but
     * nothing visibly redrew until a later, unrelated screen update forced
     * it), and a dedicated screen sidesteps that class of issue entirely. */
    int rows, cols;
    getmaxyx(stdscr, rows, cols);
    erase();

    g_popup_h = rows - 2;
    g_popup_w = cols - 2;
    if (g_popup_h < 4)
        g_popup_h = rows > 4 ? 4 : rows;
    if (g_popup_w < 10)
        g_popup_w = cols > 10 ? 10 : cols;

    WINDOW *win = derwin(stdscr, g_popup_h, g_popup_w, 1, 1);
    ui_box(win, g_popup_h, g_popup_w);
    ui_box_title(win, g_popup_w, g_connected ? "Settings" : "Settings - not connected");

    char hhmm_begin[8], hhmm_end[8];
    fmt_hhmm(g_sess.alt_time_begin, hhmm_begin, sizeof(hhmm_begin));
    fmt_hhmm(g_sess.alt_time_end, hhmm_end, sizeof(hhmm_end));

    char line[F_COUNT][640];
    snprintf(line[F_HOST], sizeof(line[0]), "Host:                        %s", st->cfg->host);
    snprintf(line[F_PORT], sizeof(line[0]), "Port:                        %d", st->cfg->port);
    snprintf(line[F_USERNAME], sizeof(line[0]), "Username:                    %s",
             st->cfg->username[0] ? st->cfg->username : "(none)");
    snprintf(line[F_PASSWORD], sizeof(line[0]), "Password:                    %s",
             st->cfg->password[0] ? "********" : "(none)");
    snprintf(line[F_SHOW_SPLASH], sizeof(line[0]), "Splash screen:               %s",
             st->cfg->show_splash ? "On" : "Off");
    snprintf(line[F_DOWNLOAD_DIR], sizeof(line[0]), "Download folder:             %s",
             g_connected ? g_sess.download_dir : "(not connected)");
    snprintf(line[F_SPEED_DOWN_ENABLED], sizeof(line[0]), "Download limit:              %s",
             g_sess.speed_down_enabled ? "On" : "Off");
    snprintf(line[F_SPEED_DOWN], sizeof(line[0]), "  Limit (KB/s):              %ld", g_sess.speed_down);
    snprintf(line[F_SPEED_UP_ENABLED], sizeof(line[0]), "Upload limit:                %s",
             g_sess.speed_up_enabled ? "On" : "Off");
    snprintf(line[F_SPEED_UP], sizeof(line[0]), "  Limit (KB/s):              %ld", g_sess.speed_up);
    snprintf(line[F_ALT_ENABLED], sizeof(line[0]), "Alternative speed:           %s",
             g_sess.alt_enabled ? "On" : "Off");
    snprintf(line[F_ALT_DOWN], sizeof(line[0]), "  Down (KB/s):               %ld", g_sess.alt_down);
    snprintf(line[F_ALT_UP], sizeof(line[0]), "  Up (KB/s):                 %ld", g_sess.alt_up);
    snprintf(line[F_ALT_TIME_ENABLED], sizeof(line[0]), "  Schedule:                  %s",
             g_sess.alt_time_enabled ? "On" : "Off");
    snprintf(line[F_ALT_TIME_BEGIN], sizeof(line[0]), "    Start:                   %s", hhmm_begin);
    snprintf(line[F_ALT_TIME_END], sizeof(line[0]), "    End:                     %s", hhmm_end);
    snprintf(line[F_SEED_RATIO_ENABLED], sizeof(line[0]), "Ratio limit:                 %s",
             g_sess.seed_ratio_enabled ? "On" : "Off");
    snprintf(line[F_SEED_RATIO], sizeof(line[0]), "  Ratio:                     %.2f", g_sess.seed_ratio_limit);

    int top = 2;
    int field_w = g_popup_w - 4;
    for (int i = 0; i < F_COUNT; i++) {
        int y = top + field_row(i);
        if (y >= g_popup_h - 2)
            break;
        int attr = (i == g_cursor) ? (COLOR_PAIR(CP_SELROW) | A_BOLD) : 0;
        wattron(win, attr);
        mvwprintw(win, y, 2, "%-*.*s", field_w, field_w, line[i]);
        wattroff(win, attr);
    }

    wattron(win, COLOR_PAIR(CP_BORDER));
    mvwprintw(win, g_popup_h - 2, 2, "%.*s", field_w,
              g_connected
                  ? "Esc/q=close  Enter/space=change  \xe2\x86\x91\xe2\x86\x93=navigate"
                  : "Esc/q=close  Enter/space=change  r=reconnect  \xe2\x86\x91\xe2\x86\x93=navigate");
    wattroff(win, COLOR_PAIR(CP_BORDER));

    wnoutrefresh(win);
    delwin(win);
    doupdate();
}

static void apply_field(AppState *st, int field)
{
    cJSON *args = cJSON_CreateObject();
    char err[256];
    int ok = 1;
    int daemon_changed = 0;
    int local_changed = 0; /* config.ini-only setting, no reconnect needed */

    switch (field) {
    case F_HOST: {
        char v[256];
        snprintf(v, sizeof(v), "%s", st->cfg->host);
        if (ui_prompt("Host", v, v, sizeof(v)) && v[0]) {
            snprintf(st->cfg->host, sizeof(st->cfg->host), "%s", v);
            daemon_changed = 1;
        }
        ok = 0; /* not a session-set field */
        break;
    }
    case F_PORT: {
        char v[16];
        snprintf(v, sizeof(v), "%d", st->cfg->port);
        if (ui_prompt("Port", v, v, sizeof(v))) {
            int p = atoi(v);
            if (p > 0 && p <= 65535) {
                st->cfg->port = p;
                daemon_changed = 1;
            } else {
                ui_set_status(st, "Invalid port");
            }
        }
        ok = 0;
        break;
    }
    case F_USERNAME: {
        char v[128];
        snprintf(v, sizeof(v), "%s", st->cfg->username);
        if (ui_prompt("Username", v, v, sizeof(v))) {
            snprintf(st->cfg->username, sizeof(st->cfg->username), "%s", v);
            daemon_changed = 1;
        }
        ok = 0;
        break;
    }
    case F_PASSWORD: {
        char v[128];
        snprintf(v, sizeof(v), "%s", st->cfg->password);
        if (ui_prompt("Password", v, v, sizeof(v))) {
            snprintf(st->cfg->password, sizeof(st->cfg->password), "%s", v);
            daemon_changed = 1;
        }
        ok = 0;
        break;
    }
    case F_SHOW_SPLASH:
        st->cfg->show_splash = !st->cfg->show_splash;
        local_changed = 1;
        ok = 0;
        break;
    case F_DOWNLOAD_DIR: {
        char v[512];
        int got;
        if (ui_confirm("Download folder", "Browse for a folder? (No = type path)")) {
            got = ui_folder_browser(g_sess.download_dir, v, sizeof(v));
        } else {
            snprintf(v, sizeof(v), "%s", g_sess.download_dir);
            got = ui_prompt("Download folder", v, v, sizeof(v));
        }
        if (got) {
            snprintf(g_sess.download_dir, sizeof(g_sess.download_dir), "%s", v);
            cJSON_AddStringToObject(args, "download-dir", g_sess.download_dir);
        } else {
            ok = 0;
        }
        break;
    }
    case F_SPEED_DOWN_ENABLED:
        g_sess.speed_down_enabled = !g_sess.speed_down_enabled;
        cJSON_AddBoolToObject(args, "speed-limit-down-enabled", g_sess.speed_down_enabled);
        break;
    case F_SPEED_DOWN: {
        char v[32];
        snprintf(v, sizeof(v), "%ld", g_sess.speed_down);
        if (ui_prompt("Download limit KB/s", v, v, sizeof(v))) {
            g_sess.speed_down = strtol(v, NULL, 10);
            cJSON_AddNumberToObject(args, "speed-limit-down", (double)g_sess.speed_down);
        } else {
            ok = 0;
        }
        break;
    }
    case F_SPEED_UP_ENABLED:
        g_sess.speed_up_enabled = !g_sess.speed_up_enabled;
        cJSON_AddBoolToObject(args, "speed-limit-up-enabled", g_sess.speed_up_enabled);
        break;
    case F_SPEED_UP: {
        char v[32];
        snprintf(v, sizeof(v), "%ld", g_sess.speed_up);
        if (ui_prompt("Upload limit KB/s", v, v, sizeof(v))) {
            g_sess.speed_up = strtol(v, NULL, 10);
            cJSON_AddNumberToObject(args, "speed-limit-up", (double)g_sess.speed_up);
        } else {
            ok = 0;
        }
        break;
    }
    case F_ALT_ENABLED:
        g_sess.alt_enabled = !g_sess.alt_enabled;
        cJSON_AddBoolToObject(args, "alt-speed-enabled", g_sess.alt_enabled);
        break;
    case F_ALT_DOWN: {
        char v[32];
        snprintf(v, sizeof(v), "%ld", g_sess.alt_down);
        if (ui_prompt("Alternative download speed KB/s", v, v, sizeof(v))) {
            g_sess.alt_down = strtol(v, NULL, 10);
            cJSON_AddNumberToObject(args, "alt-speed-down", (double)g_sess.alt_down);
        } else {
            ok = 0;
        }
        break;
    }
    case F_ALT_UP: {
        char v[32];
        snprintf(v, sizeof(v), "%ld", g_sess.alt_up);
        if (ui_prompt("Alternative upload speed KB/s", v, v, sizeof(v))) {
            g_sess.alt_up = strtol(v, NULL, 10);
            cJSON_AddNumberToObject(args, "alt-speed-up", (double)g_sess.alt_up);
        } else {
            ok = 0;
        }
        break;
    }
    case F_ALT_TIME_ENABLED:
        g_sess.alt_time_enabled = !g_sess.alt_time_enabled;
        cJSON_AddBoolToObject(args, "alt-speed-time-enabled", g_sess.alt_time_enabled);
        break;
    case F_ALT_TIME_BEGIN: {
        char v[8];
        fmt_hhmm(g_sess.alt_time_begin, v, sizeof(v));
        if (ui_prompt("Start time (HH:MM)", v, v, sizeof(v))) {
            int mins = parse_hhmm(v);
            if (mins < 0) {
                ui_set_status(st, "Invalid time, use HH:MM");
                ok = 0;
            } else {
                g_sess.alt_time_begin = mins;
                cJSON_AddNumberToObject(args, "alt-speed-time-begin", mins);
            }
        } else {
            ok = 0;
        }
        break;
    }
    case F_ALT_TIME_END: {
        char v[8];
        fmt_hhmm(g_sess.alt_time_end, v, sizeof(v));
        if (ui_prompt("End time (HH:MM)", v, v, sizeof(v))) {
            int mins = parse_hhmm(v);
            if (mins < 0) {
                ui_set_status(st, "Invalid time, use HH:MM");
                ok = 0;
            } else {
                g_sess.alt_time_end = mins;
                cJSON_AddNumberToObject(args, "alt-speed-time-end", mins);
            }
        } else {
            ok = 0;
        }
        break;
    }
    case F_SEED_RATIO_ENABLED:
        g_sess.seed_ratio_enabled = !g_sess.seed_ratio_enabled;
        cJSON_AddBoolToObject(args, "seedRatioLimited", g_sess.seed_ratio_enabled);
        break;
    case F_SEED_RATIO: {
        char v[32];
        snprintf(v, sizeof(v), "%.2f", g_sess.seed_ratio_limit);
        if (ui_prompt("Ratio limit", v, v, sizeof(v))) {
            g_sess.seed_ratio_limit = strtod(v, NULL);
            cJSON_AddNumberToObject(args, "seedRatioLimit", g_sess.seed_ratio_limit);
        } else {
            ok = 0;
        }
        break;
    }
    default:
        ok = 0;
        break;
    }

    if (daemon_changed) {
        /* Live-apply to the RPC client and start a fresh CSRF handshake -
         * the daemon we're now talking to may not know our old session id. */
        snprintf(st->rpc->host, sizeof(st->rpc->host), "%s", st->cfg->host);
        st->rpc->port = st->cfg->port;
        snprintf(st->rpc->user, sizeof(st->rpc->user), "%s", st->cfg->username);
        snprintf(st->rpc->pass, sizeof(st->rpc->pass), "%s", st->cfg->password);
        st->rpc->session_id[0] = '\0';

        if (config_save(st->cfg, err, sizeof(err)) != 0)
            ui_set_status(st, "Error saving: %s", err);
        else
            ui_set_status(st, "Connection settings saved");
        g_attempted = 0; /* re-fetch session-get against the (possibly new) daemon */
        st->need_poll_now = 1;
        cJSON_Delete(args);
        return;
    }

    if (local_changed) {
        if (config_save(st->cfg, err, sizeof(err)) != 0)
            ui_set_status(st, "Error saving: %s", err);
        else
            ui_set_status(st, "Setting saved");
        cJSON_Delete(args);
        return;
    }

    if (!ok || cJSON_GetArraySize(args) == 0) {
        cJSON_Delete(args);
        return;
    }

    if (push_session(st, args, err, sizeof(err)) != 0) {
        ui_set_status(st, "Error: %s", err);
        return;
    }

    /* Remember what was just set so it can be restored if the daemon later
     * forgets it - see reconcile_cache() and Config's cache_* fields. */
    Config *cfg = st->cfg;
    cfg->session_cache_set = 1;
    snprintf(cfg->cache_download_dir, sizeof(cfg->cache_download_dir), "%s", g_sess.download_dir);
    cfg->cache_speed_down = g_sess.speed_down;
    cfg->cache_speed_down_enabled = g_sess.speed_down_enabled;
    cfg->cache_speed_up = g_sess.speed_up;
    cfg->cache_speed_up_enabled = g_sess.speed_up_enabled;
    cfg->cache_alt_enabled = g_sess.alt_enabled;
    cfg->cache_alt_down = g_sess.alt_down;
    cfg->cache_alt_up = g_sess.alt_up;
    cfg->cache_alt_time_enabled = g_sess.alt_time_enabled;
    cfg->cache_alt_time_begin = g_sess.alt_time_begin;
    cfg->cache_alt_time_end = g_sess.alt_time_end;
    cfg->cache_seed_ratio_limit = g_sess.seed_ratio_limit;
    cfg->cache_seed_ratio_enabled = g_sess.seed_ratio_enabled;
    config_save(cfg, err, sizeof(err)); /* best effort - a failure here just means no cache this time */

    ui_set_status(st, "Setting saved");
}

void ui_settings_handle_key(AppState *st, int ch)
{
    switch (ch) {
    case 'q':
    case 27:
        g_attempted = 0;
        st->view = VIEW_LIST;
        break;
    case 'r':
        if (!g_connected) {
            ui_offer_reconnect_help(st);
            g_attempted = 0; /* retry session-get on next render either way */
        }
        break;
    case KEY_UP:
    case 'k':
        if (g_cursor > 0)
            g_cursor--;
        break;
    case KEY_DOWN:
    case 'j':
        if (g_cursor + 1 < F_COUNT)
            g_cursor++;
        break;
    case '\n':
    case '\r':
    case KEY_ENTER:
    case ' ':
        apply_field(st, g_cursor);
        break;
    default:
        break;
    }
}
