/*
 * dif.c — DTOB diff encoder/decoder.
 *
 * Commands:
 *   dif init <file>                  — create a .dif with the initial version
 *   dif commit <file.dif> <newfile>  — compute diff, append entry
 *   dif apply <file.dif> [version]   — reconstruct content at version N (default: latest)
 *   dif log <file.dif>               — show version history
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>

#include "dif.h"

/* ---- file I/O ---- */

static uint8_t *read_file(const char *path, size_t *out_len) {
    FILE *f = fopen(path, "rb");
    if (!f) { perror(path); return NULL; }
    fseek(f, 0, SEEK_END);
    long sz = ftell(f);
    if (sz < 0) { fclose(f); return NULL; }
    fseek(f, 0, SEEK_SET);
    uint8_t *buf = malloc((size_t)sz);
    if (!buf) { fclose(f); return NULL; }
    fread(buf, 1, (size_t)sz, f);
    fclose(f);
    *out_len = (size_t)sz;
    return buf;
}

static int write_file(const char *path, const uint8_t *data, size_t len) {
    FILE *f = fopen(path, "wb");
    if (!f) { perror(path); return -1; }
    fwrite(data, 1, len, f);
    fclose(f);
    return 0;
}

/* ---- hashing ---- */

/* SHA-256 — minimal implementation */
static void sha256_hash(const uint8_t *data, size_t len, uint8_t out[32]) {
    /* Use a subprocess: sha256sum or openssl */
    FILE *p;
    char cmd[64];
    snprintf(cmd, sizeof(cmd), "openssl dgst -sha256 -binary");
    p = popen(cmd, "w");
    if (!p) { memset(out, 0, 32); return; }
    /* We need to read the output, so use a temp file approach */
    pclose(p);

    /* Simpler: write to temp, hash with openssl */
    char tmp[] = "/tmp/dif-sha-XXXXXX";
    int fd = mkstemp(tmp);
    if (fd < 0) { memset(out, 0, 32); return; }
    write(fd, data, len);
    close(fd);

    char cmd2[256];
    snprintf(cmd2, sizeof(cmd2), "openssl dgst -sha256 -binary %s", tmp);
    p = popen(cmd2, "r");
    if (p) {
        fread(out, 1, 32, p);
        pclose(p);
    } else {
        memset(out, 0, 32);
    }
    unlink(tmp);
}

/* CID via potc — returns 32 bytes or zeros */
static void bromberg_hash(const uint8_t *data, size_t len, uint8_t out[32]) {
    char tmp[] = "/tmp/dif-cid-XXXXXX";
    int fd = mkstemp(tmp);
    if (fd < 0) { memset(out, 0, 32); return; }
    write(fd, data, len);
    close(fd);

    const char *potc = getenv("POTC_BIN");
    if (!potc) potc = "potc";

    char cmd[512];
    snprintf(cmd, sizeof(cmd), "%s singlehash -f %s", potc, tmp);
    FILE *p = popen(cmd, "r");
    if (p) {
        char hex[65] = {0};
        if (fgets(hex, sizeof(hex), p) && strlen(hex) >= 64) {
            for (int i = 0; i < 32; i++) {
                unsigned int b;
                sscanf(hex + i * 2, "%02x", &b);
                out[i] = (uint8_t)b;
            }
        } else {
            memset(out, 0, 32);
        }
        pclose(p);
    } else {
        memset(out, 0, 32);
    }
    unlink(tmp);
}

/* ---- diff computation (Myers-like, simple O(ND)) ---- */

/*
 * Compute a diff from `old` to `new_data` as a sequence of copy/add ops.
 * Uses a simple LCS-based approach: find longest common substrings,
 * emit copy for matches, add for new data.
 *
 * This is a greedy forward scan — not optimal but correct and fast.
 */

typedef struct {
    DtobValue *diff;  /* the diff struct being built */
} DiffBuilder;

