#define _POSIX_C_SOURCE 200809L

#include "trigger_manager.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>

#include "cjson/cJSON.h"
#include "debug.h"
#include "nostr_handler.h"
#include "llm.h"

void agent_on_trigger(const char* skill_d_tag,
                      const char* skill_content,
                      cJSON* triggering_event,
                      const char* relay_url);

typedef struct {
    trigger_manager_t* mgr;
    char skill_d_tag[TRIGGER_SKILL_D_TAG_MAX];
} trigger_subscription_ctx_t;

static int clamp_enabled(int enabled) {
    return enabled ? 1 : 0;
}

trigger_type_t trigger_type_from_string(const char* s) {
    if (!s) {
        return TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
    }
    if (strcmp(s, "webhook") == 0) {
        return TRIGGER_TYPE_WEBHOOK;
    }
    if (strcmp(s, "cron") == 0) {
        return TRIGGER_TYPE_CRON;
    }
    if (strcmp(s, "chain") == 0) {
        return TRIGGER_TYPE_CHAIN;
    }
    if (strcmp(s, "dm") == 0) {
        return TRIGGER_TYPE_DM;
    }
    return TRIGGER_TYPE_NOSTR_SUBSCRIPTION;
}

const char* trigger_type_to_string(trigger_type_t t) {
    switch (t) {
        case TRIGGER_TYPE_WEBHOOK:
            return "webhook";
        case TRIGGER_TYPE_CRON:
            return "cron";
        case TRIGGER_TYPE_CHAIN:
            return "chain";
        case TRIGGER_TYPE_DM:
            return "dm";
        case TRIGGER_TYPE_NOSTR_SUBSCRIPTION:
        default:
            return "nostr-subscription";
    }
}

static int is_int_in_csv_list(const char* text, int value) {
    if (!text || text[0] == '\0') {
        return 0;
    }

    const char* p = text;
    while (*p) {
        while (*p == ' ') p++;
        char* endptr = NULL;
        long n = strtol(p, &endptr, 10);
        if (endptr == p) {
            return 0;
        }
        if ((int)n == value) {
            return 1;
        }
        p = endptr;
        while (*p == ' ') p++;
        if (*p == ',') {
            p++;
            continue;
        }
        if (*p == '\0') {
            break;
        }
        return 0;
    }

    return 0;
}

static int cron_field_token_matches(const char* token, int value, int min_v, int max_v) {
    if (!token || token[0] == '\0') {
        return 0;
    }

    if (strcmp(token, "*") == 0) {
        return 1;
    }

    const char* slash = strchr(token, '/');
    int step = 0;
    char base[64];
    if (slash) {
        size_t base_len = (size_t)(slash - token);
        if (base_len == 0 || base_len >= sizeof(base)) {
            return 0;
        }
        memcpy(base, token, base_len);
        base[base_len] = '\0';

        char* endptr = NULL;
        long step_l = strtol(slash + 1, &endptr, 10);
        if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
            return 0;
        }
        step = (int)step_l;
    } else {
        snprintf(base, sizeof(base), "%s", token);
    }

    int range_start = min_v;
    int range_end = max_v;

    if (strcmp(base, "*") == 0) {
        range_start = min_v;
        range_end = max_v;
    } else {
        const char* dash = strchr(base, '-');
        if (dash) {
            char left[32];
            char right[32];
            size_t left_len = (size_t)(dash - base);
            size_t right_len = strlen(dash + 1);
            if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
                return 0;
            }
            memcpy(left, base, left_len);
            left[left_len] = '\0';
            memcpy(right, dash + 1, right_len + 1U);

            char* e1 = NULL;
            char* e2 = NULL;
            long a = strtol(left, &e1, 10);
            long b = strtol(right, &e2, 10);
            if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
                return 0;
            }
            range_start = (int)a;
            range_end = (int)b;
        } else if (strchr(base, ',')) {
            if (!is_int_in_csv_list(base, value)) {
                return 0;
            }
            if (step <= 1) {
                return 1;
            }
            return ((value - min_v) % step) == 0;
        } else {
            char* e = NULL;
            long n = strtol(base, &e, 10);
            if (!e || *e != '\0') {
                return 0;
            }
            range_start = (int)n;
            range_end = (int)n;
        }
    }

    if (range_start < min_v || range_end > max_v || range_start > range_end) {
        return 0;
    }
    if (value < range_start || value > range_end) {
        return 0;
    }
    if (step > 1 && ((value - range_start) % step) != 0) {
        return 0;
    }
    return 1;
}

static int cron_field_matches(const char* field, int value, int min_v, int max_v) {
    if (!field || field[0] == '\0') {
        return 0;
    }

    char tmp[128];
    snprintf(tmp, sizeof(tmp), "%s", field);

    char* saveptr = NULL;
    char* token = strtok_r(tmp, ",", &saveptr);
    while (token) {
        while (*token == ' ') token++;
        if (cron_field_token_matches(token, value, min_v, max_v)) {
            return 1;
        }
        token = strtok_r(NULL, ",", &saveptr);
    }

    return 0;
}

static int cron_normalize_expression(const char* expr, char* out, size_t out_sz, int* out_expanded) {
    if (out_expanded) {
        *out_expanded = 0;
    }
    if (!expr || !out || out_sz == 0) {
        return -1;
    }

    while (*expr && isspace((unsigned char)*expr)) {
        expr++;
    }
    if (*expr == '\0') {
        return -1;
    }

    const char* mapped = NULL;
    if (strcmp(expr, "*") == 0) {
        mapped = "* * * * *";
    } else if (strcmp(expr, "@yearly") == 0 || strcmp(expr, "@annually") == 0) {
        mapped = "0 0 1 1 *";
    } else if (strcmp(expr, "@monthly") == 0) {
        mapped = "0 0 1 * *";
    } else if (strcmp(expr, "@weekly") == 0) {
        mapped = "0 0 * * 0";
    } else if (strcmp(expr, "@daily") == 0 || strcmp(expr, "@midnight") == 0) {
        mapped = "0 0 * * *";
    } else if (strcmp(expr, "@hourly") == 0) {
        mapped = "0 * * * *";
    }

    const char* src = mapped ? mapped : expr;
    int n = snprintf(out, out_sz, "%s", src);
    if (n < 0 || (size_t)n >= out_sz) {
        return -1;
    }

    if (mapped && out_expanded) {
        *out_expanded = 1;
    }
    return 0;
}

