/*
 * C helper for folk-runtime-embed.
 *
 * zend_try/zend_catch/zend_end_try are macros built on setjmp/longjmp.
 * They cannot be called directly from Rust FFI — we wrap them in C functions.
 *
 * This file also implements the custom Folk SAPI module, which provides
 * proper HTTP request/response lifecycle to PHP (headers, body, cookies,
 * $_SERVER, $_GET, $_POST, $_COOKIE).
 */

#include "main/php.h"
#include "main/SAPI.h"
#include "main/php_main.h"
#include "main/php_variables.h"
#include "Zend/zend.h"
#include "Zend/zend_exceptions.h"
#include "Zend/zend_execute.h"
#include "Zend/zend_API.h"
#include "Zend/zend_call_stack.h"

#include <setjmp.h>
#include <signal.h>
#include <string.h>
#include <pthread.h>
#include <sys/resource.h>

#ifdef ZTS
#include "TSRM/TSRM.h"
#endif

/* ── Thread-local SIGSEGV protection (forward decl) ───────────── */

static __thread sigjmp_buf folk_sigsegv_jmpbuf;
static __thread volatile int folk_sigsegv_active = 0;

/* ── TSRM thread context management (ZTS only) ───────────────── */

#ifdef ZTS
#include "Zend/zend_ini.h"

/*
 * PHP 8.3 ZTS thread init:
 *
 * ts_resource(0) → allocate_new_resource() runs ctors then calls
 * zend_new_thread_end_handler() while tsmm_mutex is held. The handler
 * calls zend_copy_ini_directives() + zend_ini_refresh_caches() which
 * trigger OnUpdate callbacks (e.g., mbstring's PCRE regex compile).
 * These callbacks access globals via ts_resource_ex() → deadlock on
 * the non-recursive tsmm_mutex.
 *
 * Fix: TSRM.c is patched to NOT call the handler inside allocate_new_resource.
 * We call it manually here, after ts_resource returns and tsmm_mutex is released.
 *
 * Serialization mutex prevents concurrent ts_resource calls.
 */
static pthread_mutex_t folk_tsrm_init_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif

/**
 * Register the calling thread with TSRM (PHP 8.0+).
 * Must be called from each worker thread before any PHP operations.
 * Returns non-NULL on ZTS builds, NULL on NTS builds.
 */
void *folk_thread_init(void) {
#ifdef ZTS
    pthread_mutex_lock(&folk_tsrm_init_mutex);
    void *ctx = ts_resource(0);
    /* tsmm_mutex is now released by ts_resource_ex.
     * Manually run what zend_new_thread_end_handler does:
     * copy INI directives + refresh caches + init stack + timer. */
    zend_copy_ini_directives();
    zend_ini_refresh_caches(ZEND_INI_STAGE_STARTUP);
    zend_call_stack_init();
    pthread_mutex_unlock(&folk_tsrm_init_mutex);
    return ctx;
#else
    /* NTS: re-detect stack limits for the worker thread. */
    zend_call_stack_init();
    return NULL;
#endif
}

/**
 * Set the TSRM interpreter context for the calling thread.
 * In PHP 8.0+, TSRM context is thread-local (automatic) — this is a no-op.
 */
void folk_thread_set_ctx(void *ctx) {
    (void)ctx;
}

/**
 * Free TSRM resources for the calling thread.
 */
void folk_thread_shutdown(void *ctx) {
#ifdef ZTS
    (void)ctx;
    ts_free_thread();
#else
    (void)ctx;
#endif
}

/**
 * Check if this is a ZTS build.
 */
int folk_is_zts(void) {
#ifdef ZTS
    return 1;
#else
    return 0;
#endif
}

/* ── Thread-local request context ─────────────────────────────── */

/*
 * Per-thread request context. Rust sets this before php_request_startup(),
 * and our SAPI callbacks read from it.
 */