static void diff_build(const uint8_t *old_data, size_t old_len,
                        const uint8_t *new_data, size_t new_len,
                        DtobValue *diff) {
    size_t ni = 0;  /* position in new_data */

    while (ni < new_len) {
        /* try to find the longest match in old_data starting at ni */
        size_t best_oi = 0, best_len = 0;

        /* scan old for a matching run */
        for (size_t oi = 0; oi < old_len; oi++) {
            if (old_data[oi] != new_data[ni]) continue;

            /* measure match length */
            size_t ml = 0;
            while (oi + ml < old_len && ni + ml < new_len &&
                   old_data[oi + ml] == new_data[ni + ml])
                ml++;

            if (ml > best_len) {
                best_oi = oi;
                best_len = ml;
            }
        }

        /* minimum match length to be worth a copy (avoid tiny copies) */
        if (best_len >= 8) {
            /* emit add for any unmatched bytes before this copy */
            /* (handled by the caller accumulating adds) */
            dif_diff_push(diff, dif_copy(best_oi, best_oi + best_len - 1));
            ni += best_len;
        } else {
            /* find the next byte that starts a good match, accumulate as add */
            size_t add_start = ni;
            ni++;

            /* look ahead for the next match */
            while (ni < new_len) {
                int found = 0;
                for (size_t oi = 0; oi < old_len; oi++) {
                    if (old_data[oi] != new_data[ni]) continue;
                    size_t ml = 0;
                    while (oi + ml < old_len && ni + ml < new_len &&
                           old_data[oi + ml] == new_data[ni + ml])
                        ml++;
                    if (ml >= 8) { found = 1; break; }
                }
                if (found) break;
                ni++;
            }

            dif_diff_push(diff, dif_add(new_data + add_start, ni - add_start));
        }
    }
}

/* ---- DTOB encoding with types header ---- */

static uint8_t *encode_dif(DtobValue *root, size_t *out_len) {
    DtobTypesHeader types;
    build_dif_custom_types(&types);

    uint8_t *buf = dtob_encode_with_types(root, &types, out_len);

    dtob_types_cleanup(&types);
    return buf;
}

/* ---- read uint64 from custom value (big-endian) ---- */

static uint64_t read_u64(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;
}

/* ---- apply: reconstruct content from diff chain ---- */

static uint8_t *apply_entry(const uint8_t *prev, size_t prev_len,
                             const DtobValue *diff_val, size_t *out_len) {
    /* diff_val is a DIF_DIFF struct with DIF_OP enum children */
    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;

        /* op is a DIF_OP enum; check inner_code to see copy vs add */
        uint16_t variant = op->inner_code;

        if (variant == DIF_COPY) {
            /* copy(start, end) — struct children are on the op value */
            if (op->num_elements < 2) continue;
            uint64_t s = read_u64(op->elements[0]);
            uint64_t e = read_u64(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(data) — raw data is on the op value */
            if (op->data && op->data_len > 0) {
                while (rlen + op->data_len > cap) { cap *= 2; result = realloc(result, cap); }
                memcpy(result + rlen, op->data, op->data_len);
                rlen += op->data_len;
            }
        }
    }

    *out_len = rlen;
    return result;
}

/* ---- commands ---- */

static int cmd_init(const char *input_path) {
    size_t flen;
    uint8_t *data = read_file(input_path, &flen);
    if (!data) return 1;

    /* compute hashes */
    uint8_t cid[32], sha[32];
    bromberg_hash(data, flen, cid);
    sha256_hash(data, flen, sha);

    /* build diff: single add of the whole document */
    DtobValue *diff = dif_diff_new();
    dif_diff_push(diff, dif_add(data, flen));

    /* build entry */
    DtobValue *entry = dif_entry(cid, sha, diff);

    /* build root array */
    DtobValue *root = dtob_array();
    dtob_array_push(root, entry);

    /* encode */
    size_t out_len;
    uint8_t *encoded = encode_dif(root, &out_len);
    dtob_free(root);

    /* write output */
    char out_path[4096];
    snprintf(out_path, sizeof(out_path), "%s.dif", input_path);
    int rc = write_file(out_path, encoded, out_len);
    free(encoded);
    free(data);

    if (rc == 0) {
        fprintf(stderr, "init: %s → %s (%zu bytes, content %zu bytes)\n",
                input_path, out_path, out_len, flen);
        fprintf(stderr, "  cid: ");
        for (int i = 0; i < 32; i++) fprintf(stderr, "%02x", cid[i]);
        fprintf(stderr, "\n");
    }
    return rc;
}

