#define _GNU_SOURCE
#include <gio/gio.h>
#include <gio/gunixfdlist.h>
#include <pipewire/pipewire.h>
#include <spa/param/video/format-utils.h>
#include <spa/param/video/type-info.h>
#include <spa/utils/result.h>
#include <spa/buffer/meta.h>
#include <libei.h>
#include <xkbcommon/xkbcommon.h>
#include <png.h>
#include <linux/input-event-codes.h>
#include <dlfcn.h>
#include <errno.h>
#include <math.h>
#include <poll.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

#define PORTAL_BUS "org.freedesktop.portal.Desktop"
#define PORTAL_PATH "/org/freedesktop/portal/desktop"
#define REMOTE_IFACE "org.freedesktop.portal.RemoteDesktop"
#define SCREENCAST_IFACE "org.freedesktop.portal.ScreenCast"
#define REQUEST_IFACE "org.freedesktop.portal.Request"
#define SESSION_IFACE "org.freedesktop.portal.Session"
#define MAX_STREAMS 16
#define MAX_EI_DEVICES 32
#define CURSOR_META_SIZE(w,h) (sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + (w) * (h) * 4)
#ifndef EI_DEVICE_CAP_TEXT
#define EI_DEVICE_CAP_TEXT ((enum ei_device_capability)(1u << 6))
#endif

typedef void (*ei_text_utf8_fn)(struct ei_device *, const char *);

typedef struct {
  uint8_t *data;
  size_t len;
  size_t cap;
} ByteBuffer;

typedef struct {
  bool visible;
  int x, y, hotspot_x, hotspot_y;
  uint32_t width, height;
  uint8_t *rgba;
} CursorState;

typedef struct App App;
typedef struct StreamState StreamState;

struct StreamState {
  App *app;
  uint32_t node_id;
  uint64_t serial;
  char *id;
  char *mapping_id;
  int x, y, width, height;
  struct pw_stream *stream;
  struct spa_hook listener;
  struct spa_video_info_raw format;
  uint8_t *rgba;
  uint32_t pixel_width, pixel_height;
  uint64_t frame_seq;
  CursorState cursor;
};

struct App {
  GDBusConnection *bus;
  GMainLoop *wait_loop;
  char *expected_request;
  uint32_t request_response;
  GVariant *request_results;
  guint request_sub;
  guint session_sub;
  char *session_handle;
  bool session_started;
  bool session_closed;
  uint32_t granted_devices;
  int portal_timeout_ms;
  int frame_timeout_ms;
  StreamState streams[MAX_STREAMS];
  size_t n_streams;

  int pipewire_fd;
  struct pw_thread_loop *pw_loop;
  struct pw_context *pw_context;
  struct pw_core *pw_core;

  int eis_fd;
  struct ei *ei;
  struct ei_device *ei_devices[MAX_EI_DEVICES];
  size_t n_ei_devices;
  uint32_t sequence;
  ei_text_utf8_fn text_utf8;
};

static void fail_line(const char *code, const char *message) {
  gchar *b64 = g_base64_encode((const guchar *)message, strlen(message));
  printf("ERR\t%s\t%s\n", code, b64);
  fflush(stdout);
  g_free(b64);
}

static void ok_empty(void) {
  puts("OK");
  fflush(stdout);
}

static void ok_payload_text(const char *text) {
  gchar *b64 = g_base64_encode((const guchar *)text, strlen(text));
  printf("OK\t%s\n", b64);
  fflush(stdout);
  g_free(b64);
}

static uint64_t monotonic_ms(void) {
  struct timespec ts;
  clock_gettime(CLOCK_MONOTONIC, &ts);
  return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}

static bool bb_append(ByteBuffer *b, const uint8_t *data, size_t n) {
  if (b->len + n > b->cap) {
    size_t next = b->cap == 0 ? 65536 : b->cap;
    while (next < b->len + n) next *= 2;
    uint8_t *p = realloc(b->data, next);
    if (!p) return false;
    b->data = p;
    b->cap = next;
  }
  memcpy(b->data + b->len, data, n);
  b->len += n;
  return true;
}

static void png_write_cb(png_structp png, png_bytep data, png_size_t length) {
  ByteBuffer *b = png_get_io_ptr(png);
  if (!bb_append(b, data, length)) png_error(png, "out of memory");
}

static void png_flush_cb(png_structp png) { (void)png; }

static bool encode_png(const uint8_t *rgba, uint32_t width, uint32_t height, ByteBuffer *out) {
  png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  if (!png) return false;
  png_infop info = png_create_info_struct(png);
  if (!info) { png_destroy_write_struct(&png, NULL); return false; }
  if (setjmp(png_jmpbuf(png))) { png_destroy_write_struct(&png, &info); return false; }
  png_set_write_fn(png, out, png_write_cb, png_flush_cb);
  png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
  png_write_info(png, info);
  png_bytep *rows = calloc(height, sizeof(*rows));
  if (!rows) { png_destroy_write_struct(&png, &info); return false; }
  for (uint32_t y = 0; y < height; y++) rows[y] = (png_bytep)(rgba + (size_t)y * width * 4);
  png_write_image(png, rows);
  png_write_end(png, info);
  free(rows);
  png_destroy_write_struct(&png, &info);
  return true;
}

static void request_response_cb(GDBusConnection *connection, const gchar *sender_name,
    const gchar *object_path, const gchar *interface_name, const gchar *signal_name,
    GVariant *parameters, gpointer user_data) {
  (void)connection; (void)sender_name; (void)interface_name; (void)signal_name;
  App *app = user_data;
  if (!app->expected_request || strcmp(app->expected_request, object_path) != 0) return;
  if (app->request_results) g_variant_unref(app->request_results);
  app->request_results = NULL;
  g_variant_get(parameters, "(u@a{sv})", &app->request_response, &app->request_results);
  if (app->wait_loop) g_main_loop_quit(app->wait_loop);
}

static void session_closed_cb(GDBusConnection *connection, const gchar *sender_name,
    const gchar *object_path, const gchar *interface_name, const gchar *signal_name,
    GVariant *parameters, gpointer user_data) {
  (void)connection; (void)sender_name; (void)object_path; (void)interface_name; (void)signal_name; (void)parameters;
  App *app = user_data;
  app->session_closed = true;
}


static gboolean quit_loop_timeout_cb(gpointer data) {
  g_main_loop_quit((GMainLoop *)data);
  return G_SOURCE_REMOVE;
}

