#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <stdarg.h>
#include "dtob.h"
#include "dtob_types.h"
#include "xdiff/xinclude.h"
#include "bromberg_sl2.h"
#include "xanadoc_types.h"

#define DIF_CCID       (DTOB_CUSTOM_MIN + 0)   /* 16 */
#define DIF_OP         (DTOB_CUSTOM_MIN + 1)   /* 17 */
#define DIF_COPY       (DTOB_CUSTOM_MIN + 2)   /* 18 — struct {start, end} in OLD content */
#define DIF_ADD        (DTOB_CUSTOM_MIN + 3)   /* 19 — struct {start, end, data} in NEW content */
#define DIF_START      (DTOB_CUSTOM_MIN + 4)   /* 20 — uint64 byte offset */
#define DIF_END        (DTOB_CUSTOM_MIN + 5)   /* 21 — uint64 byte offset (inclusive) */
#define DIF_DATA       (DTOB_CUSTOM_MIN + 6)   /* 22 — raw bytes payload */

DTOB_DEFINE_CUSTOM_TYPES(sculblog_dif, th,
    DTOB_CUSTOM_TYPE_RAW    (th, DIF_CCID,      "ccid")
    DTOB_CUSTOM_TYPE_ENUM   (th, DIF_OP,        "op",   DIF_COPY, DIF_ADD)
    DTOB_CUSTOM_TYPE_STRUCT (th, DIF_COPY,      "copy", DIF_START, DIF_END)
    DTOB_CUSTOM_TYPE_STRUCT (th, DIF_ADD,       "add",  DIF_START, DIF_END, DIF_DATA)
    DTOB_CUSTOM_TYPE_UINT64 (th, DIF_START,     "start")
    DTOB_CUSTOM_TYPE_UINT64 (th, DIF_END,       "end")
    DTOB_CUSTOM_TYPE_RAW    (th, DIF_DATA,      "data")
)



/* Create a named uint64 field (DIF_START or DIF_END) with 8 big-endian bytes.
 * DTOB_CUSTOM_TYPE_UINT64 has n_opcodes=1, so the encoder writes no inner opcode tag —
 * just the custom ctrl word followed by the trit-encoded 8 bytes. */
static DtobValue* sculblog_dif_u64_field(uint16_t code, uint64_t val) {
    uint8_t bytes[8];
    for (int i = 7; i >= 0; i--) { bytes[i] = (uint8_t)(val & 0xFF); val >>= 8; }
    DtobValue* v = dtob_custom(code, bytes, 8);
    v->inner_code = DTOB_CODE_UINT64;
    return v;
}

/* copy(start, end): byte range [start, end] inclusive in the OLD content. */
static DtobValue* sculblog_dif_copy(uint64_t s, uint64_t e) {
    DtobValue* op = dtob_custom(DIF_OP, NULL, 0);
    op->inner_code = DIF_COPY;
    dtob_custom_push(op, sculblog_dif_u64_field(DIF_START, s));
    dtob_custom_push(op, sculblog_dif_u64_field(DIF_END, e));
    return op;
}

/* add(start, end, data): byte range [start, end] inclusive in the NEW content, plus literal bytes. */
static DtobValue* sculblog_dif_add(uint64_t s, uint64_t e, const uint8_t* data, size_t len) {
    DtobValue* op = dtob_custom(DIF_OP, NULL, 0);
    op->inner_code = DIF_ADD;
    dtob_custom_push(op, sculblog_dif_u64_field(DIF_START, s));
    dtob_custom_push(op, sculblog_dif_u64_field(DIF_END, e));
    DtobValue* dv = dtob_custom(DIF_DATA, data, len);
    dv->inner_code = DTOB_CODE_RAW;
    dtob_custom_push(op, dv);
    return op;
}

static DtobValue* sculblog_dif_diff_new(void) {
    return dtob_array();
}

static void sculblog_dif_diff_push(DtobValue* diff, DtobValue* op) {
    dtob_array_push(diff, op);
}

int BUG_exit_code = 128;
int bug_called_must_BUG = 0;

void *xmalloc(size_t size) {
    if (size == 0) {
        size = 1;
    }
    void *ptr = malloc(size);
    if (!ptr) {
        fprintf(stderr, "xmalloc: allocation failed for %zu bytes\n", size);
        abort();
    }
    return ptr;
}

