#include "server.h"
#include "client.h"

#include <crypto/x509.h>
#include <crypto/rsa.h>
#include <exception.h>
#include <eventapi.h>

#include <cstring>
#include <cstdio>
#include <fstream>
#include <filesystem>
#include <algorithm>
#include <thread>
#include <chrono>
#include <set>

#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>

static inline int pp_open(const char* path, int flags, int mode) {
    return ::_open(path, flags | _O_BINARY, mode);
}
static inline int pp_close(int fd) { return ::_close(fd); }
static inline off_t pp_lseek(int fd, off_t off, int whence) {
    return ::_lseek(fd, off, whence);
}
static inline ssize_t pp_pread(int fd, void* buf, size_t count, off_t offset) {
    off_t prev = ::_lseek(fd, 0, SEEK_CUR);
    if (prev < 0) return -1;
    if (::_lseek(fd, offset, SEEK_SET) < 0) return -1;
    int n = ::_read(fd, buf, static_cast<unsigned int>(count));
    ::_lseek(fd, prev, SEEK_SET);
    return n;
}
static inline ssize_t pp_pwrite(int fd, const void* buf, size_t count, off_t offset) {
    off_t prev = ::_lseek(fd, 0, SEEK_CUR);
    if (prev < 0) return -1;
    if (::_lseek(fd, offset, SEEK_SET) < 0) return -1;
    int n = ::_write(fd, buf, static_cast<unsigned int>(count));
    ::_lseek(fd, prev, SEEK_SET);
    return n;
}
static inline int pp_unlink(const char* path) { return ::_unlink(path); }
#else
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

static inline int pp_open(const char* path, int flags, int mode) {
    return ::open(path, flags, mode);
}
static inline int pp_close(int fd) { return ::close(fd); }
static inline off_t pp_lseek(int fd, off_t off, int whence) {
    return ::lseek(fd, off, whence);
}
static inline ssize_t pp_pread(int fd, void* buf, size_t count, off_t offset) {
    return ::pread(fd, buf, count, offset);
}
static inline ssize_t pp_pwrite(int fd, const void* buf, size_t count, off_t offset) {
    return ::pwrite(fd, buf, count, offset);
}
static inline int pp_unlink(const char* path) { return ::unlink(path); }
static inline int pp_fdatasync(int fd) {
#ifdef __APPLE__
    return ::fcntl(fd, F_FULLFSYNC);
#else
    return ::fdatasync(fd);
#endif
}
#endif