static GVariant *portal_request(App *app, const char *iface, const char *method,
    GVariant *parameters, int timeout_ms, GError **error) {
  GVariant *reply = g_dbus_connection_call_sync(app->bus, PORTAL_BUS, PORTAL_PATH, iface, method,
      parameters, G_VARIANT_TYPE("(o)"), G_DBUS_CALL_FLAGS_NONE, timeout_ms, NULL, error);
  if (!reply) return NULL;
  const char *request_path = NULL;
  g_variant_get(reply, "(&o)", &request_path);
  g_free(app->expected_request);
  app->expected_request = g_strdup(request_path);
  g_variant_unref(reply);

  app->request_response = UINT32_MAX;
  if (app->request_results) { g_variant_unref(app->request_results); app->request_results = NULL; }
  app->wait_loop = g_main_loop_new(NULL, FALSE);
  guint timeout_id = g_timeout_add((guint)timeout_ms, quit_loop_timeout_cb, app->wait_loop);
  g_main_loop_run(app->wait_loop);
  if (timeout_id && app->request_response != UINT32_MAX) g_source_remove(timeout_id);
  g_main_loop_unref(app->wait_loop);
  app->wait_loop = NULL;

  if (app->request_response == UINT32_MAX) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "Portal request timed out: %s.%s", iface, method);
    return NULL;
  }
  if (app->request_response != 0) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Portal request denied/cancelled: %s.%s (response %u)", iface, method, app->request_response);
    return NULL;
  }
  return app->request_results ? g_variant_ref(app->request_results) : g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0);
}

static GVariant *vardict0(void) {
  GVariantBuilder b;
  g_variant_builder_init(&b, G_VARIANT_TYPE_VARDICT);
  return g_variant_builder_end(&b);
}

static int portal_fd_call(App *app, const char *iface, const char *method, GVariant *parameters, int timeout_ms, GError **error) {
  GUnixFDList *fd_list = NULL;
  GVariant *reply = g_dbus_connection_call_with_unix_fd_list_sync(app->bus, PORTAL_BUS, PORTAL_PATH,
      iface, method, parameters, G_VARIANT_TYPE("(h)"), G_DBUS_CALL_FLAGS_NONE, timeout_ms,
      NULL, &fd_list, NULL, error);
  if (!reply) return -1;
  gint32 handle = -1;
  g_variant_get(reply, "(h)", &handle);
  g_variant_unref(reply);
  int fd = g_unix_fd_list_get(fd_list, handle, error);
  g_object_unref(fd_list);
  return fd;
}

static void clear_cursor(CursorState *c) {
  free(c->rgba);
  memset(c, 0, sizeof(*c));
}

static bool convert_pixels(uint8_t *dst, const uint8_t *src, uint32_t width, uint32_t height,
    int32_t stride, enum spa_video_format fmt) {
  int bpp = 4;
  if (fmt == SPA_VIDEO_FORMAT_RGB || fmt == SPA_VIDEO_FORMAT_BGR) bpp = 3;
  if (stride == 0) stride = (int32_t)width * bpp;
  for (uint32_t y = 0; y < height; y++) {
    const uint8_t *row = src + (size_t)y * (size_t)stride;
    for (uint32_t x = 0; x < width; x++) {
      const uint8_t *p = row + (size_t)x * bpp;
      uint8_t *q = dst + ((size_t)y * width + x) * 4;
      switch (fmt) {
        case SPA_VIDEO_FORMAT_RGBx: q[0]=p[0]; q[1]=p[1]; q[2]=p[2]; q[3]=255; break;
        case SPA_VIDEO_FORMAT_RGBA: q[0]=p[0]; q[1]=p[1]; q[2]=p[2]; q[3]=p[3]; break;
        case SPA_VIDEO_FORMAT_BGRx: q[0]=p[2]; q[1]=p[1]; q[2]=p[0]; q[3]=255; break;
        case SPA_VIDEO_FORMAT_BGRA: q[0]=p[2]; q[1]=p[1]; q[2]=p[0]; q[3]=p[3]; break;
        case SPA_VIDEO_FORMAT_ARGB: q[0]=p[2]; q[1]=p[1]; q[2]=p[0]; q[3]=p[3]; break;
        case SPA_VIDEO_FORMAT_ABGR: q[0]=p[0]; q[1]=p[1]; q[2]=p[2]; q[3]=p[3]; break;
        case SPA_VIDEO_FORMAT_RGB: q[0]=p[0]; q[1]=p[1]; q[2]=p[2]; q[3]=255; break;
        case SPA_VIDEO_FORMAT_BGR: q[0]=p[2]; q[1]=p[1]; q[2]=p[0]; q[3]=255; break;
        default: return false;
      }
    }
  }
  return true;
}

static void on_stream_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) {
  StreamState *s = userdata;
  if (!param || id != SPA_PARAM_Format) return;
  if (spa_format_video_raw_parse(param, &s->format) < 0) return;
  uint8_t buffer[2048];
  struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
  const struct spa_pod *params[2];
  params[0] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
      SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(4, 2, 16),
      SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
      SPA_PARAM_BUFFERS_size, SPA_POD_Int((int)(s->format.size.width * s->format.size.height * 4)),
      SPA_PARAM_BUFFERS_stride, SPA_POD_Int((int)(s->format.size.width * 4)),
      SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int((1u << SPA_DATA_MemFd) | (1u << SPA_DATA_MemPtr)));
  params[1] = spa_pod_builder_add_object(&b, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
      SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor),
      SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(CURSOR_META_SIZE(64,64), CURSOR_META_SIZE(1,1), CURSOR_META_SIZE(256,256)));
  pw_stream_update_params(s->stream, params, 2);
}

static void update_cursor(StreamState *s, struct spa_buffer *buf) {
  struct spa_meta_cursor *mcs = spa_buffer_find_meta_data(buf, SPA_META_Cursor, sizeof(*mcs));
  if (!mcs || !spa_meta_cursor_is_valid(mcs)) return;
  s->cursor.visible = true;
  s->cursor.x = mcs->position.x;
  s->cursor.y = mcs->position.y;
  s->cursor.hotspot_x = mcs->hotspot.x;
  s->cursor.hotspot_y = mcs->hotspot.y;
  if (mcs->bitmap_offset < sizeof(*mcs)) return;
  struct spa_meta_bitmap *mb = SPA_PTROFF(mcs, mcs->bitmap_offset, struct spa_meta_bitmap);
  if (!spa_meta_bitmap_is_valid(mb) || mb->offset < sizeof(*mb)) {
    clear_cursor(&s->cursor);
    return;
  }
  uint8_t *next = malloc((size_t)mb->size.width * mb->size.height * 4);
  if (!next) return;
  uint8_t *src = SPA_PTROFF(mb, mb->offset, uint8_t);
  if (!convert_pixels(next, src, mb->size.width, mb->size.height, mb->stride, (enum spa_video_format)mb->format)) {
    free(next); return;
  }
  free(s->cursor.rgba);
  s->cursor.rgba = next;
  s->cursor.width = mb->size.width;
  s->cursor.height = mb->size.height;
}