static int cron_field_token_valid(const char* token, int min_v, int max_v) {
    if (!token || token[0] == '\0') {
        return 0;
    }

    if (strcmp(token, "*") == 0) {
        return 1;
    }

    const char* slash = strchr(token, '/');
    int step = 0;
    char base[64];
    if (slash) {
        size_t base_len = (size_t)(slash - token);
        if (base_len == 0 || base_len >= sizeof(base)) {
            return 0;
        }
        memcpy(base, token, base_len);
        base[base_len] = '\0';

        char* endptr = NULL;
        long step_l = strtol(slash + 1, &endptr, 10);
        if (endptr == slash + 1 || *endptr != '\0' || step_l <= 0 || step_l > 1024) {
            return 0;
        }
        step = (int)step_l;
    } else {
        int n = snprintf(base, sizeof(base), "%s", token);
        if (n < 0 || (size_t)n >= sizeof(base)) {
            return 0;
        }
    }

    if (strcmp(base, "*") == 0) {
        return step == 0 || step <= (max_v - min_v + 1);
    }

    const char* dash = strchr(base, '-');
    if (dash) {
        char left[32];
        char right[32];
        size_t left_len = (size_t)(dash - base);
        size_t right_len = strlen(dash + 1);
        if (left_len == 0 || right_len == 0 || left_len >= sizeof(left) || right_len >= sizeof(right)) {
            return 0;
        }
        memcpy(left, base, left_len);
        left[left_len] = '\0';
        memcpy(right, dash + 1, right_len + 1U);

        char* e1 = NULL;
        char* e2 = NULL;
        long a = strtol(left, &e1, 10);
        long b = strtol(right, &e2, 10);
        if (!e1 || *e1 != '\0' || !e2 || *e2 != '\0') {
            return 0;
        }
        if ((int)a < min_v || (int)b > max_v || (int)a > (int)b) {
            return 0;
        }
        return 1;
    }

    char* e = NULL;
    long n = strtol(base, &e, 10);
    if (!e || *e != '\0') {
        return 0;
    }
    if ((int)n < min_v || (int)n > max_v) {
        return 0;
    }

    return 1;
}

static int cron_field_valid(const char* field, int min_v, int max_v) {
    if (!field || field[0] == '\0') {
        return 0;
    }

    char tmp[128];
    int n = snprintf(tmp, sizeof(tmp), "%s", field);
    if (n < 0 || (size_t)n >= sizeof(tmp)) {
        return 0;
    }

    int tokens = 0;
    char* saveptr = NULL;
    char* token = strtok_r(tmp, ",", &saveptr);
    while (token) {
        while (*token == ' ') token++;
        if (!cron_field_token_valid(token, min_v, max_v)) {
            return 0;
        }
        tokens++;
        token = strtok_r(NULL, ",", &saveptr);
    }

    return tokens > 0;
}

static int cron_expr_is_valid(const char* expr, char* normalized_out, size_t normalized_out_size) {
    if (!normalized_out || normalized_out_size == 0) {
        return 0;
    }

    if (cron_normalize_expression(expr, normalized_out, normalized_out_size, NULL) != 0) {
        return 0;
    }

    char parse_buf[TRIGGER_FILTER_JSON_MAX];
    int n = snprintf(parse_buf, sizeof(parse_buf), "%s", normalized_out);
    if (n < 0 || (size_t)n >= sizeof(parse_buf)) {
        return 0;
    }

    char* fields[5] = {0};
    int nf = 0;
    char* saveptr = NULL;
    char* tok = strtok_r(parse_buf, " \t", &saveptr);
    while (tok && nf < 5) {
        fields[nf++] = tok;
        tok = strtok_r(NULL, " \t", &saveptr);
    }
    if (nf != 5 || tok != NULL) {
        return 0;
    }

    if (!cron_field_valid(fields[0], 0, 59)) return 0;
    if (!cron_field_valid(fields[1], 0, 23)) return 0;
    if (!cron_field_valid(fields[2], 1, 31)) return 0;
    if (!cron_field_valid(fields[3], 1, 12)) return 0;
    if (!cron_field_valid(fields[4], 0, 6)) return 0;

    return 1;
}

static int cron_matches_now(const char* expr, time_t now_ts) {
    if (!expr || expr[0] == '\0') {
        return 0;
    }

    char normalized[TRIGGER_FILTER_JSON_MAX];
    if (!cron_expr_is_valid(expr, normalized, sizeof(normalized))) {
        return 0;
    }

    char parse_buf[TRIGGER_FILTER_JSON_MAX];
    snprintf(parse_buf, sizeof(parse_buf), "%s", normalized);

    char* fields[5] = {0};
    int nf = 0;
    char* saveptr = NULL;
    char* tok = strtok_r(parse_buf, " \t", &saveptr);
    while (tok && nf < 5) {
        fields[nf++] = tok;
        tok = strtok_r(NULL, " \t", &saveptr);
    }
    if (nf != 5 || tok != NULL) {
        return 0;
    }

    struct tm tm_now;
    localtime_r(&now_ts, &tm_now);

    int minute = tm_now.tm_min;
    int hour = tm_now.tm_hour;
    int mday = tm_now.tm_mday;
    int month = tm_now.tm_mon + 1;
    int wday = tm_now.tm_wday;

    if (!cron_field_matches(fields[0], minute, 0, 59)) return 0;
    if (!cron_field_matches(fields[1], hour, 0, 23)) return 0;
    if (!cron_field_matches(fields[2], mday, 1, 31)) return 0;
    if (!cron_field_matches(fields[3], month, 1, 12)) return 0;
    if (!cron_field_matches(fields[4], wday, 0, 6)) return 0;

    return 1;
}

static int ensure_capacity(trigger_manager_t* mgr, int needed) {
    if (!mgr || needed <= 0) {
        return -1;
    }

    if (mgr->capacity >= needed) {
        return 0;
    }

    int new_cap = mgr->capacity > 0 ? mgr->capacity : TRIGGER_DEFAULT_MAX_ACTIVE;
    while (new_cap < needed) {
        if (new_cap > 1024) {
            return -1;
        }
        new_cap *= 2;
    }

    active_trigger_t* grown = (active_trigger_t*)realloc(mgr->triggers, (size_t)new_cap * sizeof(active_trigger_t));
    if (!grown) {
        return -1;
    }

    if (new_cap > mgr->capacity) {
        memset(&grown[mgr->capacity], 0, (size_t)(new_cap - mgr->capacity) * sizeof(active_trigger_t));
    }

    mgr->triggers = grown;
    mgr->capacity = new_cap;
    return 0;
}

static int find_trigger_index_locked(trigger_manager_t* mgr, const char* skill_d_tag) {
    if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
        return -1;
    }

    for (int i = 0; i < mgr->count; i++) {
        if (strncmp(mgr->triggers[i].skill_d_tag, skill_d_tag, TRIGGER_SKILL_D_TAG_MAX) == 0) {
            return i;
        }
    }

    return -1;
}