namespace paritypp {

// --- memory_block_store ---

memory_block_store::memory_block_store(size_t max_bytes)
    : max_bytes_(max_bytes) {}

size_t memory_block_store::group_bytes(uint64_t group_id) const {
    auto git = blocks_.find(group_id);
    if (git == blocks_.end()) return 0;
    size_t total = 0;
    for (const auto& kv : git->second)
        total += kv.second.size();
    return total;
}

void memory_block_store::touch(uint64_t group_id) {
    auto it = lru_map_.find(group_id);
    if (it != lru_map_.end()) {
        lru_.splice(lru_.begin(), lru_, it->second);
    } else {
        lru_.push_front(group_id);
        lru_map_[group_id] = lru_.begin();
    }
}

void memory_block_store::evict_lru() {
    while (!lru_.empty() && max_bytes_ > 0 && used_bytes_ > max_bytes_) {
        uint64_t victim = lru_.back();
        used_bytes_ -= group_bytes(victim);
        blocks_.erase(victim);
        lru_map_.erase(victim);
        lru_.pop_back();
    }
}

size_t memory_block_store::used_bytes() const {
    // Note: not locked — caller should hold mutex_ or accept a racy read
    return used_bytes_;
}

bool memory_block_store::store(uint64_t group_id, uint32_t block_index,
                                const uint8_t* data, size_t len) {
    std::lock_guard<std::mutex> lock(mutex_);

    // Remove old block size from accounting (if replacing)
    auto git = blocks_.find(group_id);
    if (git != blocks_.end()) {
        auto bit = git->second.find(block_index);
        if (bit != git->second.end())
            used_bytes_ -= bit->second.size();
    }

    blocks_[group_id][block_index].assign(data, data + len);
    used_bytes_ += len;
    touch(group_id);

    if (max_bytes_ > 0)
        evict_lru();
    return true;
}

bool memory_block_store::fetch(uint64_t group_id, uint32_t block_index,
                                std::vector<uint8_t>& out) {
    std::lock_guard<std::mutex> lock(mutex_);
    auto git = blocks_.find(group_id);
    if (git == blocks_.end()) return false;
    auto bit = git->second.find(block_index);
    if (bit == git->second.end()) return false;
    out = bit->second;
    touch(group_id);
    return true;
}

bool memory_block_store::remove_group(uint64_t group_id) {
    std::lock_guard<std::mutex> lock(mutex_);
    used_bytes_ -= group_bytes(group_id);
    auto it = lru_map_.find(group_id);
    if (it != lru_map_.end()) {
        lru_.erase(it->second);
        lru_map_.erase(it);
    }
    return blocks_.erase(group_id) > 0;
}

std::vector<uint32_t> memory_block_store::list_blocks(uint64_t group_id) {
    std::lock_guard<std::mutex> lock(mutex_);
    std::vector<uint32_t> result;
    auto git = blocks_.find(group_id);
    if (git != blocks_.end()) {
        for (const auto& kv : git->second) {
            result.push_back(kv.first);
        }
    }
    return result;
}

uint32_t memory_block_store::total_blocks() {
    std::lock_guard<std::mutex> lock(mutex_);
    uint32_t count = 0;
    for (const auto& g : blocks_) {
        count += static_cast<uint32_t>(g.second.size());
    }
    return count;
}

uint32_t memory_block_store::total_groups() {
    std::lock_guard<std::mutex> lock(mutex_);
    return static_cast<uint32_t>(blocks_.size());
}

std::vector<uint64_t> memory_block_store::list_groups() {
    std::lock_guard<std::mutex> lock(mutex_);
    std::vector<uint64_t> result;
    result.reserve(blocks_.size());
    for (const auto& kv : blocks_) {
        result.push_back(kv.first);
    }
    return result;
}

// --- file_block_store (append-only binary file with write-hole protection) ---

// CRC32 (IEEE 802.3 polynomial 0xEDB88320, reflected) — table-driven
static constexpr uint32_t crc32_make_entry(uint32_t idx) {
    uint32_t c = idx;
    for (int j = 0; j < 8; ++j)
        c = (c >> 1) ^ (0xEDB88320 & (-(c & 1)));
    return c;
}

static constexpr struct Crc32Table {
    uint32_t t[256]{};
    constexpr Crc32Table() {
        for (uint32_t i = 0; i < 256; ++i)
            t[i] = crc32_make_entry(i);
    }
} crc32_table;

// ─── PCLMULQDQ-accelerated CRC32 (runtime dispatch) ───
#if (defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86))
#include <immintrin.h>

#if defined(__GNUC__) || defined(__clang__)
__attribute__((target("pclmul,sse4.1")))
#endif
static uint32_t crc32_pclmul(const uint8_t* data, size_t len) {
    uint32_t crc = 0xFFFFFFFF;

    // Process bytes until 16-byte aligned or if len < 64
    while (len > 0 && ((uintptr_t)data & 15)) {
        crc = (crc >> 8) ^ crc32_table.t[(crc ^ *data++) & 0xFF];
        --len;
    }

    if (len >= 64) {
        // CRC32 fold constants for IEEE polynomial (reflected)
        // k1 = x^(512+64) mod P << 1, k2 = x^(512) mod P << 1
        const __m128i k1k2 = _mm_set_epi64x(0x1c6e41596ULL, 0x154442bd4ULL);
        // k3 = x^(128+64) mod P << 1, k4 = x^(128) mod P << 1
        const __m128i k3k4 = _mm_set_epi64x(0x0ccaa009eULL, 0x1751997d0ULL);
        const __m128i k5   = _mm_set_epi64x(0ULL, 0x163cd6124ULL);
        const __m128i poly = _mm_set_epi64x(0x1f7011641ULL, 0x1db710641ULL);

        // XOR CRC into first 16 bytes
        __m128i x0 = _mm_xor_si128(_mm_loadu_si128((const __m128i*)data),
                                    _mm_cvtsi32_si128((int)crc));
        __m128i x1 = _mm_loadu_si128((const __m128i*)(data + 16));
        __m128i x2 = _mm_loadu_si128((const __m128i*)(data + 32));
        __m128i x3 = _mm_loadu_si128((const __m128i*)(data + 48));
        data += 64; len -= 64;

        // Fold 64 bytes at a time
        while (len >= 64) {
            __m128i d0 = _mm_loadu_si128((const __m128i*)data);
            __m128i d1 = _mm_loadu_si128((const __m128i*)(data + 16));
            __m128i d2 = _mm_loadu_si128((const __m128i*)(data + 32));
            __m128i d3 = _mm_loadu_si128((const __m128i*)(data + 48));
            x0 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x0, k1k2, 0x00),
                                              _mm_clmulepi64_si128(x0, k1k2, 0x11)), d0);
            x1 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x1, k1k2, 0x00),
                                              _mm_clmulepi64_si128(x1, k1k2, 0x11)), d1);
            x2 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x2, k1k2, 0x00),
                                              _mm_clmulepi64_si128(x2, k1k2, 0x11)), d2);
            x3 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x3, k1k2, 0x00),
                                              _mm_clmulepi64_si128(x3, k1k2, 0x11)), d3);
            data += 64; len -= 64;
        }

        // Fold 4→1 using k3/k4 (128-bit fold constants)
        x0 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x0, k3k4, 0x00),
                                          _mm_clmulepi64_si128(x0, k3k4, 0x11)), x1);
        x0 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x0, k3k4, 0x00),
                                          _mm_clmulepi64_si128(x0, k3k4, 0x11)), x2);
        x0 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x0, k3k4, 0x00),
                                          _mm_clmulepi64_si128(x0, k3k4, 0x11)), x3);

        // Fold 16 bytes at a time
        while (len >= 16) {
            __m128i d = _mm_loadu_si128((const __m128i*)data);
            x0 = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(x0, k3k4, 0x00),
                                              _mm_clmulepi64_si128(x0, k3k4, 0x11)), d);
            data += 16; len -= 16;
        }

        // Fold 128→64 bits (produces ~96-bit value)
        __m128i t = _mm_xor_si128(_mm_clmulepi64_si128(x0, k3k4, 0x10),
                                  _mm_srli_si128(x0, 8));

        // Fold 96→64 bits using k5
        const __m128i mask32 = _mm_setr_epi32(~0, 0, ~0, 0);
        __m128i t2 = _mm_srli_si128(t, 4);           // t >> 32
        t = _mm_and_si128(t, mask32);                 // keep low 32 bits
        t = _mm_clmulepi64_si128(t, k5, 0x00);       // low32 * k5
        t = _mm_xor_si128(t, t2);

        // Barrett reduction 64→32
        t2 = _mm_and_si128(t, mask32);                // keep low 32 bits
        t2 = _mm_clmulepi64_si128(t2, poly, 0x10);   // low32 * mu
        t2 = _mm_and_si128(t2, mask32);               // keep low 32 bits
        t2 = _mm_clmulepi64_si128(t2, poly, 0x00);   // low32 * P(x)
        t = _mm_xor_si128(t, t2);
        crc = (uint32_t)_mm_extract_epi32(t, 1);
    }

    // Tail bytes
    while (len-- > 0)
        crc = (crc >> 8) ^ crc32_table.t[(crc ^ *data++) & 0xFF];

    return crc ^ 0xFFFFFFFF;
}

static bool cpu_has_pclmul() {
#if defined(__GNUC__) || defined(__clang__)
    return __builtin_cpu_supports("pclmul") && __builtin_cpu_supports("sse4.1");
#else
    return false;
#endif
}
#endif // x86

uint32_t file_block_store::crc32(const uint8_t* data, size_t len) {
#if (defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86))
    static const bool has_pclmul = cpu_has_pclmul();
    if (has_pclmul) return crc32_pclmul(data, len);
#endif
    uint32_t crc = 0xFFFFFFFF;
    for (size_t i = 0; i < len; ++i)
        crc = (crc >> 8) ^ crc32_table.t[(crc ^ data[i]) & 0xFF];
    return crc ^ 0xFFFFFFFF;
}

// --- WAL journal ---

void file_block_store::wal_open() {
    wal_fd_ = pp_open(wal_path_.c_str(), O_RDWR | O_CREAT, 0644);
}

void file_block_store::wal_close() {
    if (wal_fd_ >= 0) {
        pp_close(wal_fd_);
        wal_fd_ = -1;
    }
}