void *xcalloc(size_t nmemb, size_t size) {
    if (nmemb == 0 || size == 0) {
        nmemb = 1;
        size = 1;
    }
    void *ptr = calloc(nmemb, size);
    if (!ptr) {
        fprintf(stderr, "xcalloc: allocation failed for %zu x %zu bytes\n", nmemb, size);
        abort();
    }
    return ptr;
}

void *xrealloc(void *ptr, size_t size) {
    if (size == 0) {
        size = 1;
    }
    void *next = realloc(ptr, size);
    if (!next) {
        fprintf(stderr, "xrealloc: allocation failed for %zu bytes\n", size);
        abort();
    }
    return next;
}

void BUG_fl(const char *file, int line, const char *fmt, ...) {
    va_list args;
    fprintf(stderr, "BUG at %s:%d: ", file, line);
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
    fputc('\n', stderr);
    abort();
}

static long record_offset(mmfile_t *mf, xdfile_t *xdf, long line_index) {
    if (line_index <= 0) {
        return 0;
    }
    if ((size_t)line_index >= xdf->nrec) {
        return mf->size;
    }
    return (long)(xdf->recs[line_index].ptr - (uint8_t const *)mf->ptr);
}

static void emit_copy_range(DtobValue *diff, long start, long end_exclusive) {
    if (end_exclusive > start) {
        sculblog_dif_diff_push(diff, sculblog_dif_copy((uint64_t)start, (uint64_t)(end_exclusive - 1)));
    }
}

static void emit_add_range(DtobValue *diff, const uint8_t *data, long start, long end_exclusive) {
    if (end_exclusive > start) {
        sculblog_dif_diff_push(diff, sculblog_dif_add(
            (uint64_t)start, (uint64_t)(end_exclusive - 1),
            data + start, (size_t)(end_exclusive - start)
        ));
    }
}

// ---------------------------------------------------------
// Expose simple builders for Rust
// ---------------------------------------------------------

void ffi_bromberg_init(void) {
    bromberg_init();
}

void ffi_bromberg_hash(const uint8_t* in, size_t len, uint8_t out[32]) {
    hash_matrix_t m = bromberg_hash(in, len);
    // bromberg_to_hex returns 64 chars, but we want raw 32 bytes
    // In bromberg_to_hex, each uint64 (4 of them) is just formatted.
    // For raw bytes, we can just copy the 4 uint64_t's into the out array in big-endian or native format.
    // Wait, the potc CLI outputs hex. The python script decodes hex to bytes.
    // Let's just output raw memory content.
    char hex[65];
    bromberg_to_hex(m, hex);
    for (int i=0; i<32; i++) {
        sscanf(&hex[i*2], "%2hhx", &out[i]);
    }
}
void ffi_diff_build(const uint8_t *old_data, size_t old_len,
                    const uint8_t *new_data, size_t new_len,
                    DtobValue *diff) {
    mmfile_t mf1 = { (char *)old_data, (long)old_len };
    mmfile_t mf2 = { (char *)new_data, (long)new_len };
    xpparam_t xpp = { 0 };
    xdfenv_t env = { 0 };
    xdchange_t *script = NULL;

    if (xdl_do_diff(&mf1, &mf2, &xpp, &env) < 0) {
        sculblog_dif_diff_push(diff, sculblog_dif_add(0, new_len > 0 ? new_len - 1 : 0, new_data, new_len));
        return;
    }

    if (xdl_change_compact(&env.xdf1, &env.xdf2, xpp.flags) < 0 ||
        xdl_build_script(&env, &script) < 0) {
        xdl_free_env(&env);
        sculblog_dif_diff_push(diff, sculblog_dif_add(0, new_len > 0 ? new_len - 1 : 0, new_data, new_len));
        return;
    }

    long old_cursor = 0;
    for (xdchange_t *change = script; change; change = change->next) {
        long unchanged_end = record_offset(&mf1, &env.xdf1, change->i1);
        emit_copy_range(diff, old_cursor, unchanged_end);

        long new_start = record_offset(&mf2, &env.xdf2, change->i2);
        long new_end = record_offset(&mf2, &env.xdf2, change->i2 + change->chg2);
        emit_add_range(diff, new_data, new_start, new_end);

        old_cursor = record_offset(&mf1, &env.xdf1, change->i1 + change->chg1);
    }

    emit_copy_range(diff, old_cursor, (long)old_len);

    xdl_free_script(script);
    xdl_free_env(&env);
}

