/*******************************************************************************
 * Copyright (c) 2026, Jan Koester jan.koester@gmx.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 * Neither the name of the <organization> nor the
 *      names of its contributors may be used to endorse or promote products
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/

#include <cstring>
#include <iostream>
#include <algorithm>
#include <thread>
#include <chrono>
#include <map>
#include <tuple>

#include <crypto/sha.h>
#include <netplus/exception.h>

#include "cluster.h"
#include "../cluster.h"

#ifndef NDEBUG
#define DBG_LOG(x) do { std::cerr << x; } while(0)
#else
#define DBG_LOG(x) do {} while(0)
#endif

namespace authdb {

    uint64_t ClusterBackend::domainGroupId() const {
        std::vector<uint8_t> input(_Domain.begin(), _Domain.end());
        auto hash = netplus::sha256_hash(input);
        uint64_t id = 0;
        for (int i = 0; i < 8; i++)
            id = (id << 8) | hash[i];
        return id;
    }

    // Helper: extract revision from a backend buffer
    static size_t bufferRevision(const std::vector<uint8_t> &buf) {
        if (buf.size() < sizeof(AuthHeader))
            return 0;
        AuthHeader head;
        std::memcpy(&head, buf.data(), sizeof(AuthHeader));
        return head.Revesion;
    }

    // Validate that all records in buffer have consistent offsets.
    // Returns false if any record's datasize would exceed the buffer.
    static bool bufferValid(const std::vector<uint8_t> &buf) {
        if (buf.size() < sizeof(AuthHeader))
            return buf.empty(); // empty is ok, too-small header is not

        size_t rd = sizeof(AuthHeader);
        while (rd + sizeof(AuthData::Record) <= buf.size()) {
            AuthData::Record rec;
            std::memcpy(&rec, buf.data() + rd, sizeof(AuthData::Record));
            rd += sizeof(AuthData::Record);
            if (rd + rec.datasize > buf.size())
                return false;
            rd += rec.datasize;
        }
        return true;
    }

    void ClusterBackend::fetchFromCluster() {
        if (!g_Cluster || !g_Cluster->isRunning())
            return;

        uint64_t dgid = domainGroupId();
        DBG_LOG("[CLUSTER-BE] fetchFromCluster domain=" << _Domain
                  << " buf_size=" << _Buffer.size() << " dirty=" << _Dirty.load() << "\n");

        // Only fetch from peers every 30 seconds — auth data changes rarely
        std::vector<uint8_t> peer_data;
        auto now = std::chrono::steady_clock::now();
        bool do_peer_fetch = (now - _LastPeerFetch) >= std::chrono::seconds(30);

        if (do_peer_fetch) {
            auto &client = g_Cluster->getClient();
            if (client) {
                try {
                    auto &cmtx = g_Cluster->getClientMutex();
                    std::unique_lock<std::timed_mutex> lock(cmtx, std::chrono::seconds(1));
                    if (lock.owns_lock()) {
                        peer_data = client->retrieve(dgid);
                        _LastPeerFetch = now;
                    }
                } catch (const std::exception &e) {
                    std::cerr << "ClusterBackend::fetchFromCluster: retrieve failed: "
                              << e.what() << std::endl;
                    // "not enough blocks" means data exists but too few peers are reachable
                    if (std::string(e.what()).find("not enough blocks") != std::string::npos)
                        _ClusterDataExists.store(true);
                } catch (...) {
                    std::cerr << "ClusterBackend::fetchFromCluster: retrieve failed (unknown)" << std::endl;
                }
            }
        }

        if (!bufferValid(peer_data))
            peer_data.clear();

        size_t peer_rev = bufferRevision(peer_data);
        size_t cur_rev  = bufferRevision(_Buffer);

        bool peer_has_records = (!peer_data.empty() && peer_data.size() > sizeof(AuthHeader));

        if (!peer_has_records)
            return;

        // Ensure _Buffer has at least a valid header as merge base
        if (_Buffer.size() < sizeof(AuthHeader)) {
            if (peer_data.size() >= sizeof(AuthHeader))
                _Buffer.assign(peer_data.begin(),
                               peer_data.begin() + sizeof(AuthHeader));
            else
                return;
        }

        // Append records from peer data into _Buffer.
        // vacuum() will deduplicate afterwards.
        _Buffer.insert(_Buffer.end(),
                       peer_data.begin() + sizeof(AuthHeader),
                       peer_data.end());

        size_t max_rev = std::max(cur_rev, peer_rev);
        AuthHeader head;
        std::memcpy(&head, _Buffer.data(), sizeof(AuthHeader));
        head.Revesion = max_rev;
        std::memcpy(_Buffer.data(), &head, sizeof(AuthHeader));

        vacuum();
    }

    void ClusterBackend::pushToCluster() {
        // Legacy entry point for destructor
        if (!_Dirty) return;
        newRevesion();
        uint64_t dgid = domainGroupId();
        std::vector<uint8_t> buf_copy = _Buffer;
        if (pushToClusterAsync(dgid, buf_copy))
            _Dirty = false;
    }

    ClusterBackend::ClusterBackend(const char *domain, size_t version, const char *filepath)
        : _Domain(domain), _Pos(0), _Dirty(false)
    {
        // Try to fetch existing data from cluster (single attempt, non-blocking).
        // If peers aren't reachable yet, lock() will fetch on first use.
        fetchFromCluster();

        if (_Buffer.empty()) {
            // Initialize with a fresh header locally.
            // Do NOT push to cluster — we may have failed to fetch existing data
            // and pushing an empty header would overwrite real data on other nodes.
            AuthHeader head;
            head.Version = version;
            head.Revesion = 0;
            head.start = 0xFE;
            memset(head.domain, 0, 255);
            snprintf(head.domain, 255, "%s", domain);

            _Buffer.resize(sizeof(AuthHeader));
            std::memcpy(_Buffer.data(), &head, sizeof(AuthHeader));
            _Dirty = false;
        }
    }

    ClusterBackend::~ClusterBackend() {
        if (_Dirty) {
            try {
                pushToCluster();
            } catch (const std::exception &e) {
                std::cerr << "ClusterBackend::~ClusterBackend: pushToCluster failed: "
                          << e.what() << std::endl;
            } catch (...) {
                std::cerr << "ClusterBackend::~ClusterBackend: pushToCluster failed (unknown error)"
                          << std::endl;
            }
        }
    }

    void ClusterBackend::read(unsigned char *src, size_t srcsize) {
        if (_Pos + srcsize > _Buffer.size())
            throw AuthBackendError("cluster backend: read past end of data!");

        std::memcpy(src, _Buffer.data() + _Pos, srcsize);
        _Pos += srcsize;
    }

    void ClusterBackend::write(const unsigned char *dest, size_t destsize) {
        if (_Pos + destsize > _Buffer.size())
            _Buffer.resize(_Pos + destsize);

        std::memcpy(_Buffer.data() + _Pos, dest, destsize);
        _Pos += destsize;
        _Dirty = true;
    }

    void ClusterBackend::lock() {
        bool was_dirty = _Dirty.load();
        DBG_LOG("[CLUSTER-BE] lock() domain=" << _Domain
                  << " buf_size=" << _Buffer.size() << " dirty=" << was_dirty << "\n");
        fetchFromCluster();
        DBG_LOG("[CLUSTER-BE] lock() after fetch buf_size=" << _Buffer.size() << "\n");
        // fetchFromCluster/vacuum may set _Dirty; restore if no user writes pending
        if (!was_dirty)
            _Dirty = false;
    }

    void ClusterBackend::unlock() {
        DBG_LOG("[CLUSTER-BE] unlock() domain=" << _Domain
                  << " buf_size=" << _Buffer.size() << " dirty=" << _Dirty.load() << "\n");
        if (_Dirty) {
            newRevesion();
            uint64_t dgid = domainGroupId();
            // Push in a detached background thread so unlock() never blocks
            // the HTTP response on cluster timeouts.
            auto buf_copy = std::make_shared<std::vector<uint8_t>>(_Buffer);
            std::thread([dgid, buf_copy]() {
                if (!g_Cluster || !g_Cluster->isRunning()) return;
                auto &client = g_Cluster->getClient();
                if (!client) return;
                try {
                    auto &cmtx = g_Cluster->getClientMutex();
                    std::unique_lock<std::timed_mutex> lock(cmtx, std::chrono::seconds(5));
                    if (lock.owns_lock()) {
                        client->store(dgid, buf_copy->data(), buf_copy->size());
                    }
                } catch (const std::exception &e) {
                    std::cerr << "Cluster replicate (bg): " << e.what() << std::endl;
                }
            }).detach();
            // Optimistically clear dirty — data is captured in bg thread
            _Dirty = false;
        }
    }

    bool ClusterBackend::pushToClusterAsync(uint64_t dgid, const std::vector<uint8_t> &buf) {
        if (!g_Cluster || !g_Cluster->isRunning())
            return false;

        auto &client = g_Cluster->getClient();
        if (client) {
            try {
                auto &cmtx = g_Cluster->getClientMutex();
                // Retry up to 3 times to avoid silently dropping replication
                for (int attempt = 0; attempt < 3; ++attempt) {
                    std::unique_lock<std::timed_mutex> lock(cmtx, std::chrono::seconds(3));
                    if (lock.owns_lock()) {
                        client->store(dgid, buf.data(), buf.size());
                        return true;
                    }
                    if (attempt == 2) {
                        std::cerr << "ClusterBackend::pushToCluster: replication failed after retries" << std::endl;
                    }
                }
            } catch (const netplus::NetException &e) {
                std::cerr << "Cluster replicate failed: " << e.what() << std::endl;
            } catch (const std::exception &e) {
                std::cerr << "Cluster replicate failed: " << e.what() << std::endl;
            }
        }
        return false;
    }

    void ClusterBackend::setPos(size_t pos) {
        _Pos = pos;
    }

    size_t ClusterBackend::getPos() {
        return _Pos;
    }

    size_t ClusterBackend::end() {
        return _Buffer.size();
    }

    size_t ClusterBackend::getRevesion() {
        if (_Buffer.size() < sizeof(AuthHeader))
            return 0;

        size_t cur = _Pos;
        AuthHeader head;
        std::memcpy(&head, _Buffer.data(), sizeof(AuthHeader));
        return head.Revesion;
    }

    void ClusterBackend::newRevesion() {
        if (_Buffer.size() < sizeof(AuthHeader))
            return;

        AuthHeader head;
        std::memcpy(&head, _Buffer.data(), sizeof(AuthHeader));
        ++head.Revesion;
        std::memcpy(_Buffer.data(), &head, sizeof(AuthHeader));
        _Dirty = true;
    }

    void ClusterBackend::purge() {
        if (!g_Cluster || !g_Cluster->isRunning())
            return;

        uint64_t dgid = domainGroupId();

        // Remove from peer nodes
        auto &client = g_Cluster->getClient();
        if (client) {
            try {
                auto &cmtx = g_Cluster->getClientMutex();
                std::unique_lock<std::timed_mutex> lock(cmtx, std::chrono::seconds(3));
                if (lock.owns_lock()) {
                    client->remove(dgid);
                }
            } catch (const std::exception &e) {
                std::cerr << "ClusterBackend::purge: peer remove failed: " << e.what() << std::endl;
            }
        }

        _Buffer.clear();
        _Dirty = false;
    }

    void ClusterBackend::vacuum() {
        if (_Buffer.size() <= sizeof(AuthHeader))
            return;

        AuthHeader head;
        std::memcpy(&head, _Buffer.data(), sizeof(AuthHeader));

        // Collect valid records, deduplicate keeping last occurrence
        struct RecordEntry {
            AuthData::Record rec;
            std::vector<unsigned char> data;
        };

        std::map<std::tuple<std::string,std::string,int>, RecordEntry> dedup;
        std::vector<std::tuple<std::string,std::string,int>> insertOrder;

        size_t rd = sizeof(AuthHeader);

        while (rd + sizeof(AuthData::Record) <= _Buffer.size()) {
            RecordEntry entry;
            std::memcpy(&entry.rec, _Buffer.data() + rd, sizeof(AuthData::Record));
            size_t datasize = entry.rec.datasize;
            rd += sizeof(AuthData::Record);

            if (entry.rec.type == DataType::EmptyData) {
                rd += datasize;
                continue;
            }

            if (rd + datasize > _Buffer.size())
                break;

            entry.data.resize(datasize);
            if (datasize > 0)
                std::memcpy(entry.data.data(), _Buffer.data() + rd, datasize);
            rd += datasize;

            std::string ruid_str(reinterpret_cast<char*>(entry.rec.ruid), 16);
            std::string fname(entry.rec.fieldname);
            auto key = std::make_tuple(ruid_str, fname, entry.rec.type);

            if (dedup.find(key) == dedup.end())
                insertOrder.push_back(key);
            dedup[key] = std::move(entry);
        }

        // Rebuild buffer
        std::vector<uint8_t> newbuf(sizeof(AuthHeader));
        std::memcpy(newbuf.data(), &head, sizeof(AuthHeader));

        for (const auto &key : insertOrder) {
            auto it = dedup.find(key);
            if (it == dedup.end()) continue;
            const auto &entry = it->second;

            size_t pos = newbuf.size();
            newbuf.resize(pos + sizeof(AuthData::Record) + entry.data.size());
            std::memcpy(newbuf.data() + pos, &entry.rec, sizeof(AuthData::Record));
            if (!entry.data.empty())
                std::memcpy(newbuf.data() + pos + sizeof(AuthData::Record),
                            entry.data.data(), entry.data.size());
        }

        // Only mark dirty if buffer content actually changed
        if (newbuf.size() != _Buffer.size() ||
            std::memcmp(newbuf.data(), _Buffer.data(), newbuf.size()) != 0) {
            _Buffer = std::move(newbuf);
            _Dirty = true;
        }
    }

}
