/* SPDX-License-Identifier: GPL-3.0-or-later
 *
 * Reads a Tox save file and prints information about it, including DHT nodes,
 * TCP relays, path nodes, and other saved state sections.
 *
 * Build (standalone, no libtoxcore needed):
 *   gcc -o read_savedata read_savedata.c -lm
 *
 * Usage:
 *   ./read_savedata save.tox
 *
 * The save file format is documented in toxcore/state.h and toxcore/DHT.c:
 *
 * Global header (8 bytes):
 *   [0x00000000 (4 bytes LE)] [STATE_COOKIE_GLOBAL = 0x15ed1b1f (4 bytes LE)]
 *
 * Sections (repeated):
 *   [length (4 bytes LE)] [section_type (2 bytes LE)] [cookie_type = 0x01CE (2 bytes LE)]
 *   [data (length bytes)]
 *
 * DHT section (STATE_TYPE_DHT = 2):
 *   [DHT_STATE_COOKIE_GLOBAL = 0x0159000D (4 bytes LE)]
 *   Sub-sections: [length (4 LE)] [cookie_type_hi = 0x11CE (2)] [type_lo (2)]
 *     Type 4 (DHT_STATE_TYPE_NODES): packed Node_format array
 *
 * Packed node format (pack_nodes / unpack_nodes):
 *   [ip.family (1 byte)]
 *   IPv4:  [ip.v4 (4 bytes)] [port (2 bytes BE)]
 *   IPv6:  [ip.v6 (16 bytes)] [port (2 bytes BE)]
 *   [public_key (32 bytes)]
 *
 * TCP relay section (STATE_TYPE_TCP_RELAY = 10):
 *   Packed nodes with tcp_enabled = true
 *
 * Path node section (STATE_TYPE_PATH_NODE = 11):
 *   Packed nodes with tcp_enabled = false
 */
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Constants from the Tox core codebase */

#define STATE_COOKIE_GLOBAL 0x15ed1b1f
#define STATE_COOKIE_TYPE   0x01ce

#define DHT_STATE_COOKIE_GLOBAL 0x0159000d
#define DHT_STATE_COOKIE_TYPE   0x11ce
#define DHT_STATE_TYPE_NODES    4

#define CRYPTO_PUBLIC_KEY_SIZE 32

/* IP family constants */
#define NET_FAMILY_IPV4     2
#define NET_FAMILY_IPV6     10
#define NET_FAMILY_TCP_IPV4 130
#define NET_FAMILY_TCP_IPV6 138
#define NET_FAMILY_UNSPEC   0

/* State types from state.h */
static const char *state_type_name(uint16_t type)
{
    switch (type) {
        case 1:  return "NOSPAMKEYS";
        case 2:  return "DHT";
        case 3:  return "FRIENDS";
        case 4:  return "NAME";
        case 5:  return "STATUSMESSAGE";
        case 6:  return "STATUS";
        case 7:  return "GROUPS";
        case 10: return "TCP_RELAY";
        case 11: return "PATH_NODE";
        case 20: return "CONFERENCES";
        case 255: return "END";
        default: return "UNKNOWN";
    }
}

/* Read a little-endian uint16 from memory */
static uint16_t read_le16(const uint8_t *data)
{
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}

/* Read a little-endian uint32 from memory */
static uint32_t read_le32(const uint8_t *data)
{
    return (uint32_t)data[0]
           | ((uint32_t)data[1] << 8)
           | ((uint32_t)data[2] << 16)
           | ((uint32_t)data[3] << 24);
}

/* Read a big-endian uint16 from memory (network byte order) */
static uint16_t read_be16(const uint8_t *data)
{
    return (uint16_t)data[1] | ((uint16_t)data[0] << 8);
}

/* Convert IP family to human-readable string */
static const char *family_name(uint8_t family)
{
    switch (family) {
        case NET_FAMILY_IPV4:     return "IPv4";
        case NET_FAMILY_IPV6:     return "IPv6";
        case NET_FAMILY_TCP_IPV4: return "TCP_IPv4";
        case NET_FAMILY_TCP_IPV6: return "TCP_IPv6";
        default:                  return "UNKNOWN";
    }
}

/* Print a 32-byte public key in hex */
static void print_pubkey(const uint8_t *key)
{
    for (int i = 0; i < CRYPTO_PUBLIC_KEY_SIZE; i++) {
        printf("%02X", key[i]);
    }
}

/* Unpack and print a single node from data.
 * Returns the number of bytes consumed, or -1 on error. */
