#pragma once
/// TRACES: AR-010 | SR-002
///
/// SceneBoundaries — the join point of the decode butterfly.
///
/// The topology forks after decode: one branch runs TransNetV2 over dense
/// frames, the other runs face detection over the sampled cadence. Boundaries
/// found on the first branch have to reach the second, and they cannot be
/// carried in the frames themselves because the branches are parallel.
///
/// **Why this needs a watermark.** TransNetV2 buffers `kWindow` frames before it
/// can score any of them, so at any instant the detector has an opinion about
/// everything up to some time T and nothing after it. Without recording T, a
/// consumer asking "is there a boundary at t?" cannot distinguish *no* from
/// *not yet* — and those demand opposite behaviour. Silently treating unscored
/// frames as boundary-free is exactly the class of failure that makes a
/// verification pass vacuously.
///
/// The consumer is held back by channel depth (see main.cpp) so that by the time
/// it pulls a frame, the detector has already scored past it. `scored_through()`
/// is what lets that assumption be *checked* rather than assumed.

#include <algorithm>
#include <condition_variable>
#include <mutex>
#include <vector>

class SceneBoundaries {
public:
    /// TRACES: AR-011 | SR-002
    /// Peaks closer than this are one boundary.
    ///
    /// Supplied by the detector, derived from the cadence it was actually fed
    /// (SceneDetectorFunc::dedup_window_sec), NOT assumed. It used to be a hard
    /// 0.04 here, and AR-011 is recorded as having replaced that literal --
    /// which it did, but only for scenes.json. This path, the one that feeds
    /// is_scene_boundary into the tracker, kept the constant while the comment
    /// above it claimed "matches the dedup scenes.json applies, so the two
    /// views agree". They did not agree. 0.04 s is one frame at 25 fps and
    /// wider than a frame at 30, so two cuts on consecutive frames merged into
    /// one and the loss was invisible: the pipeline simply saw fewer
    /// boundaries.
    ///
    /// Zero until the detector sets it, which makes the pre-cadence state a
    /// no-op dedup rather than a wrong one -- adjacent peaks stay separate
    /// until there is evidence about how far apart frames are, and is_boundary
    /// absorbs duplicates in its tolerance anyway.
    void set_merge_window(double sec) {
        std::lock_guard<std::mutex> g(mu_);
        merge_sec_ = sec;
    }

    /// Called by the scene detector as each window is scored. `through` is the
    /// timestamp up to which its verdict is now final.
    void publish(const std::vector<double>& ts, double through) {
        {
        std::lock_guard<std::mutex> g(mu_);
        const double merge = merge_sec_;
        // Dedup on insert, matching what scenes.json does at write time. A run
        // of adjacent high-scoring frames is one boundary, not several, and
        // leaving them raw made this view report 357 where the file said 13 —
        // the same event counted many times. Harmless for is_boundary(), which
        // absorbs them in its tolerance, but a count nobody can reconcile with
        // the output file is a bad diagnostic.
        bounds_.insert(bounds_.end(), ts.begin(), ts.end());
        std::sort(bounds_.begin(), bounds_.end());
        bounds_.erase(std::unique(bounds_.begin(), bounds_.end(),
                                  [merge](double a, double b) { return b - a < merge; }),
                      bounds_.end());
        scored_through_ = std::max(scored_through_, through);
        }
        cv_.notify_all();
    }

    /// True if a boundary falls within `tol` of `t`.
    ///
    /// `tol` exists because the two branches sample at different rates: a
    /// boundary found on a dense frame rarely lands exactly on a sampled one.
    /// Half a sample interval is the natural width — it attributes the boundary
    /// to the nearest sampled frame and no further.
    bool is_boundary(double t, double tol) const {
        std::lock_guard<std::mutex> g(mu_);
        auto it = std::lower_bound(bounds_.begin(), bounds_.end(), t - tol);
        return it != bounds_.end() && *it <= t + tol;
    }

    /// The timestamp through which the detector's verdict is final. A consumer
    /// past this point is asking about frames nobody has looked at yet.
    double scored_through() const {
        std::lock_guard<std::mutex> g(mu_);
        return scored_through_;
    }

    /// Block until the detector's verdict covers `t`, or it finishes.
    ///
    /// Channel depth alone does NOT create the required lag: it only holds
    /// frames back when the consumer is slower, and the face branch is roughly
    /// four orders of magnitude faster per frame than TransNetV2. So the join
    /// has to wait explicitly.
    ///
    /// Returns false if the detector finished without ever covering `t`, which
    /// happens for the tail frames after its last full window. The caller must
    /// distinguish that from a genuine "no boundary" rather than assuming.
    bool wait_until_scored(double t) const {
        std::unique_lock<std::mutex> lk(mu_);
        cv_.wait(lk, [&] { return finished_ || scored_through_ >= t; });
        return scored_through_ >= t;
    }

    /// Called when the detector will publish nothing further. Without this the
    /// join would deadlock on the tail: those frames are never covered by a full
    /// window, so waiting for them would wait forever.
    void finish() {
        {
            std::lock_guard<std::mutex> g(mu_);
            finished_ = true;
        }
        cv_.notify_all();
    }

    std::size_t count() const {
        std::lock_guard<std::mutex> g(mu_);
        return bounds_.size();
    }

    /// Consumers that outran the detector. Nonzero means the face branch is not
    /// buffered deeply enough for the detector's window, so some frames were
    /// annotated from an incomplete verdict — a real misconfiguration, and one
    /// that would otherwise be invisible.
    void note_outran() const {
        std::lock_guard<std::mutex> g(mu_);
        ++outran_;
    }
    std::size_t outran() const {
        std::lock_guard<std::mutex> g(mu_);
        return outran_;
    }

private:
    mutable std::mutex              mu_;
    mutable std::condition_variable cv_;
    bool                            finished_{false};
    double              merge_sec_{0.0};   ///< set by the detector; see set_merge_window
    std::vector<double> bounds_;
    double              scored_through_{-1.0};
    mutable std::size_t outran_{0};
};
