#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <sys/stat.h>
#endif

#include "bio_bdl.h"

typedef struct bio_bdl_header {
    uint32_t magic;
    uint32_t version;
    uint64_t delta_len;
    uint64_t steps;
    uint64_t checksum;
} bio_bdl_header;

uint64_t bio_bdl_file_size(const char *path);

uint64_t bio_bdl_checksum(const int8_t *delta, size_t delta_len) {
    uint64_t x = 1469598103934665603ull;
    if (delta == NULL) {
        return 0u;
    }
    for (size_t i = 0; i < delta_len; ++i) {
        x ^= (uint8_t)delta[i];
        x *= 1099511628211ull;
    }
    return x;
}

bio_v1_status bio_bdl_save(const char *path, const int8_t *delta, size_t delta_len,
                           uint64_t steps) {
    FILE *f = NULL;
    bio_bdl_header h;
    char tmp_path[1024];
    char prev_path[1024];
    if (path == NULL || *path == '\0' || delta == NULL || delta_len == 0u) {
        return BIO_ERR_ARGS;
    }
    snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
    snprintf(prev_path, sizeof(prev_path), "%s.prev", path);
    f = fopen(tmp_path, "wb");
    if (f == NULL) {
        return BIO_ERR_IO;
    }
    h.magic = BIO_BDL_MAGIC;
    h.version = 1u;
    h.delta_len = (uint64_t)delta_len;
    h.steps = steps;
    h.checksum = bio_bdl_checksum(delta, delta_len);
    if (fwrite(&h, sizeof(h), 1u, f) != 1u ||
        fwrite(delta, 1u, delta_len, f) != delta_len) {
        fclose(f);
        remove(tmp_path);
        return BIO_ERR_IO;
    }
    if (fclose(f) != 0) {
        remove(tmp_path);
        return BIO_ERR_IO;
    }
    if (bio_bdl_file_size(path) != 0u) {
        remove(prev_path);
        if (rename(path, prev_path) != 0) {
            remove(tmp_path);
            return BIO_ERR_IO;
        }
    }
    if (rename(tmp_path, path) != 0) {
        if (bio_bdl_file_size(prev_path) != 0u) {
            (void)rename(prev_path, path);
        }
        remove(tmp_path);
        return BIO_ERR_IO;
    }
    return BIO_OK;
}

bio_v1_status bio_bdl_load(const char *path, int8_t *delta, size_t delta_len,
                           uint64_t *steps_out) {
    FILE *f = NULL;
    bio_bdl_header h;
    if (path == NULL || *path == '\0' || delta == NULL || delta_len == 0u) {
        return BIO_ERR_ARGS;
    }
    f = fopen(path, "rb");
    if (f == NULL) {
        return BIO_ERR_IO;
    }
    if (fread(&h, sizeof(h), 1u, f) != 1u || h.magic != BIO_BDL_MAGIC ||
        h.version != 1u || h.delta_len != (uint64_t)delta_len ||
        fread(delta, 1u, delta_len, f) != delta_len ||
        bio_bdl_checksum(delta, delta_len) != h.checksum) {
        fclose(f);
        return BIO_ERR_PARSE;
    }
    fclose(f);
    if (steps_out != NULL) {
        *steps_out = h.steps;
    }
    return BIO_OK;
}

uint64_t bio_bdl_file_size(const char *path) {
#if defined(_WIN32)
    HANDLE file = INVALID_HANDLE_VALUE;
    LARGE_INTEGER size;
    if (path == NULL || *path == '\0') {
        return 0u;
    }
    file = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
                       OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (file == INVALID_HANDLE_VALUE) {
        return 0u;
    }
    if (!GetFileSizeEx(file, &size)) {
        CloseHandle(file);
        return 0u;
    }
    CloseHandle(file);
    return size.QuadPart < 0 ? 0u : (uint64_t)size.QuadPart;
#else
    struct stat st;
    if (path == NULL || *path == '\0' || stat(path, &st) != 0 || st.st_size < 0) {
        return 0u;
    }
    return (uint64_t)st.st_size;
#endif
}

bio_v1_status bio_bdl_append_learn_page(const char *path, const int8_t *delta,
                                        size_t delta_len, uint64_t step,
                                        size_t page_bytes) {
    FILE *f = NULL;
    uint8_t buf[4096];
    size_t written = 0;
    uint64_t x = step ^ 1469598103934665603ull;
    if (path == NULL || *path == '\0' || delta == NULL || delta_len == 0u ||
        page_bytes == 0u) {
        return BIO_ERR_ARGS;
    }
    f = fopen(path, "ab");
    if (f == NULL) {
        return BIO_ERR_IO;
    }
    while (written < page_bytes) {
        const size_t chunk =
            page_bytes - written < sizeof(buf) ? page_bytes - written : sizeof(buf);
        for (size_t i = 0; i < chunk; ++i) {
            x ^= (uint8_t)delta[(written + i + step) % delta_len];
            x *= 1099511628211ull;
            buf[i] = (uint8_t)(x >> 32);
        }
        if (fwrite(buf, 1u, chunk, f) != chunk) {
            fclose(f);
            return BIO_ERR_IO;
        }
        written += chunk;
    }
    fclose(f);
    return BIO_OK;
}

bio_v1_status bio_bdl_prune_pages(const char *path, uint64_t keep_bytes) {
    FILE *src = NULL;
    FILE *dst = NULL;
    char tmp_path[1024];
    uint8_t buf[65536];
    uint64_t size = 0;
    uint64_t remaining = 0;
    if (path == NULL || *path == '\0') {
        return BIO_ERR_ARGS;
    }
    size = bio_bdl_file_size(path);
    if (size <= keep_bytes) {
        return BIO_OK;
    }
    snprintf(tmp_path, sizeof(tmp_path), "%s.prune.tmp", path);
    src = fopen(path, "rb");
    dst = fopen(tmp_path, "wb");
    if (src == NULL || dst == NULL) {
        if (src != NULL) {
            fclose(src);
        }
        if (dst != NULL) {
            fclose(dst);
        }
        return BIO_ERR_IO;
    }
    if (fseek(src, (long)(size - keep_bytes), SEEK_SET) != 0) {
        fclose(src);
        fclose(dst);
        return BIO_ERR_IO;
    }
    remaining = keep_bytes;
    while (remaining != 0u) {
        const size_t want = remaining < sizeof(buf) ? (size_t)remaining : sizeof(buf);
        const size_t got = fread(buf, 1u, want, src);
        if (got == 0u) {
            break;
        }
        if (fwrite(buf, 1u, got, dst) != got) {
            fclose(src);
            fclose(dst);
            return BIO_ERR_IO;
        }
        remaining -= got;
    }
    fclose(src);
    fclose(dst);
    remove(path);
    if (rename(tmp_path, path) != 0) {
        return BIO_ERR_IO;
    }
    return BIO_OK;
}