/* Big-endian uint64 from DTOB int/custom payload bytes (matches ffi_dtob_get_u64). */
static uint64_t dtob_data_u64_be(const DtobValue *v) {
    if (!v || !v->data) return 0;
    uint64_t val = 0;
    for (size_t i = 0; i < v->data_len && i < 8; i++)
        val = (val << 8) | v->data[i];
    return val;
}

uint8_t* ffi_apply_diff(const uint8_t *prev, size_t prev_len, const DtobValue *diff_val, size_t *out_len) {
    size_t cap = prev_len > 0 ? prev_len * 2 : 4096;
    uint8_t *result = malloc(cap);
    size_t rlen = 0;

    for (size_t i = 0; i < diff_val->num_elements; i++) {
        const DtobValue *op = diff_val->elements[i];
        if (!op) continue;

        uint16_t variant = op->inner_code;
        if (variant == DIF_COPY) {
            if (op->num_elements < 2) continue;
            uint64_t s = dtob_data_u64_be(op->elements[0]);
            uint64_t e = dtob_data_u64_be(op->elements[1]);
            if (e >= s && e < prev_len) {
                size_t clen = (size_t)(e - s + 1);
                while (rlen + clen > cap) { cap *= 2; result = realloc(result, cap); }
                memcpy(result + rlen, prev + s, clen);
                rlen += clen;
            }
        } else if (variant == DIF_ADD) {
            /* add struct: elements[0]=DIF_START, elements[1]=DIF_END, elements[2]=DIF_DATA */
            if (op->num_elements < 3) continue;
            const DtobValue *dv = op->elements[2];
            if (dv && dv->data && dv->data_len > 0) {
                while (rlen + dv->data_len > cap) { cap *= 2; result = realloc(result, cap); }
                memcpy(result + rlen, dv->data, dv->data_len);
                rlen += dv->data_len;
            }
        }
    }
    *out_len = rlen;
    return result;
}

// ---------------------------------------------------------
// Expose simple builders for Rust
// ---------------------------------------------------------

DtobValue* ffi_array_new(void) { return dtob_array(); }
DtobValue* ffi_kvset_new(void) { return dtob_kvset(); }
void ffi_array_push(DtobValue* arr, DtobValue* val) { dtob_array_push(arr, val); }
void ffi_kvset_put(DtobValue* kv, const char* key, DtobValue* val) { dtob_kvset_put(kv, key, val); }
DtobValue* ffi_kvset_get(DtobValue* kv, const char* key) { return dtob_kvset_get(kv, key); }

DtobValue* ffi_raw_new(const uint8_t* data, size_t len) { return dtob_raw(data, len); }
DtobValue* ffi_uint_new(uint64_t val) { return dtob_uint(val); }

DtobValue* ffi_xanadoc_ids_new(const uint8_t* ccid, const uint8_t* cid, const uint8_t* sha256) {
    DtobValue* op = dtob_custom(XANADOC_IDS, NULL, 0);
    DtobValue* v_ccid = dtob_custom(XANADOC_CCID, ccid, 32);
    v_ccid->inner_code = DTOB_CODE_RAW;
    dtob_custom_push(op, v_ccid);
    
    DtobValue* v_cid = dtob_custom(XANADOC_CID, cid, 32);
    v_cid->inner_code = DTOB_CODE_RAW;
    dtob_custom_push(op, v_cid);
    
    DtobValue* v_sha256 = dtob_custom(XANADOC_SHA256CID, sha256, 32);
    v_sha256->inner_code = DTOB_CODE_RAW;
    dtob_custom_push(op, v_sha256);
    return op;
}

// dif-specific
DtobValue* ffi_dif_ops_new(void) { return sculblog_dif_diff_new(); }
void ffi_dif_ops_push_add(DtobValue* diff, const uint8_t* data, size_t len) {
    /* snapshot full-content add: range covers entire new content [0, len-1] */
    sculblog_dif_diff_push(diff, sculblog_dif_add(0, len > 0 ? len - 1 : 0, data, len));
}

// Each dif entry IS the ops array directly — no wrapper, no ccid.
// All metadata (ccid, cid, title, etc.) lives inside the xanadoc bytes in the data payload.
DtobValue* ffi_build_dif_entry(
    DtobValue* ops
) {
    return ops;
}

