// puzzle_binding.c — parse a step's optional puzzle binding from Runtime 3 IR.
#include "puzzle_binding.h"
#include "cJSON.h"
#include <string.h>

esp_err_t puzzle_binding_from_ir(const char *ir_json, const char *step_id,
                                 puzzle_binding_t *out)
{
    if (!ir_json || !step_id || !out) return ESP_ERR_INVALID_ARG;

    memset(out, 0, sizeof(*out));

    cJSON *root = cJSON_Parse(ir_json);
    if (!root) return ESP_ERR_INVALID_ARG;
    if (!cJSON_IsObject(root)) { cJSON_Delete(root); return ESP_ERR_INVALID_ARG; }

    const cJSON *steps = cJSON_GetObjectItemCaseSensitive(root, "steps");
    if (!cJSON_IsArray(steps) || cJSON_GetArraySize(steps) == 0) {
        cJSON_Delete(root);
        return ESP_ERR_NOT_FOUND;
    }

    // Find the step with matching id.
    const cJSON *step = NULL;
    const cJSON *item = NULL;
    cJSON_ArrayForEach(item, steps) {
        const cJSON *sid = cJSON_GetObjectItemCaseSensitive(item, "id");
        if (cJSON_IsString(sid) && strcmp(sid->valuestring, step_id) == 0) {
            step = item;
            break;
        }
    }
    if (!step) { cJSON_Delete(root); return ESP_ERR_NOT_FOUND; }

    // Step found. Check for puzzle member.
    const cJSON *puzzle = cJSON_GetObjectItemCaseSensitive(step, "puzzle");
    if (!puzzle || cJSON_IsNull(puzzle)) {
        // No puzzle — out->type stays PB_NONE, ESP_OK.
        cJSON_Delete(root);
        return ESP_OK;
    }

    // --- puzzle.id ---
    const cJSON *jid = cJSON_GetObjectItemCaseSensitive(puzzle, "id");
    if (!cJSON_IsNumber(jid) || jid->valueint < 1 || jid->valueint > PB_MAX_PUZZLE_ID) {
        cJSON_Delete(root);
        memset(out, 0, sizeof(*out));
        return ESP_ERR_INVALID_ARG;
    }

    // --- puzzle.type ---
    const cJSON *jtype = cJSON_GetObjectItemCaseSensitive(puzzle, "type");
    if (!cJSON_IsString(jtype)) {
        cJSON_Delete(root);
        memset(out, 0, sizeof(*out));
        return ESP_ERR_INVALID_ARG;
    }
    puzzle_binding_type_t ptype;
    if (strcmp(jtype->valuestring, "qr") == 0) {
        ptype = PB_QR;
    } else if (strcmp(jtype->valuestring, "sound") == 0) {
        ptype = PB_SOUND;
    } else if (strcmp(jtype->valuestring, "morse") == 0) {
        ptype = PB_MORSE;
    } else if (strcmp(jtype->valuestring, "nfc") == 0) {
        ptype = PB_NFC;
    } else {
        cJSON_Delete(root);
        memset(out, 0, sizeof(*out));
        return ESP_ERR_INVALID_ARG;
    }

    // --- fragment (required) ---
    const cJSON *jfrag = cJSON_GetObjectItemCaseSensitive(puzzle, "fragment");
    if (!cJSON_IsArray(jfrag)) {
        cJSON_Delete(root);
        memset(out, 0, sizeof(*out));
        return ESP_ERR_INVALID_ARG;
    }
    int frag_len = cJSON_GetArraySize(jfrag);
    if (frag_len < 1 || frag_len > PB_MAX_FRAG) {
        cJSON_Delete(root);
        memset(out, 0, sizeof(*out));
        return ESP_ERR_INVALID_ARG;
    }
    for (int i = 0; i < frag_len; i++) {
        const cJSON *fv = cJSON_GetArrayItem(jfrag, i);
        if (!cJSON_IsNumber(fv) || fv->valueint < 0 || fv->valueint > 9) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        out->fragment[i] = (uint8_t)fv->valueint;
    }
    out->fragment_len = (uint8_t)frag_len;

    // --- type-specific fields ---
    if (ptype == PB_QR) {
        const cJSON *jcodes = cJSON_GetObjectItemCaseSensitive(puzzle, "codes");
        if (!cJSON_IsArray(jcodes)) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        int n = cJSON_GetArraySize(jcodes);
        if (n < 1 || n > PB_MAX_CODES) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        for (int i = 0; i < n; i++) {
            const cJSON *cv = cJSON_GetArrayItem(jcodes, i);
            if (!cJSON_IsString(cv)) {
                cJSON_Delete(root);
                memset(out, 0, sizeof(*out));
                return ESP_ERR_INVALID_ARG;
            }
            if (strlen(cv->valuestring) >= PB_MAX_LABEL) {
                cJSON_Delete(root);
                memset(out, 0, sizeof(*out));
                return ESP_ERR_INVALID_ARG;
            }
            strncpy(out->codes[i], cv->valuestring, PB_MAX_LABEL - 1);
            out->codes[i][PB_MAX_LABEL - 1] = '\0';
        }
        out->code_count = (size_t)n;
    } else if (ptype == PB_SOUND) {
        const cJSON *jmelody = cJSON_GetObjectItemCaseSensitive(puzzle, "melody");
        if (!cJSON_IsArray(jmelody)) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        int n = cJSON_GetArraySize(jmelody);
        if (n < 1 || n > PB_MAX_NOTES) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        for (int i = 0; i < n; i++) {
            const cJSON *nv = cJSON_GetArrayItem(jmelody, i);
            if (!cJSON_IsNumber(nv)) {
                cJSON_Delete(root);
                memset(out, 0, sizeof(*out));
                return ESP_ERR_INVALID_ARG;
            }
            out->melody[i] = nv->valueint;
        }
        out->note_count = (size_t)n;

        // tolerance: optional, default 1, must be >= 0
        const cJSON *jtol = cJSON_GetObjectItemCaseSensitive(puzzle, "tolerance");
        if (jtol) {
            if (!cJSON_IsNumber(jtol) || jtol->valueint < 0) {
                cJSON_Delete(root);
                memset(out, 0, sizeof(*out));
                return ESP_ERR_INVALID_ARG;
            }
            out->tolerance = jtol->valueint;
        } else {
            out->tolerance = 1;
        }
    } else if (ptype == PB_MORSE) {
        // puzzle.expected: required string, uppercase word, e.g. "ZACUS"
        const cJSON *jexp = cJSON_GetObjectItemCaseSensitive(puzzle, "expected");
        if (!cJSON_IsString(jexp) || !jexp->valuestring || !jexp->valuestring[0]) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        if (strlen(jexp->valuestring) >= sizeof(out->morse_expected)) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        strncpy(out->morse_expected, jexp->valuestring,
                sizeof(out->morse_expected) - 1);
        out->morse_expected[sizeof(out->morse_expected) - 1] = '\0';
    } else if (ptype == PB_NFC) {
        // puzzle.uid: required string, colon-hex UID, e.g. "A1:B2:C3:D4"
        const cJSON *juid = cJSON_GetObjectItemCaseSensitive(puzzle, "uid");
        if (!cJSON_IsString(juid) || !juid->valuestring || !juid->valuestring[0]) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        if (strlen(juid->valuestring) >= sizeof(out->nfc_uid)) {
            cJSON_Delete(root);
            memset(out, 0, sizeof(*out));
            return ESP_ERR_INVALID_ARG;
        }
        strncpy(out->nfc_uid, juid->valuestring, sizeof(out->nfc_uid) - 1);
        out->nfc_uid[sizeof(out->nfc_uid) - 1] = '\0';
    }

    // id/type written last — all error exits above leave *out zeroed
    out->id   = (uint8_t)jid->valueint;
    out->type = ptype;

    cJSON_Delete(root);
    return ESP_OK;
}

