/*
 * retoor <retoor@molodetz.nl>
 * DWN - Desktop Window Manager
 * WebSocket API implementation with event subscription system
 */

#include "api.h"
#include "dwn.h"
#include "config.h"
#include "util.h"
#include "workspace.h"
#include "client.h"
#include "layout.h"
#include "keys.h"
#include "screenshot.h"
#include "ocr.h"
#include "notifications.h"
#include "systray.h"
#include "panel.h"
#include "ai.h"
#include "news.h"
#include "demo.h"
#include "cJSON.h"
#include "mongoose.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/extensions/XTest.h>

#define MAX_SUBSCRIBED_CONNECTIONS 64

static struct mg_mgr mgr;
static bool initialized = false;

typedef struct {
    struct mg_connection *conn;
    bool subscriptions[EVENT_COUNT];
    bool subscribed_all;
} SubscribedConnection;

static SubscribedConnection subscribers[MAX_SUBSCRIBED_CONNECTIONS];
static int subscriber_count = 0;

static const char *event_names[] = {
    [EVENT_WINDOW_CREATED] = "window_created",
    [EVENT_WINDOW_DESTROYED] = "window_destroyed",
    [EVENT_WINDOW_FOCUSED] = "window_focused",
    [EVENT_WINDOW_UNFOCUSED] = "window_unfocused",
    [EVENT_WINDOW_MOVED] = "window_moved",
    [EVENT_WINDOW_RESIZED] = "window_resized",
    [EVENT_WINDOW_MINIMIZED] = "window_minimized",
    [EVENT_WINDOW_RESTORED] = "window_restored",
    [EVENT_WINDOW_MAXIMIZED] = "window_maximized",
    [EVENT_WINDOW_UNMAXIMIZED] = "window_unmaximized",
    [EVENT_WINDOW_FULLSCREEN] = "window_fullscreen",
    [EVENT_WINDOW_UNFULLSCREEN] = "window_unfullscreen",
    [EVENT_WINDOW_FLOATING] = "window_floating",
    [EVENT_WINDOW_UNFLOATING] = "window_unfloating",
    [EVENT_WINDOW_TITLE_CHANGED] = "window_title_changed",
    [EVENT_WINDOW_RAISED] = "window_raised",
    [EVENT_WINDOW_LOWERED] = "window_lowered",
    [EVENT_WORKSPACE_SWITCHED] = "workspace_switched",
    [EVENT_WORKSPACE_WINDOW_ADDED] = "workspace_window_added",
    [EVENT_WORKSPACE_WINDOW_REMOVED] = "workspace_window_removed",
    [EVENT_WORKSPACE_LAYOUT_CHANGED] = "workspace_layout_changed",
    [EVENT_WORKSPACE_MASTER_RATIO_CHANGED] = "workspace_master_ratio_changed",
    [EVENT_WORKSPACE_MASTER_COUNT_CHANGED] = "workspace_master_count_changed",
    [EVENT_WORKSPACE_ARRANGED] = "workspace_arranged",
    [EVENT_LAYOUT_CHANGED] = "layout_changed",
    [EVENT_KEY_PRESSED] = "key_pressed",
    [EVENT_KEY_RELEASED] = "key_released",
    [EVENT_SHORTCUT_TRIGGERED] = "shortcut_triggered",
    [EVENT_MOUSE_MOVED] = "mouse_moved",
    [EVENT_MOUSE_BUTTON_PRESSED] = "mouse_button_pressed",
    [EVENT_MOUSE_BUTTON_RELEASED] = "mouse_button_released",
    [EVENT_DRAG_STARTED] = "drag_started",
    [EVENT_DRAG_ENDED] = "drag_ended",
    [EVENT_SHOW_DESKTOP_TOGGLED] = "show_desktop_toggled",
    [EVENT_NOTIFICATION_SHOWN] = "notification_shown",
    [EVENT_NOTIFICATION_CLOSED] = "notification_closed",
    [EVENT_FADE_SPEED_CHANGED] = "fade_speed_changed",
    [EVENT_FADE_INTENSITY_CHANGED] = "fade_intensity_changed",
    [EVENT_WINDOW_SNAPPED] = "window_snapped",
    [EVENT_AUDIO_VOLUME_CHANGED] = "audio_volume_changed",
    [EVENT_AUDIO_MUTE_TOGGLED] = "audio_mute_toggled",
    [EVENT_PANEL_VISIBILITY_CHANGED] = "panel_visibility_changed",
    [EVENT_CONFIG_RELOADED] = "config_reloaded",
    [EVENT_AI_RESPONSE_RECEIVED] = "ai_response_received",
    [EVENT_EXA_SEARCH_COMPLETED] = "exa_search_completed",
    [EVENT_NEWS_ARTICLE_CHANGED] = "news_article_changed",
    [EVENT_DEMO_STARTED] = "demo_started",
    [EVENT_DEMO_STOPPED] = "demo_stopped",
    [EVENT_DEMO_PHASE_CHANGED] = "demo_phase_changed",
    [EVENT_TUTORIAL_STARTED] = "tutorial_started",
    [EVENT_TUTORIAL_STOPPED] = "tutorial_stopped",
};

const char *api_event_name(ApiEventType type) {
    if (type >= 0 && type < EVENT_COUNT) {
        return event_names[type];
    }
    return "unknown";
}

static ApiEventType event_type_from_name(const char *name) {
    if (!name) return EVENT_COUNT;
    for (int i = 0; i < EVENT_COUNT; i++) {
        if (strcmp(name, event_names[i]) == 0) {
            return (ApiEventType)i;
        }
    }
    return EVENT_COUNT;
}

static SubscribedConnection *find_subscriber(struct mg_connection *c) {
    for (int i = 0; i < subscriber_count; i++) {
        if (subscribers[i].conn == c) {
            return &subscribers[i];
        }
    }
    return NULL;
}

static SubscribedConnection *add_subscriber(struct mg_connection *c) {
    SubscribedConnection *sub = find_subscriber(c);
    if (sub) return sub;
    if (subscriber_count >= MAX_SUBSCRIBED_CONNECTIONS) return NULL;

    sub = &subscribers[subscriber_count++];
    sub->conn = c;
    sub->subscribed_all = false;
    memset(sub->subscriptions, 0, sizeof(sub->subscriptions));
    return sub;
}

static void remove_subscriber(struct mg_connection *c) {
    for (int i = 0; i < subscriber_count; i++) {
        if (subscribers[i].conn == c) {
            memmove(&subscribers[i], &subscribers[i + 1],
                    (subscriber_count - i - 1) * sizeof(SubscribedConnection));
            subscriber_count--;
            return;
        }
    }
}

static void broadcast_event(ApiEventType type, cJSON *data) {
    if (!initialized || !data) {
        if (data) cJSON_Delete(data);
        return;
    }

    cJSON *event = cJSON_CreateObject();
    cJSON_AddStringToObject(event, "type", "event");
    cJSON_AddStringToObject(event, "event", api_event_name(type));
    cJSON_AddItemToObject(event, "data", data);

    char *json = cJSON_PrintUnformatted(event);
    size_t len = strlen(json);

    for (int i = 0; i < subscriber_count; i++) {
        SubscribedConnection *sub = &subscribers[i];
        if (sub->subscribed_all || sub->subscriptions[type]) {
            mg_ws_send(sub->conn, json, len, WEBSOCKET_OP_TEXT);
        }
    }

    free(json);
    cJSON_Delete(event);
}

static void send_json_response(struct mg_connection *c, cJSON *root) {
    if (!c || !root) {
        if (root) cJSON_Delete(root);
        return;
    }
    char *json = cJSON_PrintUnformatted(root);
    mg_ws_send(c, json, strlen(json), WEBSOCKET_OP_TEXT);
    free(json);
    cJSON_Delete(root);
}

static void send_json_error(struct mg_connection *c, const char *msg) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "error");
    cJSON_AddStringToObject(root, "message", msg);
    send_json_response(c, root);
}

static void send_json_ok(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    send_json_response(c, root);
}

static cJSON *client_to_json(Client *cl) {
    if (!cl) return NULL;

    cJSON *item = cJSON_CreateObject();
    cJSON_AddNumberToObject(item, "window", (double)cl->window);
    cJSON_AddStringToObject(item, "title", cl->title);
    cJSON_AddStringToObject(item, "class", cl->class);
    cJSON_AddNumberToObject(item, "workspace", (double)cl->workspace);
    cJSON_AddNumberToObject(item, "x", (double)cl->x);
    cJSON_AddNumberToObject(item, "y", (double)cl->y);
    cJSON_AddNumberToObject(item, "width", (double)cl->width);
    cJSON_AddNumberToObject(item, "height", (double)cl->height);
    cJSON_AddBoolToObject(item, "floating", client_is_floating(cl));
    cJSON_AddBoolToObject(item, "fullscreen", client_is_fullscreen(cl));
    cJSON_AddBoolToObject(item, "maximized", client_is_maximized(cl));
    cJSON_AddBoolToObject(item, "minimized", client_is_minimized(cl));
    cJSON_AddBoolToObject(item, "focused", (workspace_get_current() && workspace_get_current()->focused == cl));
    return item;
}

static KeySym keysym_from_name(const char *name) {
    if (!name) return NoSymbol;
    return XStringToKeysym(name);
}

static void handle_subscribe(struct mg_connection *c, cJSON *json) {
    SubscribedConnection *sub = add_subscriber(c);
    if (!sub) {
        send_json_error(c, "Too many subscribed connections");
        return;
    }

    cJSON *events = cJSON_GetObjectItem(json, "events");
    cJSON *all = cJSON_GetObjectItem(json, "all");

    if (cJSON_IsBool(all) && cJSON_IsTrue(all)) {
        sub->subscribed_all = true;
    } else if (cJSON_IsArray(events)) {
        cJSON *ev;
        cJSON_ArrayForEach(ev, events) {
            if (cJSON_IsString(ev)) {
                ApiEventType type = event_type_from_name(ev->valuestring);
                if (type < EVENT_COUNT) {
                    sub->subscriptions[type] = true;
                }
            }
        }
    } else {
        sub->subscribed_all = true;
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "subscribed_all", sub->subscribed_all);

    cJSON *subs = cJSON_AddArrayToObject(root, "subscriptions");
    for (int i = 0; i < EVENT_COUNT; i++) {
        if (sub->subscribed_all || sub->subscriptions[i]) {
            cJSON_AddItemToArray(subs, cJSON_CreateString(event_names[i]));
        }
    }

    send_json_response(c, root);
}

static void handle_unsubscribe(struct mg_connection *c, cJSON *json) {
    SubscribedConnection *sub = find_subscriber(c);
    if (!sub) {
        send_json_ok(c);
        return;
    }

    cJSON *events = cJSON_GetObjectItem(json, "events");
    cJSON *all = cJSON_GetObjectItem(json, "all");

    if (cJSON_IsBool(all) && cJSON_IsTrue(all)) {
        remove_subscriber(c);
    } else if (cJSON_IsArray(events)) {
        cJSON *ev;
        cJSON_ArrayForEach(ev, events) {
            if (cJSON_IsString(ev)) {
                ApiEventType type = event_type_from_name(ev->valuestring);
                if (type < EVENT_COUNT) {
                    sub->subscriptions[type] = false;
                }
            }
        }
        sub->subscribed_all = false;
    } else {
        remove_subscriber(c);
    }

    send_json_ok(c);
}