typedef struct {
    /* Request info */
    const char *request_method;   /* "GET", "POST", etc. */
    const char *request_uri;      /* "/path?query" */
    const char *query_string;     /* "key=val&..." */
    const char *content_type;     /* "application/json" etc. */
    size_t      content_length;
    const char *path_translated;  /* physical file path */

    /* Request body */
    const char *post_data;
    size_t      post_data_len;
    size_t      post_data_read;   /* how much read_post has consumed */

    /* Request cookies */
    const char *cookie_data;      /* raw Cookie header value */

    /* HTTP headers (for $_SERVER HTTP_* vars) */
    const char **header_names;    /* array of header names */
    const char **header_values;   /* array of header values */
    size_t      header_count;

    /* Server info */
    const char *server_name;
    int         server_port;
    const char *protocol;         /* "HTTP/1.1" */
} folk_request_context_t;

static __thread folk_request_context_t *folk_current_request = NULL;

/* ── Thread-local response context ────────────────────────────── */

typedef struct {
    int         status_code;

    /* Response headers: dynamic array */
    char      **headers;          /* "Content-Type: text/html" */
    size_t      header_count;
    size_t      header_cap;
} folk_response_context_t;

static __thread folk_response_context_t folk_response = {0};

/* ── Thread-local output buffer ───────────────────────────────── */

static __thread char *folk_output_buf = NULL;
static __thread size_t folk_output_len = 0;
static __thread size_t folk_output_cap = 0;

/* ── SAPI callbacks ───────────────────────────────────────────── */

static size_t folk_sapi_ub_write(const char *str, size_t str_length) {
    if (folk_output_buf == NULL) {
        folk_output_cap = (str_length > 4096) ? str_length * 2 : 4096;
        folk_output_buf = malloc(folk_output_cap);
        folk_output_len = 0;
    }

    if (folk_output_len + str_length > folk_output_cap) {
        folk_output_cap = (folk_output_len + str_length) * 2;
        folk_output_buf = realloc(folk_output_buf, folk_output_cap);
    }

    memcpy(folk_output_buf + folk_output_len, str, str_length);
    folk_output_len += str_length;

    return str_length;
}

static void folk_sapi_flush(void *server_context) {
    (void)server_context;
    /* No-op — we buffer everything and return it at end of request */
}

static int folk_sapi_send_headers(sapi_headers_struct *sapi_headers) {
    (void)sapi_headers;
    /* Capture the HTTP response code */
    folk_response.status_code = SG(sapi_headers).http_response_code;
    return SAPI_HEADER_SENT_SUCCESSFULLY;
}

static void folk_sapi_send_header(sapi_header_struct *sapi_header, void *server_context) {
    (void)server_context;

    if (sapi_header == NULL || sapi_header->header == NULL || sapi_header->header_len == 0) {
        return;
    }

    /* Grow headers array if needed */
    if (folk_response.header_count >= folk_response.header_cap) {
        folk_response.header_cap = folk_response.header_cap == 0 ? 16 : folk_response.header_cap * 2;
        folk_response.headers = realloc(folk_response.headers, folk_response.header_cap * sizeof(char *));
    }

    /* Copy the header string */
    char *copy = malloc(sapi_header->header_len + 1);
    memcpy(copy, sapi_header->header, sapi_header->header_len);
    copy[sapi_header->header_len] = '\0';

    folk_response.headers[folk_response.header_count++] = copy;
}

static size_t folk_sapi_read_post(char *buffer, size_t count_bytes) {
    if (folk_current_request == NULL || folk_current_request->post_data == NULL) {
        return 0;
    }

    size_t remaining = folk_current_request->post_data_len - folk_current_request->post_data_read;
    if (remaining == 0) {
        return 0;
    }

    size_t to_read = (count_bytes < remaining) ? count_bytes : remaining;
    memcpy(buffer, folk_current_request->post_data + folk_current_request->post_data_read, to_read);
    folk_current_request->post_data_read += to_read;

    return to_read;
}

static char *folk_sapi_read_cookies(void) {
    if (folk_current_request == NULL || folk_current_request->cookie_data == NULL) {
        return "";
    }
    /* PHP expects a non-const pointer — it won't modify it */
    return (char *)folk_current_request->cookie_data;
}