static void on_stream_process(void *userdata) {
  StreamState *s = userdata;
  struct pw_buffer *b = NULL, *next;
  while ((next = pw_stream_dequeue_buffer(s->stream)) != NULL) {
    if (b) pw_stream_queue_buffer(s->stream, b);
    b = next;
  }
  if (!b) return;
  struct spa_buffer *buf = b->buffer;
  if (!buf->n_datas || !buf->datas[0].data || !buf->datas[0].chunk) goto done;
  uint32_t w = s->format.size.width, h = s->format.size.height;
  if (!w || !h) goto done;
  uint8_t *next_rgba = malloc((size_t)w * h * 4);
  if (!next_rgba) goto done;
  uint8_t *src = (uint8_t *)buf->datas[0].data + buf->datas[0].chunk->offset;
  if (!convert_pixels(next_rgba, src, w, h, buf->datas[0].chunk->stride, s->format.format)) {
    free(next_rgba); goto done;
  }
  free(s->rgba);
  s->rgba = next_rgba;
  s->pixel_width = w;
  s->pixel_height = h;
  s->frame_seq++;
  update_cursor(s, buf);
done:
  pw_stream_queue_buffer(s->stream, b);
}

static const struct pw_stream_events stream_events = {
  PW_VERSION_STREAM_EVENTS,
  .param_changed = on_stream_param_changed,
  .process = on_stream_process,
};

static bool setup_pipewire(App *app, GError **error) {
  pw_init(NULL, NULL);
  app->pw_loop = pw_thread_loop_new("chatgpt-computer-wayland", NULL);
  if (!app->pw_loop) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "pw_thread_loop_new failed"); return false; }
  app->pw_context = pw_context_new(pw_thread_loop_get_loop(app->pw_loop), NULL, 0);
  if (!app->pw_context) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "pw_context_new failed"); return false; }
  app->pw_core = pw_context_connect_fd(app->pw_context, app->pipewire_fd, NULL, 0);
  if (!app->pw_core) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "pw_context_connect_fd failed"); return false; }
  if (pw_thread_loop_start(app->pw_loop) < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "pw_thread_loop_start failed"); return false; }

  pw_thread_loop_lock(app->pw_loop);
  for (size_t i = 0; i < app->n_streams; i++) {
    StreamState *s = &app->streams[i];
    struct pw_properties *props = pw_properties_new(
      PW_KEY_MEDIA_TYPE, "Video",
      PW_KEY_MEDIA_CATEGORY, "Capture",
      PW_KEY_MEDIA_ROLE, "Screen",
      NULL);
    s->stream = pw_stream_new(app->pw_core, "chatgpt-computer-capture", props);
    if (!s->stream) { pw_thread_loop_unlock(app->pw_loop); g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "pw_stream_new failed"); return false; }
    pw_stream_add_listener(s->stream, &s->listener, &stream_events, s);
    uint8_t buffer[1024];
    struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
    const struct spa_pod *params[1];
    params[0] = spa_pod_builder_add_object(&b,
      SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat,
      SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video),
      SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw),
      SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(6,
        SPA_VIDEO_FORMAT_BGRx,
        SPA_VIDEO_FORMAT_BGRx,
        SPA_VIDEO_FORMAT_BGRA,
        SPA_VIDEO_FORMAT_RGBx,
        SPA_VIDEO_FORMAT_RGBA,
        SPA_VIDEO_FORMAT_RGB));
    int res = pw_stream_connect(s->stream, PW_DIRECTION_INPUT, s->node_id,
      PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, params, 1);
    if (res < 0) { pw_thread_loop_unlock(app->pw_loop); g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "pw_stream_connect failed: %s", spa_strerror(res)); return false; }
  }
  pw_thread_loop_unlock(app->pw_loop);
  return true;
}

static void process_ei_events(App *app) {
  if (!app->ei) return;
  ei_dispatch(app->ei);
  struct ei_event *event;
  while ((event = ei_get_event(app->ei)) != NULL) {
    enum ei_event_type type = ei_event_get_type(event);
    if (type == EI_EVENT_SEAT_ADDED) {
      struct ei_seat *seat = ei_event_get_seat(event);
      if (app->text_utf8 && ei_seat_has_capability(seat, EI_DEVICE_CAP_TEXT)) {
        ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE,
          EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_TEXT, NULL);
      } else {
        ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE,
          EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, EI_DEVICE_CAP_KEYBOARD, NULL);
      }
    } else if (type == EI_EVENT_DEVICE_ADDED) {
      struct ei_device *d = ei_event_get_device(event);
      if (app->n_ei_devices < MAX_EI_DEVICES) {
        app->ei_devices[app->n_ei_devices++] = ei_device_ref(d);
        ei_device_start_emulating(d, ++app->sequence);
      }
    } else if (type == EI_EVENT_DEVICE_REMOVED) {
      struct ei_device *d = ei_event_get_device(event);
      for (size_t i = 0; i < app->n_ei_devices; i++) {
        if (app->ei_devices[i] == d) {
          ei_device_unref(app->ei_devices[i]);
          memmove(&app->ei_devices[i], &app->ei_devices[i+1], (app->n_ei_devices-i-1)*sizeof(app->ei_devices[0]));
          app->n_ei_devices--; break;
        }
      }
    }
    ei_event_unref(event);
  }
}

static void sync_stream_geometry_from_ei(App *app) {
  for (size_t i = 0; i < app->n_streams; i++) {
    StreamState *stream = &app->streams[i];
    if (!stream->mapping_id) continue;
    for (size_t d = 0; d < app->n_ei_devices; d++) {
      struct ei_device *device = app->ei_devices[d];
      if (!ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE)) continue;
      for (size_t r = 0;; r++) {
        struct ei_region *region = ei_device_get_region(device, r);
        if (!region) break;
        const char *mapping_id = ei_region_get_mapping_id(region);
        if (mapping_id && strcmp(mapping_id, stream->mapping_id) == 0) {
          stream->x = (int)ei_region_get_x(region);
          stream->y = (int)ei_region_get_y(region);
          stream->width = (int)ei_region_get_width(region);
          stream->height = (int)ei_region_get_height(region);
          break;
        }
      }
    }
  }
}

static bool ei_devices_ready(App *app) {
  bool absolute = false, button = false, scroll = false, keyboard = false;
  for (size_t i = 0; i < app->n_ei_devices; i++) {
    struct ei_device *d = app->ei_devices[i];
    if (ei_device_has_capability(d, EI_DEVICE_CAP_POINTER_ABSOLUTE)) {
      for (size_t r = 0; ei_device_get_region(d, r) != NULL; r++) { absolute = true; break; }
    }
    button = button || ei_device_has_capability(d, EI_DEVICE_CAP_BUTTON);
    scroll = scroll || ei_device_has_capability(d, EI_DEVICE_CAP_SCROLL);
    keyboard = keyboard || ei_device_has_capability(d, EI_DEVICE_CAP_KEYBOARD);
  }
  return absolute && button && scroll && keyboard;
}