static cJSON* find_tag_value_string(cJSON* tags, const char* key) {
    if (!tags || !key || !cJSON_IsArray(tags)) return NULL;

    int n = cJSON_GetArraySize(tags);
    for (int i = 0; i < n; i++) {
        cJSON* tag = cJSON_GetArrayItem(tags, i);
        if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;

        cJSON* k = cJSON_GetArrayItem(tag, 0);
        cJSON* v = cJSON_GetArrayItem(tag, 1);
        if (k && v && cJSON_IsString(k) && cJSON_IsString(v) &&
            k->valuestring && v->valuestring && strcmp(k->valuestring, key) == 0) {
            return v;
        }
    }

    return NULL;
}

static int parse_address_tag(const char* addr, int* out_kind, char out_pubkey[65], char out_d_tag[65]) {
    if (!addr || !out_kind || !out_pubkey || !out_d_tag) {
        return -1;
    }

    const char* p1 = strchr(addr, ':');
    if (!p1) return -1;
    const char* p2 = strchr(p1 + 1, ':');
    if (!p2) return -1;

    char kind_buf[16] = {0};
    size_t kind_len = (size_t)(p1 - addr);
    if (kind_len == 0 || kind_len >= sizeof(kind_buf)) return -1;
    memcpy(kind_buf, addr, kind_len);

    int kind = atoi(kind_buf);
    if (kind != 31123 && kind != 31124) return -1;

    size_t pub_len = (size_t)(p2 - (p1 + 1));
    if (pub_len != 64U) return -1;
    memcpy(out_pubkey, p1 + 1, 64U);
    out_pubkey[64] = '\0';

    size_t d_tag_len = strlen(p2 + 1);
    if (d_tag_len == 0 || d_tag_len >= 65U) return -1;
    memcpy(out_d_tag, p2 + 1, d_tag_len + 1U);

    *out_kind = kind;
    return 0;
}

static const char* trim_ws_local(const char* s) {
    while (s && *s && isspace((unsigned char)*s)) s++;
    return s ? s : "";
}

static void parse_trigger_runtime_tags(cJSON* tags,
                                       const char** out_llm,
                                       const char** out_tools,
                                       int* out_has_max_tokens,
                                       int* out_max_tokens,
                                       int* out_has_temperature,
                                       double* out_temperature,
                                       int* out_has_seed,
                                       int* out_seed) {
    if (out_llm) *out_llm = NULL;
    if (out_tools) *out_tools = NULL;
    if (out_has_max_tokens) *out_has_max_tokens = 0;
    if (out_max_tokens) *out_max_tokens = 0;
    if (out_has_temperature) *out_has_temperature = 0;
    if (out_temperature) *out_temperature = 0.0;
    if (out_has_seed) *out_has_seed = 0;
    if (out_seed) *out_seed = 0;

    if (!tags || !cJSON_IsArray(tags)) {
        return;
    }

    cJSON* llm = find_tag_value_string(tags, "llm");
    cJSON* tools = find_tag_value_string(tags, "tools");
    cJSON* max_tokens = find_tag_value_string(tags, "max_tokens");
    cJSON* temperature = find_tag_value_string(tags, "temperature");
    cJSON* seed = find_tag_value_string(tags, "seed");

    if (out_llm && llm && cJSON_IsString(llm) && llm->valuestring && llm->valuestring[0] != '\0') {
        *out_llm = llm->valuestring;
    }
    if (out_tools && tools && cJSON_IsString(tools) && tools->valuestring && tools->valuestring[0] != '\0') {
        *out_tools = tools->valuestring;
    }

    if (out_has_max_tokens && out_max_tokens && max_tokens && cJSON_IsString(max_tokens) && max_tokens->valuestring) {
        char* endptr = NULL;
        long n = strtol(trim_ws_local(max_tokens->valuestring), &endptr, 10);
        if (endptr && *trim_ws_local(endptr) == '\0' && n > 0 && n <= 1000000) {
            *out_has_max_tokens = 1;
            *out_max_tokens = (int)n;
        }
    }

    if (out_has_temperature && out_temperature && temperature && cJSON_IsString(temperature) && temperature->valuestring) {
        char* endptr = NULL;
        double n = strtod(trim_ws_local(temperature->valuestring), &endptr);
        if (endptr && *trim_ws_local(endptr) == '\0' && n >= 0.0 && n <= 2.0) {
            *out_has_temperature = 1;
            *out_temperature = n;
        }
    }

    if (out_has_seed && out_seed && seed && cJSON_IsString(seed) && seed->valuestring) {
        char* endptr = NULL;
        long n = strtol(trim_ws_local(seed->valuestring), &endptr, 10);
        if (endptr && *trim_ws_local(endptr) == '\0') {
            *out_has_seed = 1;
            *out_seed = (int)n;
        }
    }
}

static void apply_trigger_runtime_to_llm_config(const active_trigger_t* t, llm_config_t* cfg) {
    if (!t || !cfg) return;

    if (t->llm_spec[0] != '\0') {
        const char* spec = t->llm_spec;
        while (*spec && isspace((unsigned char)*spec)) spec++;

        const char* comma = strchr(spec, ',');
        size_t len = comma ? (size_t)(comma - spec) : strlen(spec);
        while (len > 0 && isspace((unsigned char)spec[len - 1])) len--;

        const char* slash = memchr(spec, '/', len);
        if (slash) {
            size_t provider_len = (size_t)(slash - spec);
            const char* model = slash + 1;
            size_t model_len = len - (size_t)(model - spec);

            while (provider_len > 0 && isspace((unsigned char)spec[provider_len - 1])) provider_len--;
            while (model_len > 0 && isspace((unsigned char)*model)) {
                model++;
                model_len--;
            }
            while (model_len > 0 && isspace((unsigned char)model[model_len - 1])) model_len--;

            if (provider_len > 0 && provider_len < sizeof(cfg->provider)) {
                snprintf(cfg->provider, sizeof(cfg->provider), "%.*s", (int)provider_len, spec);
            }
            if (model_len > 0 && model_len < sizeof(cfg->model)) {
                snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)model_len, model);
            }
        } else if (len > 0 && len < sizeof(cfg->model)) {
            snprintf(cfg->model, sizeof(cfg->model), "%.*s", (int)len, spec);
        }
    }

    if (t->has_max_tokens && t->max_tokens > 0) {
        cfg->max_tokens = t->max_tokens;
    }
    if (t->has_temperature) {
        cfg->temperature = t->temperature;
    }
}