static void folk_sapi_register_variables(zval *track_vars_array) {
    /* Always register default PHP variables */
    php_import_environment_variables(track_vars_array);

    if (folk_current_request == NULL) {
        return;
    }

    /* Standard $_SERVER variables */
    if (folk_current_request->request_method) {
        php_register_variable_safe("REQUEST_METHOD",
            (char *)folk_current_request->request_method,
            strlen(folk_current_request->request_method),
            track_vars_array);
    }

    if (folk_current_request->request_uri) {
        php_register_variable_safe("REQUEST_URI",
            (char *)folk_current_request->request_uri,
            strlen(folk_current_request->request_uri),
            track_vars_array);
    }

    if (folk_current_request->query_string) {
        php_register_variable_safe("QUERY_STRING",
            (char *)folk_current_request->query_string,
            strlen(folk_current_request->query_string),
            track_vars_array);
    }

    if (folk_current_request->content_type) {
        php_register_variable_safe("CONTENT_TYPE",
            (char *)folk_current_request->content_type,
            strlen(folk_current_request->content_type),
            track_vars_array);
    }

    if (folk_current_request->content_length > 0) {
        char cl_buf[32];
        snprintf(cl_buf, sizeof(cl_buf), "%zu", folk_current_request->content_length);
        php_register_variable_safe("CONTENT_LENGTH", cl_buf, strlen(cl_buf), track_vars_array);
    }

    if (folk_current_request->path_translated) {
        php_register_variable_safe("SCRIPT_FILENAME",
            (char *)folk_current_request->path_translated,
            strlen(folk_current_request->path_translated),
            track_vars_array);
    }

    if (folk_current_request->server_name) {
        php_register_variable_safe("SERVER_NAME",
            (char *)folk_current_request->server_name,
            strlen(folk_current_request->server_name),
            track_vars_array);
    }

    if (folk_current_request->server_port > 0) {
        char port_buf[8];
        snprintf(port_buf, sizeof(port_buf), "%d", folk_current_request->server_port);
        php_register_variable_safe("SERVER_PORT", port_buf, strlen(port_buf), track_vars_array);
    }

    if (folk_current_request->protocol) {
        php_register_variable_safe("SERVER_PROTOCOL",
            (char *)folk_current_request->protocol,
            strlen(folk_current_request->protocol),
            track_vars_array);
    }

    /* SAPI name */
    php_register_variable_safe("SERVER_SOFTWARE", "folk-embed", 10, track_vars_array);

    /* HTTP headers → HTTP_* variables */
    for (size_t i = 0; i < folk_current_request->header_count; i++) {
        const char *name = folk_current_request->header_names[i];
        const char *value = folk_current_request->header_values[i];
        if (name == NULL || value == NULL) continue;

        /* Convert "Content-Type" → "HTTP_CONTENT_TYPE" */
        size_t name_len = strlen(name);
        /* "HTTP_" prefix + name + NUL */
        size_t var_len = 5 + name_len + 1;
        char *var_name = alloca(var_len);
        memcpy(var_name, "HTTP_", 5);
        for (size_t j = 0; j < name_len; j++) {
            char c = name[j];
            if (c == '-') c = '_';
            else if (c >= 'a' && c <= 'z') c = c - 32;
            var_name[5 + j] = c;
        }
        var_name[5 + name_len] = '\0';

        php_register_variable_safe(var_name, (char *)value, strlen(value), track_vars_array);
    }
}

static void folk_sapi_log_message(const char *message, int syslog_type_int) {
    (void)syslog_type_int;
    /* TODO: route to tracing */
    fprintf(stderr, "[folk-sapi] %s\n", message);
}

/* ── Custom SAPI startup wrapper ───────────────────────────────── */

static int folk_sapi_startup(sapi_module_struct *sapi_module) {
    return php_module_startup(sapi_module, NULL);
}

/* ── Custom SAPI module definition ────────────────────────────── */

static sapi_module_struct folk_sapi_module = {
    "folk-embed",                  /* name */
    "Folk Embedded PHP",           /* pretty name */

    folk_sapi_startup,             /* startup */
    php_module_shutdown_wrapper,   /* shutdown */

    NULL,                          /* activate (per-request init) */
    NULL,                          /* deactivate (per-request cleanup) */

    folk_sapi_ub_write,            /* ub_write */
    folk_sapi_flush,               /* flush */
    NULL,                          /* get_stat */
    NULL,                          /* getenv */

    php_error,                     /* sapi_error */

    NULL,                          /* header_handler */
    folk_sapi_send_headers,        /* send_headers */
    folk_sapi_send_header,         /* send_header */

    folk_sapi_read_post,           /* read_post */
    folk_sapi_read_cookies,        /* read_cookies */

    folk_sapi_register_variables,  /* register_server_variables */
    folk_sapi_log_message,         /* log_message */
    NULL,                          /* get_request_time */
    NULL,                          /* terminate_process */

    STANDARD_SAPI_MODULE_PROPERTIES
};