static bool setup_ei(App *app, GError **error) {
  app->ei = ei_new_sender(app);
  if (!app->ei) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "ei_new_sender failed"); return false; }
  ei_configure_name(app->ei, "ChatGPT-Computer-MCP");
  if (ei_setup_backend_fd(app->ei, app->eis_fd) < 0) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "ei_setup_backend_fd failed"); return false; }
  app->text_utf8 = (ei_text_utf8_fn)dlsym(RTLD_DEFAULT, "ei_device_text_utf8");
  uint64_t deadline = monotonic_ms() + (uint64_t)app->frame_timeout_ms;
  while (monotonic_ms() < deadline) {
    struct pollfd pfd = { .fd = ei_get_fd(app->ei), .events = POLLIN };
    poll(&pfd, 1, 50);
    process_ei_events(app);
    if (ei_devices_ready(app)) break;
  }
  if (!ei_devices_ready(app)) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "Timed out waiting for required libei pointer/keyboard devices");
    return false;
  }
  sync_stream_geometry_from_ei(app);
  return true;
}

static bool wait_frames(App *app, int timeout_ms) {
  uint64_t deadline = monotonic_ms() + (uint64_t)timeout_ms;
  while (monotonic_ms() < deadline) {
    bool ready = app->n_streams > 0;
    if (app->pw_loop) pw_thread_loop_lock(app->pw_loop);
    for (size_t i = 0; i < app->n_streams; i++) if (!app->streams[i].rgba) ready = false;
    if (app->pw_loop) pw_thread_loop_unlock(app->pw_loop);
    if (ready) return true;
    usleep(10000);
  }
  return false;
}

static bool parse_streams(App *app, GVariant *results, GError **error) {
  GVariant *streams = g_variant_lookup_value(results, "streams", G_VARIANT_TYPE("a(ua{sv})"));
  if (!streams) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Portal returned no screen streams"); return false; }
  GVariantIter iter;
  g_variant_iter_init(&iter, streams);
  guint32 node;
  GVariant *props;
  while (app->n_streams < MAX_STREAMS && g_variant_iter_next(&iter, "(u@a{sv})", &node, &props)) {
    StreamState *s = &app->streams[app->n_streams++];
    memset(s, 0, sizeof(*s)); s->app = app; s->node_id = node;
    const char *str = NULL;
    if (g_variant_lookup(props, "id", "&s", &str)) s->id = g_strdup(str);
    if (g_variant_lookup(props, "mapping_id", "&s", &str)) s->mapping_id = g_strdup(str);
    guint64 serial = 0; if (g_variant_lookup(props, "pipewire-serial", "t", &serial)) s->serial = serial;
    gint32 x=0,y=0,w=0,h=0;
    g_variant_lookup(props, "position", "(ii)", &x, &y);
    if (!g_variant_lookup(props, "size", "(ii)", &w, &h) || w <= 0 || h <= 0) { w = 1; h = 1; }
    s->x=x; s->y=y; s->width=w; s->height=h;
    if (!s->id) s->id = g_strdup_printf("wayland-%u", node);
    g_variant_unref(props);
  }
  g_variant_unref(streams);
  if (!app->n_streams) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "No monitor stream selected"); return false; }
  return true;
}

static bool get_portal_u32_property(App *app, const char *iface, const char *property, uint32_t *value, GError **error) {
  GVariant *reply = g_dbus_connection_call_sync(app->bus, PORTAL_BUS, PORTAL_PATH,
      "org.freedesktop.DBus.Properties", "Get", g_variant_new("(ss)", iface, property),
      G_VARIANT_TYPE("(v)"), G_DBUS_CALL_FLAGS_NONE, app->portal_timeout_ms, NULL, error);
  if (!reply) return false;
  GVariant *boxed = NULL;
  g_variant_get(reply, "(@v)", &boxed);
  GVariant *inner = g_variant_get_variant(boxed);
  bool ok = g_variant_is_of_type(inner, G_VARIANT_TYPE_UINT32);
  if (ok) *value = g_variant_get_uint32(inner);
  else g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Portal property %s.%s is not uint32", iface, property);
  g_variant_unref(inner);
  g_variant_unref(boxed);
  g_variant_unref(reply);
  return ok;
}

static bool init_session(App *app, GError **error) {
  if (app->session_started && !app->session_closed) return true;
  if (app->session_closed) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_CLOSED, "Wayland portal session was closed/revoked"); return false; }
  app->bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, error);
  if (!app->bus) return false;

  uint32_t device_types = 0, source_types = 0, cursor_modes = 0;
  if (!get_portal_u32_property(app, REMOTE_IFACE, "AvailableDeviceTypes", &device_types, error)) return false;
  if ((device_types & (1u | 2u)) != (1u | 2u)) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "RemoteDesktop portal does not expose both keyboard and pointer devices");
    return false;
  }
  if (!get_portal_u32_property(app, SCREENCAST_IFACE, "AvailableSourceTypes", &source_types, error)) return false;
  if ((source_types & 1u) == 0) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "ScreenCast portal does not expose monitor capture");
    return false;
  }
  if (!get_portal_u32_property(app, SCREENCAST_IFACE, "AvailableCursorModes", &cursor_modes, error)) return false;
  if ((cursor_modes & 4u) == 0) {
    g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "ScreenCast portal does not support cursor metadata mode required for controllable includePointer behavior");
    return false;
  }
  app->request_sub = g_dbus_connection_signal_subscribe(app->bus, PORTAL_BUS, REQUEST_IFACE, "Response", NULL, NULL,
      G_DBUS_SIGNAL_FLAGS_NONE, request_response_cb, app, NULL);

  GVariantBuilder create;
  g_variant_builder_init(&create, G_VARIANT_TYPE_VARDICT);
  char token[64]; snprintf(token, sizeof(token), "chatgpt_computer_%u", (unsigned)getpid());
  g_variant_builder_add(&create, "{sv}", "handle_token", g_variant_new_string(token));
  char stok[64]; snprintf(stok, sizeof(stok), "chatgpt_computer_session_%u", (unsigned)getpid());
  g_variant_builder_add(&create, "{sv}", "session_handle_token", g_variant_new_string(stok));
  GVariant *results = portal_request(app, REMOTE_IFACE, "CreateSession", g_variant_new("(@a{sv})", g_variant_builder_end(&create)), app->portal_timeout_ms, error);
  if (!results) return false;
  const char *session = NULL;
  if (!g_variant_lookup(results, "session_handle", "&s", &session)) { g_variant_unref(results); g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "Portal did not return session_handle"); return false; }
  app->session_handle = g_strdup(session);
  g_variant_unref(results);
  app->session_sub = g_dbus_connection_signal_subscribe(app->bus, PORTAL_BUS, SESSION_IFACE, "Closed", app->session_handle, NULL,
      G_DBUS_SIGNAL_FLAGS_NONE, session_closed_cb, app, NULL);

  GVariantBuilder devices;
  g_variant_builder_init(&devices, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add(&devices, "{sv}", "types", g_variant_new_uint32(1u|2u));
  results = portal_request(app, REMOTE_IFACE, "SelectDevices", g_variant_new("(o@a{sv})", app->session_handle, g_variant_builder_end(&devices)), app->portal_timeout_ms, error);
  if (!results) return false;
  g_variant_unref(results);

  GVariantBuilder sources;
  g_variant_builder_init(&sources, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add(&sources, "{sv}", "types", g_variant_new_uint32(1u));
  g_variant_builder_add(&sources, "{sv}", "multiple", g_variant_new_boolean(TRUE));
  g_variant_builder_add(&sources, "{sv}", "cursor_mode", g_variant_new_uint32(4u));
  results = portal_request(app, SCREENCAST_IFACE, "SelectSources", g_variant_new("(o@a{sv})", app->session_handle, g_variant_builder_end(&sources)), app->portal_timeout_ms, error);
  if (!results) return false;
  g_variant_unref(results);

  results = portal_request(app, REMOTE_IFACE, "Start", g_variant_new("(os@a{sv})", app->session_handle, "", vardict0()), app->portal_timeout_ms, error);
  if (!results) return false;
  g_variant_lookup(results, "devices", "u", &app->granted_devices);
  if (!parse_streams(app, results, error)) { g_variant_unref(results); return false; }
  g_variant_unref(results);

  app->pipewire_fd = portal_fd_call(app, SCREENCAST_IFACE, "OpenPipeWireRemote", g_variant_new("(o@a{sv})", app->session_handle, vardict0()), app->portal_timeout_ms, error);
  if (app->pipewire_fd < 0) return false;
  app->eis_fd = portal_fd_call(app, REMOTE_IFACE, "ConnectToEIS", g_variant_new("(o@a{sv})", app->session_handle, vardict0()), app->portal_timeout_ms, error);
  if (app->eis_fd < 0) return false;
  if (!setup_pipewire(app, error)) return false;
  if (!setup_ei(app, error)) return false;
  if (!wait_frames(app, app->frame_timeout_ms)) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "Timed out waiting for PipeWire screen frames"); return false; }
  app->session_started = true;
  return true;
}

