#pragma once
/// TRACES: AR-024, AR-025 | SR-002
///
/// EvidenceDiscounter — how much a single observation is allowed to move a
/// track's belief.
///
/// **The independence problem.** Per-frame identity evidence is accumulated as
/// log-odds along a track (AR-025), which is only valid for *independent*
/// observations. Consecutive frames of one track are nothing of the kind: near
/// identical pose, lighting and expression. Treating them as independent drives
/// the posterior to certainty on what is effectively one measurement — thirty
/// frames of the same face at the same angle is not thirty pieces of evidence.
///
/// The mitigation is to weight each observation by how much it *adds*: a view
/// the track has already contributed is discounted toward zero, a genuinely new
/// pose counts in full. This reuses the same judgement the diversity buffer
/// makes for gallery expansion (AR-019) — which embeddings on a track are
/// mutually distinct — rather than inventing a second notion of novelty.
///
/// Owned by TrackRegistry rather than left to callers. A caller that forgot to
/// discount, or applied it twice, would silently produce confident wrong
/// answers, and the registry is the one place where all evidence converges.
///
/// **Similarity enters as a calibrated probability, never a raw cosine**
/// (AR-024): "is this the same view" is a decision, and a bare cosine threshold
/// means something different for every model and every face size.

#include "types.hpp"

#include <algorithm>
#include <cmath>
#include <functional>
#include <vector>

class EvidenceDiscounter {
public:
    /// cosine similarity → P(same view). Supplied by the caller so the
    /// calibration fitted for the active embedder is used (AR-023/AR-024).
    using Calibrate = std::function<float(float)>;

    struct Config {
        int   max_views{8};        ///< distinct views remembered per track
        float admit_below{0.6f};   ///< P(same view) under this ⇒ a new view
        float floor{0.0f};         ///< minimum weight for a redundant observation
    };

    // Two constructors rather than a defaulted argument: `Config{}` as a default
    // argument would reference Config's own member initializers before the
    // enclosing class is complete, which is ill-formed.
    explicit EvidenceDiscounter(Calibrate cal)
        : cal_(std::move(cal)), cfg_() {}

    EvidenceDiscounter(Calibrate cal, Config cfg)
        : cal_(std::move(cal)), cfg_(cfg) {}

    /// Weight in [0,1] for one observation, updating `views` when the
    /// observation is novel enough to count as a distinct look at the subject.
    ///
    /// The first observation on a track always counts in full: there is nothing
    /// for it to be redundant with.
    float weight(std::vector<Embedding>& views, const Embedding& e) const {
        if (views.empty()) {
            views.push_back(e);
            return 1.0f;
        }

        float p_same = 0.0f;
        for (const auto& v : views)
            p_same = std::max(p_same, cal_(cosine_similarity(v, e)));

        // Weight is the probability this is *not* a repeat of something already
        // counted. A near-duplicate contributes ~0; an unseen pose ~1.
        const float w = std::max(cfg_.floor, 1.0f - p_same);

        if (p_same < cfg_.admit_below &&
            static_cast<int>(views.size()) < cfg_.max_views) {
            views.push_back(e);
        }
        return w;
    }

private:
    Calibrate cal_;
    Config    cfg_;
};