static void handle_list_events(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");

    cJSON *window_events = cJSON_AddArrayToObject(root, "window_events");
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_created"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_destroyed"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_focused"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_unfocused"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_moved"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_resized"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_minimized"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_restored"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_maximized"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_unmaximized"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_fullscreen"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_unfullscreen"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_floating"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_unfloating"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_title_changed"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_raised"));
    cJSON_AddItemToArray(window_events, cJSON_CreateString("window_lowered"));

    cJSON *workspace_events = cJSON_AddArrayToObject(root, "workspace_events");
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_switched"));
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_window_added"));
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_window_removed"));
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_layout_changed"));
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_master_ratio_changed"));
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_master_count_changed"));
    cJSON_AddItemToArray(workspace_events, cJSON_CreateString("workspace_arranged"));

    cJSON *input_events = cJSON_AddArrayToObject(root, "input_events");
    cJSON_AddItemToArray(input_events, cJSON_CreateString("key_pressed"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("key_released"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("shortcut_triggered"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("mouse_moved"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("mouse_button_pressed"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("mouse_button_released"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("drag_started"));
    cJSON_AddItemToArray(input_events, cJSON_CreateString("drag_ended"));

    cJSON *system_events = cJSON_AddArrayToObject(root, "system_events");
    cJSON_AddItemToArray(system_events, cJSON_CreateString("show_desktop_toggled"));
    cJSON_AddItemToArray(system_events, cJSON_CreateString("notification_shown"));
    cJSON_AddItemToArray(system_events, cJSON_CreateString("notification_closed"));
    cJSON_AddItemToArray(system_events, cJSON_CreateString("window_snapped"));
    cJSON_AddItemToArray(system_events, cJSON_CreateString("panel_visibility_changed"));
    cJSON_AddItemToArray(system_events, cJSON_CreateString("config_reloaded"));

    cJSON *fade_events = cJSON_AddArrayToObject(root, "fade_events");
    cJSON_AddItemToArray(fade_events, cJSON_CreateString("fade_speed_changed"));
    cJSON_AddItemToArray(fade_events, cJSON_CreateString("fade_intensity_changed"));

    cJSON *audio_events = cJSON_AddArrayToObject(root, "audio_events");
    cJSON_AddItemToArray(audio_events, cJSON_CreateString("audio_volume_changed"));
    cJSON_AddItemToArray(audio_events, cJSON_CreateString("audio_mute_toggled"));

    cJSON *ai_events = cJSON_AddArrayToObject(root, "ai_events");
    cJSON_AddItemToArray(ai_events, cJSON_CreateString("ai_response_received"));
    cJSON_AddItemToArray(ai_events, cJSON_CreateString("exa_search_completed"));

    cJSON *news_events = cJSON_AddArrayToObject(root, "news_events");
    cJSON_AddItemToArray(news_events, cJSON_CreateString("news_article_changed"));

    cJSON *demo_events = cJSON_AddArrayToObject(root, "demo_events");
    cJSON_AddItemToArray(demo_events, cJSON_CreateString("demo_started"));
    cJSON_AddItemToArray(demo_events, cJSON_CreateString("demo_stopped"));
    cJSON_AddItemToArray(demo_events, cJSON_CreateString("demo_phase_changed"));

    cJSON *tutorial_events = cJSON_AddArrayToObject(root, "tutorial_events");
    cJSON_AddItemToArray(tutorial_events, cJSON_CreateString("tutorial_started"));
    cJSON_AddItemToArray(tutorial_events, cJSON_CreateString("tutorial_stopped"));

    send_json_response(c, root);
}

void api_emit_window_created(Window window, const char *title, const char *class_name, int workspace) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "title", title ? title : "");
    cJSON_AddStringToObject(data, "class", class_name ? class_name : "");
    cJSON_AddNumberToObject(data, "workspace", workspace);
    broadcast_event(EVENT_WINDOW_CREATED, data);
}

void api_emit_window_destroyed(Window window, const char *title, int workspace) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "title", title ? title : "");
    cJSON_AddNumberToObject(data, "workspace", workspace);
    broadcast_event(EVENT_WINDOW_DESTROYED, data);
}

void api_emit_window_focused(Window window, const char *title, Window prev_window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "title", title ? title : "");
    if (prev_window != None) {
        cJSON_AddNumberToObject(data, "previous_window", (double)prev_window);
    }
    broadcast_event(EVENT_WINDOW_FOCUSED, data);
}

void api_emit_window_unfocused(Window window, const char *title) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "title", title ? title : "");
    broadcast_event(EVENT_WINDOW_UNFOCUSED, data);
}

void api_emit_window_moved(Window window, int old_x, int old_y, int new_x, int new_y) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddNumberToObject(data, "old_x", old_x);
    cJSON_AddNumberToObject(data, "old_y", old_y);
    cJSON_AddNumberToObject(data, "new_x", new_x);
    cJSON_AddNumberToObject(data, "new_y", new_y);
    broadcast_event(EVENT_WINDOW_MOVED, data);
}

void api_emit_window_resized(Window window, int old_w, int old_h, int new_w, int new_h) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddNumberToObject(data, "old_width", old_w);
    cJSON_AddNumberToObject(data, "old_height", old_h);
    cJSON_AddNumberToObject(data, "new_width", new_w);
    cJSON_AddNumberToObject(data, "new_height", new_h);
    broadcast_event(EVENT_WINDOW_RESIZED, data);
}

void api_emit_window_minimized(Window window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    broadcast_event(EVENT_WINDOW_MINIMIZED, data);
}

void api_emit_window_restored(Window window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    broadcast_event(EVENT_WINDOW_RESTORED, data);
}

void api_emit_window_maximized(Window window, bool maximized) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddBoolToObject(data, "maximized", maximized);
    broadcast_event(maximized ? EVENT_WINDOW_MAXIMIZED : EVENT_WINDOW_UNMAXIMIZED, data);
}

void api_emit_window_fullscreen(Window window, bool fullscreen) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddBoolToObject(data, "fullscreen", fullscreen);
    broadcast_event(fullscreen ? EVENT_WINDOW_FULLSCREEN : EVENT_WINDOW_UNFULLSCREEN, data);
}

void api_emit_window_floating(Window window, bool floating) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddBoolToObject(data, "floating", floating);
    broadcast_event(floating ? EVENT_WINDOW_FLOATING : EVENT_WINDOW_UNFLOATING, data);
}

void api_emit_window_title_changed(Window window, const char *old_title, const char *new_title) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "old_title", old_title ? old_title : "");
    cJSON_AddStringToObject(data, "new_title", new_title ? new_title : "");
    broadcast_event(EVENT_WINDOW_TITLE_CHANGED, data);
}

void api_emit_window_raised(Window window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    broadcast_event(EVENT_WINDOW_RAISED, data);
}

void api_emit_window_lowered(Window window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    broadcast_event(EVENT_WINDOW_LOWERED, data);
}

void api_emit_workspace_switched(int old_workspace, int new_workspace) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "old_workspace", old_workspace);
    cJSON_AddNumberToObject(data, "new_workspace", new_workspace);
    broadcast_event(EVENT_WORKSPACE_SWITCHED, data);
}

void api_emit_workspace_window_added(int workspace, Window window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "workspace", workspace);
    cJSON_AddNumberToObject(data, "window", (double)window);
    broadcast_event(EVENT_WORKSPACE_WINDOW_ADDED, data);
}

void api_emit_workspace_window_removed(int workspace, Window window) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "workspace", workspace);
    cJSON_AddNumberToObject(data, "window", (double)window);
    broadcast_event(EVENT_WORKSPACE_WINDOW_REMOVED, data);
}

void api_emit_workspace_layout_changed(int workspace, int old_layout, int new_layout) {
    const char *layout_names[] = {"tiling", "floating", "monocle"};
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "workspace", workspace);
    cJSON_AddStringToObject(data, "old_layout", layout_names[old_layout % 3]);
    cJSON_AddStringToObject(data, "new_layout", layout_names[new_layout % 3]);
    broadcast_event(EVENT_WORKSPACE_LAYOUT_CHANGED, data);
}

void api_emit_workspace_master_ratio_changed(int workspace, float old_ratio, float new_ratio) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "workspace", workspace);
    cJSON_AddNumberToObject(data, "old_ratio", old_ratio);
    cJSON_AddNumberToObject(data, "new_ratio", new_ratio);
    broadcast_event(EVENT_WORKSPACE_MASTER_RATIO_CHANGED, data);
}

void api_emit_workspace_master_count_changed(int workspace, int old_count, int new_count) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "workspace", workspace);
    cJSON_AddNumberToObject(data, "old_count", old_count);
    cJSON_AddNumberToObject(data, "new_count", new_count);
    broadcast_event(EVENT_WORKSPACE_MASTER_COUNT_CHANGED, data);
}

void api_emit_workspace_arranged(int workspace, int layout) {
    const char *layout_names[] = {"tiling", "floating", "monocle"};
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "workspace", workspace);
    cJSON_AddStringToObject(data, "layout", layout_names[layout % 3]);
    broadcast_event(EVENT_WORKSPACE_ARRANGED, data);
}

void api_emit_key_pressed(unsigned int keycode, unsigned int keysym, unsigned int modifiers) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "keycode", keycode);
    cJSON_AddNumberToObject(data, "keysym", keysym);
    cJSON_AddNumberToObject(data, "modifiers", modifiers);
    const char *key_name = XKeysymToString(keysym);
    if (key_name) {
        cJSON_AddStringToObject(data, "key_name", key_name);
    }
    broadcast_event(EVENT_KEY_PRESSED, data);
}

void api_emit_key_released(unsigned int keycode, unsigned int keysym, unsigned int modifiers) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "keycode", keycode);
    cJSON_AddNumberToObject(data, "keysym", keysym);
    cJSON_AddNumberToObject(data, "modifiers", modifiers);
    const char *key_name = XKeysymToString(keysym);
    if (key_name) {
        cJSON_AddStringToObject(data, "key_name", key_name);
    }
    broadcast_event(EVENT_KEY_RELEASED, data);
}

void api_emit_shortcut_triggered(const char *name, const char *description) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddStringToObject(data, "name", name ? name : "");
    cJSON_AddStringToObject(data, "description", description ? description : "");
    broadcast_event(EVENT_SHORTCUT_TRIGGERED, data);
}

void api_emit_mouse_moved(int x, int y) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "x", x);
    cJSON_AddNumberToObject(data, "y", y);
    broadcast_event(EVENT_MOUSE_MOVED, data);
}

void api_emit_mouse_button_pressed(int button, int x, int y) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "button", button);
    cJSON_AddNumberToObject(data, "x", x);
    cJSON_AddNumberToObject(data, "y", y);
    broadcast_event(EVENT_MOUSE_BUTTON_PRESSED, data);
}

void api_emit_mouse_button_released(int button, int x, int y) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "button", button);
    cJSON_AddNumberToObject(data, "x", x);
    cJSON_AddNumberToObject(data, "y", y);
    broadcast_event(EVENT_MOUSE_BUTTON_RELEASED, data);
}

void api_emit_drag_started(Window window, bool is_resize) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "type", is_resize ? "resize" : "move");
    broadcast_event(EVENT_DRAG_STARTED, data);
}

void api_emit_drag_ended(Window window, bool is_resize) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddStringToObject(data, "type", is_resize ? "resize" : "move");
    broadcast_event(EVENT_DRAG_ENDED, data);
}

void api_emit_show_desktop_toggled(bool shown) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "shown", shown);
    broadcast_event(EVENT_SHOW_DESKTOP_TOGGLED, data);
}

void api_emit_notification_shown(unsigned int id, const char *summary, const char *body) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "id", id);
    cJSON_AddStringToObject(data, "summary", summary ? summary : "");
    cJSON_AddStringToObject(data, "body", body ? body : "");
    broadcast_event(EVENT_NOTIFICATION_SHOWN, data);
}

void api_emit_notification_closed(unsigned int id) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "id", id);
    broadcast_event(EVENT_NOTIFICATION_CLOSED, data);
}

void api_emit_fade_speed_changed(float old_speed, float new_speed) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "old_speed", old_speed);
    cJSON_AddNumberToObject(data, "new_speed", new_speed);
    broadcast_event(EVENT_FADE_SPEED_CHANGED, data);
}

void api_emit_fade_intensity_changed(float old_intensity, float new_intensity) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "old_intensity", old_intensity);
    cJSON_AddNumberToObject(data, "new_intensity", new_intensity);
    broadcast_event(EVENT_FADE_INTENSITY_CHANGED, data);
}

static void handle_get_status(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "version", DWN_VERSION);
    cJSON_AddNumberToObject(root, "current_workspace", dwn->current_workspace);
    cJSON_AddNumberToObject(root, "client_count", dwn->client_count);
    send_json_response(c, root);
}

static void handle_get_workspaces(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON *arr = cJSON_AddArrayToObject(root, "workspaces");

    for (int i = 0; i < MAX_WORKSPACES; i++) {
        cJSON *ws = cJSON_CreateObject();
        cJSON_AddNumberToObject(ws, "id", i);
        cJSON_AddStringToObject(ws, "name", dwn->workspaces[i].name);
        cJSON_AddNumberToObject(ws, "layout", (double)dwn->workspaces[i].layout);

        int clients_on_ws = 0;
        for (Client *cl = dwn->client_list; cl; cl = cl->next) {
            if (cl->workspace == (unsigned int)i) clients_on_ws++;
        }
        cJSON_AddNumberToObject(ws, "client_count", clients_on_ws);
        cJSON_AddItemToArray(arr, ws);
    }

    send_json_response(c, root);
}

static void handle_get_clients(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON *arr = cJSON_AddArrayToObject(root, "clients");

    for (Client *cl = dwn->client_list; cl; cl = cl->next) {
        cJSON *item = client_to_json(cl);
        if (item) cJSON_AddItemToArray(arr, item);
    }

    send_json_response(c, root);
}

static void handle_find_clients(struct mg_connection *c, cJSON *json) {
    cJSON *title_filter = cJSON_GetObjectItem(json, "title");
    cJSON *class_filter = cJSON_GetObjectItem(json, "class");
    cJSON *workspace_filter = cJSON_GetObjectItem(json, "workspace");

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON *arr = cJSON_AddArrayToObject(root, "clients");

    for (Client *cl = dwn->client_list; cl; cl = cl->next) {
        bool match = true;

        if (cJSON_IsString(title_filter) && title_filter->valuestring) {
            if (strstr(cl->title, title_filter->valuestring) == NULL) {
                match = false;
            }
        }

        if (match && cJSON_IsString(class_filter) && class_filter->valuestring) {
            if (strstr(cl->class, class_filter->valuestring) == NULL) {
                match = false;
            }
        }

        if (match && cJSON_IsNumber(workspace_filter)) {
            if ((int)cl->workspace != workspace_filter->valueint) {
                match = false;
            }
        }

        if (match) {
            cJSON *item = client_to_json(cl);
            if (item) cJSON_AddItemToArray(arr, item);
        }
    }

    send_json_response(c, root);
}