static void virtual_bounds(App *app, int *x, int *y, int *w, int *h) {
  int minx=app->streams[0].x, miny=app->streams[0].y;
  int maxx=app->streams[0].x+app->streams[0].width, maxy=app->streams[0].y+app->streams[0].height;
  for (size_t i=1;i<app->n_streams;i++) {
    StreamState *s=&app->streams[i];
    if (s->x < minx) minx = s->x;
    if (s->y < miny) miny = s->y;
    if (s->x + s->width > maxx) maxx = s->x + s->width;
    if (s->y + s->height > maxy) maxy = s->y + s->height;
  }
  *x=minx;*y=miny;*w=maxx-minx;*h=maxy-miny;
}

static char *info_json(App *app) {
  int vx,vy,vw,vh; virtual_bounds(app,&vx,&vy,&vw,&vh);
  GString *j=g_string_new("{\"backend\":\"wayland\",\"sessionType\":\"wayland\",\"coordinateUnit\":\"logical-pixel\",\"coordinateSpaces\":[\"desktop\",\"display\"],\"virtualBounds\":{");
  g_string_append_printf(j,"\"x\":%d,\"y\":%d,\"width\":%d,\"height\":%d},\"displays\":[",vx,vy,vw,vh);
  if (app->pw_loop) pw_thread_loop_lock(app->pw_loop);
  for(size_t i=0;i<app->n_streams;i++){
    StreamState*s=&app->streams[i];
    gchar *eid=g_strescape(s->id,NULL); double scale=s->width?((double)s->pixel_width/s->width):1.0;
    if(i)g_string_append_c(j,',');
    g_string_append_printf(j,"{\"id\":\"%s\",\"name\":\"%s\",\"x\":%d,\"y\":%d,\"width\":%d,\"height\":%d,\"primary\":false,\"pixelWidth\":%u,\"pixelHeight\":%u,\"scale\":%.6f}",eid,eid,s->x,s->y,s->width,s->height,s->pixel_width,s->pixel_height,scale);
    g_free(eid);
  }
  if (app->pw_loop) pw_thread_loop_unlock(app->pw_loop);
  bool ptr=(app->granted_devices&2u)!=0, kbd=(app->granted_devices&1u)!=0;
  g_string_append_printf(j,"],\"capabilities\":{\"observe\":true,\"pointer\":%s,\"keyboard\":%s,\"text\":%s}}",ptr?"true":"false",kbd?"true":"false",kbd?"true":"false");
  return g_string_free(j,FALSE);
}

static bool point_in_stream(StreamState *s,double x,double y){return x>=s->x&&y>=s->y&&x<s->x+s->width&&y<s->y+s->height;}
static bool rect_intersects(StreamState*s,int x,int y,int w,int h){return x<s->x+s->width&&x+w>s->x&&y<s->y+s->height&&y+h>s->y;}

static struct ei_device *find_pointer_device(App *app, StreamState *stream, enum ei_device_capability required, struct ei_region **out_region) {
  process_ei_events(app);
  for(size_t i=0;i<app->n_ei_devices;i++){
    struct ei_device*d=app->ei_devices[i];
    if(!ei_device_has_capability(d,EI_DEVICE_CAP_POINTER_ABSOLUTE)||!ei_device_has_capability(d,required))continue;
    for(size_t r=0;;r++){
      struct ei_region*region=ei_device_get_region(d,r); if(!region)break;
      const char*mid=ei_region_get_mapping_id(region);
      if ((stream->mapping_id && mid && strcmp(stream->mapping_id, mid) == 0) || (!stream->mapping_id && app->n_streams == 1 && r == 0)) {
        if (out_region) *out_region = region;
        return d;
      }
    }
  }
  return NULL;
}

static bool move_pointer(App *app,double x,double y,char **err) {
  StreamState *stream=NULL; for(size_t i=0;i<app->n_streams;i++)if(point_in_stream(&app->streams[i],x,y)){stream=&app->streams[i];break;}
  if(!stream){*err=g_strdup("Pointer coordinate is outside the selected Wayland monitor regions.");return false;}
  struct ei_region *region=NULL; struct ei_device*d=find_pointer_device(app,stream,EI_DEVICE_CAP_POINTER_ABSOLUTE,&region);
  if(!d||!region){*err=g_strdup("No libei absolute pointer region matches the selected PipeWire stream mapping_id.");return false;}
  double lx=(x-stream->x)/(double)stream->width, ly=(y-stream->y)/(double)stream->height;
  double rx=ei_region_get_x(region)+lx*ei_region_get_width(region), ry=ei_region_get_y(region)+ly*ei_region_get_height(region);
  ei_device_pointer_motion_absolute(d,rx,ry); ei_device_frame(d,ei_now(app->ei)); return true;
}