static int cmd_commit(const char *dif_path, const char *new_path) {
    /* read existing .dif */
    size_t dif_len;
    uint8_t *dif_data = read_file(dif_path, &dif_len);
    if (!dif_data) return 1;

    DtobValue *root = dtob_decode(dif_data, dif_len);
    free(dif_data);
    if (!root || root->type != DTOB_ARRAY || root->num_elements == 0) {
        fprintf(stderr, "error: invalid .dif file\n");
        if (root) dtob_free(root);
        return 1;
    }

    /* reconstruct latest content by applying all entries */
    uint8_t *prev = NULL;
    size_t prev_len = 0;

    for (size_t i = 0; i < root->num_elements; i++) {
        DtobValue *entry = root->elements[i];
        if (!entry || entry->type != DTOB_ARRAY || entry->num_elements < 3) continue;
        DtobValue *diff_val = entry->elements[2];  /* [cid, sha256cid, diff] */

        size_t new_len;
        uint8_t *reconstructed = apply_entry(prev, prev_len, diff_val, &new_len);
        free(prev);
        prev = reconstructed;
        prev_len = new_len;
    }

    /* read new file */
    size_t new_len;
    uint8_t *new_data = read_file(new_path, &new_len);
    if (!new_data) {
        free(prev);
        dtob_free(root);
        return 1;
    }

    /* compute hashes of new content */
    uint8_t cid[32], sha[32];
    bromberg_hash(new_data, new_len, cid);
    sha256_hash(new_data, new_len, sha);

    /* compute diff from prev to new */
    DtobValue *diff = dif_diff_new();
    if (prev && prev_len > 0) {
        diff_build(prev, prev_len, new_data, new_len, diff);
    } else {
        dif_diff_push(diff, dif_add(new_data, new_len));
    }

    /* build entry and append */
    DtobValue *entry = dif_entry(cid, sha, diff);
    dtob_array_push(root, entry);

    /* re-encode */
    size_t n_versions = root->num_elements;
    size_t out_len;
    uint8_t *encoded = encode_dif(root, &out_len);
    dtob_free(root);
    free(prev);
    free(new_data);

    int rc = write_file(dif_path, encoded, out_len);
    free(encoded);

    if (rc == 0) {
        fprintf(stderr, "commit: appended version %zu (%zu bytes)\n",
                n_versions, out_len);
        fprintf(stderr, "  cid: ");
        for (int i = 0; i < 32; i++) fprintf(stderr, "%02x", cid[i]);
        fprintf(stderr, "\n");
    }
    return rc;
}

static int cmd_apply(const char *dif_path, int version) {
    size_t dif_len;
    uint8_t *dif_data = read_file(dif_path, &dif_len);
    if (!dif_data) return 1;

    DtobValue *root = dtob_decode(dif_data, dif_len);
    free(dif_data);
    if (!root || root->type != DTOB_ARRAY) {
        fprintf(stderr, "error: invalid .dif file\n");
        if (root) dtob_free(root);
        return 1;
    }

    size_t target = (version < 0) ? root->num_elements : (size_t)version;
    if (target > root->num_elements) target = root->num_elements;

    uint8_t *content = NULL;
    size_t content_len = 0;

    for (size_t i = 0; i < target; i++) {
        DtobValue *entry = root->elements[i];
        if (!entry || entry->type != DTOB_ARRAY || entry->num_elements < 3) continue;

        size_t new_len;
        uint8_t *reconstructed = apply_entry(content, content_len,
                                              entry->elements[2], &new_len);
        free(content);
        content = reconstructed;
        content_len = new_len;
    }

    if (content && content_len > 0)
        fwrite(content, 1, content_len, stdout);

    free(content);
    dtob_free(root);
    return 0;
}