static void execute_llm_action(const active_trigger_t* t, cJSON* event, const char* relay_url) {
    if (!t || !event) {
        return;
    }

    llm_config_t old_cfg;
    int had_old_cfg = (llm_get_config(&old_cfg) == 0);
    int overridden = 0;

    if (had_old_cfg &&
        (t->llm_spec[0] != '\0' || t->has_max_tokens || t->has_temperature)) {
        llm_config_t next = old_cfg;
        apply_trigger_runtime_to_llm_config(t, &next);
        if (llm_set_config(&next) == 0) {
            overridden = 1;
        }
    }

    agent_on_trigger(t->skill_d_tag, t->skill_content, event, relay_url);

    if (overridden) {
        (void)llm_set_config(&old_cfg);
    }
}

static int maybe_fire_trigger_locked(trigger_manager_t* mgr, int index, cJSON* event, const char* relay_url) {
    active_trigger_t* t = &mgr->triggers[index];
    if (!t->enabled) {
        return 0;
    }

    cJSON* created_at = cJSON_GetObjectItemCaseSensitive(event, "created_at");
    time_t created_ts = (created_at && cJSON_IsNumber(created_at)) ? (time_t)created_at->valuedouble : 0;
    if (created_ts > 0 && created_ts <= t->last_seen_created_at) {
        return 0;
    }

    time_t now = time(NULL);
    int cooldown = mgr->cfg->triggers.cooldown_seconds;
    if (cooldown < 0) cooldown = 0;
    if (cooldown > 0 && t->last_fired > 0 && (now - t->last_fired) < cooldown) {
        return 0;
    }

    t->last_fired = now;
    if (created_ts > t->last_seen_created_at) {
        t->last_seen_created_at = created_ts;
    }

    active_trigger_t trigger_copy = *t;
    pthread_mutex_unlock(&mgr->mutex);

    (void)mgr;
    execute_llm_action(&trigger_copy, event, relay_url);

    pthread_mutex_lock(&mgr->mutex);
    return 1;
}

static void on_trigger_subscription_eose(cJSON** events, int event_count, void* user_data) {
    (void)events;
    (void)event_count;
    (void)user_data;
}

static void close_trigger_subscription_locked(active_trigger_t* t) {
    if (!t) {
        return;
    }

    if (t->subscription) {
        (void)nostr_handler_close_subscription(t->subscription);
        t->subscription = NULL;
    }

    if (t->subscription_ctx) {
        free(t->subscription_ctx);
        t->subscription_ctx = NULL;
    }
}

static void on_trigger_subscription_event(cJSON* event, const char* relay_url, void* user_data) {
    trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)user_data;
    if (!ctx || !ctx->mgr || !event) {
        return;
    }

    (void)trigger_manager_fire(ctx->mgr, ctx->skill_d_tag, event, relay_url);
}

static int register_trigger_subscription_locked(trigger_manager_t* mgr, active_trigger_t* t) {
    if (!mgr || !t) {
        return -1;
    }

    close_trigger_subscription_locked(t);

    if (t->trigger_type != TRIGGER_TYPE_NOSTR_SUBSCRIPTION) {
        return 0;
    }

    if (!t->enabled || t->filter_json[0] == '\0') {
        return 0;
    }

    cJSON* filter = cJSON_Parse(t->filter_json);
    if (!filter || !cJSON_IsObject(filter)) {
        cJSON_Delete(filter);
        return -1;
    }

    cJSON* since = cJSON_GetObjectItemCaseSensitive(filter, "since");
    if (!since || !cJSON_IsNumber(since)) {
        time_t now = time(NULL);
        time_t default_since = now > 30 ? now - 30 : 0;
        cJSON_AddNumberToObject(filter, "since", (double)default_since);
    }

    cJSON* limit = cJSON_GetObjectItemCaseSensitive(filter, "limit");
    if (!limit || !cJSON_IsNumber(limit)) {
        cJSON_AddNumberToObject(filter, "limit", 200);
    }

    trigger_subscription_ctx_t* ctx = (trigger_subscription_ctx_t*)calloc(1, sizeof(*ctx));
    if (!ctx) {
        cJSON_Delete(filter);
        return -1;
    }

    ctx->mgr = mgr;
    snprintf(ctx->skill_d_tag, sizeof(ctx->skill_d_tag), "%s", t->skill_d_tag);

    nostr_pool_subscription_t* sub = nostr_handler_subscribe_with_filter(
        filter,
        on_trigger_subscription_event,
        on_trigger_subscription_eose,
        ctx,
        0,
        0,
        NOSTR_POOL_EOSE_FULL_SET,
        30,
        120);

    cJSON_Delete(filter);

    if (!sub) {
        free(ctx);
        return -1;
    }

    t->subscription = sub;
    t->subscription_ctx = ctx;

    return 0;
}

int trigger_manager_init(trigger_manager_t* mgr, didactyl_config_t* cfg) {
    if (!mgr || !cfg) {
        return -1;
    }

    memset(mgr, 0, sizeof(*mgr));
    mgr->cfg = cfg;

    if (pthread_mutex_init(&mgr->mutex, NULL) != 0) {
        return -1;
    }

    int initial_cap = cfg->triggers.max_active > 0 ? cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE;
    mgr->capacity = initial_cap;
    mgr->triggers = (active_trigger_t*)calloc((size_t)mgr->capacity, sizeof(active_trigger_t));
    if (!mgr->triggers) {
        pthread_mutex_destroy(&mgr->mutex);
        memset(mgr, 0, sizeof(*mgr));
        return -1;
    }

    mgr->last_poll_at = 0;

    DEBUG_INFO("[didactyl] trigger manager initialized (capacity=%d)", mgr->capacity);
    return 0;
}

