// mde_audit — scan a live mediadb cluster for media whose underlying
// data cannot be reconstructed, without downloading full file contents.
//
// Usage: mde_audit [-c yaml:/path/to/config.yaml]
//
// Walks every store -> album -> media entry known to the cluster
// metadata and does a minimal 1-byte fetch_range check against each
// media's "media:<id>" group (this reads only stripe 0, the same
// stripe that showed up missing in production). Prints every item
// that fails so damage from a data-loss incident can be found in one
// pass instead of one broken thumbnail at a time.
//
// Read-only: fetch_range() never mutates cluster state.

#include "../src/cluster.h"
#include "../src/backend.h"

#include <cmdplus/cmdplus.h>
#include <confplus/conf.h>

#include <algorithm>
#include <array>
#include <chrono>
#include <future>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

using namespace mediadb;

namespace {

std::string conf_string(confplus::Config& cfg, const std::string& key, const std::string& fallback) {
    try {
        auto* kd = cfg.getKey(key);
        if (!kd) return fallback;
        const std::string& val = cfg.getValue(kd, 0);
        return val.empty() ? fallback : val;
    } catch (...) {
        return fallback;
    }
}

int conf_int(confplus::Config& cfg, const std::string& key, int fallback) {
    try {
        auto* kd = cfg.getKey(key);
        if (!kd) return fallback;
        return cfg.getIntValue(kd, 0);
    } catch (...) {
        return fallback;
    }
}

} // namespace