void file_block_store::wal_log(uint8_t op, uint64_t group_id,
                                uint32_t block_index, uint32_t data_len,
                                uint32_t crc) {
    if (wal_fd_ < 0) return;
    uint8_t rec[WAL_RECORD_SIZE];
    rec[0] = op;
    std::memcpy(&rec[1],  &group_id,    8);
    std::memcpy(&rec[9],  &block_index, 4);
    std::memcpy(&rec[13], &data_len,    4);
    std::memcpy(&rec[17], &crc,         4);
    off_t end = pp_lseek(wal_fd_, 0, SEEK_END);
    (void)pp_pwrite(wal_fd_, rec, WAL_RECORD_SIZE, end);
}

void file_block_store::wal_commit() {
    if (wal_fd_ < 0) return;
    uint8_t rec[WAL_RECORD_SIZE] = {};
    rec[0] = WAL_OP_COMMIT;
    off_t end = pp_lseek(wal_fd_, 0, SEEK_END);
    (void)pp_pwrite(wal_fd_, rec, WAL_RECORD_SIZE, end);
#ifndef _WIN32
    pp_fdatasync(wal_fd_);
#endif
}

void file_block_store::wal_clear() {
    if (wal_fd_ < 0) return;
#ifdef _WIN32
    _chsize(wal_fd_, 0);
#else
    if (::ftruncate(wal_fd_, 0) != 0) { /* ignore */ }
    pp_fdatasync(wal_fd_);
#endif
}

void file_block_store::wal_recover() {
    // Read all WAL entries. If the last entry is not a COMMIT, the flush
    // was interrupted — truncate the data file to its pre-write state and
    // rebuild the index.
    if (wal_fd_ < 0) return;
    off_t wal_end = pp_lseek(wal_fd_, 0, SEEK_END);
    if (wal_end <= 0) return; // empty WAL, nothing to recover

    size_t num_entries = static_cast<size_t>(wal_end) / WAL_RECORD_SIZE;
    if (num_entries == 0) return;

    // Read last entry
    uint8_t last[WAL_RECORD_SIZE];
    ssize_t n = pp_pread(wal_fd_, last, WAL_RECORD_SIZE,
                         static_cast<off_t>((num_entries - 1) * WAL_RECORD_SIZE));
    if (n != static_cast<ssize_t>(WAL_RECORD_SIZE)) {
        // Corrupt WAL — rebuild index from data file (already done by scan_and_build_index)
        wal_clear();
        return;
    }

    if (last[0] == WAL_OP_COMMIT) {
        // Last operation was committed — data file is consistent
        wal_clear();
        return;
    }

    // Incomplete write — find the file size before the uncommitted ops.
    // Walk entries to find the last COMMIT, then roll back everything after.
    off_t safe_size = -1;
    for (size_t i = 0; i < num_entries; ++i) {
        uint8_t rec[WAL_RECORD_SIZE];
        ssize_t r = pp_pread(wal_fd_, rec, WAL_RECORD_SIZE,
                              static_cast<off_t>(i * WAL_RECORD_SIZE));
        if (r != static_cast<ssize_t>(WAL_RECORD_SIZE)) break;

        if (rec[0] == WAL_OP_COMMIT) {
            // Everything up to here is safe — next STORE tells us the file offset
            // Continue scanning to find the end of committed data
            safe_size = -1; // reset; will be set by next STORE if any
        } else if (rec[0] == WAL_OP_STORE) {
            // This STORE was not followed by a COMMIT — truncate to before it
            if (safe_size < 0) {
                // We need to know where this record was appended.
                // The data_len is in the WAL entry; walk the data file to find
                // the last good position. Since we haven't built the index yet,
                // truncate to the file size before this batch.
                // Use file_size_ which scan_and_build_index would set.
                // Actually, we should just re-scan after truncation.
                // The safest approach: truncate any trailing incomplete records.
            }
        }
    }

    // Simpler approach: if WAL has uncommitted entries, re-scan the data file.
    // scan_and_build_index() already handles truncated records by stopping at them.
    // So we just need to physically truncate the file at the last valid record.

    // Re-scan to find valid end
    off_t data_end = pp_lseek(fd_, 0, SEEK_END);
    off_t pos = 0;
    off_t last_valid_end = 0;

    while (pos + static_cast<off_t>(RECORD_HEADER_SIZE) <= data_end) {
        uint8_t hdr[RECORD_HEADER_SIZE];
        ssize_t r = pp_pread(fd_, hdr, RECORD_HEADER_SIZE, pos);
        if (r != static_cast<ssize_t>(RECORD_HEADER_SIZE)) break;

        uint32_t data_len = 0;
        std::memcpy(&data_len, &hdr[13], 4);

        off_t rec_end = pos + static_cast<off_t>(RECORD_HEADER_SIZE + data_len);
        if (rec_end > data_end) break; // truncated record

        // Verify CRC32
        uint32_t stored_crc = 0;
        std::memcpy(&stored_crc, &hdr[17], 4);

        std::vector<uint8_t> payload(data_len);
        if (data_len > 0) {
            ssize_t dr = pp_pread(fd_, payload.data(), data_len,
                                   pos + RECORD_HEADER_SIZE);
            if (dr != static_cast<ssize_t>(data_len)) break;
        }

        uint32_t computed_crc = crc32(payload.data(), data_len);
        if (stored_crc != computed_crc) break; // corrupt record

        last_valid_end = rec_end;
        pos = rec_end;
    }

    // Truncate data file to last valid record
    if (last_valid_end < data_end) {
#ifdef _WIN32
        _chsize(fd_, static_cast<long>(last_valid_end));
#else
        if (::ftruncate(fd_, last_valid_end) != 0) { /* ignore */ }
        pp_fdatasync(fd_);
#endif
    }

    wal_clear();
}

void file_block_store::fsync_dir() {
#ifndef _WIN32
    int dir_fd = ::open(base_dir_.c_str(), O_RDONLY | O_DIRECTORY);
    if (dir_fd >= 0) {
        ::fsync(dir_fd);
        ::close(dir_fd);
    }
#endif
}

file_block_store::file_block_store(const std::string& base_dir, size_t write_buf_threshold)
    : base_dir_(base_dir), data_path_(base_dir + "/blocks.bin"),
      wal_path_(base_dir + "/blocks.wal"),
      write_buf_threshold_(write_buf_threshold) {
    std::filesystem::create_directories(base_dir);
    fd_ = pp_open(data_path_.c_str(), O_RDWR | O_CREAT, 0644);
    if (fd_ >= 0) {
        wal_open();
        wal_recover();
        scan_and_build_index();
    }
}