static void handle_get_focused_client(struct mg_connection *c) {
    Workspace *ws = workspace_get_current();
    if (!ws || !ws->focused) {
        cJSON *root = cJSON_CreateObject();
        cJSON_AddStringToObject(root, "status", "ok");
        cJSON_AddNullToObject(root, "client");
        send_json_response(c, root);
        return;
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON *item = client_to_json(ws->focused);
    cJSON_AddItemToObject(root, "client", item);
    send_json_response(c, root);
}

static void handle_key_press(struct mg_connection *c, cJSON *json) {
    cJSON *keysym_item = cJSON_GetObjectItem(json, "keysym");
    cJSON *keycode_item = cJSON_GetObjectItem(json, "keycode");

    KeyCode keycode = 0;

    if (cJSON_IsString(keysym_item)) {
        KeySym ks = keysym_from_name(keysym_item->valuestring);
        if (ks == NoSymbol) {
            send_json_error(c, "Invalid keysym name");
            return;
        }
        keycode = XKeysymToKeycode(dwn->display, ks);
    } else if (cJSON_IsNumber(keysym_item)) {
        keycode = XKeysymToKeycode(dwn->display, (KeySym)keysym_item->valueint);
    } else if (cJSON_IsNumber(keycode_item)) {
        keycode = (KeyCode)keycode_item->valueint;
    } else {
        send_json_error(c, "keysym or keycode required");
        return;
    }

    if (keycode == 0) {
        send_json_error(c, "Invalid keycode");
        return;
    }

    XTestFakeKeyEvent(dwn->display, keycode, True, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_key_release(struct mg_connection *c, cJSON *json) {
    cJSON *keysym_item = cJSON_GetObjectItem(json, "keysym");
    cJSON *keycode_item = cJSON_GetObjectItem(json, "keycode");

    KeyCode keycode = 0;

    if (cJSON_IsString(keysym_item)) {
        KeySym ks = keysym_from_name(keysym_item->valuestring);
        if (ks == NoSymbol) {
            send_json_error(c, "Invalid keysym name");
            return;
        }
        keycode = XKeysymToKeycode(dwn->display, ks);
    } else if (cJSON_IsNumber(keysym_item)) {
        keycode = XKeysymToKeycode(dwn->display, (KeySym)keysym_item->valueint);
    } else if (cJSON_IsNumber(keycode_item)) {
        keycode = (KeyCode)keycode_item->valueint;
    } else {
        send_json_error(c, "keysym or keycode required");
        return;
    }

    if (keycode == 0) {
        send_json_error(c, "Invalid keycode");
        return;
    }

    XTestFakeKeyEvent(dwn->display, keycode, False, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_key_tap(struct mg_connection *c, cJSON *json) {
    cJSON *keysym_item = cJSON_GetObjectItem(json, "keysym");
    cJSON *keycode_item = cJSON_GetObjectItem(json, "keycode");
    cJSON *modifiers = cJSON_GetObjectItem(json, "modifiers");

    KeyCode keycode = 0;

    if (cJSON_IsString(keysym_item)) {
        KeySym ks = keysym_from_name(keysym_item->valuestring);
        if (ks == NoSymbol) {
            send_json_error(c, "Invalid keysym name");
            return;
        }
        keycode = XKeysymToKeycode(dwn->display, ks);
    } else if (cJSON_IsNumber(keysym_item)) {
        keycode = XKeysymToKeycode(dwn->display, (KeySym)keysym_item->valueint);
    } else if (cJSON_IsNumber(keycode_item)) {
        keycode = (KeyCode)keycode_item->valueint;
    } else {
        send_json_error(c, "keysym or keycode required");
        return;
    }

    if (keycode == 0) {
        send_json_error(c, "Invalid keycode");
        return;
    }

    KeyCode mod_keycodes[8] = {0};
    int mod_count = 0;

    if (cJSON_IsArray(modifiers)) {
        cJSON *mod;
        cJSON_ArrayForEach(mod, modifiers) {
            if (cJSON_IsString(mod) && mod_count < 8) {
                KeySym mod_sym = keysym_from_name(mod->valuestring);
                if (mod_sym != NoSymbol) {
                    KeyCode mod_kc = XKeysymToKeycode(dwn->display, mod_sym);
                    if (mod_kc != 0) {
                        mod_keycodes[mod_count++] = mod_kc;
                    }
                }
            }
        }
    }

    for (int i = 0; i < mod_count; i++) {
        XTestFakeKeyEvent(dwn->display, mod_keycodes[i], True, CurrentTime);
    }

    XTestFakeKeyEvent(dwn->display, keycode, True, CurrentTime);
    XTestFakeKeyEvent(dwn->display, keycode, False, CurrentTime);

    for (int i = mod_count - 1; i >= 0; i--) {
        XTestFakeKeyEvent(dwn->display, mod_keycodes[i], False, CurrentTime);
    }

    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_key_type(struct mg_connection *c, cJSON *json) {
    cJSON *text = cJSON_GetObjectItem(json, "text");
    if (!cJSON_IsString(text) || !text->valuestring) {
        send_json_error(c, "text parameter required");
        return;
    }

    const char *str = text->valuestring;
    KeyCode shift_kc = XKeysymToKeycode(dwn->display, XK_Shift_L);

    for (size_t i = 0; str[i] != '\0'; i++) {
        char ch = str[i];
        KeySym ks = NoSymbol;
        bool need_shift = false;

        if (ch >= 'A' && ch <= 'Z') {
            ks = XStringToKeysym("a") + (ch - 'A');
            need_shift = true;
        } else if (ch >= 'a' && ch <= 'z') {
            ks = XStringToKeysym("a") + (ch - 'a');
        } else if (ch >= '0' && ch <= '9') {
            ks = XStringToKeysym("0") + (ch - '0');
        } else if (ch == ' ') {
            ks = XK_space;
        } else if (ch == '\n') {
            ks = XK_Return;
        } else if (ch == '\t') {
            ks = XK_Tab;
        } else {
            char name[2] = {ch, '\0'};
            ks = XStringToKeysym(name);
            if (ks == NoSymbol) {
                switch (ch) {
                    case '!': ks = XK_1; need_shift = true; break;
                    case '@': ks = XK_2; need_shift = true; break;
                    case '#': ks = XK_3; need_shift = true; break;
                    case '$': ks = XK_4; need_shift = true; break;
                    case '%': ks = XK_5; need_shift = true; break;
                    case '^': ks = XK_6; need_shift = true; break;
                    case '&': ks = XK_7; need_shift = true; break;
                    case '*': ks = XK_8; need_shift = true; break;
                    case '(': ks = XK_9; need_shift = true; break;
                    case ')': ks = XK_0; need_shift = true; break;
                    case '-': ks = XK_minus; break;
                    case '_': ks = XK_minus; need_shift = true; break;
                    case '=': ks = XK_equal; break;
                    case '+': ks = XK_equal; need_shift = true; break;
                    case '[': ks = XK_bracketleft; break;
                    case '{': ks = XK_bracketleft; need_shift = true; break;
                    case ']': ks = XK_bracketright; break;
                    case '}': ks = XK_bracketright; need_shift = true; break;
                    case '\\': ks = XK_backslash; break;
                    case '|': ks = XK_backslash; need_shift = true; break;
                    case ';': ks = XK_semicolon; break;
                    case ':': ks = XK_semicolon; need_shift = true; break;
                    case '\'': ks = XK_apostrophe; break;
                    case '"': ks = XK_apostrophe; need_shift = true; break;
                    case ',': ks = XK_comma; break;
                    case '<': ks = XK_comma; need_shift = true; break;
                    case '.': ks = XK_period; break;
                    case '>': ks = XK_period; need_shift = true; break;
                    case '/': ks = XK_slash; break;
                    case '?': ks = XK_slash; need_shift = true; break;
                    case '`': ks = XK_grave; break;
                    case '~': ks = XK_grave; need_shift = true; break;
                    default: continue;
                }
            }
        }

        if (ks == NoSymbol) continue;

        KeyCode kc = XKeysymToKeycode(dwn->display, ks);
        if (kc == 0) continue;

        if (need_shift && shift_kc != 0) {
            XTestFakeKeyEvent(dwn->display, shift_kc, True, CurrentTime);
        }

        XTestFakeKeyEvent(dwn->display, kc, True, CurrentTime);
        XTestFakeKeyEvent(dwn->display, kc, False, CurrentTime);

        if (need_shift && shift_kc != 0) {
            XTestFakeKeyEvent(dwn->display, shift_kc, False, CurrentTime);
        }
    }

    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_mouse_move(struct mg_connection *c, cJSON *json) {
    cJSON *x_item = cJSON_GetObjectItem(json, "x");
    cJSON *y_item = cJSON_GetObjectItem(json, "y");

    if (!cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item)) {
        send_json_error(c, "x and y parameters required");
        return;
    }

    int x = x_item->valueint;
    int y = y_item->valueint;

    XTestFakeMotionEvent(dwn->display, DefaultScreen(dwn->display), x, y, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_mouse_move_relative(struct mg_connection *c, cJSON *json) {
    cJSON *dx_item = cJSON_GetObjectItem(json, "dx");
    cJSON *dy_item = cJSON_GetObjectItem(json, "dy");

    if (!cJSON_IsNumber(dx_item) || !cJSON_IsNumber(dy_item)) {
        send_json_error(c, "dx and dy parameters required");
        return;
    }

    int dx = dx_item->valueint;
    int dy = dy_item->valueint;

    XTestFakeRelativeMotionEvent(dwn->display, dx, dy, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_mouse_click(struct mg_connection *c, cJSON *json) {
    cJSON *button_item = cJSON_GetObjectItem(json, "button");
    cJSON *x_item = cJSON_GetObjectItem(json, "x");
    cJSON *y_item = cJSON_GetObjectItem(json, "y");

    if (!cJSON_IsNumber(button_item)) {
        send_json_error(c, "button parameter required (1=left, 2=middle, 3=right)");
        return;
    }

    unsigned int button = (unsigned int)button_item->valueint;
    if (button < 1 || button > 5) {
        send_json_error(c, "button must be 1-5");
        return;
    }

    if (cJSON_IsNumber(x_item) && cJSON_IsNumber(y_item)) {
        XTestFakeMotionEvent(dwn->display, DefaultScreen(dwn->display),
                             x_item->valueint, y_item->valueint, CurrentTime);
    }

    XTestFakeButtonEvent(dwn->display, button, True, CurrentTime);
    XTestFakeButtonEvent(dwn->display, button, False, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_mouse_press(struct mg_connection *c, cJSON *json) {
    cJSON *button_item = cJSON_GetObjectItem(json, "button");

    if (!cJSON_IsNumber(button_item)) {
        send_json_error(c, "button parameter required");
        return;
    }

    unsigned int button = (unsigned int)button_item->valueint;
    if (button < 1 || button > 5) {
        send_json_error(c, "button must be 1-5");
        return;
    }

    XTestFakeButtonEvent(dwn->display, button, True, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_mouse_release(struct mg_connection *c, cJSON *json) {
    cJSON *button_item = cJSON_GetObjectItem(json, "button");

    if (!cJSON_IsNumber(button_item)) {
        send_json_error(c, "button parameter required");
        return;
    }

    unsigned int button = (unsigned int)button_item->valueint;
    if (button < 1 || button > 5) {
        send_json_error(c, "button must be 1-5");
        return;
    }

    XTestFakeButtonEvent(dwn->display, button, False, CurrentTime);
    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_mouse_scroll(struct mg_connection *c, cJSON *json) {
    cJSON *direction_item = cJSON_GetObjectItem(json, "direction");
    cJSON *amount_item = cJSON_GetObjectItem(json, "amount");

    if (!cJSON_IsString(direction_item)) {
        send_json_error(c, "direction parameter required (up/down)");
        return;
    }

    unsigned int button;
    if (strcmp(direction_item->valuestring, "up") == 0) {
        button = 4;
    } else if (strcmp(direction_item->valuestring, "down") == 0) {
        button = 5;
    } else {
        send_json_error(c, "direction must be 'up' or 'down'");
        return;
    }

    int amount = 1;
    if (cJSON_IsNumber(amount_item)) {
        amount = amount_item->valueint;
        if (amount < 1) amount = 1;
        if (amount > 100) amount = 100;
    }

    for (int i = 0; i < amount; i++) {
        XTestFakeButtonEvent(dwn->display, button, True, CurrentTime);
        XTestFakeButtonEvent(dwn->display, button, False, CurrentTime);
    }

    XFlush(dwn->display);
    send_json_ok(c);
}

static void handle_get_mouse_position(struct mg_connection *c) {
    Window root_return, child_return;
    int root_x, root_y, win_x, win_y;
    unsigned int mask;

    XQueryPointer(dwn->display, dwn->root, &root_return, &child_return,
                  &root_x, &root_y, &win_x, &win_y, &mask);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "x", root_x);
    cJSON_AddNumberToObject(root, "y", root_y);
    cJSON_AddNumberToObject(root, "window_x", win_x);
    cJSON_AddNumberToObject(root, "window_y", win_y);

    if (child_return != None) {
        Client *cl = client_find_by_window(child_return);
        if (!cl) cl = client_find_by_frame(child_return);
        if (cl) {
            cJSON_AddNumberToObject(root, "window", (double)cl->window);
        }
    }

    send_json_response(c, root);
}

static void handle_close_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_close(cl);
    send_json_ok(c);
}

static void handle_kill_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_kill(cl);
    send_json_ok(c);
}

static void handle_minimize_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_minimize(cl);
    send_json_ok(c);
}

static void handle_restore_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_restore(cl);
    send_json_ok(c);
}

static void handle_maximize_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    cJSON *state = cJSON_GetObjectItem(json, "state");

    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    if (cJSON_IsBool(state)) {
        client_set_maximize(cl, cJSON_IsTrue(state));
    } else {
        client_toggle_maximize(cl);
    }

    send_json_ok(c);
}

static void handle_fullscreen_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    cJSON *state = cJSON_GetObjectItem(json, "state");

    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    if (cJSON_IsBool(state)) {
        client_set_fullscreen(cl, cJSON_IsTrue(state));
    } else {
        client_toggle_fullscreen(cl);
    }

    send_json_ok(c);
}

static void handle_float_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    cJSON *state = cJSON_GetObjectItem(json, "state");

    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    if (cJSON_IsBool(state)) {
        client_set_floating(cl, cJSON_IsTrue(state));
    } else {
        client_toggle_floating(cl);
    }

    workspace_arrange_current();
    send_json_ok(c);
}

static void handle_move_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    cJSON *x_item = cJSON_GetObjectItem(json, "x");
    cJSON *y_item = cJSON_GetObjectItem(json, "y");

    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    if (!cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item)) {
        send_json_error(c, "x and y parameters required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_move(cl, x_item->valueint, y_item->valueint);
    send_json_ok(c);
}

static void handle_resize_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    cJSON *width_item = cJSON_GetObjectItem(json, "width");
    cJSON *height_item = cJSON_GetObjectItem(json, "height");

    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    if (!cJSON_IsNumber(width_item) || !cJSON_IsNumber(height_item)) {
        send_json_error(c, "width and height parameters required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_resize(cl, width_item->valueint, height_item->valueint);
    send_json_ok(c);
}

static void handle_move_client_to_workspace(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    if (!cJSON_IsNumber(ws_item)) {
        send_json_error(c, "workspace parameter required");
        return;
    }

    int ws = ws_item->valueint;
    if (ws < 0 || ws >= MAX_WORKSPACES) {
        send_json_error(c, "workspace must be 0-8");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    workspace_move_client(cl, ws);
    send_json_ok(c);
}

static void handle_raise_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_raise(cl);
    send_json_ok(c);
}

static void handle_lower_client(struct mg_connection *c, cJSON *json) {
    cJSON *win = cJSON_GetObjectItem(json, "window");
    if (!cJSON_IsNumber(win)) {
        send_json_error(c, "window parameter required");
        return;
    }

    Client *cl = client_find_by_window((Window)win->valuedouble);
    if (!cl) {
        send_json_error(c, "window not found");
        return;
    }

    client_lower(cl);
    send_json_ok(c);
}

static void handle_set_layout(struct mg_connection *c, cJSON *json) {
    cJSON *layout_item = cJSON_GetObjectItem(json, "layout");
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    if (!cJSON_IsString(layout_item) && !cJSON_IsNumber(layout_item)) {
        send_json_error(c, "layout parameter required (tiling/floating/monocle or 0/1/2)");
        return;
    }

    LayoutType layout;
    if (cJSON_IsString(layout_item)) {
        const char *name = layout_item->valuestring;
        if (strcmp(name, "tiling") == 0) {
            layout = LAYOUT_TILING;
        } else if (strcmp(name, "floating") == 0) {
            layout = LAYOUT_FLOATING;
        } else if (strcmp(name, "monocle") == 0) {
            layout = LAYOUT_MONOCLE;
        } else {
            send_json_error(c, "layout must be tiling/floating/monocle");
            return;
        }
    } else {
        int val = layout_item->valueint;
        if (val < 0 || val > 2) {
            send_json_error(c, "layout must be 0-2");
            return;
        }
        layout = (LayoutType)val;
    }

    int ws = dwn->current_workspace;
    if (cJSON_IsNumber(ws_item)) {
        ws = ws_item->valueint;
        if (ws < 0 || ws >= MAX_WORKSPACES) {
            send_json_error(c, "workspace must be 0-8");
            return;
        }
    }

    workspace_set_layout(ws, layout);
    workspace_arrange(ws);
    send_json_ok(c);
}

static void handle_cycle_layout(struct mg_connection *c, cJSON *json) {
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    int ws = dwn->current_workspace;
    if (cJSON_IsNumber(ws_item)) {
        ws = ws_item->valueint;
        if (ws < 0 || ws >= MAX_WORKSPACES) {
            send_json_error(c, "workspace must be 0-8");
            return;
        }
    }

    workspace_cycle_layout(ws);
    send_json_ok(c);
}

static void handle_get_screen_info(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "screen_width", dwn->screen_width);
    cJSON_AddNumberToObject(root, "screen_height", dwn->screen_height);
    cJSON_AddNumberToObject(root, "screen", dwn->screen);
    cJSON_AddNumberToObject(root, "monitor_count", dwn->monitor_count);

    cJSON *monitors = cJSON_AddArrayToObject(root, "monitors");
    for (int i = 0; i < dwn->monitor_count; i++) {
        cJSON *mon = cJSON_CreateObject();
        cJSON_AddNumberToObject(mon, "index", dwn->monitors[i].index);
        cJSON_AddNumberToObject(mon, "x", dwn->monitors[i].x);
        cJSON_AddNumberToObject(mon, "y", dwn->monitors[i].y);
        cJSON_AddNumberToObject(mon, "width", dwn->monitors[i].width);
        cJSON_AddNumberToObject(mon, "height", dwn->monitors[i].height);
        cJSON_AddBoolToObject(mon, "primary", dwn->monitors[i].primary);
        cJSON_AddItemToArray(monitors, mon);
    }

    int usable_x, usable_y, usable_w, usable_h;
    layout_get_usable_area(&usable_x, &usable_y, &usable_w, &usable_h);
    cJSON *usable = cJSON_AddObjectToObject(root, "usable_area");
    cJSON_AddNumberToObject(usable, "x", usable_x);
    cJSON_AddNumberToObject(usable, "y", usable_y);
    cJSON_AddNumberToObject(usable, "width", usable_w);
    cJSON_AddNumberToObject(usable, "height", usable_h);

    send_json_response(c, root);
}

static void handle_show_desktop(struct mg_connection *c, cJSON *json) {
    cJSON *state = cJSON_GetObjectItem(json, "state");

    if (cJSON_IsBool(state)) {
        bool show = cJSON_IsTrue(state);
        if (show != dwn->desktop_shown) {
            key_show_desktop();
        }
    } else {
        key_show_desktop();
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "desktop_shown", dwn->desktop_shown);
    send_json_response(c, root);
}

typedef struct {
    struct mg_connection *conn;
    bool use_async;
} ScreenshotApiContext;

static void screenshot_api_callback(ScreenshotRequest *req)
{
    ScreenshotApiContext *ctx = (ScreenshotApiContext *)req->user_data;
    if (ctx == NULL || ctx->conn == NULL) {
        screenshot_result_free(&req->result);
        dwn_free(ctx);
        return;
    }

    if (req->state == SCREENSHOT_STATE_COMPLETED) {
        size_t base64_len = 0;
        char *base64_data = screenshot_to_base64(&req->result, &base64_len);

        if (base64_data == NULL) {
            send_json_error(ctx->conn, "Failed to encode screenshot");
        } else {
            cJSON *root = cJSON_CreateObject();
            cJSON_AddStringToObject(root, "status", "ok");
            cJSON_AddStringToObject(root, "format", "png");
            cJSON_AddStringToObject(root, "encoding", "base64");
            cJSON_AddNumberToObject(root, "width", req->result.width);
            cJSON_AddNumberToObject(root, "height", req->result.height);
            cJSON_AddStringToObject(root, "data", base64_data);
            send_json_response(ctx->conn, root);
            dwn_free(base64_data);
        }
    } else {
        send_json_error(ctx->conn, screenshot_status_string(req->status));
    }

    screenshot_result_free(&req->result);
    dwn_free(ctx);
}

static void handle_screenshot(struct mg_connection *c, cJSON *json) {
    cJSON *mode_item = cJSON_GetObjectItem(json, "mode");
    cJSON *async_item = cJSON_GetObjectItem(json, "async");
    const char *mode = "fullscreen";
    bool use_async = true;

    if (cJSON_IsString(mode_item) && mode_item->valuestring) {
        mode = mode_item->valuestring;
    }

    if (cJSON_IsBool(async_item)) {
        use_async = cJSON_IsTrue(async_item);
    }

    if (use_async) {
        ScreenshotApiContext *ctx = dwn_malloc(sizeof(ScreenshotApiContext));
        if (ctx == NULL) {
            send_json_error(c, "Out of memory");
            return;
        }
        ctx->conn = c;
        ctx->use_async = true;

        ScreenshotRequest *req = NULL;

        if (strcmp(mode, "fullscreen") == 0) {
            req = screenshot_capture_async(SCREENSHOT_MODE_FULLSCREEN, screenshot_api_callback);
        } else if (strcmp(mode, "active") == 0) {
            req = screenshot_capture_async(SCREENSHOT_MODE_ACTIVE, screenshot_api_callback);
        } else if (strcmp(mode, "window") == 0) {
            cJSON *window_item = cJSON_GetObjectItem(json, "window");
            if (!cJSON_IsNumber(window_item)) {
                dwn_free(ctx);
                send_json_error(c, "window parameter required for mode=window");
                return;
            }
            req = screenshot_capture_window_async((Window)window_item->valuedouble, screenshot_api_callback);
        } else if (strcmp(mode, "area") == 0) {
            cJSON *x_item = cJSON_GetObjectItem(json, "x");
            cJSON *y_item = cJSON_GetObjectItem(json, "y");
            cJSON *w_item = cJSON_GetObjectItem(json, "width");
            cJSON *h_item = cJSON_GetObjectItem(json, "height");

            if (!cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item) ||
                !cJSON_IsNumber(w_item) || !cJSON_IsNumber(h_item)) {
                dwn_free(ctx);
                send_json_error(c, "x, y, width, height required for mode=area");
                return;
            }
            req = screenshot_capture_area_async(x_item->valueint, y_item->valueint,
                                                w_item->valueint, h_item->valueint,
                                                screenshot_api_callback);
        } else {
            dwn_free(ctx);
            send_json_error(c, "mode must be fullscreen, window, active, or area");
            return;
        }

        if (req == NULL) {
            dwn_free(ctx);
            send_json_error(c, "Failed to start screenshot capture");
            return;
        }

        req->user_data = ctx;
        return;
    }

    ScreenshotResult result = {0};
    ScreenshotStatus status;

    if (strcmp(mode, "fullscreen") == 0) {
        status = screenshot_capture_fullscreen(&result);
    } else if (strcmp(mode, "active") == 0) {
        status = screenshot_capture_active(&result);
    } else if (strcmp(mode, "window") == 0) {
        cJSON *window_item = cJSON_GetObjectItem(json, "window");
        if (!cJSON_IsNumber(window_item)) {
            send_json_error(c, "window parameter required for mode=window");
            return;
        }
        status = screenshot_capture_window((Window)window_item->valuedouble, &result);
    } else if (strcmp(mode, "area") == 0) {
        cJSON *x_item = cJSON_GetObjectItem(json, "x");
        cJSON *y_item = cJSON_GetObjectItem(json, "y");
        cJSON *w_item = cJSON_GetObjectItem(json, "width");
        cJSON *h_item = cJSON_GetObjectItem(json, "height");

        if (!cJSON_IsNumber(x_item) || !cJSON_IsNumber(y_item) ||
            !cJSON_IsNumber(w_item) || !cJSON_IsNumber(h_item)) {
            send_json_error(c, "x, y, width, height required for mode=area");
            return;
        }

        status = screenshot_capture_area(x_item->valueint, y_item->valueint,
                                         w_item->valueint, h_item->valueint, &result);
    } else {
        send_json_error(c, "mode must be fullscreen, window, active, or area");
        return;
    }

    if (status != SCREENSHOT_OK) {
        send_json_error(c, screenshot_status_string(status));
        return;
    }

    size_t base64_len = 0;
    char *base64_data = screenshot_to_base64(&result, &base64_len);

    if (base64_data == NULL) {
        screenshot_result_free(&result);
        send_json_error(c, "Failed to encode screenshot");
        return;
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "format", "png");
    cJSON_AddStringToObject(root, "encoding", "base64");
    cJSON_AddNumberToObject(root, "width", result.width);
    cJSON_AddNumberToObject(root, "height", result.height);
    cJSON_AddStringToObject(root, "data", base64_data);

    dwn_free(base64_data);
    screenshot_result_free(&result);

    send_json_response(c, root);
}

static int base64_decode_char(char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

static unsigned char *base64_decode(const char *input, size_t input_len, size_t *out_len) {
    if (input == NULL || input_len == 0) {
        return NULL;
    }

    size_t output_len = input_len / 4 * 3;
    if (input[input_len - 1] == '=') output_len--;
    if (input[input_len - 2] == '=') output_len--;

    unsigned char *output = dwn_malloc(output_len + 1);
    if (output == NULL) {
        return NULL;
    }

    size_t j = 0;
    for (size_t i = 0; i < input_len; i += 4) {
        int n = 0;
        int pad = 0;

        for (int k = 0; k < 4; k++) {
            if (i + k < input_len && input[i + k] != '=') {
                int val = base64_decode_char(input[i + k]);
                if (val < 0) {
                    dwn_free(output);
                    return NULL;
                }
                n = (n << 6) | val;
            } else {
                n <<= 6;
                pad++;
            }
        }

        if (j < output_len) output[j++] = (n >> 16) & 0xFF;
        if (j < output_len && pad < 2) output[j++] = (n >> 8) & 0xFF;
        if (j < output_len && pad < 1) output[j++] = n & 0xFF;
    }

    if (out_len) *out_len = j;
    return output;
}

typedef struct {
    struct mg_connection *conn;
    bool use_async;
} OcrApiContext;

static void ocr_api_callback(OcrRequest *req)
{
    OcrApiContext *ctx = (OcrApiContext *)req->user_data;
    if (ctx == NULL || ctx->conn == NULL) {
        if (req->png_data != NULL) {
            dwn_free(req->png_data);
        }
        ocr_result_free(&req->result);
        dwn_free(ctx);
        return;
    }

    if (req->state == OCR_STATE_COMPLETED) {
        cJSON *root = cJSON_CreateObject();
        cJSON_AddStringToObject(root, "status", "ok");
        cJSON_AddStringToObject(root, "text", req->result.text ? req->result.text : "");
        cJSON_AddNumberToObject(root, "confidence", req->result.confidence);
        send_json_response(ctx->conn, root);
    } else {
        send_json_error(ctx->conn, ocr_status_string(req->status));
    }

    if (req->png_data != NULL) {
        dwn_free(req->png_data);
    }
    ocr_result_free(&req->result);
    dwn_free(ctx);
}

static void handle_ocr(struct mg_connection *c, cJSON *json) {
    if (!ocr_is_available()) {
        send_json_error(c, "OCR not available");
        return;
    }

    cJSON *image_item = cJSON_GetObjectItem(json, "image");
    if (!cJSON_IsString(image_item) || !image_item->valuestring) {
        send_json_error(c, "image parameter required (base64 PNG)");
        return;
    }

    cJSON *lang_item = cJSON_GetObjectItem(json, "language");
    const char *language = "eng";
    if (cJSON_IsString(lang_item) && lang_item->valuestring) {
        language = lang_item->valuestring;
    }

    cJSON *async_item = cJSON_GetObjectItem(json, "async");
    bool use_async = true;
    if (cJSON_IsBool(async_item)) {
        use_async = cJSON_IsTrue(async_item);
    }

    size_t png_size = 0;
    unsigned char *png_data = base64_decode(image_item->valuestring,
                                            strlen(image_item->valuestring),
                                            &png_size);
    if (png_data == NULL) {
        send_json_error(c, "Failed to decode base64 image");
        return;
    }

    if (use_async) {
        OcrApiContext *ctx = dwn_malloc(sizeof(OcrApiContext));
        if (ctx == NULL) {
            dwn_free(png_data);
            send_json_error(c, "Out of memory");
            return;
        }
        ctx->conn = c;
        ctx->use_async = true;

        OcrRequest *req = ocr_extract_async(png_data, png_size, language, ocr_api_callback);
        if (req == NULL) {
            dwn_free(png_data);
            dwn_free(ctx);
            send_json_error(c, "Failed to start OCR extraction");
            return;
        }

        req->user_data = ctx;
        dwn_free(png_data);
        return;
    }

    OcrResult result = {0};
    OcrStatus status = ocr_extract_from_png(png_data, png_size, language, &result);

    dwn_free(png_data);

    if (status != OCR_OK) {
        send_json_error(c, ocr_status_string(status));
        return;
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "text", result.text ? result.text : "");
    cJSON_AddNumberToObject(root, "confidence", result.confidence);

    ocr_result_free(&result);

    send_json_response(c, root);
}

static void handle_set_master_ratio(struct mg_connection *c, cJSON *json) {
    cJSON *ratio_item = cJSON_GetObjectItem(json, "ratio");
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    if (!cJSON_IsNumber(ratio_item)) {
        send_json_error(c, "ratio parameter required (0.1-0.9)");
        return;
    }

    float ratio = (float)ratio_item->valuedouble;
    if (ratio < 0.1f || ratio > 0.9f) {
        send_json_error(c, "ratio must be between 0.1 and 0.9");
        return;
    }

    int ws = dwn->current_workspace;
    if (cJSON_IsNumber(ws_item)) {
        ws = ws_item->valueint;
        if (ws < 0 || ws >= MAX_WORKSPACES) {
            send_json_error(c, "workspace must be 0-8");
            return;
        }
    }

    workspace_set_master_ratio(ws, ratio);
    workspace_arrange(ws);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "workspace", ws);
    cJSON_AddNumberToObject(root, "master_ratio", dwn->workspaces[ws].master_ratio);
    send_json_response(c, root);
}

static void handle_adjust_master_ratio(struct mg_connection *c, cJSON *json) {
    cJSON *delta_item = cJSON_GetObjectItem(json, "delta");
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    if (!cJSON_IsNumber(delta_item)) {
        send_json_error(c, "delta parameter required");
        return;
    }

    float delta = (float)delta_item->valuedouble;
    int ws = dwn->current_workspace;
    if (cJSON_IsNumber(ws_item)) {
        ws = ws_item->valueint;
        if (ws < 0 || ws >= MAX_WORKSPACES) {
            send_json_error(c, "workspace must be 0-8");
            return;
        }
    }

    workspace_adjust_master_ratio(ws, delta);
    workspace_arrange(ws);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "workspace", ws);
    cJSON_AddNumberToObject(root, "master_ratio", dwn->workspaces[ws].master_ratio);
    send_json_response(c, root);
}

static void handle_set_master_count(struct mg_connection *c, cJSON *json) {
    cJSON *count_item = cJSON_GetObjectItem(json, "count");
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    if (!cJSON_IsNumber(count_item)) {
        send_json_error(c, "count parameter required (1-10)");
        return;
    }

    int count = count_item->valueint;
    if (count < 1 || count > 10) {
        send_json_error(c, "count must be between 1 and 10");
        return;
    }

    int ws = dwn->current_workspace;
    if (cJSON_IsNumber(ws_item)) {
        ws = ws_item->valueint;
        if (ws < 0 || ws >= MAX_WORKSPACES) {
            send_json_error(c, "workspace must be 0-8");
            return;
        }
    }

    workspace_set_master_count(ws, count);
    workspace_arrange(ws);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "workspace", ws);
    cJSON_AddNumberToObject(root, "master_count", dwn->workspaces[ws].master_count);
    send_json_response(c, root);
}

static void handle_adjust_master_count(struct mg_connection *c, cJSON *json) {
    cJSON *delta_item = cJSON_GetObjectItem(json, "delta");
    cJSON *ws_item = cJSON_GetObjectItem(json, "workspace");

    if (!cJSON_IsNumber(delta_item)) {
        send_json_error(c, "delta parameter required");
        return;
    }

    int delta = delta_item->valueint;
    int ws = dwn->current_workspace;
    if (cJSON_IsNumber(ws_item)) {
        ws = ws_item->valueint;
        if (ws < 0 || ws >= MAX_WORKSPACES) {
            send_json_error(c, "workspace must be 0-8");
            return;
        }
    }

    workspace_adjust_master_count(ws, delta);
    workspace_arrange(ws);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "workspace", ws);
    cJSON_AddNumberToObject(root, "master_count", dwn->workspaces[ws].master_count);
    send_json_response(c, root);
}

static void handle_focus_next(struct mg_connection *c) {
    workspace_focus_next();
    Workspace *ws = workspace_get_current();
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    if (ws && ws->focused) {
        cJSON_AddNumberToObject(root, "focused_window", (double)ws->focused->window);
        cJSON_AddStringToObject(root, "focused_title", ws->focused->title);
    } else {
        cJSON_AddNullToObject(root, "focused_window");
    }
    send_json_response(c, root);
}

static void handle_focus_prev(struct mg_connection *c) {
    workspace_focus_prev();
    Workspace *ws = workspace_get_current();
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    if (ws && ws->focused) {
        cJSON_AddNumberToObject(root, "focused_window", (double)ws->focused->window);
        cJSON_AddStringToObject(root, "focused_title", ws->focused->title);
    } else {
        cJSON_AddNullToObject(root, "focused_window");
    }
    send_json_response(c, root);
}

static void handle_focus_master(struct mg_connection *c) {
    workspace_focus_master();
    Workspace *ws = workspace_get_current();
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    if (ws && ws->focused) {
        cJSON_AddNumberToObject(root, "focused_window", (double)ws->focused->window);
        cJSON_AddStringToObject(root, "focused_title", ws->focused->title);
    } else {
        cJSON_AddNullToObject(root, "focused_window");
    }
    send_json_response(c, root);
}

static void handle_switch_workspace_next(struct mg_connection *c) {
    workspace_switch_next();
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "workspace", dwn->current_workspace);
    cJSON_AddStringToObject(root, "workspace_name", dwn->workspaces[dwn->current_workspace].name);
    send_json_response(c, root);
}

static void handle_switch_workspace_prev(struct mg_connection *c) {
    workspace_switch_prev();
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "workspace", dwn->current_workspace);
    cJSON_AddStringToObject(root, "workspace_name", dwn->workspaces[dwn->current_workspace].name);
    send_json_response(c, root);
}

static void handle_notify(struct mg_connection *c, cJSON *json) {
    cJSON *summary_item = cJSON_GetObjectItem(json, "summary");
    cJSON *body_item = cJSON_GetObjectItem(json, "body");
    cJSON *app_item = cJSON_GetObjectItem(json, "app_name");
    cJSON *timeout_item = cJSON_GetObjectItem(json, "timeout");

    if (!cJSON_IsString(summary_item)) {
        send_json_error(c, "summary parameter required");
        return;
    }

    const char *summary = summary_item->valuestring;
    const char *body = cJSON_IsString(body_item) ? body_item->valuestring : "";
    const char *app_name = cJSON_IsString(app_item) ? app_item->valuestring : "API";
    int timeout = cJSON_IsNumber(timeout_item) ? timeout_item->valueint : 5000;

    uint32_t id = notification_show(app_name, summary, body, NULL, timeout);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "notification_id", id);
    send_json_response(c, root);
}