static int cmd_log(const char *dif_path) {
    size_t dif_len;
    uint8_t *dif_data = read_file(dif_path, &dif_len);
    if (!dif_data) return 1;

    DtobValue *root = dtob_decode(dif_data, dif_len);
    free(dif_data);
    if (!root || root->type != DTOB_ARRAY) {
        fprintf(stderr, "error: invalid .dif file\n");
        if (root) dtob_free(root);
        return 1;
    }

    fprintf(stderr, "%zu version(s)\n\n", root->num_elements);

    for (size_t i = 0; i < root->num_elements; i++) {
        DtobValue *entry = root->elements[i];
        if (!entry || entry->type != DTOB_ARRAY || entry->num_elements < 3) continue;

        fprintf(stderr, "v%zu\n", i + 1);

        /* cid */
        DtobValue *cid_val = entry->elements[0];
        if (cid_val && cid_val->data && cid_val->data_len == 32) {
            fprintf(stderr, "  cid:       ");
            for (int j = 0; j < 32; j++) fprintf(stderr, "%02x", cid_val->data[j]);
            fprintf(stderr, "\n");
        }

        /* sha256cid */
        DtobValue *sha_val = entry->elements[1];
        if (sha_val && sha_val->data && sha_val->data_len == 32) {
            fprintf(stderr, "  sha256cid: ");
            for (int j = 0; j < 32; j++) fprintf(stderr, "%02x", sha_val->data[j]);
            fprintf(stderr, "\n");
        }

        /* diff ops */
        DtobValue *diff_val = entry->elements[2];
        if (diff_val) {
            int n_copy = 0, n_add = 0;
            size_t add_bytes = 0, copy_bytes = 0;
            for (size_t j = 0; j < diff_val->num_elements; j++) {
                DtobValue *op = diff_val->elements[j];
                if (!op) continue;
                if (op->inner_code == DIF_COPY) {
                    n_copy++;
                    if (op->num_elements >= 2) {
                        uint64_t s = read_u64(op->elements[0]);
                        uint64_t e = read_u64(op->elements[1]);
                        copy_bytes += (size_t)(e - s + 1);
                    }
                } else if (op->inner_code == DIF_ADD) {
                    n_add++;
                    add_bytes += op->data_len;
                }
            }
            fprintf(stderr, "  ops:       %d copy (%zu bytes), %d add (%zu bytes)\n",
                    n_copy, copy_bytes, n_add, add_bytes);
        }
        fprintf(stderr, "\n");
    }

    dtob_free(root);
    return 0;
}

/* ---- main ---- */

int main(int argc, char **argv) {
    if (argc < 2) {
        fprintf(stderr, "usage:\n");
        fprintf(stderr, "  dif init <file>                — create .dif with initial version\n");
        fprintf(stderr, "  dif commit <file.dif> <newfile> — append diff of new version\n");
        fprintf(stderr, "  dif apply <file.dif> [version]  — reconstruct content (default: latest)\n");
        fprintf(stderr, "  dif log <file.dif>              — show version history\n");
        return 1;
    }

    const char *cmd = argv[1];

    if (strcmp(cmd, "init") == 0) {
        if (argc < 3) { fprintf(stderr, "usage: dif init <file>\n"); return 1; }
        return cmd_init(argv[2]);
    }
    if (strcmp(cmd, "commit") == 0) {
        if (argc < 4) { fprintf(stderr, "usage: dif commit <file.dif> <newfile>\n"); return 1; }
        return cmd_commit(argv[2], argv[3]);
    }
    if (strcmp(cmd, "apply") == 0) {
        if (argc < 3) { fprintf(stderr, "usage: dif apply <file.dif> [version]\n"); return 1; }
        int ver = (argc >= 4) ? atoi(argv[3]) : -1;
        return cmd_apply(argv[2], ver);
    }
    if (strcmp(cmd, "log") == 0) {
        if (argc < 3) { fprintf(stderr, "usage: dif log <file.dif>\n"); return 1; }
        return cmd_log(argv[2]);
    }

    fprintf(stderr, "unknown command: %s\n", cmd);
    return 1;
}