int trigger_manager_load_from_skills(trigger_manager_t* mgr) {
    if (!mgr || !mgr->cfg) {
        return -1;
    }

    int loaded = 0;
    int considered = 0;
    const int kinds[2] = {31123, 31124};

    for (int k = 0; k < 2; k++) {
        char* skill_json = nostr_handler_get_self_events_by_kind_json(kinds[k]);
        if (!skill_json) {
            continue;
        }

        cJSON* skill_events = cJSON_Parse(skill_json);
        free(skill_json);
        if (!skill_events || !cJSON_IsArray(skill_events)) {
            cJSON_Delete(skill_events);
            continue;
        }

        int sn = cJSON_GetArraySize(skill_events);
        for (int i = 0; i < sn; i++) {
            cJSON* skill_event = cJSON_GetArrayItem(skill_events, i);
            cJSON* content = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "content") : NULL;
            cJSON* tags = skill_event ? cJSON_GetObjectItemCaseSensitive(skill_event, "tags") : NULL;
            if (!content || !cJSON_IsString(content) || !content->valuestring || !tags || !cJSON_IsArray(tags)) {
                continue;
            }

            cJSON* d = find_tag_value_string(tags, "d");
            cJSON* trigger = find_tag_value_string(tags, "trigger");
            cJSON* filter = find_tag_value_string(tags, "filter");
            cJSON* action = find_tag_value_string(tags, "action");

            const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
            const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
            const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
            const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";

            considered++;
            if (!d_tag || d_tag[0] == '\0') {
                continue;
            }

            int trigger_supported = trigger_s &&
                                   (strcmp(trigger_s, "nostr-subscription") == 0 ||
                                    strcmp(trigger_s, "webhook") == 0 ||
                                    strcmp(trigger_s, "cron") == 0 ||
                                    strcmp(trigger_s, "chain") == 0 ||
                                    strcmp(trigger_s, "dm") == 0);
            if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
                continue;
            }

            if (strcmp(action_s, "template") == 0) {
                DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
            }

            const char* llm_s = NULL;
            const char* tools_s = NULL;
            int has_max_tokens = 0;
            int max_tokens = 0;
            int has_temperature = 0;
            double temperature = 0.0;
            int has_seed = 0;
            int seed = 0;
            parse_trigger_runtime_tags(tags,
                                       &llm_s,
                                       &tools_s,
                                       &has_max_tokens,
                                       &max_tokens,
                                       &has_temperature,
                                       &temperature,
                                       &has_seed,
                                       &seed);

            if (trigger_manager_add(mgr,
                                    d_tag,
                                    content->valuestring,
                                    filter_s,
                                    TRIGGER_ACTION_LLM,
                                    trigger_s,
                                    1,
                                    llm_s,
                                    tools_s,
                                    has_max_tokens,
                                    max_tokens,
                                    has_temperature,
                                    temperature,
                                    has_seed,
                                    seed) == 0) {
                loaded++;
            }
        }

        cJSON_Delete(skill_events);
    }

    DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from self skills (considered=%d)", loaded, considered);
    return 0;
}

int trigger_manager_load_from_startup_events(trigger_manager_t* mgr) {
    if (!mgr || !mgr->cfg) {
        return -1;
    }

    int loaded = 0;
    int considered = 0;

    for (int i = 0; i < mgr->cfg->startup_event_count; i++) {
        startup_event_t* ev = &mgr->cfg->startup_events[i];
        if (!ev) {
            continue;
        }

        if (ev->kind != 31123 && ev->kind != 31124) {
            continue;
        }

        considered++;

        if (!ev->content || ev->content[0] == '\0' || !ev->tags_json || ev->tags_json[0] == '\0') {
            continue;
        }

        cJSON* tags = cJSON_Parse(ev->tags_json);
        if (!tags || !cJSON_IsArray(tags)) {
            cJSON_Delete(tags);
            DEBUG_WARN("[didactyl] startup trigger skip: invalid tags_json for startup event index=%d", i);
            continue;
        }

        cJSON* d = find_tag_value_string(tags, "d");
        cJSON* trigger = find_tag_value_string(tags, "trigger");
        cJSON* filter = find_tag_value_string(tags, "filter");
        cJSON* action = find_tag_value_string(tags, "action");
        cJSON* enabled = find_tag_value_string(tags, "enabled");

        const char* d_tag = (d && cJSON_IsString(d) && d->valuestring) ? d->valuestring : NULL;
        const char* trigger_s = (trigger && cJSON_IsString(trigger) && trigger->valuestring) ? trigger->valuestring : NULL;
        const char* filter_s = (filter && cJSON_IsString(filter) && filter->valuestring) ? filter->valuestring : NULL;
        const char* action_s = (action && cJSON_IsString(action) && action->valuestring) ? action->valuestring : "llm";
        const char* enabled_s = (enabled && cJSON_IsString(enabled) && enabled->valuestring) ? enabled->valuestring : "true";

        if (!d_tag || d_tag[0] == '\0') {
            cJSON_Delete(tags);
            DEBUG_WARN("[didactyl] startup trigger skip: missing d tag for startup event index=%d", i);
            continue;
        }

        int trigger_supported = trigger_s &&
                               (strcmp(trigger_s, "nostr-subscription") == 0 ||
                                strcmp(trigger_s, "webhook") == 0 ||
                                strcmp(trigger_s, "cron") == 0 ||
                                strcmp(trigger_s, "chain") == 0 ||
                                strcmp(trigger_s, "dm") == 0);
        if (!trigger_supported || !filter_s || filter_s[0] == '\0') {
            cJSON_Delete(tags);
            continue;
        }

        if (strcmp(action_s, "template") == 0) {
            DEBUG_WARN("[didactyl] startup trigger action template is deprecated; forcing llm for d_tag=%s", d_tag);
        }
        int is_enabled = (strcmp(enabled_s, "false") == 0 || strcmp(enabled_s, "0") == 0) ? 0 : 1;

        const char* llm_s = NULL;
        const char* tools_s = NULL;
        int has_max_tokens = 0;
        int max_tokens = 0;
        int has_temperature = 0;
        double temperature = 0.0;
        int has_seed = 0;
        int seed = 0;
        parse_trigger_runtime_tags(tags,
                                   &llm_s,
                                   &tools_s,
                                   &has_max_tokens,
                                   &max_tokens,
                                   &has_temperature,
                                   &temperature,
                                   &has_seed,
                                   &seed);

        if (trigger_manager_add(mgr,
                                d_tag,
                                ev->content,
                                filter_s,
                                TRIGGER_ACTION_LLM,
                                trigger_s,
                                is_enabled,
                                llm_s,
                                tools_s,
                                has_max_tokens,
                                max_tokens,
                                has_temperature,
                                temperature,
                                has_seed,
                                seed) == 0) {
            loaded++;
            DEBUG_INFO("[didactyl] startup trigger registered d_tag=%s action=%s enabled=%d", d_tag, "llm", is_enabled);
        } else {
            DEBUG_WARN("[didactyl] startup trigger failed d_tag=%s", d_tag);
        }

        cJSON_Delete(tags);
    }

    DEBUG_INFO("[didactyl] trigger manager loaded %d trigger(s) from startup config (considered=%d)", loaded, considered);
    return 0;
}

