// See instrument_bake.h.

#include "shell/instrument/instrument_bake.h"

#include <cstdint>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <vector>

#include "core/capture/wav_codec.h"                 // buildFloat32Wav (the bank byte format)
#include "core/instrument/bake/bake_plan.h"
#include "core/instrument/bake/bake_render.h"
#include "core/instrument/bake/bake_reset.h"
#include "core/instrument/note/tempo.h"
#include "core/wire/bake_wire.h"
#include "ext_keys.h"
#include "shell/instrument/reaper_bridge.h"
#include "shell/instrument/reasampler_processor.h"

namespace reasampler::vst {

using capture::buildFloat32Wav;
using instrument::bake::defaultBakeProgram;
using instrument::bake::planBake;
using instrument::bake::renderBake;
using instrument::bake::resetAfterBake;
using instrument::map::SampleRefEntry;
using instrument::map::SelectedSample;
using instrument::note::Tempo;
using instrument::note::Velocity;
using instrument::note::resolveNote;
using wire::BakeOutcome;
using wire::BakeRequest;
using wire::BakeStatus;

namespace {

namespace fs = std::filesystem;

// Deletes the staged file on EVERY exit path, success or failure — the same stack-RAII
// discipline the capture shell's FX-bypass guard follows. On success the extension has
// already COPIED the bytes into the bank, so the delete here is what keeps the temp from
// outliving the click. Best-effort: a file already gone is not an error.
//
// Orphan policy: a crash between the write and the invoke leaves one file in the OS temp
// directory, which is exactly what that directory is swept for. Nothing here scans or
// deletes files it did not itself create.
class StagedFileGuard {
public:
    explicit StagedFileGuard(std::string path) : path_(std::move(path)) {}
    ~StagedFileGuard() {
        if (path_.empty()) return;
        std::error_code ec;
        fs::remove(path_, ec);
    }
    StagedFileGuard(const StagedFileGuard&) = delete;
    StagedFileGuard& operator=(const StagedFileGuard&) = delete;

private:
    std::string path_;
};

// Clears the request key on every exit path. A key left holding a request would be picked
// up by the next bake's landing pass and re-run against a temp file that no longer exists.
class RequestKeyGuard {
public:
    RequestKeyGuard(ReaperBridge& bridge, std::string key)
        : bridge_(bridge), key_(std::move(key)) {}
    ~RequestKeyGuard() { bridge_.writeBakeExtState(key_, ""); }
    RequestKeyGuard(const RequestKeyGuard&) = delete;
    RequestKeyGuard& operator=(const RequestKeyGuard&) = delete;

private:
    ReaperBridge& bridge_;
    std::string key_;
};

BakeChainResult fail(std::string message) {
    return BakeChainResult{false, std::move(message)};
}

bool writeFileBytes(const std::string& path, const std::vector<std::uint8_t>& bytes) {
    std::ofstream f(path, std::ios::binary | std::ios::trunc);
    if (!f) return false;
    f.write(reinterpret_cast<const char*>(bytes.data()),
            static_cast<std::streamsize>(bytes.size()));
    return f.good();
}

}  // namespace

bool bakeAvailable(ReaperBridge& bridge) {
    return bridge.isConnected() &&
           bridge.extensionActionAvailable(wire::bakeActionLookupName());
}

BakeChainResult runBake(ReaSamplerProcessor& processor) {
    ReaperBridge& bridge = processor.bridge();
    if (!bakeAvailable(bridge))
        return fail("resample needs the ReaSampler extension loaded");

    const std::string selectionId = processor.selectedSampleId();
    if (selectionId.empty()) return fail("nothing loaded to resample");

    // The whole ref entry, not just its SelectedSample: the display name that the new
    // capture's is derived from sits beside the intrinsics.
    const instrument::map::SampleRefs refs = processor.sampleRefs();
    const SampleRefEntry* sourceEntry = nullptr;
    for (const SampleRefEntry& e : refs)
        if (e.sampleId == selectionId) { sourceEntry = &e; break; }
    if (!sourceEntry) return fail("the loaded capture has no resolvable file");
    const SelectedSample* source = &sourceEntry->ref;

    const int sampleRate = static_cast<int>(processor.sampleRate());
    if (sampleRate <= 0) return fail("the host has not reported a sample rate yet");

    const std::optional<Tempo> tempo = Tempo::fromBpm(bridge.projectTempoBpm());
    if (!tempo) return fail("the project tempo could not be read");

    // Fold anything the host's automation wrote into the model FIRST: the bake renders the sound
    // the user approved, and an automated value the model has not picked up yet is part of it.
    processor.drainAutomationToModel();
    const InstrumentParams dialed = processor.instrumentParams();
    const int rootNote = dialed.rootOverride ? *dialed.rootOverride : source->rootNote;

    // The snapshot comes first: the default program's window is derived from the sound it
    // carries (a Gate release, a Trigger play span), not from a constant.
    std::optional<SampleData> snapshot = processor.bakeSnapshot();
    if (!snapshot) return fail("the loaded capture could not be decoded for the render");

    // The note fires at the velocity the user has been auditioning at: three velocity curves
    // are live, so a sound dialed at 127 does not bake as one dialed at 40.
    const Velocity velocity = Velocity::of(processor.previewVelocity());
    const instrument::bake::PlannedBake planned = planBake(
        resolveNote(defaultBakeProgram(*snapshot, sampleRate, dialed.bakeHold, velocity),
                    *tempo),
        sampleRate, rootNote);
    if (!planned.plan) {
        return fail(planned.refusal == instrument::bake::BakeRefusal::PastFrameCeiling
                        ? "the dialed sound is longer than one bake can hold"
                        : "the programmed capture window is empty");
    }
    const instrument::bake::BakePlan& plan = *planned.plan;

    const instrument::bake::BakeAudio audio =
        renderBake(std::move(*snapshot), plan, processor.masterGainLinear(),
                   dialed.limiterEnabled);
    if (audio.empty()) return fail("the offline pass produced no audio");

    // buildFloat32Wav takes doubles and narrows; the narrowing back to float is the bank's
    // own 32-bit-float contract, so the round trip is exact.
    std::vector<double> interleaved(audio.interleaved.begin(), audio.interleaved.end());
    const std::vector<std::uint8_t> bytes =
        buildFloat32Wav(audio.channelCount, static_cast<std::uint32_t>(audio.sampleRate),
                        static_cast<std::size_t>(audio.frameCount()), interleaved);

    const std::string instanceGuid = processor.usageInstanceGuid();
    // Named for what it is used for here — the staged file's own name, nothing else.
    // `request.generation` below takes its OWN, later timestamp: kMaxRequestAgeSeconds
    // is a budget measured from the write, and re-using this one would silently spend it
    // on the WAV write that happens in between.
    const std::int64_t stageStamp = static_cast<std::int64_t>(std::time(nullptr));

    // OUTSIDE the bank folder, always: the bank holds indexed captures only, and a stray
    // file there would read as a prune orphan.
    std::error_code ec;
    const fs::path stagedPath =
        fs::temp_directory_path(ec) /
        ("reasampler_bake_" + instanceGuid + "_" + std::to_string(stageStamp) + ".wav");
    if (ec) return fail("no writable temp directory for the staged render");
    const std::string staged = stagedPath.string();
    StagedFileGuard stagedGuard(staged);
    if (!writeFileBytes(staged, bytes)) return fail("could not stage the rendered file");

    BakeRequest request;
    request.instanceGuid = instanceGuid;
    request.stagedFilePath = staged;
    request.sourceSampleId = selectionId;
    request.sourceRelativePath = source->relativePath;
    request.sourceDisplayName = sourceEntry->displayName;
    request.ownUsageKey = usageKeyFor(instanceGuid);
    request.rootNote = plan.note;
    // Stamped here, immediately before the publish below — AFTER the WAV write above,
    // which is what makes kMaxRequestAgeSeconds' budget (bake_wire.h) exclude staging
    // time rather than eat into it.
    request.generation = static_cast<std::int64_t>(std::time(nullptr));

    const std::string key = bakeKeyFor(instanceGuid);
    RequestKeyGuard keyGuard(bridge, key);
    // The bridge PROVES this by reading the key back, so a false here is real: the
    // extension would find nothing to land. It does not say which of the three ways failed,
    // so neither does this sentence.
    if (!bridge.writeBakeExtState(key, wire::encodeBakeRequest(request)))
        return fail("the bake request under " + key +
                    " could not be confirmed -- it was either never written or did not "
                    "read back as written, so the extension has nothing to land");

    // Synchronous: the extension's landing runs to completion inside this call and writes
    // its outcome back over the same key before returning.
    if (!bridge.invokeExtensionAction(wire::bakeActionLookupName()))
        return fail("the ReaSampler extension's bake action is not registered");

    // What the key holds now is the only evidence this side gets, and each of the five
    // non-answers is a different thing to go fix — collapsing them into one sentence is
    // what made a stale install indistinguishable from a refusal. All five name the KEY,
    // because the landing prints one console line per key it scanned and the key is what
    // correlates the two in a multi-instance session.
    const wire::BakeAnswer answer =
        wire::classifyBakeAnswer(bridge.readReasamplerExtState(key), request);
    switch (answer.kind) {
        case wire::BakeAnswerKind::Answered:
            break;
        case wire::BakeAnswerKind::Unanswered:
            return fail("the ReaSampler extension did not answer " + key +
                        " -- that key still holds this exact request, untouched. The "
                        "landing action prints one REAPER console line per key it "
                        "scanned; look for that key name there");
        case wire::BakeAnswerKind::Undecodable:
            return fail("the value under " + key +
                        " is neither a request nor an answer this build can read -- the "
                        "extension and ReaSampler 9000 may be from different builds");
        case wire::BakeAnswerKind::Cleared:
            return fail("the bake key " + key +
                        " came back empty -- either the request was cleared before an "
                        "answer was written, or this build could not read whatever was "
                        "there. Look for that key name in the landing action's console "
                        "lines");
        case wire::BakeAnswerKind::ForeignRequest:
            return fail("the bake key " + key +
                        " held a different pending request instead of an answer -- "
                        "unexpected given the bake's single-threaded call flow; if this "
                        "recurs, note the exact steps and file it");
        case wire::BakeAnswerKind::ForeignOutcome:
            return fail("the answer under " + key +
                        " is for a different bake request than this one");
    }
    // wire::answeredOutcome is the guard, not switch exhaustiveness alone: the switch
    // above has no `default`, so a future BakeAnswerKind enumerator it doesn't yet handle
    // would otherwise fall through to a raw `*answer.outcome` deref with nothing set.
    const BakeOutcome* outcomePtr = wire::answeredOutcome(answer);
    if (!outcomePtr)
        return fail(
            "the extension's answer was not one this build recognizes -- the extension "
            "and ReaSampler 9000 may be from different builds");
    const BakeOutcome& outcome = *outcomePtr;
    if (outcome.status != BakeStatus::Ok)
        return fail(outcome.message.empty() ? std::string("the bake was refused")
                                            : outcome.message);

    SampleRefEntry entry;
    entry.sampleId = outcome.sampleId;
    entry.displayName = outcome.displayName;
    entry.ref.relativePath = outcome.relativePath;
    entry.ref.rootNote = outcome.rootNote;
    entry.ref.channelCount = outcome.channelCount;
    // No loop: the loop points shaped the render and are meaningless against the new file
    // (bake_reset owns that rule for the parameter set; this is its bank-intrinsic peer).

    const instrument::bake::BakeReset reset = resetAfterBake(dialed);
    processor.adoptBakedCapture(entry, reset.params, reset.masterGainLinear);

    // The extension's own wording, which distinguishes the three landings (replaced, added,
    // and pointed at an identical existing entry) more precisely than this side can.
    return BakeChainResult{true, "resampled -- " + outcome.message};
}

}  // namespace reasampler::vst
