#include "cluster.h"

#include <iostream>
#include <netplus/exception.h>

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

namespace mediadb {

Cluster* g_Cluster = nullptr;

void Cluster::init(const ClusterConfig& cfg) {
    cfg_ = cfg;

    if (!cfg_.store_path.empty()) {
        store_ = std::make_shared<paritypp::file_block_store>(cfg_.store_path);
    } else {
        store_ = std::make_shared<paritypp::memory_block_store>();
    }

    auth_ = std::make_shared<paritypp::auth_store>();
    if (!cfg_.client_name.empty()) {
        auth_->add_client(cfg_.client_name, cfg_.client_key);
    }

    paritypp::server::config scfg;
    scfg.bind_address = cfg_.bind_address;
    scfg.port = cfg_.port;
    scfg.cert_file = cfg_.cert_file;
    scfg.key_file = cfg_.key_file;
    scfg.max_connections = 64;
    scfg.require_auth = !cfg_.client_name.empty();

    server_ = std::make_unique<paritypp::server>(scfg, store_, auth_);

    size_t peer_count = 0;
    if (!cfg_.peers.empty()) {
        std::vector<std::string> local_addrs;
        {
            char hostname[256] = {};
            gethostname(hostname, sizeof(hostname));
            local_addrs.emplace_back(hostname);
        }
#ifndef _WIN32
        {
            struct ifaddrs* ifaddr = nullptr;
            if (getifaddrs(&ifaddr) == 0) {
                for (struct ifaddrs* ifa = ifaddr; ifa; ifa = ifa->ifa_next) {
                    if (!ifa->ifa_addr) continue;
                    char buf[INET6_ADDRSTRLEN] = {};
                    if (ifa->ifa_addr->sa_family == AF_INET) {
                        inet_ntop(AF_INET,
                                  &reinterpret_cast<struct sockaddr_in*>(ifa->ifa_addr)->sin_addr,
                                  buf, sizeof(buf));
                        local_addrs.emplace_back(buf);
                    } else if (ifa->ifa_addr->sa_family == AF_INET6) {
                        inet_ntop(AF_INET6,
                                  &reinterpret_cast<struct sockaddr_in6*>(ifa->ifa_addr)->sin6_addr,
                                  buf, sizeof(buf));
                        local_addrs.emplace_back(buf);
                    }
                }
                freeifaddrs(ifaddr);
            }
        }
#endif
        std::vector<paritypp::client::node_info> nodes;
        for (size_t pi = 0; pi < cfg_.peers.size(); ++pi) {
            const auto& peer = cfg_.peers[pi];
            bool is_self = false;
            for (const auto& la : local_addrs) {
                if (peer.address == la && peer.port == cfg_.port) {
                    is_self = true;
                    break;
                }
            }
            if (is_self) {
                self_node_index_ = static_cast<int>(pi);
            }
            peer_to_client_index_[pi] = nodes.size();
            nodes.push_back({peer.address, peer.port});
        }
        peer_count = cfg_.peers.size();

        paritypp::client::credentials pcreds;
        if (!cfg_.client_name.empty()) {
            pcreds.client_name = cfg_.client_name;
            pcreds.key.assign(cfg_.client_key.begin(), cfg_.client_key.end());
        }

        if (!nodes.empty()) {
            pclient_ = std::make_unique<paritypp::client>(
                cfg_.data_blocks, cfg_.parity_blocks, nodes, pcreds);
            if (self_node_index_ >= 0) {
                pclient_->set_local_node(
                    peer_to_client_index_[static_cast<size_t>(self_node_index_)], store_);
            }
        }
    }

    server_->set_peer_status_callback([peer_count]() -> std::pair<uint32_t, uint32_t> {
        return {0, static_cast<uint32_t>(peer_count)};
    });
}

void Cluster::start() {
    if (running_) return;

    // Compact the local store on startup to reclaim tombstoned space
    auto* fbs = dynamic_cast<paritypp::file_block_store*>(store_.get());
    if (fbs) {
        DBG_LOG("[CLUSTER] startup vacuum ... ");
        if (fbs->vacuum())
            DBG_LOG("done\n");
        else
            DBG_LOG("skipped (below threshold)\n");
    }

    running_ = true;
    server_thread_ = std::thread(&Cluster::server_loop, this);
    health_thread_ = std::thread(&Cluster::health_loop, this);
}

void Cluster::stop() {
    if (!running_) return;
    running_ = false;
    if (server_) server_->stop();
    if (health_thread_.joinable()) health_thread_.join();
    if (server_thread_.joinable()) server_thread_.join();
}

std::vector<Cluster::NodeGroupInfo> Cluster::list_peer_groups() {
    std::vector<NodeGroupInfo> result;
    if (!pclient_) return result;
    size_t idx = 0;
    for (const auto& peer : cfg_.peers) {
        NodeGroupInfo ngi;
        ngi.index = idx;
        ngi.address = peer.address;
        ngi.port = peer.port;
        ngi.online = false;
        auto it = peer_to_client_index_.find(idx);
        if (it != peer_to_client_index_.end() &&
            pclient_->list_groups_on_node(it->second, ngi.groups)) {
            ngi.online = true;
        }
        result.push_back(std::move(ngi));
        ++idx;
    }
    return result;
}

bool Cluster::replicate(const std::string& key, const uint8_t* data, size_t len) {
    if (!pclient_) return false;
    uint64_t gid = cluster_group_id(key);
    try {
        DBG_LOG("[CLUSTER] replicate key=" << key << " gid=" << gid
                  << " size=" << len << "\n");
        pclient_->store(gid, data, len);
        DBG_LOG("[CLUSTER] replicate key=" << key << " store done\n");
    } catch (const netplus::NetException& e) {
        DBG_LOG("[CLUSTER] replicate NetException key=" << key << ": " << e.what() << "\n");
        return false;
    } catch (const std::exception& e) {
        DBG_LOG("[CLUSTER] replicate exception key=" << key << ": " << e.what() << "\n");
        return false;
    }
    return true;
}

std::unique_ptr<paritypp::store_session> Cluster::begin_replicate(const std::string& key, size_t total_size) {
    if (!pclient_) return nullptr;
    uint64_t gid = cluster_group_id(key);
    try {
        return pclient_->begin_store(gid, total_size);
    } catch (const std::exception& e) {
        return nullptr;
    }
}

bool Cluster::fetch(const std::string& key, std::vector<uint8_t>& out) {
    if (!pclient_) return false;
    uint64_t gid = cluster_group_id(key);
    try {
        out = pclient_->retrieve(gid);
        return !out.empty();
    } catch (const netplus::NetException& e) {
        DBG_LOG("[CLUSTER] fetch NetException key=" << key << ": " << e.what() << "\n");
    } catch (const std::exception& e) {
        DBG_LOG("[CLUSTER] fetch exception key=" << key << ": " << e.what() << "\n");
    }
    return false;
}

bool Cluster::fetch_range(const std::string& key, uint64_t offset, uint64_t length,
                           std::vector<uint8_t>& out) {
    if (!pclient_) return false;
    uint64_t gid = cluster_group_id(key);
    try {
        out = pclient_->retrieve_range(gid, offset, length);
        return !out.empty();
    } catch (const netplus::NetException& e) {
        DBG_LOG("[CLUSTER] fetch_range NetException key=" << key << ": " << e.what() << "\n");
    } catch (const std::exception& e) {
        DBG_LOG("[CLUSTER] fetch_range exception key=" << key << ": " << e.what() << "\n");
    }
    return false;
}

bool Cluster::fetch_from_peers(const std::string& key, std::vector<uint8_t>& out) {
    if (!pclient_) return false;
    uint64_t gid = cluster_group_id(key);
    try {
        out = pclient_->retrieve(gid);
        return !out.empty();
    } catch (const netplus::NetException& e) {
        DBG_LOG("[CLUSTER] fetch_from_peers NetException key=" << key << ": " << e.what() << "\n");
    } catch (const std::exception& e) {
        DBG_LOG("[CLUSTER] fetch_from_peers exception key=" << key << ": " << e.what() << "\n");
    }
    return false;
}

void Cluster::remove(const std::string& key) {
    if (!pclient_) return;
    uint64_t gid = cluster_group_id(key);
    try {
        pclient_->remove(gid);
    } catch (...) {}
}

bool Cluster::vacuum_local_store() {
    auto* fbs = dynamic_cast<paritypp::file_block_store*>(store_.get());
    if (!fbs) return false;
    return fbs->vacuum();
}

size_t Cluster::vacuum_all_nodes() {
    if (!pclient_) return 0;
    return pclient_->vacuum_all_nodes();
}

void Cluster::server_loop() {
    try {
        server_->run();
    } catch (const netplus::NetException& e) {
        DBG_LOG("[CLUSTER] server_loop NetException: " << e.what() << "\n");
    } catch (const std::exception& e) {
        DBG_LOG("[CLUSTER] server_loop exception: " << e.what() << "\n");
    }
    running_ = false;
}

void Cluster::health_loop() {
    size_t k = cfg_.data_blocks;
    size_t n = cfg_.data_blocks + cfg_.parity_blocks;
    while (running_) {
        if (pclient_) {
            uint32_t online = 0;
            uint32_t total = static_cast<uint32_t>(cfg_.peers.size());
            for (size_t pi = 0; pi < cfg_.peers.size(); ++pi) {
                auto it = peer_to_client_index_.find(pi);
                if (it == peer_to_client_index_.end()) continue;
                if (static_cast<int>(pi) == self_node_index_) {
                    ++online;
                    continue;
                }
                std::vector<uint64_t> dummy;
                if (pclient_->list_groups_on_node(it->second, dummy))
                    ++online;
            }
            server_->set_peer_status_callback([online, total]() -> std::pair<uint32_t, uint32_t> {
                return {online, total};
            });

            // Verify local store is writable
            bool store_ok = true;
            if (store_) {
                try {
                    const uint64_t sentinel_gid = 0;
                    const uint32_t sentinel_idx = UINT32_MAX;
                    uint8_t probe = 0x01;
                    store_ok = store_->store(sentinel_gid, sentinel_idx, &probe, 1);
                    if (store_ok)
                        store_->remove_group(sentinel_gid);
                } catch (...) {
                    store_ok = false;
                }
                if (!store_ok && !critical_) {
                    std::cerr << "[CLUSTER] CRITICAL — local store not writable\n";
                }
            }

            bool was_degraded = degraded_;
            if (!store_ok) {
                degraded_ = true;
                critical_ = true;
            } else if (online >= n) {
                if (degraded_) {
                    std::cerr << "[CLUSTER] recovered — all " << online << "/"
                              << n << " nodes online, starting scrub\n";
                    std::thread([this](){ try { scrub(); } catch (...) {} }).detach();
                }
                degraded_ = false;
                critical_ = false;
            } else if (online >= k) {
                if (!degraded_) {
                    std::cerr << "[CLUSTER] DEGRADED — " << online << "/"
                              << n << " nodes online\n";
                }
                degraded_ = true;
                critical_ = false;
            } else {
                if (!critical_) {
                    std::cerr << "[CLUSTER] CRITICAL — only " << online << "/"
                              << n << " nodes online (need " << k << ")\n";
                }
                degraded_ = true;
                critical_ = true;
            }
        }
        // Probe faster when degraded/critical (5s) vs normal (30s)
        int interval = (degraded_ || critical_) ? 5 : 30;
        for (int i = 0; i < interval && running_; ++i)
            std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

Cluster::ScrubResult Cluster::scrub() {
    ScrubResult result;
    if (!pclient_) return result;

    size_t n = cfg_.data_blocks + cfg_.parity_blocks;

    // 1. Collect all group IDs from every node
    std::map<uint64_t, std::set<size_t>> group_nodes;
    for (size_t pi = 0; pi < cfg_.peers.size(); ++pi) {
        auto it = peer_to_client_index_.find(pi);
        if (it == peer_to_client_index_.end()) continue;
        std::vector<uint64_t> node_groups;
        if (pclient_->list_groups_on_node(it->second, node_groups)) {
            for (uint64_t gid : node_groups)
                group_nodes[gid].insert(pi);
        }
    }

    std::cerr << "[SCRUB] found " << group_nodes.size()
              << " groups across " << n << " nodes\n";

    // 2. For each group, check block distribution and repair
    for (auto& [gid, present_nodes] : group_nodes) {
        result.groups_checked++;

        if (present_nodes.size() >= n) {
            continue;
        }

        std::cerr << "[SCRUB] group " << gid << " present on "
                  << present_nodes.size() << "/" << n
                  << " nodes — repairing\n";

        try {
            auto data = pclient_->retrieve(gid);
            if (data.empty()) {
                std::cerr << "[SCRUB] group " << gid
                          << " — retrieve returned empty, skipping\n";
                result.groups_failed++;
                continue;
            }

            pclient_->store(gid, data.data(), data.size());
            result.groups_repaired++;
            std::cerr << "[SCRUB] group " << gid << " — repaired\n";
        } catch (const std::exception& e) {
            std::cerr << "[SCRUB] group " << gid
                      << " — repair failed: " << e.what() << "\n";
            result.groups_failed++;
        }
    }

    // 3. Vacuum all nodes to reclaim space
    if (result.groups_repaired > 0) {
        pclient_->vacuum_all_nodes();
    }

    std::cerr << "[SCRUB] complete: checked=" << result.groups_checked
              << " repaired=" << result.groups_repaired
              << " failed=" << result.groups_failed << "\n";

    return result;
}

} // namespace mediadb