static struct ei_device *find_cap_device(App*app,enum ei_device_capability cap){process_ei_events(app);for(size_t i=0;i<app->n_ei_devices;i++)if(ei_device_has_capability(app->ei_devices[i],cap))return app->ei_devices[i];return NULL;}
static bool send_button(App*app,int b,bool down,char**err){struct ei_device*d=find_cap_device(app,EI_DEVICE_CAP_BUTTON);if(!d){*err=g_strdup("No libei button-capable device available.");return false;}uint32_t code=b==1?BTN_LEFT:b==2?BTN_MIDDLE:BTN_RIGHT;ei_device_button_button(d,code,down);ei_device_frame(d,ei_now(app->ei));return true;}
static bool send_scroll(App*app,int dx,int dy,char**err){struct ei_device*d=find_cap_device(app,EI_DEVICE_CAP_SCROLL);if(!d){*err=g_strdup("No libei scroll-capable device available.");return false;}ei_device_scroll_discrete(d,dx*120,dy*120);ei_device_frame(d,ei_now(app->ei));ei_device_scroll_stop(d,dx!=0,dy!=0);ei_device_frame(d,ei_now(app->ei));return true;}

static int keycode_for_name(const char*n){
  if (!strcasecmp(n, "ctrl") || !strcasecmp(n, "control")) return KEY_LEFTCTRL;
  if (!strcasecmp(n, "shift")) return KEY_LEFTSHIFT;
  if (!strcasecmp(n, "alt")) return KEY_LEFTALT;
  if (!strcasecmp(n, "super") || !strcasecmp(n, "meta") || !strcasecmp(n, "win")) return KEY_LEFTMETA;
  if (!strcasecmp(n, "return") || !strcasecmp(n, "enter")) return KEY_ENTER;
  if (!strcasecmp(n, "escape") || !strcasecmp(n, "esc")) return KEY_ESC;
  if (!strcasecmp(n, "tab")) return KEY_TAB;
  if (!strcasecmp(n, "backspace")) return KEY_BACKSPACE;
  if (!strcasecmp(n, "delete") || !strcasecmp(n, "del")) return KEY_DELETE;
  if (!strcasecmp(n, "space")) return KEY_SPACE;
  if (!strcasecmp(n, "left")) return KEY_LEFT;
  if (!strcasecmp(n, "right")) return KEY_RIGHT;
  if (!strcasecmp(n, "up")) return KEY_UP;
  if (!strcasecmp(n, "down")) return KEY_DOWN;
  if (!strcasecmp(n, "home")) return KEY_HOME;
  if (!strcasecmp(n, "end")) return KEY_END;
  if (!strcasecmp(n, "pageup") || !strcasecmp(n, "prior")) return KEY_PAGEUP;
  if (!strcasecmp(n, "pagedown") || !strcasecmp(n, "next")) return KEY_PAGEDOWN;
  if((n[0]=='F'||n[0]=='f')&&n[1]){int f=atoi(n+1);if(f>=1&&f<=10)return KEY_F1+f-1;if(f==11)return KEY_F11;if(f==12)return KEY_F12;}
  if(strlen(n)==1){char c=n[0];if(c>='a'&&c<='z')c-=32;switch(c){case'A':return KEY_A;case'B':return KEY_B;case'C':return KEY_C;case'D':return KEY_D;case'E':return KEY_E;case'F':return KEY_F;case'G':return KEY_G;case'H':return KEY_H;case'I':return KEY_I;case'J':return KEY_J;case'K':return KEY_K;case'L':return KEY_L;case'M':return KEY_M;case'N':return KEY_N;case'O':return KEY_O;case'P':return KEY_P;case'Q':return KEY_Q;case'R':return KEY_R;case'S':return KEY_S;case'T':return KEY_T;case'U':return KEY_U;case'V':return KEY_V;case'W':return KEY_W;case'X':return KEY_X;case'Y':return KEY_Y;case'Z':return KEY_Z;case'0':return KEY_0;case'1':return KEY_1;case'2':return KEY_2;case'3':return KEY_3;case'4':return KEY_4;case'5':return KEY_5;case'6':return KEY_6;case'7':return KEY_7;case'8':return KEY_8;case'9':return KEY_9;}}
  return -1;
}

static bool send_keycode(App*app,struct ei_device*d,int code,bool down){if(code<0)return false;ei_device_keyboard_key(d,(uint32_t)code,down);ei_device_frame(d,ei_now(app->ei));return true;}
static bool send_chord(App*app,const char*chord,char**err){struct ei_device*d=find_cap_device(app,EI_DEVICE_CAP_KEYBOARD);if(!d){*err=g_strdup("No libei keyboard device available.");return false;}char*copy=g_strdup(chord);char*save=NULL;int codes[16],n=0;for(char*t=strtok_r(copy,"+",&save);t&&n<16;t=strtok_r(NULL,"+",&save)){int c=keycode_for_name(t);if(c<0){*err=g_strdup_printf("Unsupported Wayland key name: %s",t);g_free(copy);return false;}codes[n++]=c;}for(int i=0;i<n;i++)send_keycode(app,d,codes[i],true);for(int i=n-1;i>=0;i--)send_keycode(app,d,codes[i],false);g_free(copy);return true;}

static bool fallback_text(App*app,const char*text,int delay_ms,char**err){
  struct ei_device*d=find_cap_device(app,EI_DEVICE_CAP_KEYBOARD);if(!d){*err=g_strdup("No libei keyboard device available for text fallback.");return false;}
  struct ei_keymap*ek=ei_device_keyboard_get_keymap(d);if(!ek){*err=g_strdup("libei keyboard device has no XKB keymap; UTF-8 text needs libei 1.6 text capability.");return false;}
  int fd=ei_keymap_get_fd(ek);size_t sz=ei_keymap_get_size(ek);char*map=mmap(NULL,sz,PROT_READ,MAP_PRIVATE,fd,0);if(map==MAP_FAILED){*err=g_strdup("Could not mmap libei XKB keymap.");return false;}
  struct xkb_context*ctx=xkb_context_new(XKB_CONTEXT_NO_FLAGS);struct xkb_keymap*km=xkb_keymap_new_from_string(ctx,map,XKB_KEYMAP_FORMAT_TEXT_V1,XKB_KEYMAP_COMPILE_NO_FLAGS);munmap(map,sz);if(!km){xkb_context_unref(ctx);*err=g_strdup("Could not parse libei XKB keymap.");return false;}
  const unsigned char*p=(const unsigned char*)text;while(*p){uint32_t cp;size_t n;if(*p<0x80){cp=*p;n=1;}else if((*p&0xe0)==0xc0){cp=((*p&0x1f)<<6)|(p[1]&0x3f);n=2;}else if((*p&0xf0)==0xe0){cp=((*p&0xf)<<12)|((p[1]&0x3f)<<6)|(p[2]&0x3f);n=3;}else{cp=((*p&7)<<18)|((p[1]&0x3f)<<12)|((p[2]&0x3f)<<6)|(p[3]&0x3f);n=4;}xkb_keysym_t sym=xkb_utf32_to_keysym(cp);bool found=false;for(xkb_keycode_t kc=xkb_keymap_min_keycode(km);kc<=xkb_keymap_max_keycode(km)&&!found;kc++){xkb_layout_index_t layouts=xkb_keymap_num_layouts_for_key(km,kc);for(xkb_layout_index_t l=0;l<layouts&&!found;l++){if(l!=0)break;xkb_level_index_t levels=xkb_keymap_num_levels_for_key(km,kc,l);for(xkb_level_index_t lv=0;lv<levels&&lv<2&&!found;lv++){const xkb_keysym_t*syms=NULL;int count=xkb_keymap_key_get_syms_by_level(km,kc,l,lv,&syms);for(int si=0;si<count;si++)if(syms[si]==sym){int ev=(int)kc-8;if(lv==1)send_keycode(app,d,KEY_LEFTSHIFT,true);send_keycode(app,d,ev,true);send_keycode(app,d,ev,false);if(lv==1)send_keycode(app,d,KEY_LEFTSHIFT,false);found=true;break;}}}}if(!found){xkb_keymap_unref(km);xkb_context_unref(ctx);*err=g_strdup_printf("Character U+%04X is not representable by the current libei keymap fallback.",cp);return false;}if(delay_ms)usleep((useconds_t)delay_ms*1000);p+=n;}
  xkb_keymap_unref(km);xkb_context_unref(ctx);return true;
}