int trigger_manager_add(trigger_manager_t* mgr,
                        const char* skill_d_tag,
                        const char* content,
                        const char* filter_json,
                        trigger_action_type_t action_type,
                        const char* trigger_type_str,
                        int enabled,
                        const char* llm_spec,
                        const char* tools_policy,
                        int has_max_tokens,
                        int max_tokens,
                        int has_temperature,
                        double temperature,
                        int has_seed,
                        int seed) {
    if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
        return -1;
    }

    if (action_type == TRIGGER_ACTION_TEMPLATE) {
        DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", skill_d_tag);
        action_type = TRIGGER_ACTION_LLM;
    }
    if (action_type != TRIGGER_ACTION_LLM) {
        return -1;
    }

    pthread_mutex_lock(&mgr->mutex);

    int existing = find_trigger_index_locked(mgr, skill_d_tag);
    if (existing >= 0) {
        pthread_mutex_unlock(&mgr->mutex);
        return trigger_manager_update(mgr,
                                      skill_d_tag,
                                      content,
                                      filter_json,
                                      action_type,
                                      trigger_type_str,
                                      enabled,
                                      llm_spec,
                                      tools_policy,
                                      has_max_tokens,
                                      max_tokens,
                                      has_temperature,
                                      temperature,
                                      has_seed,
                                      seed);
    }

    int max_active = mgr->cfg ? mgr->cfg->triggers.max_active : TRIGGER_DEFAULT_MAX_ACTIVE;
    if (max_active < 1) max_active = TRIGGER_DEFAULT_MAX_ACTIVE;
    if (mgr->count >= max_active) {
        pthread_mutex_unlock(&mgr->mutex);
        DEBUG_WARN("[didactyl] trigger add rejected: max_active reached (%d)", max_active);
        return -1;
    }

    if (ensure_capacity(mgr, mgr->count + 1) != 0) {
        pthread_mutex_unlock(&mgr->mutex);
        return -1;
    }

    active_trigger_t* t = &mgr->triggers[mgr->count];
    memset(t, 0, sizeof(*t));
    snprintf(t->skill_d_tag, sizeof(t->skill_d_tag), "%s", skill_d_tag);
    snprintf(t->skill_content, sizeof(t->skill_content), "%s", content);
    snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json);
    t->action_type = action_type;
    t->enabled = clamp_enabled(enabled);
    t->trigger_type = trigger_type_from_string(trigger_type_str);
    t->last_fired = 0;
    t->last_seen_created_at = 0;
    t->last_cron_fire = 0;
    if (t->trigger_type == TRIGGER_TYPE_CRON) {
        char normalized_cron[sizeof(t->cron_expr)] = {0};
        if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
            pthread_mutex_unlock(&mgr->mutex);
            DEBUG_WARN("[didactyl] trigger add rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
            memset(t, 0, sizeof(*t));
            return -1;
        }
        snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
    }
    snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
    snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
    t->has_max_tokens = has_max_tokens ? 1 : 0;
    t->max_tokens = max_tokens;
    t->has_temperature = has_temperature ? 1 : 0;
    t->temperature = temperature;
    t->has_seed = has_seed ? 1 : 0;
    t->seed = seed;
    t->subscription = NULL;
    t->subscription_ctx = NULL;

    if (register_trigger_subscription_locked(mgr, t) != 0) {
        pthread_mutex_unlock(&mgr->mutex);
        DEBUG_WARN("[didactyl] trigger add rejected: failed to create subscription d_tag=%s", skill_d_tag);
        memset(t, 0, sizeof(*t));
        return -1;
    }

    mgr->count++;

    pthread_mutex_unlock(&mgr->mutex);

    DEBUG_INFO("[didactyl] trigger added d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
    return 0;
}

int trigger_manager_remove(trigger_manager_t* mgr, const char* skill_d_tag) {
    if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0') {
        return -1;
    }

    pthread_mutex_lock(&mgr->mutex);

    int idx = find_trigger_index_locked(mgr, skill_d_tag);
    if (idx < 0) {
        pthread_mutex_unlock(&mgr->mutex);
        return 0;
    }

    close_trigger_subscription_locked(&mgr->triggers[idx]);

    if (idx < mgr->count - 1) {
        memmove(&mgr->triggers[idx],
                &mgr->triggers[idx + 1],
                (size_t)(mgr->count - idx - 1) * sizeof(active_trigger_t));
    }

    mgr->count--;
    memset(&mgr->triggers[mgr->count], 0, sizeof(active_trigger_t));

    pthread_mutex_unlock(&mgr->mutex);

    DEBUG_INFO("[didactyl] trigger removed d_tag=%s", skill_d_tag);
    return 0;
}

int trigger_manager_find(trigger_manager_t* mgr, const char* skill_d_tag, active_trigger_t* out) {
    if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !out) {
        return -1;
    }

    pthread_mutex_lock(&mgr->mutex);
    int idx = find_trigger_index_locked(mgr, skill_d_tag);
    if (idx < 0) {
        pthread_mutex_unlock(&mgr->mutex);
        return -1;
    }

    *out = mgr->triggers[idx];
    out->subscription = NULL;
    out->subscription_ctx = NULL;
    pthread_mutex_unlock(&mgr->mutex);
    return 0;
}

int trigger_manager_fire(trigger_manager_t* mgr,
                         const char* skill_d_tag,
                         cJSON* event,
                         const char* source_label) {
    if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !event) {
        return -1;
    }

    pthread_mutex_lock(&mgr->mutex);
    int idx = find_trigger_index_locked(mgr, skill_d_tag);
    if (idx < 0) {
        pthread_mutex_unlock(&mgr->mutex);
        return -1;
    }

    int fired = maybe_fire_trigger_locked(mgr, idx, event, source_label);
    pthread_mutex_unlock(&mgr->mutex);
    return fired;
}