// ─── Scene metadata (lenient — see header) ───────────────────────────────────

static void copy_scene_str(char *dst, size_t cap, const cJSON *node) {
    if (cJSON_IsString(node) && node->valuestring) {
        strncpy(dst, node->valuestring, cap - 1);   // silent truncation OK
        dst[cap - 1] = '\0';
    }
}

esp_err_t scene_binding_from_ir(const char *ir_json, const char *step_id,
                                scene_binding_t *out)
{
    if (!ir_json || !step_id || !out) return ESP_ERR_INVALID_ARG;

    memset(out, 0, sizeof(*out));
    out->effect = SB_FX_PULSE;

    cJSON *root = cJSON_Parse(ir_json);
    if (!root) return ESP_ERR_INVALID_ARG;
    if (!cJSON_IsObject(root)) { cJSON_Delete(root); return ESP_ERR_INVALID_ARG; }

    const cJSON *steps = cJSON_GetObjectItemCaseSensitive(root, "steps");
    if (!cJSON_IsArray(steps) || cJSON_GetArraySize(steps) == 0) {
        cJSON_Delete(root);
        return ESP_ERR_NOT_FOUND;
    }

    const cJSON *step = NULL;
    const cJSON *item = NULL;
    cJSON_ArrayForEach(item, steps) {
        const cJSON *sid = cJSON_GetObjectItemCaseSensitive(item, "id");
        if (cJSON_IsString(sid) && strcmp(sid->valuestring, step_id) == 0) {
            step = item;
            break;
        }
    }
    if (!step) { cJSON_Delete(root); return ESP_ERR_NOT_FOUND; }

    const cJSON *scene = cJSON_GetObjectItemCaseSensitive(step, "scene");
    if (!scene || !cJSON_IsObject(scene)) {
        // No scene — present stays false, ESP_OK.
        cJSON_Delete(root);
        return ESP_OK;
    }

    out->present = true;
    copy_scene_str(out->title,    sizeof(out->title),
                   cJSON_GetObjectItemCaseSensitive(scene, "title"));
    copy_scene_str(out->subtitle, sizeof(out->subtitle),
                   cJSON_GetObjectItemCaseSensitive(scene, "subtitle"));
    copy_scene_str(out->symbol,   sizeof(out->symbol),
                   cJSON_GetObjectItemCaseSensitive(scene, "symbol"));

    const cJSON *fx = cJSON_GetObjectItemCaseSensitive(scene, "effect");
    if (cJSON_IsString(fx) && fx->valuestring) {
        if      (strcmp(fx->valuestring, "glitch") == 0) out->effect = SB_FX_GLITCH;
        else if (strcmp(fx->valuestring, "gyro")   == 0) out->effect = SB_FX_GYRO;
        else if (strcmp(fx->valuestring, "none")   == 0) out->effect = SB_FX_NONE;
        else out->effect = SB_FX_PULSE;   // unknown → default, never an error
    }

    cJSON_Delete(root);
    return ESP_OK;
}