file_block_store::~file_block_store() {
    if (fd_ >= 0) {
        std::lock_guard<std::mutex> lock(mutex_);
        flush_impl();
        wal_close();
        pp_close(fd_);
    }
}

bool file_block_store::scan_and_build_index() {
    index_.clear();
    total_blocks_ = 0;
    dead_bytes_ = 0;
    write_buf_.clear();
    off_t pos = 0;
    off_t end = pp_lseek(fd_, 0, SEEK_END);
    if (end < 0) return false;
    pp_lseek(fd_, 0, SEEK_SET);
    file_size_ = end;
    write_buf_base_ = end;

    uint8_t hdr[RECORD_HEADER_SIZE];
    while (pos + static_cast<off_t>(RECORD_HEADER_SIZE) <= end) {
        ssize_t n = pp_pread(fd_, hdr, RECORD_HEADER_SIZE, pos);
        if (n != static_cast<ssize_t>(RECORD_HEADER_SIZE)) break;

        uint8_t  flags      = hdr[0];
        uint64_t group_id   = 0;
        uint32_t block_idx  = 0;
        uint32_t data_len   = 0;
        uint32_t stored_crc = 0;
        std::memcpy(&group_id,   &hdr[1], 8);
        std::memcpy(&block_idx,  &hdr[9], 4);
        std::memcpy(&data_len,   &hdr[13], 4);
        std::memcpy(&stored_crc, &hdr[17], 4);

        if (pos + static_cast<off_t>(RECORD_HEADER_SIZE + data_len) > end)
            break; // truncated record

        // Verify CRC32 for live records
        if (flags == RECORD_LIVE) {
            std::vector<uint8_t> payload(data_len);
            if (data_len > 0) {
                ssize_t dr = pp_pread(fd_, payload.data(), data_len,
                                       pos + RECORD_HEADER_SIZE);
                if (dr != static_cast<ssize_t>(data_len)) break;
            }
            uint32_t computed_crc = crc32(payload.data(), data_len);
            if (stored_crc != computed_crc) {
                // Corrupt record — stop scanning here; treat rest as garbage
                break;
            }

            // Handle duplicate LIVE records for the same (group_id, block_idx).
            // This can happen when a crash occurs after flushing the new record
            // but before the deferred tombstone of the old record is applied.
            // The later record (at higher offset) wins; the old becomes dead space.
            auto& grp = index_[group_id];
            if (grp.count(block_idx)) {
                dead_bytes_ += static_cast<off_t>(RECORD_HEADER_SIZE + grp[block_idx].data_len);
            } else {
                ++total_blocks_;
            }
            auto& entry = grp[block_idx];
            entry.offset   = pos;
            entry.data_len = data_len;
            entry.in_write_buf = false;
        } else {
            dead_bytes_ += static_cast<off_t>(RECORD_HEADER_SIZE + data_len);
        }

        pos += static_cast<off_t>(RECORD_HEADER_SIZE + data_len);
    }

    // Truncate any trailing garbage/corrupt data
    if (pos < end) {
#ifdef _WIN32
        _chsize(fd_, static_cast<long>(pos));
#else
        if (::ftruncate(fd_, pos) != 0) { /* ignore */ }
#endif
        file_size_ = pos;
        write_buf_base_ = pos;
    }

    return true;
}

void file_block_store::flush_impl() {
    // Must be called with mutex_ held
    if (write_buf_.empty()) return;
    ssize_t w = pp_pwrite(fd_, write_buf_.data(), write_buf_.size(), write_buf_base_);
    if (w == static_cast<ssize_t>(write_buf_.size())) {
        // Ensure new data reaches stable storage FIRST
#ifndef _WIN32
        pp_fdatasync(fd_);
#endif
        // New data is now durable. Safe to apply deferred tombstones
        // for old records that were replaced.
        if (!pending_tombstones_.empty()) {
            for (off_t off : pending_tombstones_) {
                uint8_t del = RECORD_DELETED;
                (void)pp_pwrite(fd_, &del, 1, off);
            }
#ifndef _WIN32
            pp_fdatasync(fd_);
#endif
            pending_tombstones_.clear();
        }
        // Mark all buffered entries as no longer in write_buf
        for (auto& [gid, blocks] : index_) {
            for (auto& [bidx, entry] : blocks) {
                entry.in_write_buf = false;
            }
        }
        // Data and tombstones are durable — NOW commit and clear the WAL
        wal_commit();
        wal_clear();
    }
    write_buf_base_ += static_cast<off_t>(write_buf_.size());
    write_buf_.clear();
}

void file_block_store::flush() {
    std::lock_guard<std::mutex> lock(mutex_);
    flush_impl();
}

bool file_block_store::store(uint64_t group_id, uint32_t block_index,
                              const uint8_t* data, size_t len) {
    std::lock_guard<std::mutex> lock(mutex_);

    // Compute CRC32 of payload
    uint32_t payload_crc = crc32(data, len);

    // WAL: log intent to store
    wal_log(WAL_OP_STORE, group_id, block_index,
            static_cast<uint32_t>(len), payload_crc);

    // If this key already exists, prepare to tombstone the old record.
    // Tombstoning is DEFERRED until the new record is durably flushed
    // to prevent data loss on crash between tombstone and flush.
    auto git = index_.find(group_id);
    if (git != index_.end()) {
        auto bit = git->second.find(block_index);
        if (bit != git->second.end()) {
            // WAL: log tombstone intent
            wal_log(WAL_OP_TOMBSTONE, group_id, block_index,
                    bit->second.data_len, 0);

            if (bit->second.in_write_buf) {
                // Old record is still in write buffer — mark deleted in buffer.
                // Safe: both old (deleted) and new records are flushed together.
                off_t buf_off = bit->second.offset - write_buf_base_;
                if (buf_off >= 0 && static_cast<size_t>(buf_off) < write_buf_.size())
                    write_buf_[static_cast<size_t>(buf_off)] = RECORD_DELETED;
            } else {
                // Old record is already on disk — defer tombstone until
                // after the new record is durably flushed.
                pending_tombstones_.push_back(bit->second.offset);
            }
            dead_bytes_ += static_cast<off_t>(RECORD_HEADER_SIZE + bit->second.data_len);
            git->second.erase(bit);
            --total_blocks_;
        }
    }

    // NOTE: wal_commit() is intentionally NOT called here.
    // It is called inside flush_impl() AFTER the data is durably written,
    // so that a crash before flush does not leave a committed WAL with
    // missing data.

    // Append to write buffer
    off_t pos = write_buf_base_ + static_cast<off_t>(write_buf_.size());

    uint8_t hdr[RECORD_HEADER_SIZE];
    hdr[0] = RECORD_LIVE;
    uint32_t dlen = static_cast<uint32_t>(len);
    std::memcpy(&hdr[1],  &group_id,     8);
    std::memcpy(&hdr[9],  &block_index,  4);
    std::memcpy(&hdr[13], &dlen,         4);
    std::memcpy(&hdr[17], &payload_crc,  4);

    write_buf_.insert(write_buf_.end(), hdr, hdr + RECORD_HEADER_SIZE);
    if (len > 0)
        write_buf_.insert(write_buf_.end(), data, data + len);

    block_entry e;
    e.offset   = pos;
    e.data_len = dlen;
    e.in_write_buf = true;
    index_[group_id][block_index] = e;
    ++total_blocks_;
    file_size_ = pos + RECORD_HEADER_SIZE + dlen;

    // Flush if buffer exceeds threshold (0 = flush every write)
    if (write_buf_.size() >= write_buf_threshold_)
        flush_impl();

    maybe_auto_vacuum();
    return true;
}