static void handle_close_notification(struct mg_connection *c, cJSON *json) {
    cJSON *id_item = cJSON_GetObjectItem(json, "id");

    if (!cJSON_IsNumber(id_item)) {
        send_json_error(c, "id parameter required");
        return;
    }

    uint32_t id = (uint32_t)id_item->valueint;
    notification_close(id);

    send_json_ok(c);
}

static void handle_get_notifications(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON *arr = cJSON_AddArrayToObject(root, "notifications");

    Notification *notif = notification_find(0);
    while (notif) {
        cJSON *item = cJSON_CreateObject();
        cJSON_AddNumberToObject(item, "id", notif->id);
        cJSON_AddStringToObject(item, "app_name", notif->app_name);
        cJSON_AddStringToObject(item, "summary", notif->summary);
        cJSON_AddStringToObject(item, "body", notif->body ? notif->body : "");
        cJSON_AddNumberToObject(item, "timeout", notif->timeout);
        cJSON_AddNumberToObject(item, "urgency", notif->urgency);
        cJSON_AddItemToArray(arr, item);
        notif = notif->next;
    }

    send_json_response(c, root);
}

static void handle_get_battery_state(struct mg_connection *c) {
    BatteryState bat = systray_get_battery_snapshot();
    MultiBatteryState mbat = systray_get_multi_battery_snapshot();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");

    cJSON *battery = cJSON_AddObjectToObject(root, "battery");
    cJSON_AddBoolToObject(battery, "present", bat.present);
    cJSON_AddBoolToObject(battery, "charging", bat.charging);
    cJSON_AddNumberToObject(battery, "percentage", bat.percentage);
    cJSON_AddNumberToObject(battery, "time_remaining", bat.time_remaining);

    if (mbat.count > 1) {
        cJSON *batteries = cJSON_AddArrayToObject(root, "batteries");
        for (int i = 0; i < mbat.count; i++) {
            cJSON *b = cJSON_CreateObject();
            cJSON_AddStringToObject(b, "name", mbat.batteries[i].name);
            cJSON_AddBoolToObject(b, "present", mbat.batteries[i].present);
            cJSON_AddBoolToObject(b, "charging", mbat.batteries[i].charging);
            cJSON_AddNumberToObject(b, "percentage", mbat.batteries[i].percentage);
            cJSON_AddItemToArray(batteries, b);
        }
        cJSON_AddBoolToObject(root, "any_charging", mbat.any_charging);
        cJSON_AddNumberToObject(root, "combined_percentage", mbat.combined_percentage);
    }

    send_json_response(c, root);
}

