// render_bounds_gate.cpp — see the header.

#include "shell/capture/render_bounds_gate.h"

#include <cmath>
#include <cstdint>
#include <filesystem>
#include <system_error>
#include <vector>

#include "core/capture/render_settings.h"
#include "core/capture/render_window.h"
#include "core/capture/wav_codec.h"
#include "core/util/file_bytes.h"

namespace reasampler::capture {

namespace {

// Deliberately a sibling of the bank folder, never inside it: prune enumerates the bank
// directory, and a refused render must be reachable by the person debugging it and by
// nothing else.
constexpr const char* kRefusedSubfolder = "reasampler_refused";

// Moves a refused render out of the bank and returns the sentence naming where it went.
// A failed move leaves the file where the renderer wrote it and says so — never a path
// that does not exist.
std::string retainRefusedRender(const std::string& renderedPath,
                                const std::string& projectDir) {
    namespace fs = std::filesystem;
    std::error_code ec;
    const std::string dir = projectDir + "/" + kRefusedSubfolder;
    fs::create_directories(dir, ec);
    if (!ec) {
        const std::string dest =
            dir + "/" + fs::path(renderedPath).filename().string();
        fs::rename(renderedPath, dest, ec);
        if (!ec)
            return " Nothing was added to the bank. The refused render was KEPT for "
                   "diagnosis at " + dest + " -- outside the bank, indexed by nothing; "
                   "delete it when done.";
    }
    return " Nothing was added to the bank. The refused render was kept for diagnosis "
           "but could not be moved out of the bank folder; it is still at " +
           renderedPath + ", indexed by nothing. Delete it when done.";
}

// ProjectMedia is the project's own media, never the bank's (docs/product/render-in-place.md
// "Where the file goes") -- a refusal takes no custody of it. No move, no bank folder, no
// mention of a bank the render was never headed for.
std::string leaveRefusedRenderInPlace(const std::string& renderedPath) {
    return " The render was left where it was written, at " + renderedPath +
           " -- delete it when done.";
}

// Dispatches the refusal's file-handling sentence by destination, so both verdict
// functions below state one true thing about the file rather than the bank sentence
// on every destination.
std::string refusalOutcome(const std::string& renderedPath,
                           const std::string& projectDir,
                           CaptureDestination destination) {
    if (destination == CaptureDestination::ProjectMedia)
        return leaveRefusedRenderInPlace(renderedPath);
    return retainRefusedRender(renderedPath, projectDir);
}

}  // namespace

std::string refusedRenderFolder(const std::string& projectDir) {
    return projectDir + "/" + kRefusedSubfolder;
}

BoundsVerdict checkRenderedFileNotEmpty(const std::string& renderedPath,
                                        const std::string& projectDir,
                                        CaptureDestination destination) {
    BoundsVerdict v;
    std::error_code ec;
    const std::uintmax_t size = std::filesystem::file_size(renderedPath, ec);
    if (ec || size != 0) return v;  // stat failure isn't this check's job — leave it be

    v.refused = true;
    v.message = "Render at " + renderedPath + " is 0 bytes -- REAPER produced an empty "
                "file, so there is nothing to check the requested range against." +
                refusalOutcome(renderedPath, projectDir, destination);
    return v;
}

BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
                                  const std::string& projectDir,
                                  const CaptureRequest& request) {
    BoundsVerdict v;
    if (request.tailMode != TailMode::None) return v;

    // Whole-file read, not a bounded/header-only one: parseWavLayout marks the data
    // chunk valid only when the buffer holds its FULL declared body, so a truncated
    // read would read as invalid on every real capture — and invalid refuses here.
    const WavLayout layout = parseWavLayout(util::readFileBytes(renderedPath));

    // Why the refusal names the render source at all: render_settings.h's renderSourceLabel.
    const std::string source =
        std::string(" Render source: ") + renderSourceLabel(request.sourceMode) + ".";

    // An unmeasurable render used to SKIP this gate and land in the bank with an
    // unknown channel count. It is a refusal now: exact bounds cannot be asserted over
    // a file whose frames were never counted.
    if (!layout.valid || layout.sampleRate == 0) {
        v.refused = true;
        v.message = "Render at " + renderedPath + " could not be measured -- it could "
                    "not be read (locked, missing, or a permissions error), its WAV "
                    "header did not parse, or it declared no sample rate -- so the "
                    "frames it holds were never checked against the requested range." +
                    source + refusalOutcome(renderedPath, projectDir, request.destination);
        return v;
    }

    const int rate = static_cast<int>(layout.sampleRate);
    const long long expectedFrames =
        frameCountFor(request.startSeconds, request.endSeconds, rate);
    const long long actualFrames = static_cast<long long>(layout.frameCount());
    if (renderHonoredBounds(expectedFrames, actualFrames)) return v;

    // Says whether this shortfall has the known shape: the END alone floored to the
    // millisecond, which is what REAPER's render was measured doing. Checked against the
    // END only -- a refusal whose START is also off-grid and independently floored would
    // not match this shape, and this note's silence on that refusal is this check not
    // covering it. Excludes 0, which every sub-millisecond window (a legitimate day-one
    // capture) also floors to, and which would otherwise match a render that produced
    // nothing.
    const long long msFlooredEnd =
        msFlooredEndFrameCount(request.startSeconds, request.endSeconds, rate);
    const std::string msNote =
        (msFlooredEnd > 0 && actualFrames == msFlooredEnd)
            ? " Those are exactly the frames this window holds with its end floored to"
              " the millisecond -- the shape REAPER's render was measured producing."
            : std::string();

    v.refused = true;
    v.message = "Render produced " + std::to_string(actualFrames) +
                " frames but the requested range is " + std::to_string(expectedFrames) +
                " at " + std::to_string(rate) +
                " Hz -- the render did not honor the requested bounds." + source +
                " Requested [" + std::to_string(request.startSeconds) + "s, " +
                std::to_string(request.endSeconds) + "s) -> frame indices [" +
                std::to_string(std::llround(request.startSeconds * rate)) + ", " +
                std::to_string(std::llround(request.endSeconds * rate)) + ")." +
                msNote + refusalOutcome(renderedPath, projectDir, request.destination);
    return v;
}

}  // namespace reasampler::capture
