// sae_audio — Python module wrapping the v1 audio signature (audio_signature.*).
//
/// TRACES: IR-004, IR-005 | SR-003
//
// Exists so a study or a test can drive the **shipped** signature code from
// Python instead of porting the DSP to numpy. A numpy port would be a third
// implementation of a fingerprint that only works if every implementation
// agrees byte for byte, and it would be the one nobody checks against the
// golden vector — so the offset-recovery validation (VR-014) calls this.
//
// Bound with nanobind, as `sae_embed` and `sae_kpn` are. Not pybind11: a second
// binding framework in one build is a second set of ABI and lifetime rules to
// get right, for a module that needs nothing nanobind lacks.
//
// The module deliberately stops at the producer's edge. Matching — sliding one
// signature against another and scoring the overlap — is the *consumer's*
// algorithm (server SPEC §3, and the jRay plugin implements it), so it is not
// bound here and a caller writing a slide in numpy is not re-implementing
// anything this repo owns.

#include "audio_signature.hpp"

#include <nanobind/nanobind.h>
#include <nanobind/ndarray.h>
#include <nanobind/stl/optional.h>
#include <nanobind/stl/pair.h>
#include <nanobind/stl/string.h>
#include <nanobind/stl/vector.h>

#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace nb = nanobind;
using namespace nb::literals;
using namespace sae::audio;

namespace {

using MonoArray = nb::ndarray<const float, nb::ndim<1>, nb::c_contig, nb::device::cpu>;

// Hand the vector's buffer to Python without copying 1.3 M samples, and let a
// capsule own it: the array outlives this call, so the storage has to as well.
nb::object own_as_ndarray(std::vector<float>&& samples) {
    auto* held = new std::vector<float>(std::move(samples));
    nb::capsule owner(held, [](void* p) noexcept {
        delete static_cast<std::vector<float>*>(p);
    });
    const std::size_t n = held->size();
    return nb::cast(nb::ndarray<nb::numpy, float, nb::ndim<1>>(held->data(), {n}, owner));
}

std::vector<float> to_vector(const MonoArray& a) {
    return std::vector<float>(a.data(), a.data() + a.shape(0));
}

}  // namespace

NB_MODULE(sae_audio, m) {
    m.doc() =
        "JRay v1 audio signature (JRay-public-server SPEC.md section 3), as the "
        "extraction pipeline computes it. The constants below are the contract: "
        "changing any of them is a v1 -> v2 change.";

    m.attr("sample_rate") = kSampleRate;
    m.attr("frame_size") = kFrameSize;
    m.attr("hop_size") = kHopSize;
    m.attr("num_bands") = kNumBands;
    m.attr("band_lo_hz") = kBandLoHz;
    m.attr("band_hi_hz") = kBandHiHz;
    m.attr("window_sec") = kWindowSec;
    m.attr("window_samples") = kWindowSamples;
    m.attr("expected_frames") = kExpectedFrames;
    m.attr("version_prefix") = std::string(kVersionPrefix);

    m.def(
        "compute_signature",
        [](const std::string& path) { return compute_signature(path); },
        "path"_a,
        "Signature of the 120 s window centred on the media's midpoint, or None "
        "for media shorter than the window (IR-007), media with no audio "
        "stream, and any decode failure — degradation, never an exception.");

    m.def(
        "decode_centre_window",
        [](const std::string& path) -> nb::object {
            std::optional<std::vector<float>> mono = decode_centre_window(path);
            if (!mono) {
                return nb::none();
            }

            return own_as_ndarray(std::move(*mono));
        },
        "path"_a,
        "The decoded centre window as float32 mono at 11025 Hz, or None. Exposed "
        "so a caller can slice or perturb real audio and re-sign it without "
        "going back through a container.");

    m.def(
        "signature_from_mono",
        [](const MonoArray& mono) { return signature_from_mono(to_vector(mono)); },
        "mono"_a,
        "Signature of mono float32 samples already at 11025 Hz, in [-1, 1). None "
        "when fewer than one whole frame is given.");

    m.def(
        "pack_frames",
        [](const MonoArray& mono) {
            std::vector<std::uint8_t> packed = pack_frames(to_vector(mono));
            return nb::bytes(reinterpret_cast<const char*>(packed.data()), packed.size());
        },
        "mono"_a,
        "One packed byte per whole STFT frame: (band << 2) | energy_class. This "
        "is the payload the signature base64-encodes.");

    m.def(
        "band_fft_bins",
        [] {
            const auto& table = band_fft_bins();
            return std::vector<std::pair<int, int>>(table.begin(), table.end());
        },
        "The half-open FFT bin range owned by each of the 32 log-spaced bands.");
}