// Encode dif file data
int ffi_verify_file_types(const char* path, int strict) {
    DtobTypesHeader types;
    build_sculblog_dif_custom_types(&types);
    int res = dtob_verify_file_types(path, &types, strict);
    dtob_types_cleanup(&types);
    return res;
}

static uint8_t* ffi_encode_internal(DtobValue* val, int chunk_mode, size_t* out_len) {
    DtobTypesHeader types;
    build_sculblog_dif_custom_types(&types);
    uint8_t* buf = chunk_mode ? 
        dtob_encode_chunk(val, &types, 1, out_len) : 
        dtob_encode_with_types(val, &types, 0, out_len);
    dtob_types_cleanup(&types);
    return buf;
}

uint8_t* ffi_encode_dif(DtobValue* root, size_t *out_len) {
    return ffi_encode_internal(root, 0, out_len);
}

uint8_t* ffi_encode_xanadoc_header(DtobValue* root, size_t *out_len) {
    DtobTypesHeader types;
    build_xanadoc_custom_types(&types);
    uint8_t* buf = dtob_encode_with_types(root, &types, 0, out_len);
    dtob_types_cleanup(&types);
    return buf;
}

// Decode dif file data
DtobValue* ffi_decode_dif(const uint8_t* buf, size_t len) {
    return dtob_decode(buf, len);
}

uint8_t* ffi_encode_chunk(DtobValue* val, size_t* out_len) {
    return ffi_encode_internal(val, 1, out_len);
}

void ffi_dtob_free(DtobValue* val) {
    dtob_free(val);
}

size_t ffi_dtob_array_len(const DtobValue* val) {
    return val ? val->num_elements : 0;
}

const DtobValue* ffi_dtob_array_get(const DtobValue* val, size_t idx) {
    if (val && idx < val->num_elements) return val->elements[idx];
    return NULL;
}

size_t ffi_dtob_kvset_len(const DtobValue* val) {
    return val ? val->num_pairs : 0;
}

const char* ffi_dtob_kvset_key(const DtobValue* val, size_t idx) {
    if (val && idx < val->num_pairs) return (const char*)val->pairs[idx].key;
    return NULL;
}

const DtobValue* ffi_dtob_kvset_value_at(const DtobValue* val, size_t idx) {
    if (val && idx < val->num_pairs) return val->pairs[idx].value;
    return NULL;
}

// Read raw data from DtobValue
const uint8_t* ffi_dtob_get_raw(const DtobValue* val, size_t* out_len) {
    if (!val || !val->data) return NULL;
    *out_len = val->data_len;
    return val->data;
}

uint64_t ffi_dtob_get_u64(const DtobValue* val) {
    return dtob_data_u64_be(val);
}

/* ── xanadoc (.xdc) reading ─────────────────────────────────────────────── */

static DtobValue *xdc_parse(const uint8_t *data, size_t len) {
    DtobTypesHeader types;
    build_xanadoc_custom_types(&types);
    DtobValue *root = dtob_decode_with_types((uint8_t *)data, len, &types);
    dtob_types_cleanup(&types);
    return root;
}

/* Extract raw content field from xdc DTOB. Returns heap-allocated bytes. */
uint8_t *ffi_xdc_decode_content(const uint8_t *data, size_t len, size_t *out_len) {
    *out_len = 0;
    DtobValue *root = xdc_parse(data, len);
    if (!root) return NULL;

    size_t field_len = 0;
    const uint8_t *ptr = dtob_kvset_raw(root, "content", &field_len);
    if (!ptr || field_len == 0) { dtob_free(root); return NULL; }

    uint8_t *result = malloc(field_len);
    memcpy(result, ptr, field_len);
    *out_len = field_len;
    dtob_free(root);
    return result;
}

/* helper to extract uint32 from a custom enum value */
static uint32_t xdc_enum_to_u32(const DtobValue *v) {
    if (!v) return 0;
    return (uint32_t)dtob_val_to_u64(v);
}

/* helper to copy raw bytes from custom raw value */
static size_t xdc_raw_copy(const DtobValue *v, uint8_t *dst, size_t max) {
    if (!v || !v->data) return 0;
    size_t n = v->data_len < max ? v->data_len : max;
    memcpy(dst, v->data, n);
    return n;
}

/* Extract 32 bytes from custom raw (cid) */
static void xdc_copy32(const DtobValue *v, uint8_t *dst) {
    memset(dst, 0, 32);
    xdc_raw_copy(v, dst, 32);
}