int trigger_manager_fire_chains(trigger_manager_t* mgr,
                                const char* source_skill_d_tag,
                                cJSON* source_event,
                                const char* source_label) {
    if (!mgr || !source_skill_d_tag || source_skill_d_tag[0] == '\0') {
        return -1;
    }

    static __thread int s_chain_depth = 0;
    if (s_chain_depth >= 5) {
        DEBUG_WARN("[didactyl] chain trigger depth limit reached for source=%s", source_skill_d_tag);
        return 0;
    }

    int fired_count = 0;
    s_chain_depth++;

    pthread_mutex_lock(&mgr->mutex);
    int count_snapshot = mgr->count;
    for (int i = 0; i < count_snapshot; i++) {
        active_trigger_t* t = &mgr->triggers[i];
        if (!t->enabled || t->trigger_type != TRIGGER_TYPE_CHAIN) {
            continue;
        }
        if (t->filter_json[0] == '\0' || strcmp(t->filter_json, source_skill_d_tag) != 0) {
            continue;
        }

        time_t now = time(NULL);
        int cooldown = mgr->cfg ? mgr->cfg->triggers.cooldown_seconds : 0;
        if (cooldown < 0) cooldown = 0;
        if (cooldown > 0 && t->last_fired > 0 && (now - t->last_fired) < cooldown) {
            continue;
        }

        t->last_fired = now;

        active_trigger_t trigger_copy = *t;
        trigger_copy.subscription = NULL;
        trigger_copy.subscription_ctx = NULL;

        pthread_mutex_unlock(&mgr->mutex);

        cJSON* event = cJSON_CreateObject();
        if (event) {
            cJSON_AddStringToObject(event, "type", "chain");
            cJSON_AddStringToObject(event, "source_d_tag", source_skill_d_tag);
            cJSON_AddStringToObject(event, "source_label", source_label ? source_label : "chain");
            cJSON_AddNumberToObject(event, "created_at", (double)now);
            if (source_event) {
                cJSON* dup = cJSON_Duplicate(source_event, 1);
                if (dup) {
                    cJSON_AddItemToObject(event, "source_event", dup);
                }
            }

            (void)mgr;
            execute_llm_action(&trigger_copy, event, "chain");
            cJSON_Delete(event);
            fired_count++;
        }

        pthread_mutex_lock(&mgr->mutex);
    }
    pthread_mutex_unlock(&mgr->mutex);

    s_chain_depth--;
    return fired_count;
}

int trigger_manager_update(trigger_manager_t* mgr,
                           const char* skill_d_tag,
                           const char* content,
                           const char* filter_json,
                           trigger_action_type_t action_type,
                           const char* trigger_type_str,
                           int enabled,
                           const char* llm_spec,
                           const char* tools_policy,
                           int has_max_tokens,
                           int max_tokens,
                           int has_temperature,
                           double temperature,
                           int has_seed,
                           int seed) {
    if (!mgr || !skill_d_tag || skill_d_tag[0] == '\0' || !content || !filter_json) {
        return -1;
    }

    if (action_type == TRIGGER_ACTION_TEMPLATE) {
        DEBUG_WARN("[didactyl] trigger action template is deprecated; forcing llm for d_tag=%s", skill_d_tag);
        action_type = TRIGGER_ACTION_LLM;
    }
    if (action_type != TRIGGER_ACTION_LLM) {
        return -1;
    }

    pthread_mutex_lock(&mgr->mutex);

    int idx = find_trigger_index_locked(mgr, skill_d_tag);
    if (idx < 0) {
        pthread_mutex_unlock(&mgr->mutex);
        return trigger_manager_add(mgr,
                                   skill_d_tag,
                                   content,
                                   filter_json,
                                   action_type,
                                   trigger_type_str,
                                   enabled,
                                   llm_spec,
                                   tools_policy,
                                   has_max_tokens,
                                   max_tokens,
                                   has_temperature,
                                   temperature,
                                   has_seed,
                                   seed);
    }

    active_trigger_t* t = &mgr->triggers[idx];
    snprintf(t->skill_content, sizeof(t->skill_content), "%s", content);
    snprintf(t->filter_json, sizeof(t->filter_json), "%s", filter_json);
    t->action_type = action_type;
    t->enabled = clamp_enabled(enabled);
    t->trigger_type = trigger_type_from_string(trigger_type_str);
    if (t->trigger_type == TRIGGER_TYPE_CRON) {
        char normalized_cron[sizeof(t->cron_expr)] = {0};
        if (!cron_expr_is_valid(filter_json, normalized_cron, sizeof(normalized_cron))) {
            pthread_mutex_unlock(&mgr->mutex);
            DEBUG_WARN("[didactyl] trigger update rejected: invalid cron expression d_tag=%s expr=%s", skill_d_tag, filter_json);
            return -1;
        }
        snprintf(t->cron_expr, sizeof(t->cron_expr), "%s", normalized_cron);
    } else {
        t->cron_expr[0] = '\0';
    }
    snprintf(t->llm_spec, sizeof(t->llm_spec), "%s", llm_spec ? llm_spec : "");
    snprintf(t->tools_policy, sizeof(t->tools_policy), "%s", tools_policy ? tools_policy : "");
    t->has_max_tokens = has_max_tokens ? 1 : 0;
    t->max_tokens = max_tokens;
    t->has_temperature = has_temperature ? 1 : 0;
    t->temperature = temperature;
    t->has_seed = has_seed ? 1 : 0;
    t->seed = seed;

    if (register_trigger_subscription_locked(mgr, t) != 0) {
        pthread_mutex_unlock(&mgr->mutex);
        DEBUG_WARN("[didactyl] trigger update failed to (re)subscribe d_tag=%s", skill_d_tag);
        return -1;
    }

    pthread_mutex_unlock(&mgr->mutex);

    DEBUG_INFO("[didactyl] trigger updated d_tag=%s action=%d enabled=%d", skill_d_tag, (int)action_type, clamp_enabled(enabled));
    return 0;
}

int trigger_manager_active_count(trigger_manager_t* mgr) {
    if (!mgr) {
        return 0;
    }

    int active = 0;
    pthread_mutex_lock(&mgr->mutex);
    for (int i = 0; i < mgr->count; i++) {
        if (mgr->triggers[i].enabled) {
            active++;
        }
    }
    pthread_mutex_unlock(&mgr->mutex);

    return active;
}

int trigger_manager_poll(trigger_manager_t* mgr) {
    if (!mgr) {
        return -1;
    }

    time_t now = time(NULL);
    if (now <= 0) {
        return 0;
    }

    if (mgr->last_poll_at > 0 && (now - mgr->last_poll_at) < 30) {
        return 0;
    }
    mgr->last_poll_at = now;

    int fired = 0;

    pthread_mutex_lock(&mgr->mutex);
    int count_snapshot = mgr->count;
    for (int i = 0; i < count_snapshot; i++) {
        active_trigger_t* t = &mgr->triggers[i];
        if (!t->enabled || t->trigger_type != TRIGGER_TYPE_CRON) {
            continue;
        }

        const char* expr = (t->cron_expr[0] != '\0') ? t->cron_expr : t->filter_json;
        char normalized_expr[64] = {0};
        if (!cron_expr_is_valid(expr, normalized_expr, sizeof(normalized_expr))) {
            DEBUG_WARN("[didactyl] cron trigger skipped: invalid expression d_tag=%s expr=%s", t->skill_d_tag, expr ? expr : "");
            continue;
        }
        if (!cron_matches_now(normalized_expr, now)) {
            continue;
        }

        DEBUG_INFO("[didactyl] cron trigger matched d_tag=%s expr=%s", t->skill_d_tag, normalized_expr);

        if (t->last_cron_fire > 0 && (now - t->last_cron_fire) < 50) {
            continue;
        }

        t->last_cron_fire = now;

        active_trigger_t trigger_copy = *t;
        trigger_copy.subscription = NULL;
        trigger_copy.subscription_ctx = NULL;

        pthread_mutex_unlock(&mgr->mutex);

        cJSON* event = cJSON_CreateObject();
        if (event) {
            cJSON_AddStringToObject(event, "type", "cron");
            cJSON_AddStringToObject(event, "d_tag", trigger_copy.skill_d_tag);
            cJSON_AddStringToObject(event, "cron_expr", normalized_expr);
            cJSON_AddNumberToObject(event, "created_at", (double)now);
            (void)mgr;
            execute_llm_action(&trigger_copy, event, "cron");
            cJSON_Delete(event);
            fired++;
        }

        pthread_mutex_lock(&mgr->mutex);
    }
    pthread_mutex_unlock(&mgr->mutex);

    return fired;
}