/* ── SAPI initialization (replaces php_embed_init) ────────────── */

/**
 * Initialize PHP with our custom Folk SAPI.
 * Returns 0 on success, -1 on failure.
 *
 * This replaces php_embed_init() — we register our own SAPI module
 * with proper HTTP callbacks.
 */
/* Default INI overrides for embed mode */
static const char folk_ini_entries[] =
    /* OPcache disabled: ZTS + SHM can deadlock in multi-threaded embed mode */
    "opcache.enable=0\0"
    "opcache.enable_cli=0\0"
    /* Worker threads have 16MB stack but PHP detects main thread's 8MB via
     * getrlimit. Set to -1 to disable stack limit check. */
    "zend.max_allowed_stack_size=134217728\0"
    "\0";

int folk_sapi_init(void) {
    /* Raise RLIMIT_STACK before PHP init — PHP 8.3 uses getrlimit() to
     * set zend.max_allowed_stack_size during zend_startup, before INI
     * entries are processed. Without this, worker threads hit the 8MB
     * limit during PHP compilation of user scripts. */
    {
        struct rlimit rl;
        if (getrlimit(RLIMIT_STACK, &rl) == 0) {
            rl.rlim_cur = rl.rlim_max; /* raise to hard limit */
            setrlimit(RLIMIT_STACK, &rl);
        }
    }

#ifdef ZTS
    /* PHP 8.3 ZTS: use php_tsrm_startup() instead of raw tsrm_startup().
     * php_tsrm_startup() calls tsrm_startup + php_reserve_tsrm_memory +
     * ts_resource_ex(0, NULL) — the correct sequence for embedding PHP. */
    php_tsrm_startup();
#endif
    folk_sapi_module.ini_entries = folk_ini_entries;
    folk_sapi_module.additional_functions = NULL;

    sapi_startup(&folk_sapi_module);

    if (folk_sapi_module.startup(&folk_sapi_module) == FAILURE) {
        return -1;
    }

    return 0;
}

/**
 * Initialize PHP with custom INI overrides (from Rust).
 * ini_overrides is a double-NUL terminated string of "key=value\0" pairs.
 * Pass NULL to use defaults only.
 */
int folk_sapi_init_with_ini(const char *ini_overrides) {
    sapi_startup(&folk_sapi_module);

    if (ini_overrides != NULL) {
        folk_sapi_module.ini_entries = ini_overrides;
    } else {
        folk_sapi_module.ini_entries = folk_ini_entries;
    }
    folk_sapi_module.additional_functions = NULL;

    if (folk_sapi_module.startup(&folk_sapi_module) == FAILURE) {
        return -1;
    }

    return 0;
}

/**
 * Shutdown our custom SAPI.
 */
void folk_sapi_shutdown(void) {
    php_module_shutdown();
    sapi_shutdown();
#ifdef ZTS
    tsrm_shutdown();
#endif
}

/* ── Request context management ───────────────────────────────── */

/**
 * Set the current request context (called from Rust before request_startup).
 * The pointer must remain valid until folk_request_context_clear() is called.
 */
void folk_request_context_set(folk_request_context_t *ctx) {
    folk_current_request = ctx;
}

/**
 * Clear the current request context.
 */
void folk_request_context_clear(void) {
    folk_current_request = NULL;
}

/**
 * Populate SG(request_info) from the current request context.
 * Must be called BEFORE php_request_startup().
 */
void folk_request_info_populate(void) {
    if (folk_current_request == NULL) return;

    SG(request_info).request_method = folk_current_request->request_method;
    SG(request_info).request_uri    = (char *)folk_current_request->request_uri;
    SG(request_info).query_string   = (char *)folk_current_request->query_string;
    SG(request_info).content_type   = folk_current_request->content_type;
    SG(request_info).content_length = folk_current_request->content_length;
    SG(request_info).path_translated = (char *)folk_current_request->path_translated;
}

