/*
 * libotppad.c — implementation of libotppad.h.
 *
 * Extracted from the otp project (src/crypto.c, src/padding.c, src/pads.c)
 * and made self-contained: no main.h, no global state, no UI.
 */
#define _POSIX_C_SOURCE 200809L

#include "libotppad.h"

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

/* ------------------------------------------------------------------ */
/* XOR transform                                                      */
/* ------------------------------------------------------------------ */

int otppad_xor(const unsigned char *data, size_t data_len,
               const unsigned char *pad_data, unsigned char *result) {
    if (!data || !pad_data || !result) {
        return 1;
    }
    for (size_t i = 0; i < data_len; i++) {
        result[i] = data[i] ^ pad_data[i];
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/* Base64                                                             */
/* ------------------------------------------------------------------ */

static const char b64_chars[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static const int b64_decode_table[256] = {
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
    52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-2,-1,-1,
    -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
    15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
    -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
    41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1
};

char *otppad_base64_encode(const unsigned char *input, int length) {
    if (!input || length < 0) return NULL;
    int output_length = 4 * ((length + 2) / 3);
    char *encoded = (char *)malloc((size_t)output_length + 1);
    if (!encoded) return NULL;

    int i, j;
    for (i = 0, j = 0; i < length;) {
        uint32_t octet_a = i < length ? input[i++] : 0;
        uint32_t octet_b = i < length ? input[i++] : 0;
        uint32_t octet_c = i < length ? input[i++] : 0;
        uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c;
        encoded[j++] = b64_chars[(triple >> 18) & 63];
        encoded[j++] = b64_chars[(triple >> 12) & 63];
        encoded[j++] = b64_chars[(triple >> 6) & 63];
        encoded[j++] = b64_chars[triple & 63];
    }
    for (int pad = 0; pad < (3 - length % 3) % 3; pad++) {
        encoded[output_length - 1 - pad] = '=';
    }
    encoded[output_length] = '\0';
    return encoded;
}

unsigned char *otppad_base64_decode(const char *input, int *output_length) {
    if (!input || !output_length) return NULL;
    int input_length = (int)strlen(input);
    if (input_length % 4 != 0) return NULL;

    *output_length = input_length / 4 * 3;
    if (input[input_length - 1] == '=') (*output_length)--;
    if (input[input_length - 2] == '=') (*output_length)--;

    unsigned char *decoded = (unsigned char *)malloc((size_t)*output_length);
    if (!decoded) return NULL;

    int i, j;
    for (i = 0, j = 0; i < input_length;) {
        int sa = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
        int sb = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
        int sc = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
        int sd = input[i] == '=' ? 0 & i++ : b64_decode_table[(unsigned char)input[i++]];
        if (sa == -1 || sb == -1 || sc == -1 || sd == -1) {
            free(decoded);
            return NULL;
        }
        uint32_t triple = ((uint32_t)sa << 18) + ((uint32_t)sb << 12) +
                          ((uint32_t)sc << 6) + (uint32_t)sd;
        if (j < *output_length) decoded[j++] = (triple >> 16) & 255;
        if (j < *output_length) decoded[j++] = (triple >> 8) & 255;
        if (j < *output_length) decoded[j++] = triple & 255;
    }
    return decoded;
}

/* ------------------------------------------------------------------ */
/* Padmé padding                                                      */
/* ------------------------------------------------------------------ */

size_t otppad_chunk_size(size_t msg_len) {
    size_t chunk = 256;
    while (chunk < msg_len + 1) {
        chunk *= 2;
    }
    return chunk;
}

int otppad_pad_apply(unsigned char *buffer, size_t msg_len, size_t chunk_size) {
    if (!buffer) return 1;
    if (chunk_size < msg_len + 1) return 2;
    buffer[msg_len] = 0x80;
    if (chunk_size > msg_len + 1) {
        memset(buffer + msg_len + 1, 0x00, chunk_size - msg_len - 1);
    }
    return 0;
}

int otppad_pad_remove(const unsigned char *buffer, size_t chunk_size,
                      size_t *msg_len) {
    if (!buffer || !msg_len) return 1;
    if (chunk_size == 0) return 2;
    for (int i = (int)chunk_size - 1; i >= 0; i--) {
        if (buffer[i] == 0x80) {
            *msg_len = (size_t)i;
            return 0;
        } else if (buffer[i] != 0x00) {
            return 3;
        }
    }
    return 4;
}

void otppad_chunk_format(size_t chunk_size, char *buffer, size_t buffer_size) {
    if (!buffer || buffer_size == 0) return;
    if (chunk_size < 1024) {
        snprintf(buffer, buffer_size, "%zu bytes", chunk_size);
    } else if (chunk_size < 1024 * 1024) {
        snprintf(buffer, buffer_size, "%.1f KB", chunk_size / 1024.0);
    } else if (chunk_size < 1024 * 1024 * 1024) {
        snprintf(buffer, buffer_size, "%.1f MB", chunk_size / (1024.0 * 1024.0));
    } else {
        snprintf(buffer, buffer_size, "%.1f GB",
                 chunk_size / (1024.0 * 1024.0 * 1024.0));
    }
}

/* ------------------------------------------------------------------ */
/* ASCII armored message format                                       */
/* ------------------------------------------------------------------ */

int otppad_armor_parse(const char *message, char *chksum, uint64_t *offset,
                       char *base64_data, size_t base64_buf_size) {
    if (!message || !chksum || !offset || !base64_data || base64_buf_size == 0) {
        return 1;
    }

    size_t msg_len = strlen(message);
    char *copy = (char *)malloc(msg_len + 1);
    if (!copy) return 1;
    strcpy(copy, message);

    char *line = strtok(copy, "\n");
    int found_begin = 0, in_data = 0, found_chksum = 0, found_offset = 0;
    chksum[0] = '\0';
    *offset = 0;
    base64_data[0] = '\0';

    while (line != NULL) {
        if (strcmp(line, OTPPAD_ARMOR_BEGIN) == 0) {
            found_begin = 1;
        } else if (strcmp(line, OTPPAD_ARMOR_END) == 0) {
            break;
        } else if (found_begin) {
            if (strncmp(line, "Pad-ChkSum: ", 12) == 0) {
                strncpy(chksum, line + 12, OTPPAD_CHKSUM_HEX_LEN);
                chksum[OTPPAD_CHKSUM_HEX_LEN] = '\0';
                found_chksum = 1;
            } else if (strncmp(line, "Pad-Offset: ", 12) == 0) {
                *offset = strtoull(line + 12, NULL, 10);
                found_offset = 1;
            } else if (strlen(line) == 0) {
                in_data = 1;
            } else if (in_data) {
                strncat(base64_data, line, base64_buf_size - strlen(base64_data) - 1);
            } else if (strncmp(line, "Version:", 8) != 0 &&
                       strncmp(line, "Pad-", 4) != 0) {
                strncat(base64_data, line, base64_buf_size - strlen(base64_data) - 1);
            }
        }
        line = strtok(NULL, "\n");
    }

    free(copy);
    if (!found_begin || !found_chksum || !found_offset) {
        return 2;
    }
    return 0;
}

int otppad_armor_generate(const char *version, const char *chksum,
                          uint64_t offset,
                          const unsigned char *encrypted_data, size_t data_length,
                          char **ascii_output) {
    if (!chksum || !encrypted_data || !ascii_output) return 1;

    char *b64 = otppad_base64_encode(encrypted_data, (int)data_length);
    if (!b64) return 2;

    size_t b64_len = strlen(b64);
    size_t total = 256 + b64_len + (b64_len / 64) + 64;
    *ascii_output = (char *)malloc(total);
    if (!*ascii_output) {
        free(b64);
        return 3;
    }

    char line[256];
    strcpy(*ascii_output, OTPPAD_ARMOR_BEGIN);
    strcat(*ascii_output, "\n");

    snprintf(line, sizeof(line), "Version: %s\n", version ? version : "v0");
    strcat(*ascii_output, line);

    snprintf(line, sizeof(line), "Pad-ChkSum: %s\n", chksum);
    strcat(*ascii_output, line);

    snprintf(line, sizeof(line), "Pad-Offset: %llu\n",
             (unsigned long long)offset);
    strcat(*ascii_output, line);

    strcat(*ascii_output, "\n");

    int b64_len_int = (int)b64_len;
    for (int i = 0; i < b64_len_int; i += 64) {
        snprintf(line, sizeof(line), "%.64s\n", b64 + i);
        strcat(*ascii_output, line);
    }

    strcat(*ascii_output, OTPPAD_ARMOR_END);
    strcat(*ascii_output, "\n");

    free(b64);
    return 0;
}

/* ------------------------------------------------------------------ */
/* Binary .otp file format                                            */
/* ------------------------------------------------------------------ */

int otppad_bin_header_write(FILE *fp, const otppad_bin_header_t *hdr) {
    if (!fp || !hdr) return 1;
    if (fwrite(OTPPAD_MAGIC, 1, OTPPAD_MAGIC_LEN, fp) != OTPPAD_MAGIC_LEN) return 2;
    if (fwrite(&hdr->version, sizeof(uint16_t), 1, fp) != 1) return 3;
    if (fwrite(hdr->pad_chksum, 1, OTPPAD_CHKSUM_BIN_LEN, fp) != OTPPAD_CHKSUM_BIN_LEN) return 4;
    if (fwrite(&hdr->pad_offset, sizeof(uint64_t), 1, fp) != 1) return 5;
    if (fwrite(&hdr->file_mode, sizeof(uint32_t), 1, fp) != 1) return 6;
    if (fwrite(&hdr->file_size, sizeof(uint64_t), 1, fp) != 1) return 7;
    return 0;
}

int otppad_bin_header_read(FILE *fp, otppad_bin_header_t *hdr) {
    if (!fp || !hdr) return 1;
    memset(hdr, 0, sizeof(*hdr));
    if (fread(hdr->magic, 1, OTPPAD_MAGIC_LEN, fp) != OTPPAD_MAGIC_LEN) return 2;
    if (fread(&hdr->version, sizeof(uint16_t), 1, fp) != 1) return 3;
    if (fread(hdr->pad_chksum, 1, OTPPAD_CHKSUM_BIN_LEN, fp) != OTPPAD_CHKSUM_BIN_LEN) return 4;
    if (fread(&hdr->pad_offset, sizeof(uint64_t), 1, fp) != 1) return 5;
    if (fread(&hdr->file_mode, sizeof(uint32_t), 1, fp) != 1) return 6;
    if (fread(&hdr->file_size, sizeof(uint64_t), 1, fp) != 1) return 7;
    return 0;
}

int otppad_bin_is_magic(const unsigned char *buf, size_t len) {
    if (!buf || len < OTPPAD_MAGIC_LEN) return 0;
    return memcmp(buf, OTPPAD_MAGIC, OTPPAD_MAGIC_LEN) == 0;
}

/* ------------------------------------------------------------------ */
/* Per-pad .state file                                                */
/* ------------------------------------------------------------------ */

int otppad_state_read(const char *pads_dir, const char *chksum, uint64_t *offset) {
    if (!pads_dir || !chksum || !offset) return 1;
    char path[1024];
    snprintf(path, sizeof(path), "%s/%s.state", pads_dir, chksum);

    FILE *f = fopen(path, "r");
    if (!f) return 2;

    char line[128];
    if (!fgets(line, sizeof(line), f)) {
        fclose(f);
        return 3;
    }
    fclose(f);

    if (strncmp(line, "offset=", 7) != 0) {
        return 4;
    }
    *offset = strtoull(line + 7, NULL, 10);
    return 0;
}

int otppad_state_write(const char *pads_dir, const char *chksum, uint64_t offset) {
    if (!pads_dir || !chksum) return 1;
    char path[1024];
    char tmp[1100];
    snprintf(path, sizeof(path), "%s/%s.state", pads_dir, chksum);
    snprintf(tmp, sizeof(tmp), "%s/%s.state.tmp.XXXXXX", pads_dir, chksum);

    int tfd = mkstemp(tmp);
    if (tfd < 0) return 2;
    FILE *f = fdopen(tfd, "w");
    if (!f) {
        close(tfd);
        unlink(tmp);
        return 3;
    }
    if (fprintf(f, "offset=%llu\n", (unsigned long long)offset) < 0) {
        fclose(f);
        unlink(tmp);
        return 4;
    }
    if (fclose(f) != 0) {
        unlink(tmp);
        return 5;
    }
    if (rename(tmp, path) != 0) {
        unlink(tmp);
        return 6;
    }
    return 0;
}

/* ------------------------------------------------------------------ */
/* Pad checksum                                                       */
/* ------------------------------------------------------------------ */

int otppad_checksum(const char *pad_path, char *checksum_hex) {
    if (!pad_path || !checksum_hex) return 1;

    FILE *file = fopen(pad_path, "rb");
    if (!file) return 2;

    unsigned char checksum[OTPPAD_CHKSUM_BIN_LEN];
    unsigned char buffer[64 * 1024];
    size_t bytes_read;
    size_t total_bytes = 0;
    memset(checksum, 0, OTPPAD_CHKSUM_BIN_LEN);

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        for (size_t i = 0; i < bytes_read; i++) {
            size_t pos = total_bytes + i;
            unsigned char bucket = (unsigned char)(pos % OTPPAD_CHKSUM_BIN_LEN);
            checksum[bucket] ^= (unsigned char)buffer[i] ^
                                (unsigned char)((pos >> 8) & 0xFF) ^
                                (unsigned char)((pos >> 16) & 0xFF) ^
                                (unsigned char)((pos >> 24) & 0xFF);
        }
        total_bytes += bytes_read;
    }
    fclose(file);

    file = fopen(pad_path, "rb");
    if (!file) return 3;
    unsigned char pad_key[OTPPAD_CHKSUM_BIN_LEN];
    if (fread(pad_key, 1, OTPPAD_CHKSUM_BIN_LEN, file) != OTPPAD_CHKSUM_BIN_LEN) {
        fclose(file);
        return 4;
    }
    fclose(file);

    unsigned char enc[OTPPAD_CHKSUM_BIN_LEN];
    for (int i = 0; i < OTPPAD_CHKSUM_BIN_LEN; i++) {
        enc[i] = checksum[i] ^ pad_key[i];
    }
    for (int i = 0; i < OTPPAD_CHKSUM_BIN_LEN; i++) {
        sprintf(checksum_hex + (i * 2), "%02x", enc[i]);
    }
    checksum_hex[OTPPAD_CHKSUM_HEX_LEN] = '\0';
    return 0;
}