bool file_block_store::fetch(uint64_t group_id, uint32_t block_index,
                              std::vector<uint8_t>& out) {
    std::lock_guard<std::mutex> lock(mutex_);
    auto git = index_.find(group_id);
    if (git == index_.end()) return false;
    auto bit = git->second.find(block_index);
    if (bit == git->second.end()) return false;

    const auto& e = bit->second;
    out.resize(e.data_len);
    if (e.data_len > 0) {
        if (e.in_write_buf) {
            // Read from write buffer
            off_t buf_off = e.offset - write_buf_base_ + RECORD_HEADER_SIZE;
            if (buf_off >= 0 && static_cast<size_t>(buf_off + e.data_len) <= write_buf_.size()) {
                std::memcpy(out.data(), write_buf_.data() + buf_off, e.data_len);
                return true;
            }
            return false;
        }
        ssize_t n = pp_pread(fd_, out.data(), e.data_len,
                            e.offset + RECORD_HEADER_SIZE);
        if (n != static_cast<ssize_t>(e.data_len)) return false;
    }
    return true;
}

bool file_block_store::remove_group(uint64_t group_id) {
    std::lock_guard<std::mutex> lock(mutex_);
    auto git = index_.find(group_id);
    if (git == index_.end()) return false;

    // Tombstone all records in this group
    for (auto& [idx, entry] : git->second) {
        if (entry.in_write_buf) {
            off_t buf_off = entry.offset - write_buf_base_;
            if (buf_off >= 0 && static_cast<size_t>(buf_off) < write_buf_.size())
                write_buf_[static_cast<size_t>(buf_off)] = RECORD_DELETED;
        } else {
            uint8_t del = RECORD_DELETED;
            (void)pp_pwrite(fd_, &del, 1, entry.offset);
        }
        dead_bytes_ += static_cast<off_t>(RECORD_HEADER_SIZE + entry.data_len);
        --total_blocks_;
    }
    index_.erase(git);
    return true;
}

std::vector<uint32_t> file_block_store::list_blocks(uint64_t group_id) {
    std::lock_guard<std::mutex> lock(mutex_);
    std::vector<uint32_t> result;
    auto git = index_.find(group_id);
    if (git != index_.end()) {
        result.reserve(git->second.size());
        for (const auto& [idx, entry] : git->second)
            result.push_back(idx);
        std::sort(result.begin(), result.end());
    }
    return result;
}

uint32_t file_block_store::total_blocks() {
    std::lock_guard<std::mutex> lock(mutex_);
    return total_blocks_;
}

uint32_t file_block_store::total_groups() {
    std::lock_guard<std::mutex> lock(mutex_);
    return static_cast<uint32_t>(index_.size());
}

std::vector<uint64_t> file_block_store::list_groups() {
    std::lock_guard<std::mutex> lock(mutex_);
    std::vector<uint64_t> result;
    result.reserve(index_.size());
    for (const auto& [gid, blocks] : index_)
        result.push_back(gid);
    return result;
}

bool file_block_store::vacuum() {
    std::lock_guard<std::mutex> lock(mutex_);
    return vacuum_impl();
}