/* ── Response context management ──────────────────────────────── */

/**
 * Get the response status code.
 */
int folk_response_status_code(void) {
    return folk_response.status_code;
}

/**
 * Get the number of captured response headers.
 */
size_t folk_response_header_count(void) {
    return folk_response.header_count;
}

/**
 * Get a response header by index. Returns NULL if out of bounds.
 * Sets *out_len to the header string length.
 */
const char *folk_response_header_get(size_t index, size_t *out_len) {
    if (index >= folk_response.header_count) {
        *out_len = 0;
        return NULL;
    }
    *out_len = strlen(folk_response.headers[index]);
    return folk_response.headers[index];
}

/**
 * Clear the response context (call between requests).
 */
void folk_response_clear(void) {
    for (size_t i = 0; i < folk_response.header_count; i++) {
        free(folk_response.headers[i]);
    }
    folk_response.header_count = 0;
    folk_response.status_code = 200;
    /* Keep the headers array allocated for reuse */
}

/**
 * Free all response resources (call on thread shutdown).
 */
void folk_response_free(void) {
    for (size_t i = 0; i < folk_response.header_count; i++) {
        free(folk_response.headers[i]);
    }
    free(folk_response.headers);
    folk_response.headers = NULL;
    folk_response.header_count = 0;
    folk_response.header_cap = 0;
    folk_response.status_code = 0;
}

/* ── Guarded request lifecycle (updated for custom SAPI) ──────── */

/**
 * php_request_startup() inside zend_try.
 * Populates SG(request_info) from the current context before startup.
 * Returns 0 on success, -1 on bailout, -2 on startup failure.
 */
int folk_request_startup_safe(void) {
    int result = 0;

    zend_try {
        folk_request_info_populate();

        if (php_request_startup() != SUCCESS) {
            result = -2;
        }
    } zend_catch {
        result = -1;
    } zend_end_try();

    return result;
}

/**
 * php_request_shutdown() inside zend_try.
 * Returns 0 on success, -1 on bailout.
 */
int folk_request_shutdown_safe(void) {
    int result = 0;

    zend_try {
        php_request_shutdown(NULL);
    } zend_catch {
        result = -1;
    } zend_end_try();

    return result;
}

/* ── Output capture (public API) ──────────────────────────────── */

const char *folk_get_output(size_t *out_len) {
    *out_len = folk_output_len;
    return folk_output_buf;
}

void folk_clear_output(void) {
    folk_output_len = 0;
}

void folk_free_output(void) {
    free(folk_output_buf);
    folk_output_buf = NULL;
    folk_output_len = 0;
    folk_output_cap = 0;
}

/* ── Legacy: install output handler for embed SAPI ────────────── */

/**
 * Install our ub_write handler into the embed SAPI module.
 * Only needed when using php_embed_init() (POC path).
 * The custom Folk SAPI already sets ub_write in the module struct.
 */
void folk_install_output_handler(void) {
    sapi_module.ub_write = folk_sapi_ub_write;
}

/* ── Guarded script execution ──────────────────────────────────── */

/**
 * Execute a PHP script file inside zend_try.
 * Uses zend_execute_scripts directly (not php_execute_script which may
 * clean up eval-defined functions).
 * Returns 0 on success, -1 on bailout, -2 on compile/exec failure.
 */
int folk_execute_script_safe(const char *filename) {
    int result = 0;

    zend_try {
        zend_file_handle file_handle;
        zend_stream_init_filename(&file_handle, filename);

        if (!php_execute_script(&file_handle)) {
            result = -2;
        }

        zend_destroy_file_handle(&file_handle);
    } zend_catch {
        result = -1;
    } zend_end_try();

    return result;
}

/* ── Guarded eval ─────────────────────────────────────────────── */

int folk_eval_string_safe(const char *code, zval *retval) {
    int result = 0;

    zend_try {
        zend_eval_string(code, retval, "folk-embed");
    } zend_catch {
        result = -1;
    } zend_end_try();

    return result;
}