static int unpack_and_print_node(const uint8_t *data, uint16_t length, bool tcp_enabled, int node_num)
{
    if (length < 1) {
        return -1;
    }

    uint8_t family = data[0];

    /* Validate family */
    bool valid = false;

    if (tcp_enabled) {
        valid = (family == NET_FAMILY_IPV4 || family == NET_FAMILY_IPV6
                 || family == NET_FAMILY_TCP_IPV4 || family == NET_FAMILY_TCP_IPV6);
    } else {
        valid = (family == NET_FAMILY_IPV4 || family == NET_FAMILY_IPV6);
    }

    if (!valid) {
        return -1;
    }

    int ip_size;
    if (family == NET_FAMILY_IPV4 || family == NET_FAMILY_TCP_IPV4) {
        ip_size = 4;
    } else if (family == NET_FAMILY_IPV6 || family == NET_FAMILY_TCP_IPV6) {
        ip_size = 16;
    } else {
        return -1;
    }

    int expected_size = 1 + ip_size + 2 + CRYPTO_PUBLIC_KEY_SIZE;

    if (length < (uint16_t)expected_size) {
        return -1;
    }

    const uint8_t *ip_data = data + 1;
    uint16_t port = read_be16(data + 1 + ip_size);
    const uint8_t *pubkey = data + 1 + ip_size + 2;

    printf("      Node %d: %s ", node_num, family_name(family));

    /* Print IP address */
    if (ip_size == 4) {
        printf("%d.%d.%d.%d",
               ip_data[0], ip_data[1], ip_data[2], ip_data[3]);
    } else {
        /* IPv6 */
        for (int i = 0; i < 16; i += 2) {
            if (i > 0) printf(":");
            printf("%02x%02x", ip_data[i], ip_data[i + 1]);
        }
    }

    printf(":%u ", port);
    printf("pubkey=");
    print_pubkey(pubkey);
    printf("\n");

    return expected_size;
}

/* Unpack and print an array of packed nodes */
static void unpack_nodes_array(const uint8_t *data, uint16_t length, bool tcp_enabled, const char *label)
{
    int offset = 0;
    int node_num = 0;

    while (offset < length) {
        int consumed = unpack_and_print_node(data + offset, length - offset, tcp_enabled, node_num);

        if (consumed < 0) {
            printf("      [error parsing node at offset %d]\n", offset);
            break;
        }

        offset += consumed;
        node_num++;
    }

    printf("    Total %s: %d node(s)\n", label, node_num);
}

/* Parse a DHT section */
static void parse_dht_section(const uint8_t *data, uint32_t length)
{
    printf("  [DHT Section]\n");

    if (length < 4) {
        printf("    (too short: %u bytes)\n", length);
        return;
    }

    uint32_t cookie = read_le32(data);
    printf("    DHT cookie: 0x%08x (expected 0x%08x)\n", cookie, DHT_STATE_COOKIE_GLOBAL);

    if (cookie != DHT_STATE_COOKIE_GLOBAL) {
        printf("    (invalid DHT cookie - skipping)\n");
        return;
    }

    uint32_t offset = 4;

    while (offset + 8 <= length) {
        uint32_t sub_len = read_le32(data + offset);
        uint32_t type_field = read_le32(data + offset + 4);
        uint16_t cookie_type_hi = (uint16_t)(type_field >> 16);
        uint16_t sub_type = (uint16_t)(type_field & 0xFFFF);

        printf("    Sub-section: length=%u, cookie=0x%04x, type=%u",
               sub_len, cookie_type_hi, sub_type);

        offset += 8;

        if (offset + sub_len > length) {
            printf(" (extends beyond section!)\n");
            break;
        }

        if (sub_type == DHT_STATE_TYPE_NODES && sub_len > 0) {
            printf(" [DHT NODES]\n");
            unpack_nodes_array(data + offset, sub_len, false, "DHT nodes");
        } else if (sub_type == DHT_STATE_TYPE_NODES) {
            printf(" (empty)\n");
        } else {
            printf(" (unknown, %u bytes of data)\n", sub_len);
        }

        offset += sub_len;
    }
}

/* Parse TCP relay section */
static void parse_tcp_relay_section(const uint8_t *data, uint32_t length)
{
    printf("  [TCP_RELAY Section]\n");

    if (length > 0) {
        unpack_nodes_array(data, length, true, "TCP relays");
    } else {
        printf("    (empty)\n");
    }
}

/* Parse path node section */
static void parse_path_node_section(const uint8_t *data, uint32_t length)
{
    printf("  [PATH_NODE Section]\n");

    if (length > 0) {
        unpack_nodes_array(data, length, false, "path nodes");
    } else {
        printf("    (empty)\n");
    }
}