static bool send_text(App*app,const char*text,int delay_ms,char**err){if(app->text_utf8){struct ei_device*d=find_cap_device(app,EI_DEVICE_CAP_TEXT);if(d){app->text_utf8(d,text);ei_device_frame(d,ei_now(app->ei));return true;}}return fallback_text(app,text,delay_ms,err);}

static void alpha_blend(uint8_t*d,const uint8_t*s){unsigned a=s[3],ia=255-a;d[0]=(uint8_t)((s[0]*a+d[0]*ia)/255);d[1]=(uint8_t)((s[1]*a+d[1]*ia)/255);d[2]=(uint8_t)((s[2]*a+d[2]*ia)/255);d[3]=255;}

static bool capture_region(App*app,int rx,int ry,int rw,int rh,bool pointer,ByteBuffer*out,uint32_t*ow,uint32_t*oh,char**err){
  if(rw<=0||rh<=0){*err=g_strdup("Invalid capture region.");return false;}if(!wait_frames(app, app->frame_timeout_ms)){*err=g_strdup("Timed out waiting for PipeWire frames.");return false;}
  double scale=1.0;bool any=false;pw_thread_loop_lock(app->pw_loop);for(size_t i=0;i<app->n_streams;i++){StreamState*s=&app->streams[i];if(rect_intersects(s,rx,ry,rw,rh)){any=true;double sx=(double)s->pixel_width/s->width,sy=(double)s->pixel_height/s->height;if(sx>scale)scale=sx;if(sy>scale)scale=sy;}}if(!any){pw_thread_loop_unlock(app->pw_loop);*err=g_strdup("Capture region does not intersect any selected Wayland monitor.");return false;}
  *ow=(uint32_t)ceil(rw*scale);*oh=(uint32_t)ceil(rh*scale);uint8_t*dst=calloc((size_t)*ow**oh,4);if(!dst){pw_thread_loop_unlock(app->pw_loop);*err=g_strdup("Out of memory composing screenshot.");return false;}for(size_t p=0;p<(size_t)*ow**oh;p++)dst[p*4+3]=255;
  for(uint32_t oy=0;oy<*oh;oy++){double gy=ry+(oy+0.5)/scale;for(uint32_t ox=0;ox<*ow;ox++){double gx=rx+(ox+0.5)/scale;for(size_t i=0;i<app->n_streams;i++){StreamState*s=&app->streams[i];if(!point_in_stream(s,gx,gy)||!s->rgba)continue;uint32_t sx=(uint32_t)((gx-s->x)*s->pixel_width/s->width),sy=(uint32_t)((gy-s->y)*s->pixel_height/s->height);if(sx>=s->pixel_width)sx=s->pixel_width-1;if(sy>=s->pixel_height)sy=s->pixel_height-1;memcpy(dst+((size_t)oy**ow+ox)*4,s->rgba+((size_t)sy*s->pixel_width+sx)*4,4);break;}}}
  if(pointer){for(size_t i=0;i<app->n_streams;i++){StreamState*s=&app->streams[i];CursorState*c=&s->cursor;if(!c->visible||!c->rgba||!s->pixel_width)continue;double ss=(double)s->pixel_width/s->width;double cgx=s->x+(c->x-c->hotspot_x)/ss,cgy=s->y+(c->y-c->hotspot_y)/ss;double cursor_scale=scale/ss;int dx0=(int)floor((cgx-rx)*scale),dy0=(int)floor((cgy-ry)*scale);int dw=(int)ceil(c->width*cursor_scale),dh=(int)ceil(c->height*cursor_scale);for(int dy=0;dy<dh;dy++){int oy=dy0+dy;if(oy<0||oy>=(int)*oh)continue;uint32_t sy=(uint32_t)(dy/cursor_scale);if(sy>=c->height)continue;for(int dx=0;dx<dw;dx++){int ox=dx0+dx;if(ox<0||ox>=(int)*ow)continue;uint32_t sx=(uint32_t)(dx/cursor_scale);if(sx>=c->width)continue;alpha_blend(dst+((size_t)oy**ow+ox)*4,c->rgba+((size_t)sy*c->width+sx)*4);}}}}
  pw_thread_loop_unlock(app->pw_loop);bool ok=encode_png(dst,*ow,*oh,out);free(dst);if(!ok)*err=g_strdup("PNG encoding failed.");return ok;
}

static char **split_tabs(char *line,int *count){int cap=16,n=0;char**a=calloc(cap,sizeof(char*));char*save=NULL;for(char*t=strtok_r(line,"\t\r\n",&save);t;t=strtok_r(NULL,"\t\r\n",&save)){if(n==cap){cap*=2;a=realloc(a,cap*sizeof(char*));}a[n++]=t;}*count=n;return a;}
static bool parse_point_fields(char**f,int n,int start,bool*has,double*x,double*y){if(start+2>=n)return false;*has=atoi(f[start])!=0;*x=atof(f[start+1]);*y=atof(f[start+2]);return true;}