/* ── Guarded function call ────────────────────────────────────── */

int folk_call_function_safe(
    const char *func_name,
    zval *retval,
    uint32_t param_count,
    zval *params
) {
    int result = 0;

    zend_try {
        zval func_zval;
        ZVAL_STRING(&func_zval, func_name);

        int call_result = call_user_function(
            EG(function_table),
            NULL,
            &func_zval,
            retval,
            param_count,
            params
        );

        zval_ptr_dtor(&func_zval);

        if (call_result != SUCCESS) {
            result = -2;
        }
    } zend_catch {
        result = -1;
    } zend_end_try();

    return result;
}

/* ── Direct data passing (Phase A+B) ──────────────────────────── */

/*
 * Phase A: Call PHP function with raw binary params (no base64).
 * Phase B: Build PHP arrays directly from C structs.
 */

/**
 * Call a PHP function with binary data arguments.
 * method_name: the RPC method name (e.g., "http.handle")
 * params: raw bytes (msgpack or arbitrary)
 * params_len: length of params
 * response_buf: output buffer pointer (caller must free with folk_free_buffer)
 * response_len: output buffer length
 *
 * Returns 0 on success, -1 on bailout, -2 on call failure, -3 on SIGSEGV.
 */
int folk_call_with_binary(
    const char *func_name,
    const char *method_name, size_t method_name_len,
    const char *params, size_t params_len,
    char **response_buf, size_t *response_len
) {
    *response_buf = NULL;
    *response_len = 0;

    folk_sigsegv_active = 1;
    if (sigsetjmp(folk_sigsegv_jmpbuf, 1) != 0) {
        folk_sigsegv_active = 0;
        return -3;
    }

    int result = 0;

    zend_try {
        /* Build arguments: method (string), params (binary string) */
        zval args[2];
        ZVAL_STRINGL(&args[0], method_name, method_name_len);
        ZVAL_STRINGL(&args[1], params, params_len);

        zval func_zval;
        ZVAL_STRING(&func_zval, func_name);

        zval retval;
        ZVAL_UNDEF(&retval);

        int call_result = call_user_function(
            EG(function_table), NULL, &func_zval, &retval, 2, args
        );

        zval_ptr_dtor(&func_zval);
        zval_ptr_dtor(&args[0]);
        zval_ptr_dtor(&args[1]);

        if (call_result != SUCCESS) {
            zval_ptr_dtor(&retval);
            result = -2;
        } else if (Z_TYPE(retval) == IS_STRING) {
            /* Copy response string to output buffer */
            *response_len = Z_STRLEN(retval);
            *response_buf = malloc(*response_len);
            if (*response_buf != NULL) {
                memcpy(*response_buf, Z_STRVAL(retval), *response_len);
            }
            zval_ptr_dtor(&retval);
        } else {
            /* Non-string return — treat as empty */
            zval_ptr_dtor(&retval);
        }
    } zend_catch {
        result = -1;
    } zend_end_try();

    folk_sigsegv_active = 0;
    return result;
}

/**
 * Free a buffer allocated by folk_call_with_binary.
 */
void folk_free_buffer(char *buf) {
    free(buf);
}

/* ── Phase B: Array construction helpers ──────────────────────── */

/**
 * Create a new PHP array zval.
 */
void folk_array_init(zval *arr) {
    array_init(arr);
}

/**
 * Add a string to a PHP array by key.
 */
void folk_array_add_string(
    zval *arr, const char *key, size_t key_len,
    const char *val, size_t val_len
) {
    zval zv;
    ZVAL_STRINGL(&zv, val, val_len);
    zend_hash_str_add(Z_ARR_P(arr), key, key_len, &zv);
}

/**
 * Add a long (integer) to a PHP array by key.
 */
void folk_array_add_long(
    zval *arr, const char *key, size_t key_len,
    zend_long val
) {
    zval zv;
    ZVAL_LONG(&zv, val);
    zend_hash_str_add(Z_ARR_P(arr), key, key_len, &zv);
}

/**
 * Add a sub-array to a PHP array by key.
 * sub_arr must already be initialized with folk_array_init.
 */
