// ============================================================================
// tools/eval_threat_vqc.cpp — Offline eval harness for threat_vqc.
//
// Why this exists: threat_vqc runs on real live T1 telemetry every loop
// cycle (main.cpp), but its θ weights were trained on a synthetic proxy
// dataset (see the header comment in src/threat_vqc.cpp) — it has never
// been measured against real, operator-confirmed labels. Per the 2026-07-27
// decision, threat_vqc must NOT be wired into any gating decision until it
// clears a real quality bar on genuine labeled data. This tool is how that
// bar gets measured, honestly, once real data exists.
//
// It links src/threat_vqc.cpp DIRECTLY (same #include-the-.cpp pattern the
// native Unity tests already use — see test/test_batch_c/test_batch_c.cpp)
// so the numbers reported here are computed by the exact scoring code that
// runs on-device, not a reimplementation that could drift from it.
//
// Input: a CSV with one labeled example per row —
//   nll_norm,wfx_occupancy,corrob_norm,rssi_norm,correlator_flag,label
// where each feature is in [0,1] and label is 0 (benign/false-positive) or
// 1 (threat/true-positive). This is exactly the shape GET
// /api/sensor/vqc-training-data on OpenForge C2 returns (feats[5] + label)
// once real vqc-samples have been matched against real triage verdicts —
// export that JSON, reshape to this CSV, and point this tool at it.
//
// Usage:
//   g++ -std=c++17 -O2 -o eval_threat_vqc tools/eval_threat_vqc.cpp
//   ./eval_threat_vqc labeled_data.csv [threshold]
//
// threshold (default 0.5): decision boundary on vqc_score for the
// benign/threat call. Adjustable at the command line so a precision/recall
// tradeoff can be explored without recompiling.
//
// IMPORTANT: this tool computing correctly on a small sanity-check CSV is
// NOT the same as threat_vqc being field-validated. Do not read a clean run
// of this tool as "the classifier works" — read it as "the harness works."
// The actual accuracy claim is only as good as the dataset you feed it, and
// as of 2026-07-27 no real labeled corpus exists yet (see
// GET /api/sensor/vqc-training-data — samples_total/matched will be 0 until
// T1 firmware + T2 gateway are updated and redeployed to actually publish
// vqc samples, which has NOT happened yet; that's a separate go/no-go for
// Mark, not something this tool assumes).
// ============================================================================

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include "../src/threat_vqc.cpp"

struct LabeledExample {
    float feats[5];
    int label; // 0 or 1
};

static std::vector<std::string> splitCsvLine(const std::string& line) {
    std::vector<std::string> out;
    std::stringstream ss(line);
    std::string cell;
    while (std::getline(ss, cell, ',')) out.push_back(cell);
    return out;
}

static bool loadCsv(const std::string& path, std::vector<LabeledExample>& out, std::string& err) {
    std::ifstream f(path);
    if (!f.is_open()) { err = "cannot open file: " + path; return false; }

    std::string line;
    size_t lineNo = 0;
    while (std::getline(f, line)) {
        lineNo++;
        if (line.empty()) continue;
        // Skip an optional header row (non-numeric first cell).
        auto cells = splitCsvLine(line);
        if (cells.size() != 6) {
            if (lineNo == 1) continue; // tolerate a header line
            err = "line " + std::to_string(lineNo) + ": expected 6 columns, got " + std::to_string(cells.size());
            return false;
        }
        try {
            LabeledExample ex{};
            for (int i = 0; i < 5; ++i) {
                ex.feats[i] = std::stof(cells[i]);
                if (ex.feats[i] < 0.0f || ex.feats[i] > 1.0f) {
                    err = "line " + std::to_string(lineNo) + ": feature " + std::to_string(i) + " out of [0,1] range";
                    return false;
                }
            }
            ex.label = std::stoi(cells[5]);
            if (ex.label != 0 && ex.label != 1) {
                err = "line " + std::to_string(lineNo) + ": label must be 0 or 1";
                return false;
            }
            out.push_back(ex);
        } catch (...) {
            if (lineNo == 1) continue; // header row with non-numeric cells
            err = "line " + std::to_string(lineNo) + ": failed to parse numeric fields";
            return false;
        }
    }
    return true;
}

int main(int argc, char** argv) {
    if (argc < 2) {
        std::cerr << "usage: " << argv[0] << " <labeled_data.csv> [threshold=0.5]\n";
        return 2;
    }
    const std::string path = argv[1];
    const float threshold = (argc >= 3) ? std::strtof(argv[2], nullptr) : 0.5f;

    std::vector<LabeledExample> data;
    std::string err;
    if (!loadCsv(path, data, err)) {
        std::cerr << "ERROR: " << err << "\n";
        return 1;
    }
    if (data.empty()) {
        std::cerr << "ERROR: no labeled examples loaded from " << path << "\n";
        return 1;
    }

    threat_vqc_init();

    int tp = 0, tn = 0, fp = 0, fn = 0;
    for (const auto& ex : data) {
        float score = threat_vqc_score(ex.feats);
        int pred = (score >= threshold) ? 1 : 0;
        if (pred == 1 && ex.label == 1) tp++;
        else if (pred == 0 && ex.label == 0) tn++;
        else if (pred == 1 && ex.label == 0) fp++;
        else fn++;
    }

    const int n = (int)data.size();
    const double accuracy  = n > 0 ? (double)(tp + tn) / n : 0.0;
    const double precision = (tp + fp) > 0 ? (double)tp / (tp + fp) : 0.0;
    const double recall    = (tp + fn) > 0 ? (double)tp / (tp + fn) : 0.0; // threat recall
    const double f1        = (precision + recall) > 0 ? 2 * precision * recall / (precision + recall) : 0.0;
    const double benignRecall = (tn + fp) > 0 ? (double)tn / (tn + fp) : 0.0;

    std::printf("threat_vqc offline eval — %d labeled examples, decision threshold %.2f\n", n, threshold);
    std::printf("  (computed by the real threat_vqc_score() from src/threat_vqc.cpp — no reimplementation)\n\n");
    std::printf("Confusion matrix:\n");
    std::printf("                 pred_benign  pred_threat\n");
    std::printf("  actual_benign  %10d   %10d\n", tn, fp);
    std::printf("  actual_threat  %10d   %10d\n", fn, tp);
    std::printf("\n");
    std::printf("Accuracy:        %.1f%%\n", accuracy * 100.0);
    std::printf("Precision:       %.1f%%\n", precision * 100.0);
    std::printf("Threat recall:   %.1f%%\n", recall * 100.0);
    std::printf("Benign recall:   %.1f%%\n", benignRecall * 100.0);
    std::printf("F1:              %.3f\n", f1);
    std::printf("\n");
    std::printf("NOTE: this measures whatever dataset you passed in. It is NOT a\n");
    std::printf("field-validation result unless that dataset is real, operator-\n");
    std::printf("confirmed data pulled from GET /api/sensor/vqc-training-data.\n");

    return 0;
}