static void handle_get_audio_state(struct mg_connection *c) {
    AudioState audio = systray_get_audio_snapshot();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "volume", audio.volume);
    cJSON_AddBoolToObject(root, "muted", audio.muted);
    send_json_response(c, root);
}

static void handle_set_audio_volume(struct mg_connection *c, cJSON *json) {
    cJSON *volume_item = cJSON_GetObjectItem(json, "volume");

    if (!cJSON_IsNumber(volume_item)) {
        send_json_error(c, "volume parameter required (0-100)");
        return;
    }

    int volume = volume_item->valueint;
    if (volume < 0) volume = 0;
    if (volume > 100) volume = 100;

    int old_volume = audio_state.volume;
    audio_set_volume(volume);

    api_emit_audio_volume_changed(old_volume, volume);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "volume", volume);
    cJSON_AddBoolToObject(root, "muted", audio_state.muted);
    send_json_response(c, root);
}

static void handle_toggle_audio_mute(struct mg_connection *c) {
    audio_toggle_mute();

    api_emit_audio_mute_toggled(audio_state.muted);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "muted", audio_state.muted);
    cJSON_AddNumberToObject(root, "volume", audio_state.volume);
    send_json_response(c, root);
}

static void handle_get_wifi_state(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "enabled", wifi_state.enabled);
    cJSON_AddBoolToObject(root, "connected", wifi_state.connected);
    cJSON_AddStringToObject(root, "ssid", wifi_state.current_ssid);
    cJSON_AddNumberToObject(root, "signal_strength", wifi_state.signal_strength);
    send_json_response(c, root);
}