void folk_array_add_array(
    zval *arr, const char *key, size_t key_len,
    zval *sub_arr
) {
    zend_hash_str_add(Z_ARR_P(arr), key, key_len, sub_arr);
}

/**
 * Append a string to a PHP array (numeric index).
 */
void folk_array_append_string(zval *arr, const char *val, size_t val_len) {
    zval zv;
    ZVAL_STRINGL(&zv, val, val_len);
    zend_hash_next_index_insert(Z_ARR_P(arr), &zv);
}

/**
 * Call a PHP function with a single array argument (the request).
 * Returns 0 on success, -1 on bailout, -2 on call failure, -3 on SIGSEGV.
 * Response is read from return value:
 *   - status: $ret['status'] (long)
 *   - body: $ret['body'] (string)
 *   - headers: $ret['headers'] (array of strings)
 */
int folk_call_with_array(
    const char *func_name,
    zval *request_arr,
    zval *retval
) {
    folk_sigsegv_active = 1;
    if (sigsetjmp(folk_sigsegv_jmpbuf, 1) != 0) {
        folk_sigsegv_active = 0;
        return -3;
    }

    int result = 0;

    zend_try {
        zval func_zval;
        ZVAL_STRING(&func_zval, func_name);

        int call_result = call_user_function(
            EG(function_table), NULL, &func_zval, retval, 1, request_arr
        );

        zval_ptr_dtor(&func_zval);

        if (call_result != SUCCESS) {
            result = -2;
        }
    } zend_catch {
        result = -1;
    } zend_end_try();

    folk_sigsegv_active = 0;
    return result;
}

/**
 * Read a string value from a PHP array by key.
 * Returns NULL if key doesn't exist or value is not a string.
 */
const char *folk_array_get_string(zval *arr, const char *key, size_t key_len, size_t *out_len) {
    if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) {
        *out_len = 0;
        return NULL;
    }
    zval *entry = zend_hash_str_find(Z_ARR_P(arr), key, key_len);
    if (entry == NULL || Z_TYPE_P(entry) != IS_STRING) {
        *out_len = 0;
        return NULL;
    }
    *out_len = Z_STRLEN_P(entry);
    return Z_STRVAL_P(entry);
}

/**
 * Read a long value from a PHP array by key.
 * Returns 0 if key doesn't exist or value is not a long.
 */
zend_long folk_array_get_long(zval *arr, const char *key, size_t key_len) {
    if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) {
        return 0;
    }
    zval *entry = zend_hash_str_find(Z_ARR_P(arr), key, key_len);
    if (entry == NULL || Z_TYPE_P(entry) != IS_LONG) {
        return 0;
    }
    return Z_LVAL_P(entry);
}

/**
 * Get array count (number of elements).
 */
size_t folk_array_count(zval *arr) {
    if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) return 0;
    return zend_hash_num_elements(Z_ARR_P(arr));
}

/**
 * Get string element from array by numeric index.
 */
const char *folk_array_index_string(zval *arr, size_t index, size_t *out_len) {
    if (arr == NULL || Z_TYPE_P(arr) != IS_ARRAY) {
        *out_len = 0;
        return NULL;
    }
    zval *entry = zend_hash_index_find(Z_ARR_P(arr), index);
    if (entry == NULL || Z_TYPE_P(entry) != IS_STRING) {
        *out_len = 0;
        return NULL;
    }
    *out_len = Z_STRLEN_P(entry);
    return Z_STRVAL_P(entry);
}

/* ── Zval helpers ─────────────────────────────────────────────── */

const char *folk_zval_get_string(zval *val, size_t *out_len) {
    if (val == NULL || Z_TYPE_P(val) != IS_STRING) {
        *out_len = 0;
        return NULL;
    }
    *out_len = Z_STRLEN_P(val);
    return Z_STRVAL_P(val);
}

long folk_zval_get_long(zval *val) {
    if (val == NULL || Z_TYPE_P(val) != IS_LONG) {
        return 0;
    }
    return Z_LVAL_P(val);
}

int folk_zval_type(zval *val) {
    if (val == NULL) return 0;
    return Z_TYPE_P(val);
}

void folk_zval_dtor(zval *val) {
    if (val != NULL) {
        zval_ptr_dtor(val);
    }
}