static int dm_filter_matches_tier(const char* filter_json, didactyl_sender_tier_t tier) {
    if (!filter_json || filter_json[0] == '\0') {
        return 0;
    }

    cJSON* root = cJSON_Parse(filter_json);
    if (!root || !cJSON_IsObject(root)) {
        cJSON_Delete(root);
        return 0;
    }

    cJSON* from = cJSON_GetObjectItemCaseSensitive(root, "from");
    const char* from_s = (from && cJSON_IsString(from) && from->valuestring) ? from->valuestring : "admin";

    int match = 0;
    if (strcmp(from_s, "any") == 0) {
        match = 1;
    } else if (strcmp(from_s, "admin") == 0) {
        match = (tier == DIDACTYL_SENDER_ADMIN);
    } else if (strcmp(from_s, "wot") == 0) {
        match = (tier == DIDACTYL_SENDER_WOT);
    }

    cJSON_Delete(root);
    return match;
}

int trigger_manager_get_dm_skills(trigger_manager_t* mgr,
                                  const char* sender_pubkey_hex,
                                  didactyl_sender_tier_t tier,
                                  char** out_d_tags,
                                  int max_d_tags) {
    (void)sender_pubkey_hex;

    if (!mgr || !out_d_tags || max_d_tags <= 0) {
        return -1;
    }

    int count = 0;

    pthread_mutex_lock(&mgr->mutex);
    int count_snapshot = mgr->count;
    for (int i = 0; i < count_snapshot && count < max_d_tags; i++) {
        active_trigger_t* t = &mgr->triggers[i];
        if (!t->enabled || t->trigger_type != TRIGGER_TYPE_DM) {
            continue;
        }
        if (!dm_filter_matches_tier(t->filter_json, tier)) {
            continue;
        }

        out_d_tags[count++] = t->skill_d_tag;
    }
    pthread_mutex_unlock(&mgr->mutex);

    return count;
}

char* trigger_manager_status_json(trigger_manager_t* mgr) {
    if (!mgr) {
        cJSON* err = cJSON_CreateObject();
        if (!err) {
            return NULL;
        }
        cJSON_AddBoolToObject(err, "success", 0);
        cJSON_AddStringToObject(err, "error", "trigger manager unavailable");
        char* out = cJSON_PrintUnformatted(err);
        cJSON_Delete(err);
        return out;
    }

    cJSON* root = cJSON_CreateObject();
    cJSON* arr = cJSON_CreateArray();
    if (!root || !arr) {
        cJSON_Delete(root);
        cJSON_Delete(arr);
        return NULL;
    }

    pthread_mutex_lock(&mgr->mutex);

    cJSON_AddBoolToObject(root, "success", 1);
    cJSON_AddNumberToObject(root, "count", mgr->count);

    int active = 0;
    for (int i = 0; i < mgr->count; i++) {
        active_trigger_t* t = &mgr->triggers[i];
        if (t->enabled) {
            active++;
        }

        cJSON* item = cJSON_CreateObject();
        if (!item) {
            continue;
        }

        cJSON_AddStringToObject(item, "skill_d_tag", t->skill_d_tag);
        cJSON_AddStringToObject(item, "filter_json", t->filter_json);
        cJSON_AddStringToObject(item, "type", trigger_type_to_string(t->trigger_type));
        cJSON_AddStringToObject(item, "action", "llm");
        cJSON_AddBoolToObject(item, "enabled", t->enabled ? 1 : 0);
        cJSON_AddBoolToObject(item, "subscribed", t->subscription ? 1 : 0);
        cJSON_AddStringToObject(item, "llm", t->llm_spec);
        cJSON_AddStringToObject(item, "tools", t->tools_policy);
        cJSON_AddBoolToObject(item, "has_max_tokens", t->has_max_tokens ? 1 : 0);
        if (t->has_max_tokens) cJSON_AddNumberToObject(item, "max_tokens", t->max_tokens);
        cJSON_AddBoolToObject(item, "has_temperature", t->has_temperature ? 1 : 0);
        if (t->has_temperature) cJSON_AddNumberToObject(item, "temperature", t->temperature);
        cJSON_AddBoolToObject(item, "has_seed", t->has_seed ? 1 : 0);
        if (t->has_seed) cJSON_AddNumberToObject(item, "seed", t->seed);
        cJSON_AddNumberToObject(item, "last_fired", (double)t->last_fired);
        cJSON_AddNumberToObject(item, "last_seen_created_at", (double)t->last_seen_created_at);
        if (t->trigger_type == TRIGGER_TYPE_CRON) {
            cJSON_AddStringToObject(item, "cron_expr", t->cron_expr);
            cJSON_AddNumberToObject(item, "last_cron_fire", (double)t->last_cron_fire);
        }
        cJSON_AddItemToArray(arr, item);
    }

    cJSON_AddNumberToObject(root, "active", active);
    cJSON_AddItemToObject(root, "triggers", arr);

    pthread_mutex_unlock(&mgr->mutex);

    char* out = cJSON_PrintUnformatted(root);
    cJSON_Delete(root);
    return out;
}

void trigger_manager_cleanup(trigger_manager_t* mgr) {
    if (!mgr) {
        return;
    }

    pthread_mutex_lock(&mgr->mutex);
    for (int i = 0; i < mgr->count; i++) {
        close_trigger_subscription_locked(&mgr->triggers[i]);
    }
    free(mgr->triggers);
    mgr->triggers = NULL;
    mgr->count = 0;
    mgr->capacity = 0;
    mgr->cfg = NULL;
    mgr->last_poll_at = 0;
    pthread_mutex_unlock(&mgr->mutex);

    pthread_mutex_destroy(&mgr->mutex);
    memset(mgr, 0, sizeof(*mgr));

    DEBUG_INFO("[didactyl] trigger manager cleaned up");
}