static void handle_command(App*app,char*line){
  int n=0;char**f=split_tabs(line,&n);if(!n){free(f);return;}if(!strcmp(f[0],"QUIT")){free(f);exit(0);}GError*e=NULL;if(!init_session(app,&e)){fail_line("BACKEND_UNAVAILABLE",e?e->message:"Wayland initialization failed");if(e)g_error_free(e);free(f);return;}while(g_main_context_iteration(NULL,FALSE));if(app->session_closed){fail_line("BACKEND_UNAVAILABLE","Wayland portal session was closed or revoked.");free(f);return;}
  if(!strcmp(f[0],"INFO")){char*j=info_json(app);ok_payload_text(j);g_free(j);free(f);return;}
  if(!strcmp(f[0],"OBSERVE")&&n>=6){int x=atoi(f[1]),y=atoi(f[2]),w=atoi(f[3]),h=atoi(f[4]);bool ptr=atoi(f[5])!=0;ByteBuffer png={0};uint32_t pw=0,ph=0;char*err=NULL;if(!capture_region(app,x,y,w,h,ptr,&png,&pw,&ph,&err)){fail_line("OS_ERROR",err?err:"Capture failed");g_free(err);free(png.data);free(f);return;}gchar*img=g_base64_encode(png.data,png.len);GString*j=g_string_new("{\"data\":\"");g_string_append(j,img);g_string_append_printf(j,"\",\"pixelWidth\":%u,\"pixelHeight\":%u}",pw,ph);ok_payload_text(j->str);g_string_free(j,TRUE);g_free(img);free(png.data);free(f);return;}
  char*err=NULL;bool ok=false;
  if(!strcmp(f[0],"MOVE")&&n>=3)ok=move_pointer(app,atof(f[1]),atof(f[2]),&err);
  else if((!strcmp(f[0],"CLICK")||!strcmp(f[0],"MOUSE_DOWN")||!strcmp(f[0],"MOUSE_UP"))&&n>=5){int b=atoi(f[1]);bool hp;double x,y;parse_point_fields(f,n,2,&hp,&x,&y);ok=!hp||move_pointer(app,x,y,&err);if(ok){if(!strcmp(f[0],"CLICK")){ok=send_button(app,b,true,&err)&&send_button(app,b,false,&err);}else ok=send_button(app,b,!strcmp(f[0],"MOUSE_DOWN"),&err);}}
  else if(!strcmp(f[0],"DOUBLE_CLICK")&&n>=6){int b=atoi(f[1]),delay=atoi(f[2]);bool hp;double x,y;parse_point_fields(f,n,3,&hp,&x,&y);ok=!hp||move_pointer(app,x,y,&err);if(ok){ok=send_button(app,b,true,&err)&&send_button(app,b,false,&err);if(ok&&delay)usleep((useconds_t)delay*1000);if(ok)ok=send_button(app,b,true,&err)&&send_button(app,b,false,&err);}}
  else if(!strcmp(f[0],"SCROLL")&&n>=6){int dx=atoi(f[1]),dy=atoi(f[2]);bool hp;double x,y;parse_point_fields(f,n,3,&hp,&x,&y);ok=!hp||move_pointer(app,x,y,&err);if(ok)ok=send_scroll(app,dx,dy,&err);}
  else if(!strcmp(f[0],"DRAG")&&n>=6){
    int b=atoi(f[1]);
    ok=move_pointer(app,atof(f[2]),atof(f[3]),&err);
    if(ok) ok=send_button(app,b,true,&err);
    if(ok){
      bool moved=move_pointer(app,atof(f[4]),atof(f[5]),&err);
      char *release_err=NULL;
      bool released=send_button(app,b,false,&release_err);
      if(!moved) ok=false;
      else if(!released){g_free(err);err=release_err;release_err=NULL;ok=false;}
      g_free(release_err);
    }
  }
  else if(!strcmp(f[0],"KEY")&&n>=2){int count=atoi(f[1]);ok=true;if(n<count+2)ok=false;for(int i=0;ok&&i<count;i++){gsize len=0;guchar*decoded=g_base64_decode(f[i+2],&len);char*key=g_strndup((char*)decoded,len);ok=send_chord(app,key,&err);g_free(key);g_free(decoded);}}
  else if(!strcmp(f[0],"TEXT")&&n>=3){int delay=atoi(f[1]);gsize len=0;guchar*decoded=g_base64_decode(f[2],&len);char*text=g_strndup((char*)decoded,len);ok=send_text(app,text,delay,&err);g_free(text);g_free(decoded);}
  else{err=g_strdup("Malformed or unsupported helper command.");}
  if(ok)ok_empty();else fail_line("OS_ERROR",err?err:"Wayland input action failed.");g_free(err);free(f);
}

static void cleanup(App*app){
  if(app->session_handle&&app->bus)g_dbus_connection_call_sync(app->bus,PORTAL_BUS,app->session_handle,SESSION_IFACE,"Close",NULL,NULL,G_DBUS_CALL_FLAGS_NONE,2000,NULL,NULL);
  if(app->pw_loop){pw_thread_loop_lock(app->pw_loop);for(size_t i=0;i<app->n_streams;i++)if(app->streams[i].stream)pw_stream_destroy(app->streams[i].stream);pw_thread_loop_unlock(app->pw_loop);pw_thread_loop_stop(app->pw_loop);}if(app->pw_core)pw_core_disconnect(app->pw_core);if(app->pw_context)pw_context_destroy(app->pw_context);if(app->pw_loop)pw_thread_loop_destroy(app->pw_loop);
  if(app->ei){for(size_t i=0;i<app->n_ei_devices;i++){ei_device_stop_emulating(app->ei_devices[i]);ei_device_unref(app->ei_devices[i]);}ei_unref(app->ei);}for(size_t i=0;i<app->n_streams;i++){g_free(app->streams[i].id);g_free(app->streams[i].mapping_id);free(app->streams[i].rgba);clear_cursor(&app->streams[i].cursor);}if(app->bus)g_object_unref(app->bus);g_free(app->session_handle);g_free(app->expected_request);if(app->request_results)g_variant_unref(app->request_results);
}

static int env_timeout_ms(const char *name, int fallback, int min, int max) {
  const char *raw = getenv(name);
  if (!raw || !*raw) return fallback;
  char *end = NULL;
  long value = strtol(raw, &end, 10);
  if (!end || *end != '\0' || value < min || value > max) return fallback;
  return (int)value;
}

int main(void) {
  setvbuf(stdout, NULL, _IOLBF, 0);
  App app = {0};
  app.pipewire_fd = -1;
  app.eis_fd = -1;
  app.portal_timeout_ms = env_timeout_ms("CHATGPT_COMPUTER_MCP_PORTAL_TIMEOUT_MS", 120000, 1000, 300000);
  app.frame_timeout_ms = env_timeout_ms("CHATGPT_COMPUTER_MCP_FRAME_TIMEOUT_MS", 5000, 100, 30000);
  puts("HELLO\t1");
  char *line = NULL;
  size_t cap = 0;
  while (getline(&line, &cap, stdin) >= 0) handle_command(&app, line);
  free(line);
  cleanup(&app);
  return 0;
}