static void handle_get_panel_state(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");

    cJSON *top = cJSON_AddObjectToObject(root, "top_panel");
    if (dwn->top_panel) {
        cJSON_AddBoolToObject(top, "enabled", dwn->config->top_panel_enabled);
        cJSON_AddBoolToObject(top, "visible", dwn->top_panel->visible);
        cJSON_AddNumberToObject(top, "x", dwn->top_panel->x);
        cJSON_AddNumberToObject(top, "y", dwn->top_panel->y);
        cJSON_AddNumberToObject(top, "width", dwn->top_panel->width);
        cJSON_AddNumberToObject(top, "height", dwn->top_panel->height);
    } else {
        cJSON_AddBoolToObject(top, "enabled", false);
        cJSON_AddBoolToObject(top, "visible", false);
    }

    cJSON *bottom = cJSON_AddObjectToObject(root, "bottom_panel");
    if (dwn->bottom_panel) {
        cJSON_AddBoolToObject(bottom, "enabled", dwn->config->bottom_panel_enabled);
        cJSON_AddBoolToObject(bottom, "visible", dwn->bottom_panel->visible);
        cJSON_AddNumberToObject(bottom, "x", dwn->bottom_panel->x);
        cJSON_AddNumberToObject(bottom, "y", dwn->bottom_panel->y);
        cJSON_AddNumberToObject(bottom, "width", dwn->bottom_panel->width);
        cJSON_AddNumberToObject(bottom, "height", dwn->bottom_panel->height);
    } else {
        cJSON_AddBoolToObject(bottom, "enabled", false);
        cJSON_AddBoolToObject(bottom, "visible", false);
    }

    send_json_response(c, root);
}

static void handle_show_panel(struct mg_connection *c, cJSON *json) {
    cJSON *panel_item = cJSON_GetObjectItem(json, "panel");

    if (!cJSON_IsString(panel_item)) {
        send_json_error(c, "panel parameter required (top/bottom)");
        return;
    }

    const char *panel_name = panel_item->valuestring;
    Panel *panel = NULL;

    if (strcmp(panel_name, "top") == 0) {
        panel = dwn->top_panel;
    } else if (strcmp(panel_name, "bottom") == 0) {
        panel = dwn->bottom_panel;
    } else {
        send_json_error(c, "panel must be 'top' or 'bottom'");
        return;
    }

    if (panel) {
        panel_show(panel);
        api_emit_panel_visibility_changed(panel_name, true);
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "panel", panel_name);
    cJSON_AddBoolToObject(root, "visible", panel ? panel->visible : false);
    send_json_response(c, root);
}

static void handle_hide_panel(struct mg_connection *c, cJSON *json) {
    cJSON *panel_item = cJSON_GetObjectItem(json, "panel");

    if (!cJSON_IsString(panel_item)) {
        send_json_error(c, "panel parameter required (top/bottom)");
        return;
    }

    const char *panel_name = panel_item->valuestring;
    Panel *panel = NULL;

    if (strcmp(panel_name, "top") == 0) {
        panel = dwn->top_panel;
    } else if (strcmp(panel_name, "bottom") == 0) {
        panel = dwn->bottom_panel;
    } else {
        send_json_error(c, "panel must be 'top' or 'bottom'");
        return;
    }

    if (panel) {
        panel_hide(panel);
        api_emit_panel_visibility_changed(panel_name, false);
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "panel", panel_name);
    cJSON_AddBoolToObject(root, "visible", panel ? panel->visible : false);
    send_json_response(c, root);
}

static void handle_toggle_panel(struct mg_connection *c, cJSON *json) {
    cJSON *panel_item = cJSON_GetObjectItem(json, "panel");

    if (!cJSON_IsString(panel_item)) {
        send_json_error(c, "panel parameter required (top/bottom)");
        return;
    }

    const char *panel_name = panel_item->valuestring;
    Panel *panel = NULL;

    if (strcmp(panel_name, "top") == 0) {
        panel = dwn->top_panel;
    } else if (strcmp(panel_name, "bottom") == 0) {
        panel = dwn->bottom_panel;
    } else {
        send_json_error(c, "panel must be 'top' or 'bottom'");
        return;
    }

    if (panel) {
        panel_toggle(panel);
        api_emit_panel_visibility_changed(panel_name, panel->visible);
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "panel", panel_name);
    cJSON_AddBoolToObject(root, "visible", panel ? panel->visible : false);
    send_json_response(c, root);
}

static void handle_get_config(struct mg_connection *c, cJSON *json) {
    cJSON *key_item = cJSON_GetObjectItem(json, "key");
    Config *cfg = dwn->config;

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");

    if (cJSON_IsString(key_item)) {
        const char *key = key_item->valuestring;

        if (strcmp(key, "terminal") == 0) {
            cJSON_AddStringToObject(root, "value", cfg->terminal);
        } else if (strcmp(key, "launcher") == 0) {
            cJSON_AddStringToObject(root, "value", cfg->launcher);
        } else if (strcmp(key, "file_manager") == 0) {
            cJSON_AddStringToObject(root, "value", cfg->file_manager);
        } else if (strcmp(key, "focus_mode") == 0) {
            cJSON_AddStringToObject(root, "value", cfg->focus_mode == FOCUS_CLICK ? "click" : "follow");
        } else if (strcmp(key, "border_width") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->border_width);
        } else if (strcmp(key, "title_height") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->title_height);
        } else if (strcmp(key, "panel_height") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->panel_height);
        } else if (strcmp(key, "gap") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->gap);
        } else if (strcmp(key, "default_master_ratio") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->default_master_ratio);
        } else if (strcmp(key, "default_master_count") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->default_master_count);
        } else if (strcmp(key, "ai_enabled") == 0) {
            cJSON_AddBoolToObject(root, "value", cfg->ai_enabled);
        } else if (strcmp(key, "ai_model") == 0) {
            cJSON_AddStringToObject(root, "value", cfg->ai_model);
        } else if (strcmp(key, "fade_speed") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->fade_speed);
        } else if (strcmp(key, "fade_intensity") == 0) {
            cJSON_AddNumberToObject(root, "value", cfg->fade_intensity);
        } else {
            cJSON_AddNullToObject(root, "value");
        }
    } else {
        cJSON *config = cJSON_AddObjectToObject(root, "config");
        cJSON_AddStringToObject(config, "terminal", cfg->terminal);
        cJSON_AddStringToObject(config, "launcher", cfg->launcher);
        cJSON_AddStringToObject(config, "file_manager", cfg->file_manager);
        cJSON_AddStringToObject(config, "focus_mode", cfg->focus_mode == FOCUS_CLICK ? "click" : "follow");
        cJSON_AddNumberToObject(config, "focus_follow_delay_ms", cfg->focus_follow_delay_ms);
        cJSON_AddBoolToObject(config, "show_decorations", cfg->show_decorations);
        cJSON_AddNumberToObject(config, "border_width", cfg->border_width);
        cJSON_AddNumberToObject(config, "title_height", cfg->title_height);
        cJSON_AddNumberToObject(config, "panel_height", cfg->panel_height);
        cJSON_AddNumberToObject(config, "gap", cfg->gap);
        cJSON_AddStringToObject(config, "font_name", cfg->font_name);
        cJSON_AddNumberToObject(config, "default_master_ratio", cfg->default_master_ratio);
        cJSON_AddNumberToObject(config, "default_master_count", cfg->default_master_count);
        cJSON_AddBoolToObject(config, "top_panel_enabled", cfg->top_panel_enabled);
        cJSON_AddBoolToObject(config, "bottom_panel_enabled", cfg->bottom_panel_enabled);
        cJSON_AddBoolToObject(config, "ai_enabled", cfg->ai_enabled);
        cJSON_AddStringToObject(config, "ai_model", cfg->ai_model);
        cJSON_AddBoolToObject(config, "api_enabled", cfg->api_enabled);
        cJSON_AddNumberToObject(config, "api_port", cfg->api_port);
        cJSON_AddNumberToObject(config, "fade_speed", cfg->fade_speed);
        cJSON_AddNumberToObject(config, "fade_intensity", cfg->fade_intensity);
    }

    send_json_response(c, root);
}

static void handle_reload_config(struct mg_connection *c) {
    bool success = config_reload(dwn->config);

    if (success) {
        api_emit_config_reloaded();
    }

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", success ? "ok" : "error");
    if (!success) {
        cJSON_AddStringToObject(root, "message", "Failed to reload configuration");
    }
    send_json_response(c, root);
}

static void handle_snap_client(struct mg_connection *c, cJSON *json) {
    cJSON *direction_item = cJSON_GetObjectItem(json, "direction");
    cJSON *window_item = cJSON_GetObjectItem(json, "window");

    if (!cJSON_IsString(direction_item)) {
        send_json_error(c, "direction parameter required (left/right/up/down)");
        return;
    }

    Client *cl = NULL;
    if (cJSON_IsNumber(window_item)) {
        cl = client_find_by_window((Window)window_item->valuedouble);
    } else {
        Workspace *ws = workspace_get_current();
        if (ws) cl = ws->focused;
    }

    if (!cl) {
        send_json_error(c, "No window to snap");
        return;
    }

    const char *direction = direction_item->valuestring;

    if (strcmp(direction, "left") == 0) {
        key_snap_left();
    } else if (strcmp(direction, "right") == 0) {
        key_snap_right();
    } else if (strcmp(direction, "up") == 0) {
        key_snap_up();
    } else if (strcmp(direction, "down") == 0) {
        key_snap_down();
    } else {
        send_json_error(c, "direction must be left/right/up/down");
        return;
    }

    api_emit_window_snapped(cl->window, cl->snap.horizontal, cl->snap.vertical);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "window", (double)cl->window);

    cJSON *snap = cJSON_AddObjectToObject(root, "snap");
    cJSON_AddNumberToObject(snap, "horizontal", cl->snap.horizontal);
    cJSON_AddNumberToObject(snap, "vertical", cl->snap.vertical);

    send_json_response(c, root);
}

static void handle_get_news(struct mg_connection *c) {
    news_lock();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "current_article", news_state.current_article);
    cJSON_AddNumberToObject(root, "article_count", news_state.article_count);
    cJSON_AddBoolToObject(root, "fetching", news_state.fetching);
    cJSON_AddBoolToObject(root, "has_error", news_state.has_error);

    cJSON *articles = cJSON_AddArrayToObject(root, "articles");
    for (int i = 0; i < news_state.article_count; i++) {
        cJSON *article = cJSON_CreateObject();
        cJSON_AddStringToObject(article, "title", news_state.articles[i].title);
        cJSON_AddStringToObject(article, "content", news_state.articles[i].content);
        cJSON_AddStringToObject(article, "link", news_state.articles[i].link);
        cJSON_AddStringToObject(article, "author", news_state.articles[i].author);
        cJSON_AddNumberToObject(article, "sentiment", news_state.articles[i].sentiment);
        cJSON_AddNumberToObject(article, "sentiment_score", news_state.articles[i].sentiment_score);
        cJSON_AddItemToArray(articles, article);
    }

    news_unlock();
    send_json_response(c, root);
}

static void handle_news_next(struct mg_connection *c) {
    news_lock();
    int old_index = news_state.current_article;
    news_unlock();

    news_next_article();

    news_lock();
    int new_index = news_state.current_article;
    const char *title = news_state.article_count > 0 ?
                        news_state.articles[new_index].title : "";
    news_unlock();

    api_emit_news_article_changed(old_index, new_index, title);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "current_article", new_index);
    cJSON_AddStringToObject(root, "title", title);
    send_json_response(c, root);
}