void folk_zval_undef(zval *val) {
    ZVAL_UNDEF(val);
}

void folk_zval_set_string(zval *val, const char *str, size_t len) {
    ZVAL_STRINGL(val, str, len);
}

/* ── Signal save/restore ──────────────────────────────────────── */

/*
 * PHP's module startup installs its own signal handlers (SIGTERM, SIGINT,
 * SIGSEGV, etc.), overwriting any handlers set by Rust/tokio. We save
 * the pre-PHP handlers and restore them after init so tokio's signal
 * handling continues to work.
 */

#define FOLK_SAVED_SIGNALS_COUNT 5
static int folk_saved_signal_nums[FOLK_SAVED_SIGNALS_COUNT] = {
    SIGTERM, SIGINT, SIGQUIT, SIGPIPE, SIGSEGV
};
static struct sigaction folk_saved_handlers[FOLK_SAVED_SIGNALS_COUNT];

/**
 * Save the current signal handlers for signals PHP might overwrite.
 * Call BEFORE folk_sapi_init().
 */
void folk_signals_save(void) {
    for (int i = 0; i < FOLK_SAVED_SIGNALS_COUNT; i++) {
        sigaction(folk_saved_signal_nums[i], NULL, &folk_saved_handlers[i]);
    }
}

/**
 * Restore the saved signal handlers.
 * Call AFTER folk_sapi_init() returns.
 */
void folk_signals_restore(void) {
    for (int i = 0; i < FOLK_SAVED_SIGNALS_COUNT; i++) {
        sigaction(folk_saved_signal_nums[i], &folk_saved_handlers[i], NULL);
    }
}

/* ── SIGSEGV protection for worker threads ────────────────────── */

/*
 * Per-thread SIGSEGV recovery using sigsetjmp/siglongjmp.
 *
 * Before calling PHP, the worker thread calls sigsetjmp().
 * If a SIGSEGV occurs during PHP execution, our signal handler
 * calls siglongjmp() to return to the setjmp point.
 *
 * This is NOT defined behavior by POSIX for all cases, but works
 * in practice for catching NULL dereferences in C extensions.
 * It's the same technique used by PostgreSQL, Apache, and others.
 */

static void folk_sigsegv_handler(int sig, siginfo_t *info, void *ucontext) {
    (void)info;
    (void)ucontext;
    (void)sig;

    if (folk_sigsegv_active) {
        /* Jump back to the sigsetjmp point in the worker thread */
        siglongjmp(folk_sigsegv_jmpbuf, 1);
    }

    /* Not in a protected region — re-raise for default behavior (crash) */
    signal(SIGSEGV, SIG_DFL);
    raise(SIGSEGV);
}

/**
 * Install our SIGSEGV handler for worker threads.
 * Call once from any thread after folk_sapi_init() + folk_signals_restore().
 */
void folk_sigsegv_handler_install(void) {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = folk_sigsegv_handler;
    sa.sa_flags = SA_SIGINFO | SA_NODEFER;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
}

/**
 * Execute a PHP eval with SIGSEGV protection.
 * Returns 0 on success, -1 on bailout, -2 on eval failure, -3 on SIGSEGV.
 */
int folk_eval_string_protected(const char *code, zval *retval) {
    folk_sigsegv_active = 1;

    if (sigsetjmp(folk_sigsegv_jmpbuf, 1) != 0) {
        /* Returned from SIGSEGV — worker thread survived */
        folk_sigsegv_active = 0;
        return -3;
    }

    int result = folk_eval_string_safe(code, retval);

    folk_sigsegv_active = 0;
    return result;
}

/**
 * Execute a PHP function call with SIGSEGV protection.
 * Returns 0 on success, -1 on bailout, -2 on call failure, -3 on SIGSEGV.
 */
int folk_call_function_protected(
    const char *func_name,
    zval *retval,
    uint32_t param_count,
    zval *params
) {
    folk_sigsegv_active = 1;

    if (sigsetjmp(folk_sigsegv_jmpbuf, 1) != 0) {
        folk_sigsegv_active = 0;
        return -3;
    }

    int result = folk_call_function_safe(func_name, retval, param_count, params);

    folk_sigsegv_active = 0;
    return result;
}