int main(int argc, char* argv[]) {
    auto& cmd = cmdplus::CmdController::getInstance();
    cmd.registerCmd("config", 'c', false, "yaml:/etc/mediadb/config.yaml", "Path to config file (e.g. yaml:/path/to/config.yaml)");
    cmd.registerCmd("help",   'h', false, "", "Show this help message");
    cmd.parseCmd(argc, argv);

    if (cmd.getCmdbyKey("help")->getFound()) {
        cmd.printHelp();
        return 0;
    }

    const std::string config_path = cmd.getCmdbyKey("config")->getValue();
    confplus::Config cfg(config_path);

    if (conf_string(cfg, "/MEDIADB/CLUSTER/ENABLE", "false") != "true") {
        std::cerr << "mde_audit: cluster is not enabled in this config — nothing to audit\n";
        return 1;
    }

    ClusterConfig ccfg;
    ccfg.enabled       = true;
    // This tool runs read-only, typically alongside the real mediadb
    // daemon on the same host — it must not bind the cluster port that
    // daemon already owns, nor open its block-store files directly.
    ccfg.client_only   = true;
    ccfg.bind_address  = conf_string(cfg, "/MEDIADB/CLUSTER/BIND", "0.0.0.0");
    ccfg.port          = conf_int(cfg, "/MEDIADB/CLUSTER/PORT", 4433);
    ccfg.cert_file     = conf_string(cfg, "/MEDIADB/HTTP/CERT", "");
    ccfg.key_file      = conf_string(cfg, "/MEDIADB/HTTP/KEY", "");
    ccfg.client_name   = conf_string(cfg, "/MEDIADB/CLUSTER/AUTH_NAME", "");
    ccfg.client_key    = conf_string(cfg, "/MEDIADB/CLUSTER/AUTH_KEY", "");
    ccfg.store_path    = conf_string(cfg, "/MEDIADB/CLUSTER/PATH", "");
    ccfg.data_blocks   = static_cast<size_t>(conf_int(cfg, "/MEDIADB/CLUSTER/DATA_BLOCKS", 2));
    ccfg.parity_blocks = static_cast<size_t>(conf_int(cfg, "/MEDIADB/CLUSTER/PARITY_BLOCKS", 1));

    auto* peersKey = cfg.getKey("/MEDIADB/CLUSTER/PEERS");
    if (peersKey) {
        for (int pi = 0; ; pi++) {
            try {
                std::string peerStr = cfg.getValue(peersKey, pi);
                if (peerStr.empty()) break;
                auto colon = peerStr.rfind(':');
                if (colon != std::string::npos) {
                    ClusterNode node;
                    node.address = peerStr.substr(0, colon);
                    node.port = std::stoi(peerStr.substr(colon + 1));
                    ccfg.peers.push_back(node);
                }
            } catch (...) { break; }
        }
    }

    Cluster cluster;
    cluster.init(ccfg);

    const int cache_size_mb = conf_int(cfg, "/MEDIADB/CACHE/SIZE_MB", 256);
    BlobCache cache(static_cast<std::size_t>(cache_size_mb) * 1024ULL * 1024ULL, blob_value_size);
    auto backend = std::make_unique<ClusterMediaBackend>(cluster, cache);

    std::cerr << "mde_audit: connecting to cluster and waiting for peers...\n";
    cluster.start();
    size_t online = cluster.waitForPeers(120);
    std::cerr << "mde_audit: " << online << "/" << ccfg.peers.size() << " peer node(s) online\n";

    backend->start_sync();
    // Give the initial store/album/media index sync a moment to land
    // before reading it back below.
    std::this_thread::sleep_for(std::chrono::seconds(3));

    // Check the cluster's own metadata groups first — same parity-group
    // mechanics as "media:<id>", just different key names, and just as
    // capable of having been silently destroyed by the same historical
    // bug. Cluster::fetch() can fail *silently* (empty return, no
    // exception) when too few blocks are found, so this is the only way
    // to get a definitive yes/no rather than inferring from sync retries.
    for (const char* key : {"index", "tombstones"}) {
        std::vector<std::uint8_t> out;
        bool ok = false;
        try {
            ok = cluster.fetch(key, out) && !out.empty();
        } catch (...) {
            ok = false;
        }
        std::cout << "metadata key=\"" << key << "\": "
                  << (ok ? ("OK, " + std::to_string(out.size()) + " bytes") : std::string("BROKEN"))
                  << "\n";
    }

    // Gather the full store/album/media list first (cheap local metadata
    // reads) so the actual cluster checks below can run concurrently.
    struct MediaCheck {
        std::string store_id, album_id, media_id, filename;
    };
    std::vector<MediaCheck> to_check;

    auto stores = backend->list_stores();
    for (auto& store : stores) {
        std::vector<std::uint8_t> out;
        bool ok = false;
        try {
            ok = cluster.fetch("store:" + store.id, out) && !out.empty();
        } catch (...) {
            ok = false;
        }
        std::cout << "metadata key=\"store:" << store.id << "\": "
                  << (ok ? ("OK, " + std::to_string(out.size()) + " bytes") : std::string("BROKEN"))
                  << "\n";

        for (auto& album : backend->list_albums(store.id)) {
            for (auto& m : backend->list_media(album.id)) {
                to_check.push_back({store.id, album.id, m.id, m.original_filename});
            }
        }
    }

    std::cerr << "mde_audit: " << stores.size() << " store(s), " << to_check.size()
              << " media item(s) — scanning concurrently (each broken item retries "
              << "for up to ~30s before being counted)\n";

    // Check media items concurrently — matches Cluster::READ_POOL_SIZE, so
    // a handful of fetches genuinely run in parallel instead of queueing
    // behind each other (a broken item alone can take ~30s of retries).
    static constexpr size_t MAX_CONCURRENT = 4;
    std::mutex result_mutex;
    size_t checked = 0, broken = 0;
    std::vector<std::array<std::string, 4>> broken_items; // store,album,media,filename

    auto check_one = [&](const MediaCheck& mc) {
        std::vector<std::uint8_t> out;
        bool ok = false;
        try {
            ok = cluster.fetch_range("media:" + mc.media_id, 0, 1, out) && !out.empty();
        } catch (...) {
            ok = false;
        }

        std::lock_guard<std::mutex> lk(result_mutex);
        ++checked;
        if (!ok) {
            ++broken;
            broken_items.push_back({mc.store_id, mc.album_id, mc.media_id, mc.filename});
            std::cerr << "  BROKEN: store=" << mc.store_id << " album=" << mc.album_id
                      << " media=" << mc.media_id << " file=\"" << mc.filename << "\"\n";
        }
        if (checked % 100 == 0) {
            std::cerr << "mde_audit: checked " << checked << "/" << to_check.size()
                      << " so far (" << broken << " broken)\n";
        }
    };

    for (size_t next = 0; next < to_check.size(); ) {
        size_t batch_end = std::min(next + MAX_CONCURRENT, to_check.size());
        std::vector<std::future<void>> futures;
        for (size_t i = next; i < batch_end; ++i)
            futures.push_back(std::async(std::launch::async, check_one, std::cref(to_check[i])));
        for (auto& f : futures) f.get();
        next = batch_end;
    }

    std::cout << "\n=== Audit summary ===\n";
    std::cout << "Checked: " << checked << " media item(s)\n";
    std::cout << "Broken:  " << broken << " media item(s)\n";
    if (!broken_items.empty()) {
        std::cout << "\nstore_id,album_id,media_id,original_filename\n";
        for (auto& row : broken_items) {
            std::cout << row[0] << "," << row[1] << "," << row[2] << "," << row[3] << "\n";
        }
    }

    backend.reset();
    cluster.stop();
    return broken == 0 ? 0 : 1;
}