static void handle_news_prev(struct mg_connection *c) {
    news_lock();
    int old_index = news_state.current_article;
    news_unlock();

    news_prev_article();

    news_lock();
    int new_index = news_state.current_article;
    const char *title = news_state.article_count > 0 ?
                        news_state.articles[new_index].title : "";
    news_unlock();

    api_emit_news_article_changed(old_index, new_index, title);

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "current_article", new_index);
    cJSON_AddStringToObject(root, "title", title);
    send_json_response(c, root);
}

static void handle_news_open(struct mg_connection *c) {
    news_lock();
    const char *link = "";
    if (news_state.article_count > 0 && news_state.current_article < news_state.article_count) {
        link = news_state.articles[news_state.current_article].link;
    }
    news_unlock();

    news_open_current();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddStringToObject(root, "link", link);
    send_json_response(c, root);
}

static void handle_start_demo(struct mg_connection *c) {
    demo_start();
    api_emit_demo_started();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    send_json_response(c, root);
}

static void handle_stop_demo(struct mg_connection *c) {
    demo_stop();
    api_emit_demo_stopped();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    send_json_response(c, root);
}

static void handle_get_demo_state(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "active", demo_is_active());
    cJSON_AddNumberToObject(root, "phase", demo_get_phase());
    cJSON_AddStringToObject(root, "phase_name", demo_get_phase_name(demo_get_phase()));
    cJSON_AddNumberToObject(root, "step", demo_get_step());
    send_json_response(c, root);
}

static void handle_start_tutorial(struct mg_connection *c) {
    tutorial_start();
    api_emit_tutorial_started();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    send_json_response(c, root);
}

static void handle_stop_tutorial(struct mg_connection *c) {
    tutorial_stop();
    api_emit_tutorial_stopped();

    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    send_json_response(c, root);
}

static void handle_get_tutorial_state(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "active", tutorial_is_active());
    send_json_response(c, root);
}

static void handle_ai_is_available(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "available", ai_is_available());
    cJSON_AddStringToObject(root, "model", dwn->config->ai_model);
    send_json_response(c, root);
}

typedef struct {
    struct mg_connection *conn;
} AIApiContext;

static void ai_api_callback(AIRequest *req) {
    AIApiContext *ctx = (AIApiContext *)req->user_data;
    if (!ctx || !ctx->conn) {
        dwn_free(ctx);
        return;
    }

    cJSON *root = cJSON_CreateObject();
    if (req->state == AI_STATE_COMPLETED) {
        cJSON_AddStringToObject(root, "status", "ok");
        cJSON_AddStringToObject(root, "response", req->response ? req->response : "");
        api_emit_ai_response_received(req->prompt, req->response, true);
    } else {
        cJSON_AddStringToObject(root, "status", "error");
        cJSON_AddStringToObject(root, "message", "AI request failed");
        api_emit_ai_response_received(req->prompt, NULL, false);
    }
    send_json_response(ctx->conn, root);
    dwn_free(ctx);
}

static void handle_ai_command(struct mg_connection *c, cJSON *json) {
    cJSON *prompt_item = cJSON_GetObjectItem(json, "prompt");
    cJSON *async_item = cJSON_GetObjectItem(json, "async");

    if (!cJSON_IsString(prompt_item)) {
        send_json_error(c, "prompt parameter required");
        return;
    }

    if (!ai_is_available()) {
        send_json_error(c, "AI not available (no API key configured)");
        return;
    }

    bool use_async = true;
    if (cJSON_IsBool(async_item)) {
        use_async = cJSON_IsTrue(async_item);
    }

    const char *prompt = prompt_item->valuestring;

    if (use_async) {
        AIApiContext *ctx = dwn_malloc(sizeof(AIApiContext));
        if (!ctx) {
            send_json_error(c, "Out of memory");
            return;
        }
        ctx->conn = c;

        AIRequest *req = ai_send_request(prompt, ai_api_callback);
        if (!req) {
            dwn_free(ctx);
            send_json_error(c, "Failed to send AI request");
            return;
        }
        req->user_data = ctx;
    } else {
        cJSON *root = cJSON_CreateObject();
        cJSON_AddStringToObject(root, "status", "pending");
        cJSON_AddStringToObject(root, "message", "Use async=true for AI requests");
        send_json_response(c, root);
    }
}

static void handle_exa_is_available(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddBoolToObject(root, "available", exa_is_available());
    send_json_response(c, root);
}

typedef struct {
    struct mg_connection *conn;
} ExaApiContext;

static void exa_api_callback(ExaRequest *req) {
    ExaApiContext *ctx = (ExaApiContext *)req->user_data;
    if (!ctx || !ctx->conn) {
        dwn_free(ctx);
        return;
    }

    cJSON *root = cJSON_CreateObject();
    if (req->state == AI_STATE_COMPLETED) {
        cJSON_AddStringToObject(root, "status", "ok");
        cJSON *results = cJSON_AddArrayToObject(root, "results");
        for (int i = 0; i < req->result_count; i++) {
            cJSON *item = cJSON_CreateObject();
            cJSON_AddStringToObject(item, "title", req->results[i].title);
            cJSON_AddStringToObject(item, "url", req->results[i].url);
            cJSON_AddStringToObject(item, "snippet", req->results[i].snippet);
            cJSON_AddNumberToObject(item, "score", req->results[i].score);
            cJSON_AddItemToArray(results, item);
        }
        api_emit_exa_search_completed(req->query, req->result_count, true);
    } else {
        cJSON_AddStringToObject(root, "status", "error");
        cJSON_AddStringToObject(root, "message", "Exa search failed");
        api_emit_exa_search_completed(req->query, 0, false);
    }
    send_json_response(ctx->conn, root);
    dwn_free(ctx);
}

static void handle_exa_search(struct mg_connection *c, cJSON *json) {
    cJSON *query_item = cJSON_GetObjectItem(json, "query");
    cJSON *async_item = cJSON_GetObjectItem(json, "async");

    if (!cJSON_IsString(query_item)) {
        send_json_error(c, "query parameter required");
        return;
    }

    if (!exa_is_available()) {
        send_json_error(c, "Exa not available (no API key configured)");
        return;
    }

    bool use_async = true;
    if (cJSON_IsBool(async_item)) {
        use_async = cJSON_IsTrue(async_item);
    }

    const char *query = query_item->valuestring;

    if (use_async) {
        ExaApiContext *ctx = dwn_malloc(sizeof(ExaApiContext));
        if (!ctx) {
            send_json_error(c, "Out of memory");
            return;
        }
        ctx->conn = c;

        ExaRequest *req = exa_search(query, exa_api_callback);
        if (!req) {
            dwn_free(ctx);
            send_json_error(c, "Failed to send Exa search request");
            return;
        }
        req->user_data = ctx;
    } else {
        cJSON *root = cJSON_CreateObject();
        cJSON_AddStringToObject(root, "status", "pending");
        cJSON_AddStringToObject(root, "message", "Use async=true for Exa searches");
        send_json_response(c, root);
    }
}

static void handle_get_keybindings(struct mg_connection *c) {
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON *bindings = cJSON_AddArrayToObject(root, "keybindings");

    struct {
        const char *key;
        const char *modifiers;
        const char *description;
    } default_bindings[] = {
        {"t", "ctrl+alt", "Open terminal"},
        {"Super_L", "", "Open application launcher"},
        {"F2", "alt", "Open application launcher"},
        {"e", "super", "Open file manager"},
        {"b", "super", "Open web browser"},
        {"F4", "alt", "Close focused window"},
        {"Tab", "alt", "Cycle to next window"},
        {"Tab", "alt+shift", "Cycle to previous window"},
        {"F9", "alt", "Toggle minimize"},
        {"F10", "alt", "Toggle maximize"},
        {"F11", "alt", "Toggle fullscreen"},
        {"F9", "super", "Toggle floating"},
        {"F1", "", "Switch to workspace 1"},
        {"F2", "", "Switch to workspace 2"},
        {"F3", "", "Switch to workspace 3"},
        {"F4", "", "Switch to workspace 4"},
        {"F5", "", "Switch to workspace 5"},
        {"F6", "", "Switch to workspace 6"},
        {"F7", "", "Switch to workspace 7"},
        {"F8", "", "Switch to workspace 8"},
        {"F9", "", "Switch to workspace 9"},
        {"F1", "shift", "Move to workspace 1"},
        {"F2", "shift", "Move to workspace 2"},
        {"F3", "shift", "Move to workspace 3"},
        {"F4", "shift", "Move to workspace 4"},
        {"F5", "shift", "Move to workspace 5"},
        {"F6", "shift", "Move to workspace 6"},
        {"F7", "shift", "Move to workspace 7"},
        {"F8", "shift", "Move to workspace 8"},
        {"F9", "shift", "Move to workspace 9"},
        {"Right", "ctrl+alt", "Switch to next workspace"},
        {"Left", "ctrl+alt", "Switch to previous workspace"},
        {"space", "super", "Cycle layout mode"},
        {"h", "super", "Shrink master area"},
        {"l", "super", "Expand master area"},
        {"i", "super", "Increase master count"},
        {"d", "super", "Decrease master count / Show desktop"},
        {"Left", "super", "Snap left"},
        {"Right", "super", "Snap right"},
        {"Up", "super", "Snap up"},
        {"Down", "super", "Snap down"},
        {"a", "super", "AI context analysis"},
        {"a", "super+shift", "AI command palette"},
        {"e", "super+shift", "Exa semantic search"},
        {"Return", "super", "Open current news article"},
        {"s", "super", "Show keyboard shortcuts"},
        {"t", "super", "Start/continue tutorial"},
        {"d", "super+shift", "Start/stop demo mode"},
        {"BackSpace", "super", "Quit DWN"},
        {NULL, NULL, NULL}
    };

    for (int i = 0; default_bindings[i].key != NULL; i++) {
        cJSON *binding = cJSON_CreateObject();
        cJSON_AddStringToObject(binding, "key", default_bindings[i].key);
        cJSON_AddStringToObject(binding, "modifiers", default_bindings[i].modifiers);
        cJSON_AddStringToObject(binding, "description", default_bindings[i].description);
        cJSON_AddItemToArray(bindings, binding);
    }

    send_json_response(c, root);
}