/* Parse friend list section */
static void parse_friends_section(const uint8_t *data, uint32_t length)
{
    printf("  [FRIENDS Section] (%u bytes)\n", length);

    /* Friend format (from Messenger.c friend_size()):
     * This layout matches toxcore v0.2.x and may change in future versions.
     *   status (1)
     *   real_pk (32)
     *   info (1024)
     *   padding (1)
     *   info_size (2) - network byte order
     *   name (128)
     *   name_length (2) - network byte order
     *   statusmessage (1007)
     *   padding (1)
     *   statusmessage_length (2) - network byte order
     *   userstatus (1)
     *   padding (3)
     *   friendrequest_nospam (4)
     *   last_seen_time (8)
     * Total = 1+32+1024+1+2+128+2+1007+1+2+1+3+4+8 = 2216
     */
    const uint32_t friend_size = 2216;

    if (length == 0 || length % friend_size != 0) {
        printf("    (unexpected size - not a multiple of %u; length=%u)\n", friend_size, length);
        printf("    Raw hex dump of first 64 bytes:\n      ");
        for (uint32_t i = 0; i < 64 && i < length; i++) {
            printf("%02x ", data[i]);
        }
        printf("\n");
        return;
    }

    uint32_t num_friends = length / friend_size;
    printf("    Number of friends: %u\n", num_friends);

    for (uint32_t i = 0; i < num_friends; i++) {
        const uint8_t *f = data + i * friend_size;
        uint8_t status = f[0];
        const uint8_t *pk = f + 1;
        /* These fields are stored in network byte order (big-endian), see
         * net_htons() calls in Messenger.c friend_save(). */
        uint16_t info_size_be = read_be16(f + 1 + 32 + 1024 + 1);

        printf("    Friend %u: status=%u pk=", i, status);
        print_pubkey(pk);
        printf(" info_size=%u", info_size_be);

        if (status >= 3) {
            uint16_t name_len_be = read_be16(f + 1 + 32 + 1024 + 1 + 2 + 128);
            uint16_t name_len = name_len_be;
            if (name_len > 128) name_len = 128;
            printf(" name=\"%.*s\"", name_len, f + 1 + 32 + 1024 + 1 + 2);
        }

        printf("\n");
    }
}

/* Parse nospam+keys section */
static void parse_nospamkeys_section(const uint8_t *data, uint32_t length)
{
    printf("  [NOSPAMKEYS Section] (%u bytes)\n", length);

    if (length < 4 + CRYPTO_PUBLIC_KEY_SIZE + 32) {
        printf("    (too short)\n");
        return;
    }

    uint32_t nospam = read_le32(data);
    printf("    nospam: 0x%08x (%u)\n", nospam, nospam);
    printf("    public_key: ");
    print_pubkey(data + 4);
    printf("\n");
    printf("    secret_key: <hidden>\n");
}

/* Parse name section */
static void parse_name_section(const uint8_t *data, uint32_t length)
{
    printf("  [NAME Section] (%u bytes): \"", length);

    for (uint32_t i = 0; i < length && i < 128; i++) {
        if (data[i] >= 32 && data[i] < 127) {
            putchar(data[i]);
        } else {
            printf("\\x%02x", data[i]);
        }
    }

    printf("\"\n");
}

/* Parse status message section */
static void parse_statusmessage_section(const uint8_t *data, uint32_t length)
{
    printf("  [STATUSMESSAGE Section] (%u bytes): \"", length);

    for (uint32_t i = 0; i < length && i < 200; i++) {
        if (data[i] >= 32 && data[i] < 127) {
            putchar(data[i]);
        } else {
            printf("\\x%02x", data[i]);
        }
    }

    printf("\"\n");
}

/* Parse status section */
static void parse_status_section(const uint8_t *data, uint32_t length)
{
    printf("  [STATUS Section] (%u bytes): ", length);

    if (length >= 1) {
        switch (data[0]) {
            case 0: printf("online"); break;
            case 1: printf("away"); break;
            case 2: printf("busy"); break;
            default: printf("unknown (%u)", data[0]); break;
        }
    }

    printf("\n");
}