/*
 * ffi_xdc_decode_references
 *
 * Returns a flat buffer of reference records for link injection.
 * Each record is 41 bytes:
 *   u8  type (0=transclusion, 1=microlink, 2=macrolink)
 *   32B def_cid (for href)
 *   u32 ref_start (byte offset in markdown content)
 *   u32 length (byte length in markdown content)
 *
 * Layout: u32 count, then count * 41 bytes.
 */
static void write_u32_le(uint8_t *p, uint32_t v) {
    p[0] = v & 0xFF; p[1] = (v>>8)&0xFF; p[2] = (v>>16)&0xFF; p[3] = (v>>24)&0xFF;
}

uint8_t *ffi_xdc_decode_references(const uint8_t *data, size_t len, size_t *out_len) {
    *out_len = 0;
    DtobValue *root = xdc_parse(data, len);
    if (!root) return NULL;

    DtobValue *refs = dtob_kvset_get(root, "references");
    DtobValue *paleolinks = refs ? dtob_kvset_get(refs, "paleolinks") : NULL;
    DtobValue *paleo_trans = paleolinks ? dtob_kvset_get(paleolinks, "transclusions") : NULL;
    DtobValue *auto_micro = paleolinks ? dtob_kvset_get(paleolinks, "microlinks") : NULL;
    DtobValue *macro_arr  = paleolinks ? dtob_kvset_get(paleolinks, "macrolinks") : NULL;

    size_t nt = paleo_trans ? paleo_trans->num_elements : 0;
    size_t nm = auto_micro  ? auto_micro->num_elements  : 0;
    size_t nM = macro_arr   ? macro_arr->num_elements   : 0;
    size_t count = nt + nm + nM;

    size_t total = 4 + count * 41;
    uint8_t *out = malloc(total);
    if (!out) { dtob_free(root); return NULL; }
    memset(out, 0, total);
    uint8_t *p = out;

    write_u32_le(p, (uint32_t)count); p += 4;

    /* transclusions: def_cid = first 32 bytes of potc (expectedDoc) */
    for (size_t i = 0; i < nt; i++) {
        DtobValue *t = paleo_trans->elements[i];
        if (!t || t->num_elements < 4) { p += 41; continue; }
        *p++ = 0; /* type = transclusion */
        xdc_raw_copy(t->elements[0], p, 32); /* potc[0..31] = expectedDoc = def cid */
        p += 32;
        uint32_t ref_start = xdc_enum_to_u32(t->elements[3]);
        uint32_t length    = xdc_enum_to_u32(t->elements[1]);
        write_u32_le(p, ref_start); p += 4;
        write_u32_le(p, length);    p += 4;
    }

    /* microlinks: def_cid = defIds.cid (element 0 is ids struct, its element 1 is cid) */
    for (size_t i = 0; i < nm; i++) {
        DtobValue *m = auto_micro->elements[i];
        if (!m || m->num_elements < 6) { p += 41; continue; }
        *p++ = 1; /* type = microlink */
        DtobValue *def_ids = m->elements[0];
        if (def_ids && def_ids->num_elements >= 3)
            xdc_copy32(def_ids->elements[1], p); /* cid */
        p += 32;
        uint32_t ref_start  = xdc_enum_to_u32(m->elements[5]);
        uint32_t ref_length = xdc_enum_to_u32(m->elements[4]);
        write_u32_le(p, ref_start);  p += 4;
        write_u32_le(p, ref_length); p += 4;
    }

    /* macrolinks: def_cid = defIds.cid (element 0 is ids struct) */
    for (size_t i = 0; i < nM; i++) {
        DtobValue *m = macro_arr->elements[i];
        if (!m || m->num_elements < 4) { p += 41; continue; }
        *p++ = 2; /* type = macrolink */
        DtobValue *def_ids = m->elements[0];
        if (def_ids && def_ids->num_elements >= 3)
            xdc_copy32(def_ids->elements[1], p); /* cid */
        p += 32;
        uint32_t ref_start = xdc_enum_to_u32(m->elements[3]);
        uint32_t length    = xdc_enum_to_u32(m->elements[2]);
        write_u32_le(p, ref_start); p += 4;
        write_u32_le(p, length);    p += 4;
    }

    *out_len = total;
    dtob_free(root);
    return out;
}