static void handle_command(struct mg_connection *c, cJSON *json) {
    cJSON *cmd = cJSON_GetObjectItem(json, "command");
    if (!cJSON_IsString(cmd)) {
        send_json_error(c, "missing or invalid 'command' field");
        return;
    }

    const char *command = cmd->valuestring;

    if (strcmp(command, "subscribe") == 0) {
        handle_subscribe(c, json);
    } else if (strcmp(command, "unsubscribe") == 0) {
        handle_unsubscribe(c, json);
    } else if (strcmp(command, "list_events") == 0) {
        handle_list_events(c);
    } else if (strcmp(command, "get_status") == 0) {
        handle_get_status(c);
    } else if (strcmp(command, "get_workspaces") == 0) {
        handle_get_workspaces(c);
    } else if (strcmp(command, "get_clients") == 0) {
        handle_get_clients(c);
    } else if (strcmp(command, "find_clients") == 0) {
        handle_find_clients(c, json);
    } else if (strcmp(command, "get_focused_client") == 0) {
        handle_get_focused_client(c);
    } else if (strcmp(command, "switch_workspace") == 0) {
        cJSON *id = cJSON_GetObjectItem(json, "workspace");
        if (cJSON_IsNumber(id)) {
            int ws = id->valueint;
            if (ws < 0 || ws >= MAX_WORKSPACES) {
                send_json_error(c, "workspace must be 0-8");
                return;
            }
            workspace_switch(ws);
            handle_get_status(c);
        } else {
            send_json_error(c, "workspace parameter required");
        }
    } else if (strcmp(command, "run_command") == 0) {
        cJSON *exec = cJSON_GetObjectItem(json, "exec");
        if (cJSON_IsString(exec)) {
            spawn_async(exec->valuestring);
            cJSON *resp = cJSON_CreateObject();
            cJSON_AddStringToObject(resp, "status", "launched");
            send_json_response(c, resp);
        }
    } else if (strcmp(command, "focus_client") == 0) {
        cJSON *win = cJSON_GetObjectItem(json, "window");
        if (cJSON_IsNumber(win)) {
            Window window_id = (Window)win->valuedouble;
            if (window_id == None || window_id == 0) {
                send_json_error(c, "invalid window ID");
                return;
            }
            Client *cl = client_find_by_window(window_id);
            if (cl) {
                if (cl->workspace != (unsigned int)dwn->current_workspace) {
                    workspace_switch(cl->workspace);
                }
                client_focus(cl, true);
                handle_get_status(c);
            } else {
                send_json_error(c, "window not found");
            }
        } else {
            send_json_error(c, "window parameter required");
        }
    } else if (strcmp(command, "key_press") == 0) {
        handle_key_press(c, json);
    } else if (strcmp(command, "key_release") == 0) {
        handle_key_release(c, json);
    } else if (strcmp(command, "key_tap") == 0) {
        handle_key_tap(c, json);
    } else if (strcmp(command, "key_type") == 0) {
        handle_key_type(c, json);
    } else if (strcmp(command, "mouse_move") == 0) {
        handle_mouse_move(c, json);
    } else if (strcmp(command, "mouse_move_relative") == 0) {
        handle_mouse_move_relative(c, json);
    } else if (strcmp(command, "mouse_click") == 0) {
        handle_mouse_click(c, json);
    } else if (strcmp(command, "mouse_press") == 0) {
        handle_mouse_press(c, json);
    } else if (strcmp(command, "mouse_release") == 0) {
        handle_mouse_release(c, json);
    } else if (strcmp(command, "mouse_scroll") == 0) {
        handle_mouse_scroll(c, json);
    } else if (strcmp(command, "get_mouse_position") == 0) {
        handle_get_mouse_position(c);
    } else if (strcmp(command, "close_client") == 0) {
        handle_close_client(c, json);
    } else if (strcmp(command, "kill_client") == 0) {
        handle_kill_client(c, json);
    } else if (strcmp(command, "minimize_client") == 0) {
        handle_minimize_client(c, json);
    } else if (strcmp(command, "restore_client") == 0) {
        handle_restore_client(c, json);
    } else if (strcmp(command, "maximize_client") == 0) {
        handle_maximize_client(c, json);
    } else if (strcmp(command, "fullscreen_client") == 0) {
        handle_fullscreen_client(c, json);
    } else if (strcmp(command, "float_client") == 0) {
        handle_float_client(c, json);
    } else if (strcmp(command, "move_client") == 0) {
        handle_move_client(c, json);
    } else if (strcmp(command, "resize_client") == 0) {
        handle_resize_client(c, json);
    } else if (strcmp(command, "move_client_to_workspace") == 0) {
        handle_move_client_to_workspace(c, json);
    } else if (strcmp(command, "raise_client") == 0) {
        handle_raise_client(c, json);
    } else if (strcmp(command, "lower_client") == 0) {
        handle_lower_client(c, json);
    } else if (strcmp(command, "set_layout") == 0) {
        handle_set_layout(c, json);
    } else if (strcmp(command, "cycle_layout") == 0) {
        handle_cycle_layout(c, json);
    } else if (strcmp(command, "get_screen_info") == 0) {
        handle_get_screen_info(c);
    } else if (strcmp(command, "show_desktop") == 0) {
        handle_show_desktop(c, json);
    } else if (strcmp(command, "screenshot") == 0) {
        handle_screenshot(c, json);
    } else if (strcmp(command, "ocr") == 0) {
        handle_ocr(c, json);
    } else if (strcmp(command, "get_fade_settings") == 0) {
        cJSON *root = cJSON_CreateObject();
        cJSON_AddStringToObject(root, "status", "ok");
        cJSON_AddNumberToObject(root, "fade_speed", config_get_fade_speed());
        cJSON_AddNumberToObject(root, "fade_intensity", config_get_fade_intensity());
        send_json_response(c, root);
    } else if (strcmp(command, "set_fade_speed") == 0) {
        cJSON *speed_json = cJSON_GetObjectItem(json, "speed");
        if (cJSON_IsNumber(speed_json)) {
            float speed = speed_json->valuedouble;
            float old_speed = config_get_fade_speed();
            if (speed < 0.1f) speed = 0.1f;
            if (speed > 3.0f) speed = 3.0f;
            config_set_fade_speed(speed);
            if (dwn != NULL) {
                dwn->ambient_speed = speed;
            }
            api_emit_fade_speed_changed(old_speed, speed);
            cJSON *root = cJSON_CreateObject();
            cJSON_AddStringToObject(root, "status", "ok");
            cJSON_AddNumberToObject(root, "fade_speed", speed);
            send_json_response(c, root);
        } else {
            send_json_error(c, "speed parameter required (0.1-3.0)");
        }
    } else if (strcmp(command, "set_fade_intensity") == 0) {
        cJSON *intensity_json = cJSON_GetObjectItem(json, "intensity");
        if (cJSON_IsNumber(intensity_json)) {
            float intensity = intensity_json->valuedouble;
            float old_intensity = config_get_fade_intensity();
            if (intensity < 0.0f) intensity = 0.0f;
            if (intensity > 1.0f) intensity = 1.0f;
            config_set_fade_intensity(intensity);
            api_emit_fade_intensity_changed(old_intensity, intensity);
            cJSON *root = cJSON_CreateObject();
            cJSON_AddStringToObject(root, "status", "ok");
            cJSON_AddNumberToObject(root, "fade_intensity", intensity);
            send_json_response(c, root);
        } else {
            send_json_error(c, "intensity parameter required (0.0-1.0)");
        }
    } else if (strcmp(command, "set_master_ratio") == 0) {
        handle_set_master_ratio(c, json);
    } else if (strcmp(command, "adjust_master_ratio") == 0) {
        handle_adjust_master_ratio(c, json);
    } else if (strcmp(command, "set_master_count") == 0) {
        handle_set_master_count(c, json);
    } else if (strcmp(command, "adjust_master_count") == 0) {
        handle_adjust_master_count(c, json);
    } else if (strcmp(command, "focus_next") == 0) {
        handle_focus_next(c);
    } else if (strcmp(command, "focus_prev") == 0) {
        handle_focus_prev(c);
    } else if (strcmp(command, "focus_master") == 0) {
        handle_focus_master(c);
    } else if (strcmp(command, "switch_workspace_next") == 0) {
        handle_switch_workspace_next(c);
    } else if (strcmp(command, "switch_workspace_prev") == 0) {
        handle_switch_workspace_prev(c);
    } else if (strcmp(command, "notify") == 0) {
        handle_notify(c, json);
    } else if (strcmp(command, "close_notification") == 0) {
        handle_close_notification(c, json);
    } else if (strcmp(command, "get_notifications") == 0) {
        handle_get_notifications(c);
    } else if (strcmp(command, "get_battery_state") == 0) {
        handle_get_battery_state(c);
    } else if (strcmp(command, "get_audio_state") == 0) {
        handle_get_audio_state(c);
    } else if (strcmp(command, "set_audio_volume") == 0) {
        handle_set_audio_volume(c, json);
    } else if (strcmp(command, "toggle_audio_mute") == 0) {
        handle_toggle_audio_mute(c);
    } else if (strcmp(command, "get_wifi_state") == 0) {
        handle_get_wifi_state(c);
    } else if (strcmp(command, "get_panel_state") == 0) {
        handle_get_panel_state(c);
    } else if (strcmp(command, "show_panel") == 0) {
        handle_show_panel(c, json);
    } else if (strcmp(command, "hide_panel") == 0) {
        handle_hide_panel(c, json);
    } else if (strcmp(command, "toggle_panel") == 0) {
        handle_toggle_panel(c, json);
    } else if (strcmp(command, "get_config") == 0) {
        handle_get_config(c, json);
    } else if (strcmp(command, "reload_config") == 0) {
        handle_reload_config(c);
    } else if (strcmp(command, "snap_client") == 0) {
        handle_snap_client(c, json);
    } else if (strcmp(command, "get_news") == 0) {
        handle_get_news(c);
    } else if (strcmp(command, "news_next") == 0) {
        handle_news_next(c);
    } else if (strcmp(command, "news_prev") == 0) {
        handle_news_prev(c);
    } else if (strcmp(command, "news_open") == 0) {
        handle_news_open(c);
    } else if (strcmp(command, "start_demo") == 0) {
        handle_start_demo(c);
    } else if (strcmp(command, "stop_demo") == 0) {
        handle_stop_demo(c);
    } else if (strcmp(command, "get_demo_state") == 0) {
        handle_get_demo_state(c);
    } else if (strcmp(command, "start_tutorial") == 0) {
        handle_start_tutorial(c);
    } else if (strcmp(command, "stop_tutorial") == 0) {
        handle_stop_tutorial(c);
    } else if (strcmp(command, "get_tutorial_state") == 0) {
        handle_get_tutorial_state(c);
    } else if (strcmp(command, "ai_is_available") == 0) {
        handle_ai_is_available(c);
    } else if (strcmp(command, "ai_command") == 0) {
        handle_ai_command(c, json);
    } else if (strcmp(command, "exa_is_available") == 0) {
        handle_exa_is_available(c);
    } else if (strcmp(command, "exa_search") == 0) {
        handle_exa_search(c, json);
    } else if (strcmp(command, "get_keybindings") == 0) {
        handle_get_keybindings(c);
    } else {
        send_json_error(c, "unknown command");
    }
}

static void fn(struct mg_connection *c, int ev, void *ev_data) {
    if (ev == MG_EV_WS_MSG) {
        struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
        cJSON *json = cJSON_ParseWithLength(wm->data.buf, wm->data.len);
        if (json) {
            handle_command(c, json);
            cJSON_Delete(json);
        }
    } else if (ev == MG_EV_HTTP_MSG) {
        struct mg_http_message *hm = (struct mg_http_message *) ev_data;
        if (mg_match(hm->uri, mg_str("/ws"), NULL)) {
            mg_ws_upgrade(c, hm, NULL);
        } else {
            mg_http_reply(c, 200, "", "DWN WebSocket API is running at /ws\n");
        }
    } else if (ev == MG_EV_CLOSE) {
        remove_subscriber(c);
    }
}

void api_handle_json_command(const char *json_str) {
    cJSON *json = cJSON_Parse(json_str);
    if (json) {
        handle_command(NULL, json);
        cJSON_Delete(json);
    }
}

void api_init(void) {
    if (!dwn->config->api_enabled) return;

    mg_mgr_init(&mgr);
    char addr[64];
    snprintf(addr, sizeof(addr), "http://0.0.0.0:%d", dwn->config->api_port);
    if (mg_http_listen(&mgr, addr, fn, NULL) == NULL) {
        LOG_ERROR("Failed to start API server on %s", addr);
        return;
    }

    LOG_INFO("API server started on %s", addr);
    initialized = true;
}

void api_process(void) {
    if (!initialized) return;
    mg_mgr_poll(&mgr, 0);
}

void api_cleanup(void) {
    if (!initialized) return;
    mg_mgr_free(&mgr);
    initialized = false;
}

void api_emit_window_snapped(Window window, int horizontal, int vertical) {
    const char *h_names[] = {"none", "left", "right", "full"};
    const char *v_names[] = {"none", "top", "bottom", "full"};

    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "window", (double)window);
    cJSON_AddNumberToObject(data, "horizontal", horizontal);
    cJSON_AddNumberToObject(data, "vertical", vertical);
    cJSON_AddStringToObject(data, "horizontal_name", h_names[horizontal % 4]);
    cJSON_AddStringToObject(data, "vertical_name", v_names[vertical % 4]);
    broadcast_event(EVENT_WINDOW_SNAPPED, data);
}

void api_emit_audio_volume_changed(int old_volume, int new_volume) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "old_volume", old_volume);
    cJSON_AddNumberToObject(data, "new_volume", new_volume);
    broadcast_event(EVENT_AUDIO_VOLUME_CHANGED, data);
}

void api_emit_audio_mute_toggled(bool muted) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "muted", muted);
    broadcast_event(EVENT_AUDIO_MUTE_TOGGLED, data);
}

void api_emit_panel_visibility_changed(const char *panel, bool visible) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddStringToObject(data, "panel", panel ? panel : "");
    cJSON_AddBoolToObject(data, "visible", visible);
    broadcast_event(EVENT_PANEL_VISIBILITY_CHANGED, data);
}

void api_emit_config_reloaded(void) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "success", true);
    broadcast_event(EVENT_CONFIG_RELOADED, data);
}

void api_emit_ai_response_received(const char *prompt, const char *response, bool success) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddStringToObject(data, "prompt", prompt ? prompt : "");
    if (response) {
        cJSON_AddStringToObject(data, "response", response);
    } else {
        cJSON_AddNullToObject(data, "response");
    }
    cJSON_AddBoolToObject(data, "success", success);
    broadcast_event(EVENT_AI_RESPONSE_RECEIVED, data);
}

void api_emit_exa_search_completed(const char *query, int result_count, bool success) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddStringToObject(data, "query", query ? query : "");
    cJSON_AddNumberToObject(data, "result_count", result_count);
    cJSON_AddBoolToObject(data, "success", success);
    broadcast_event(EVENT_EXA_SEARCH_COMPLETED, data);
}

void api_emit_news_article_changed(int old_index, int new_index, const char *title) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "old_index", old_index);
    cJSON_AddNumberToObject(data, "new_index", new_index);
    cJSON_AddStringToObject(data, "title", title ? title : "");
    broadcast_event(EVENT_NEWS_ARTICLE_CHANGED, data);
}

void api_emit_demo_started(void) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "active", true);
    broadcast_event(EVENT_DEMO_STARTED, data);
}

void api_emit_demo_stopped(void) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "active", false);
    broadcast_event(EVENT_DEMO_STOPPED, data);
}

void api_emit_demo_phase_changed(int phase, const char *phase_name) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddNumberToObject(data, "phase", phase);
    cJSON_AddStringToObject(data, "phase_name", phase_name ? phase_name : "");
    broadcast_event(EVENT_DEMO_PHASE_CHANGED, data);
}

void api_emit_tutorial_started(void) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "active", true);
    broadcast_event(EVENT_TUTORIAL_STARTED, data);
}

void api_emit_tutorial_stopped(void) {
    cJSON *data = cJSON_CreateObject();
    cJSON_AddBoolToObject(data, "active", false);
    broadcast_event(EVENT_TUTORIAL_STOPPED, data);
}