int main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <tox_save_file>\n", argv[0]);
        fprintf(stderr, "\nReads a Tox save file and prints information about its contents.\n");
        return 1;
    }

    const char *filename = argv[1];
    FILE *fp = fopen(filename, "rb");

    if (fp == NULL) {
        fprintf(stderr, "Error: Could not open '%s'\n", filename);
        return 1;
    }

    /* Get file size */
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    if (file_size < 8) {
        fprintf(stderr, "Error: File too small to be a valid Tox save file\n");
        fclose(fp);
        return 1;
    }

    uint8_t *buffer = (uint8_t *)malloc(file_size);

    if (buffer == NULL) {
        fprintf(stderr, "Error: Memory allocation failed\n");
        fclose(fp);
        return 1;
    }

    size_t bytes_read = fread(buffer, 1, file_size, fp);
    fclose(fp);

    if ((long)bytes_read != file_size) {
        fprintf(stderr, "Error: Could not read entire file\n");
        free(buffer);
        return 1;
    }

    printf("=== Tox Save File: %s ===\n", filename);
    printf("File size: %ld bytes\n\n", file_size);

    /* Parse global header */
    uint32_t magic1 = read_le32(buffer);
    uint32_t magic2 = read_le32(buffer + 4);

    printf("Global header: [0x%08x] [0x%08x]\n", magic1, magic2);

    /* Check for ToxEncrypt magic first (before header validation, since
     * encrypted files have completely different magic bytes). */
    const uint8_t enc_magic_prefix[] = {0x01, 0x1E, 0x81, 0xC0, 0xD6, 0x7B, 0xBA, 0x9E};

    if (memcmp(buffer, enc_magic_prefix, 8) == 0) {
        printf("\n!!! This file appears to be ENCRYPTED (ToxEncrypt format) !!!\n");
        printf("!!! Decrypt it first with toxencryptsave before parsing. !!!\n\n");
        free(buffer);
        return 1;
    }

    if (magic1 != 0 || magic2 != STATE_COOKIE_GLOBAL) {
        fprintf(stderr, "Error: Invalid Tox save file magic. Expected [0x00000000] [0x%08x]\n",
                STATE_COOKIE_GLOBAL);
        free(buffer);
        return 1;
    }

    uint32_t offset = 8;
    int section_count = 0;

    while (offset + 8 <= (uint32_t)file_size) {
        uint32_t section_length = read_le32(buffer + offset);
        uint32_t type_field = read_le32(buffer + offset + 4);
        uint16_t section_type = (uint16_t)type_field;
        uint16_t cookie_type = (uint16_t)(type_field >> 16);

        offset += 8;

        if (offset + section_length > (uint32_t)file_size) {
            fprintf(stderr, "Error: Section %d extends beyond file (offset=%u, length=%u, file_size=%ld)\n",
                    section_count, offset, section_length, file_size);
            break;
        }

        printf("\nSection %d: type=%u (%s), cookie=0x%04x, length=%u\n",
               section_count, section_type, state_type_name(section_type),
               cookie_type, section_length);

        if (cookie_type != STATE_COOKIE_TYPE) {
            printf("  (unexpected cookie type - expected 0x%04x)\n", STATE_COOKIE_TYPE);
        }

        const uint8_t *section_data = buffer + offset;

        switch (section_type) {
            case 1:  /* STATE_TYPE_NOSPAMKEYS */
                parse_nospamkeys_section(section_data, section_length);
                break;

            case 2:  /* STATE_TYPE_DHT */
                parse_dht_section(section_data, section_length);
                break;

            case 3:  /* STATE_TYPE_FRIENDS */
                parse_friends_section(section_data, section_length);
                break;

            case 4:  /* STATE_TYPE_NAME */
                parse_name_section(section_data, section_length);
                break;

            case 5:  /* STATE_TYPE_STATUSMESSAGE */
                parse_statusmessage_section(section_data, section_length);
                break;

            case 6:  /* STATE_TYPE_STATUS */
                parse_status_section(section_data, section_length);
                break;

            case 7:  /* STATE_TYPE_GROUPS */
                printf("  (msgpack-encoded binary data, %u bytes)\n", section_length);
                break;

            case 10: /* STATE_TYPE_TCP_RELAY */
                parse_tcp_relay_section(section_data, section_length);
                break;

            case 11: /* STATE_TYPE_PATH_NODE */
                parse_path_node_section(section_data, section_length);
                break;

            case 20: /* STATE_TYPE_CONFERENCES */
                printf("  (conference data, %u bytes)\n", section_length);
                break;

            case 255: /* STATE_TYPE_END */
                printf("  (end marker)\n");
                /* END section must be the last one; stop reading. */
                offset += section_length;
                section_count++;
                goto end_of_file;

            default:
                printf("  (%u unknown bytes)\n", section_length);
                break;
        }

        offset += section_length;
        section_count++;
    }


end_of_file:
    printf("\n=== End of save file ===\n");
    printf("Total sections: %d\n", section_count);

    free(buffer);
    return 0;
}