bool file_block_store::vacuum_impl() {
    // Must be called with mutex_ held
    // Flush any buffered writes first so all data is on disk
    flush_impl();

    // Safety: if the index is empty but the data file contains data,
    // re-scan to recover valid records before vacuuming.
    if (index_.empty() && file_size_ > 0) {
        scan_and_build_index();
        if (index_.empty()) {
            // Still empty after re-scan — refuse to wipe the file
            return false;
        }
    }

    std::string tmp_path = data_path_ + ".tmp";
    int tmp_fd = pp_open(tmp_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (tmp_fd < 0) return false;

    off_t new_pos = 0;
    std::unordered_map<uint64_t,
        std::unordered_map<uint32_t, block_entry>> new_index;
    uint32_t new_total = 0;

    for (auto& [gid, blocks] : index_) {
        for (auto& [idx, entry] : blocks) {
            // Read old header + data
            size_t rec_size = RECORD_HEADER_SIZE + entry.data_len;
            std::vector<uint8_t> buf(rec_size);
            ssize_t n = pp_pread(fd_, buf.data(), rec_size, entry.offset);
            if (n != static_cast<ssize_t>(rec_size)) {
                pp_close(tmp_fd);
                pp_unlink(tmp_path.c_str());
                return false;
            }
            buf[0] = RECORD_LIVE; // ensure live flag

            // Recompute CRC32 for the new record
            uint32_t new_crc = crc32(buf.data() + RECORD_HEADER_SIZE, entry.data_len);
            std::memcpy(&buf[17], &new_crc, 4);

            ssize_t w = pp_pwrite(tmp_fd, buf.data(), rec_size, new_pos);
            if (w != static_cast<ssize_t>(rec_size)) {
                pp_close(tmp_fd);
                pp_unlink(tmp_path.c_str());
                return false;
            }

            block_entry ne;
            ne.offset   = new_pos;
            ne.data_len = entry.data_len;
            ne.in_write_buf = false;
            new_index[gid][idx] = ne;
            ++new_total;

            new_pos += static_cast<off_t>(rec_size);
        }
    }

    // Fsync temp file before swap to ensure all data is on stable storage
#ifndef _WIN32
    pp_fdatasync(tmp_fd);
#endif

    // Swap files
    pp_close(fd_);
    if (::rename(tmp_path.c_str(), data_path_.c_str()) != 0) {
        fd_ = pp_open(data_path_.c_str(), O_RDWR | O_CREAT, 0644);
        pp_close(tmp_fd);
        return false;
    }
    fd_ = pp_open(data_path_.c_str(), O_RDWR, 0644);
    if (fd_ < 0) return false;

    // Fsync directory to make the rename durable
    fsync_dir();

    pp_close(tmp_fd);

    index_ = std::move(new_index);
    total_blocks_ = new_total;
    dead_bytes_ = 0;
    file_size_ = new_pos;
    write_buf_base_ = new_pos;
    pending_tombstones_.clear();
    return true;
}

void file_block_store::maybe_auto_vacuum() {
    // Auto-vacuum when dead space exceeds 10MB and is more than 50% of the file
    // NOTE: caller already holds mutex_, so call vacuum_impl() directly
    static constexpr off_t MIN_DEAD_BYTES = 10 * 1024 * 1024; // 10 MB
    if (dead_bytes_ >= MIN_DEAD_BYTES && file_size_ > 0 &&
        dead_bytes_ > file_size_ / 2) {
        vacuum_impl();
    }
}

// --- server ---

static std::map<std::string, netplus::ssl::CertificateBundle> load_certs(
    const std::string& cert_file, const std::string& key_file)
{
    std::map<std::string, netplus::ssl::CertificateBundle> certs;
    netplus::ssl::CertificateBundle bundle;

    if (!bundle.cert.loadFromFile(cert_file)) {
    }

    std::ifstream kf(key_file, std::ios::binary | std::ios::ate);
    if (kf) {
        auto sz = kf.tellg();
        kf.seekg(0);
        bundle.privateKeyDer.resize(static_cast<size_t>(sz));
        kf.read(reinterpret_cast<char*>(bundle.privateKeyDer.data()), sz);
        bundle.rsa_key = netplus::rsa(bundle.privateKeyDer);
    } else {
    }

    std::string cn = bundle.cert.getSubjectCN();
    certs[cn] = std::move(bundle);
    return certs;
}

server::server(const config& cfg, std::shared_ptr<block_store> store,
               std::shared_ptr<auth_store> auth)
    : cfg_(cfg), store_(store), auth_store_(std::move(auth)) {
    stores_[0] = std::move(store);
}

server::~server() {
    stop();
    if (rebuild_thread_ && rebuild_thread_->joinable()) {
        rebuild_thread_->join();
    }
}

void server::add_store(uint8_t store_id, std::shared_ptr<block_store> store) {
    stores_[store_id] = std::move(store);
}

void server::set_peer_status_callback(peer_status_fn fn) {
    peer_status_fn_ = std::move(fn);
}

void server::set_peers(const std::vector<peer_info>& peers,
                       const peer_credentials& creds) {
    peers_ = peers;
    peer_creds_ = creds;
}

void server::trigger_rebuild() {
    if (rebuilding_) return;
    if (peers_.empty()) return;

    // Join previous rebuild thread if still around
    if (rebuild_thread_ && rebuild_thread_->joinable()) {
        rebuild_thread_->join();
    }

    rebuild_thread_ = std::make_unique<std::thread>([this]() {
        rebuild_from_peers();
    });
}

void server::rebuild_from_peers() {
    rebuilding_ = true;

    // Build a client to talk to peers
    std::vector<client::node_info> nodes;
    nodes.reserve(peers_.size());
    for (const auto& p : peers_) {
        nodes.push_back({p.address, p.port});
    }

    client::credentials creds;
    creds.client_name = peer_creds_.client_name;
    creds.key = peer_creds_.key;

    uint32_t synced_groups = 0;

    try {
        // Use k=1, m=0 — we only need simple fetch, not parity distribution
        client peer_client(1, 0, nodes, creds);

        // Collect all group IDs across all peers
        std::set<uint64_t> remote_groups;
        for (size_t i = 0; i < nodes.size() && running_; i++) {
            std::vector<uint64_t> groups;
            if (peer_client.list_groups_on_node(i, groups)) {
                remote_groups.insert(groups.begin(), groups.end());
            }
        }

        // Get our local groups
        auto local_groups_vec = store_->list_groups();
        std::set<uint64_t> local_groups(local_groups_vec.begin(),
                                         local_groups_vec.end());

        uint32_t synced_blocks = 0;

        // For each remote group, check which blocks we're missing
        for (uint64_t group_id : remote_groups) {
            if (!running_) break;

            // Get block lists from all peers for this group
            std::map<uint32_t, size_t> block_to_peer; // block_index -> first peer that has it
            for (size_t i = 0; i < nodes.size(); i++) {
                auto listing = peer_client.list(group_id);
                for (const auto& [node_idx, indices] : listing) {
                    for (uint32_t bidx : indices) {
                        if (block_to_peer.find(bidx) == block_to_peer.end()) {
                            block_to_peer[bidx] = node_idx;
                        }
                    }
                }
                break; // list() already queries all nodes
            }

            // Check which blocks we have locally
            auto local_blocks = store_->list_blocks(group_id);
            std::set<uint32_t> local_set(local_blocks.begin(), local_blocks.end());

            bool group_changed = false;
            for (const auto& [bidx, peer_idx] : block_to_peer) {
                if (!running_) break;
                if (local_set.count(bidx)) continue; // Already have it

                // Fetch this block from a peer
                std::vector<uint8_t> block_data;
                if (peer_client.fetch_from_peers(group_id, bidx, block_data)) {
                    if (store_->store(group_id, bidx, block_data.data(), block_data.size())) {
                        synced_blocks++;
                        group_changed = true;
                    }
                }
            }
            if (group_changed) synced_groups++;
        }

    } catch (const std::exception& e) {
    }

    if (synced_groups > 0)
        degraded_ = false;
    rebuilding_ = false;
}

void server::run() {
    auto certs = load_certs(cfg_.cert_file, cfg_.key_file);

    socket_ = std::make_unique<netplus::quic>(
        certs, cfg_.bind_address, cfg_.port, cfg_.max_connections, 0);

    socket_->bind();
    socket_->setNonBlock();

    // Wire up stream callback — fired from inside accept() when
    // child connections receive complete stream data.
    // NOTE: The data parameter contains the full stream payload;
    // recv_buffer has already been moved out by processStreamFrame,
    // so we must use the callback data directly.
    socket_->setStreamCallback(
        [this](netplus::socket* sock, uint64_t stream_id,
               const std::vector<uint8_t>& data, bool /*fin*/) {
            auto* conn = static_cast<netplus::quic*>(sock);
            handle_stream_data(*conn, stream_id, data);
        });

    running_ = true;

    // Socket wait instance (shared between startup probe and accept loop)
    netplus::socketwait wait;

    // Wait for at least one peer to become reachable before accepting
    // client requests. This prevents data-unavailable errors during
    // a full cluster restart where all nodes come up simultaneously.
    if (!peers_.empty() && cfg_.startup_peer_wait_secs > 0) {
        auto deadline = std::chrono::steady_clock::now()
                        + std::chrono::seconds(cfg_.startup_peer_wait_secs);
        bool peer_found = false;

        while (!peer_found && std::chrono::steady_clock::now() < deadline) {
            // Accept incoming connections while waiting (peers may connect to us)
            try {
                wait.waitRead(*socket_, 500);
                std::unique_ptr<netplus::socket> csock;
                socket_->accept(csock, true);
            } catch (...) {}

            // Probe each peer
            std::vector<client::node_info> nodes;
            for (const auto& p : peers_)
                nodes.push_back({p.address, p.port});
            try {
                client::credentials creds;
                creds.client_name = peer_creds_.client_name;
                creds.key = peer_creds_.key;
                client probe(1, 0, nodes, creds);
                for (size_t i = 0; i < nodes.size() && !peer_found; ++i) {
                    status_info info;
                    if (probe.get_node_status(i, info)) {
                        peer_found = true;
                    }
                }
            } catch (...) {}
        }

        if (!peer_found) {
            degraded_ = true;
        }
    }

    // Auto-rebuild: sync missing blocks from peers on startup
    if (!peers_.empty()) {
        trigger_rebuild();
    }

    // Simple accept loop — QUIC processes datagrams and fires
    // stream callbacks inside accept().
    auto last_resync = std::chrono::steady_clock::now();
    while (running_) {
        try {
            // Block until the UDP socket is readable (up to 50ms),
            // avoiding a busy spin when idle.
            wait.waitRead(*socket_, 50);
            std::unique_ptr<netplus::socket> csock;
            socket_->accept(csock, true);
        } catch (const netplus::NetException& e) {
            if (e.getErrorType() != netplus::NetException::Note) {
            }
        } catch (...) {}

        // Periodic resync every 60 seconds
        auto now = std::chrono::steady_clock::now();
        if (!peers_.empty() &&
            std::chrono::duration_cast<std::chrono::seconds>(now - last_resync).count() >= 60) {
            last_resync = now;
            trigger_rebuild();
        }
    }
}

void server::stop() {
    running_ = false;
}

void server::handle_stream_data(netplus::quic& conn, uint64_t stream_id,
                                 const std::vector<uint8_t>& data) {
    if (data.size() < protocol::HEADER_SIZE) return;

    msg_type type;
    uint32_t payload_len;
    uint8_t store_id;
    if (!protocol::decode_header(data.data(), data.size(), type, payload_len, store_id)) return;

    if (protocol::HEADER_SIZE + payload_len > data.size()) return;

    process_message(conn, stream_id, type,
                    data.data() + protocol::HEADER_SIZE, payload_len, store_id);
}

bool server::is_authenticated(netplus::quic* conn) {
    if (!cfg_.require_auth || !auth_store_) return true;
    std::lock_guard<std::mutex> lock(auth_mutex_);
    auto it = auth_states_.find(conn);
    return it != auth_states_.end() && it->second.authenticated;
}

void server::handle_auth_request(netplus::quic& conn, uint64_t stream_id,
                                   const uint8_t* payload, size_t len) {
    std::string client_name;
    if (!protocol::parse_auth_request(payload, len, client_name)) {
        send_error(conn, stream_id, 10, "Invalid AUTH_REQUEST");
        return;
    }

    // Check if client exists
    std::vector<uint8_t> key;
    if (!auth_store_->lookup(client_name, key)) {
        send_response(conn, stream_id, msg_type::AUTH_DENIED, {});
        return;
    }

    // Generate nonce and send challenge
    auto nonce = auth::generate_nonce();

    {
        std::lock_guard<std::mutex> lock(auth_mutex_);
        auto& state = auth_states_[&conn];
        state.client_name = client_name;
        state.pending_nonce = nonce;
        state.authenticated = false;
    }

    auto challenge = protocol::make_auth_challenge(nonce);
    send_response(conn, stream_id, msg_type::AUTH_CHALLENGE, challenge);
}

void server::handle_auth_response(netplus::quic& conn, uint64_t stream_id,
                                    const uint8_t* payload, size_t len) {
    std::vector<uint8_t> client_hmac;
    if (!protocol::parse_auth_response(payload, len, client_hmac)) {
        send_error(conn, stream_id, 10, "Invalid AUTH_RESPONSE");
        return;
    }

    std::string client_name;
    std::vector<uint8_t> nonce;
    {
        std::lock_guard<std::mutex> lock(auth_mutex_);
        auto it = auth_states_.find(&conn);
        if (it == auth_states_.end() || it->second.pending_nonce.empty()) {
            send_response(conn, stream_id, msg_type::AUTH_DENIED, {});
            return;
        }
        client_name = it->second.client_name;
        nonce = it->second.pending_nonce;
    }

    // Lookup key and verify
    std::vector<uint8_t> key;
    if (!auth_store_->lookup(client_name, key)) {
        send_response(conn, stream_id, msg_type::AUTH_DENIED, {});
        return;
    }

    if (auth::verify_hmac(key, nonce, client_hmac)) {
        {
            std::lock_guard<std::mutex> lock(auth_mutex_);
            auto& state = auth_states_[&conn];
            state.authenticated = true;
            state.pending_nonce.clear();
        }
        send_response(conn, stream_id, msg_type::AUTH_OK, {});
    } else {
        {
            std::lock_guard<std::mutex> lock(auth_mutex_);
            auth_states_.erase(&conn);
        }
        send_response(conn, stream_id, msg_type::AUTH_DENIED, {});
    }
}

void server::process_message(netplus::quic& conn, uint64_t stream_id,
                               msg_type type, const uint8_t* payload, size_t len,
                               uint8_t store_id) {
    switch (type) {
        case msg_type::AUTH_REQUEST: {
            handle_auth_request(conn, stream_id, payload, len);
            return;
        }
        case msg_type::AUTH_RESPONSE: {
            handle_auth_response(conn, stream_id, payload, len);
            return;
        }
        default:
            break;
    }

    // All other messages require authentication
    if (!is_authenticated(&conn)) {
        send_error(conn, stream_id, 11, "Authentication required");
        return;
    }

    // Resolve target store
    auto sit = stores_.find(store_id);
    if (sit == stores_.end()) {
        send_error(conn, stream_id, 12, "Unknown store_id");
        return;
    }
    auto& store = *sit->second;

    switch (type) {
        case msg_type::STORE_BLOCK: {
            uint64_t group_id;
            uint32_t block_index;
            const uint8_t* data;
            size_t data_len;
            if (!protocol::parse_store_block(payload, len, group_id, block_index, data, data_len)) {
                send_error(conn, stream_id, 1, "Invalid STORE_BLOCK payload");
                return;
            }
            if (store.store(group_id, block_index, data, data_len)) {
                send_response(conn, stream_id, msg_type::OK, {});
            } else {
                send_error(conn, stream_id, 2, "Store failed");
            }
            break;
        }

        case msg_type::FETCH_BLOCK: {
            uint64_t group_id;
            uint32_t block_index;
            if (!protocol::parse_fetch_block(payload, len, group_id, block_index)) {
                send_error(conn, stream_id, 1, "Invalid FETCH_BLOCK payload");
                return;
            }
            std::vector<uint8_t> block_data;
            if (store.fetch(group_id, block_index, block_data)) {
                auto resp = protocol::make_block_data(group_id, block_index,
                                                       block_data.data(), block_data.size());
                send_response(conn, stream_id, msg_type::BLOCK_DATA, resp);
            } else if (!peers_.empty()) {
                // Block not local — try forward-fetching from a peer.
                // This handles the full-cluster-restart scenario where rebuild
                // hasn't synced this block yet but a peer already has it.
                bool forwarded = false;
                try {
                    std::vector<client::node_info> nodes;
                    nodes.reserve(peers_.size());
                    for (const auto& p : peers_)
                        nodes.push_back({p.address, p.port});

                    client::credentials creds;
                    creds.client_name = peer_creds_.client_name;
                    creds.key = peer_creds_.key;

                    client peer_client(1, 0, nodes, creds);
                    if (peer_client.fetch_from_peers(group_id, block_index, block_data)) {
                        // Cache locally for future requests
                        store.store(group_id, block_index,
                                      block_data.data(), block_data.size());
                        auto resp = protocol::make_block_data(group_id, block_index,
                                                               block_data.data(), block_data.size());
                        send_response(conn, stream_id, msg_type::BLOCK_DATA, resp);
                        forwarded = true;
                    }
                } catch (...) {}

                if (!forwarded) {
                    std::vector<uint8_t> not_found(12);
                    protocol::encode_u64(not_found.data(), group_id);
                    protocol::encode_u32(not_found.data() + 8, block_index);
                    send_response(conn, stream_id, msg_type::BLOCK_NOT_FOUND, not_found);
                }
            } else {
                std::vector<uint8_t> not_found(12);
                protocol::encode_u64(not_found.data(), group_id);
                protocol::encode_u32(not_found.data() + 8, block_index);
                send_response(conn, stream_id, msg_type::BLOCK_NOT_FOUND, not_found);
            }
            break;
        }

        case msg_type::DELETE_GROUP: {
            if (len < 8) {
                send_error(conn, stream_id, 1, "Invalid DELETE_GROUP payload");
                return;
            }
            uint64_t group_id = protocol::decode_u64(payload);
            store.remove_group(group_id);
            send_response(conn, stream_id, msg_type::OK, {});
            break;
        }

        case msg_type::LIST_BLOCKS: {
            if (len < 8) {
                send_error(conn, stream_id, 1, "Invalid LIST_BLOCKS payload");
                return;
            }
            uint64_t group_id = protocol::decode_u64(payload);
            auto indices = store.list_blocks(group_id);

            std::vector<uint8_t> resp(8 + 4 + indices.size() * 4);
            protocol::encode_u64(resp.data(), group_id);
            protocol::encode_u32(resp.data() + 8, static_cast<uint32_t>(indices.size()));
            for (size_t i = 0; i < indices.size(); i++) {
                protocol::encode_u32(resp.data() + 12 + i * 4, indices[i]);
            }
            send_response(conn, stream_id, msg_type::BLOCK_LIST, resp);
            break;
        }

        case msg_type::GET_STATUS: {
            status_info info;
            // Gather block/group counts from the store
            info.block_count = store.total_blocks();
            info.group_count = store.total_groups();
            // Peer connectivity from callback
            if (peer_status_fn_) {
                auto [total, online] = peer_status_fn_();
                info.peers_total = total;
                info.peers_online = online;
            }
            // Determine status from peer connectivity
            if (info.peers_total > 0 && info.peers_online < info.peers_total) {
                info.status = node_status::DEGRADED;
            } else if (degraded_) {
                // Startup peer wait timed out — running without peers
                info.status = node_status::DEGRADED;
            } else {
                info.status = node_status::OK;
            }
            // Override with REBUILD if rebuild is in progress
            if (rebuilding_) {
                info.status = node_status::REBUILD;
            }
            info.scrub_running = rebuilding_.load();
            auto resp = protocol::make_status_resp(info);
            send_response(conn, stream_id, msg_type::STATUS_RESP, resp);
            break;
        }

        case msg_type::LIST_GROUPS: {
            auto groups = store.list_groups();
            auto resp = protocol::make_group_list(groups);
            send_response(conn, stream_id, msg_type::GROUP_LIST, resp);
            break;
        }

        case msg_type::VACUUM: {
            if (store.vacuum()) {
                send_response(conn, stream_id, msg_type::OK, {});
            } else {
                send_error(conn, stream_id, 4, "Vacuum failed");
            }
            break;
        }

        default:
            send_error(conn, stream_id, 3, "Unknown message type");
            break;
    }
}

void server::send_response(netplus::quic& conn, uint64_t stream_id,
                             msg_type type, const std::vector<uint8_t>& payload) {
    auto msg = protocol::encode_message(type, payload);
    conn.sendStreamData(stream_id, msg, true);
}

void server::send_error(netplus::quic& conn, uint64_t stream_id,
                          uint16_t code, const std::string& message) {
    std::vector<uint8_t> payload(2 + message.size());
    payload[0] = static_cast<uint8_t>(code >> 8);
    payload[1] = static_cast<uint8_t>(code);
    std::memcpy(payload.data() + 2, message.data(), message.size());
    auto msg = protocol::encode_message(msg_type::PROTOCOL_ERROR, payload);
    conn.sendStreamData(stream_id, msg, true);
}

} // namespace paritypp
