// bake_wire.cpp — see bake_wire.h. Pure: standard library only.

#include "core/wire/bake_wire.h"

#include <utility>

#include "core/version/app_version.h"
#include "core/wire/wire.h"

namespace reasampler::wire {

namespace {

constexpr const char* kRequestMagic = "rsbakereq1";
constexpr const char* kOutcomeMagic = "rsbakeout1";

using wire::putField;
using Cursor = wire::Cursor;

// An unrecognized integer is `Failed`, not a parse error: the two artifacts ship
// independently, and a newer extension naming a failure this build has no word for must
// still read as a failure rather than as Ok (which is what the numeric default would be).
BakeStatus statusFromWire(int raw) {
    switch (static_cast<BakeStatus>(raw)) {
        case BakeStatus::Ok:
        case BakeStatus::Failed:
        case BakeStatus::NoProject:
        case BakeStatus::StagedMissing:
        case BakeStatus::NoSource:
        case BakeStatus::IndexRejected:
        case BakeStatus::WrongProject:
            return static_cast<BakeStatus>(raw);
    }
    return BakeStatus::Failed;
}

std::string countOf(int n, const char* noun) {
    return std::to_string(n) + " " + noun + (n == 1 ? "" : "s");
}

}  // namespace

std::string bakeActionLookupName() {
    return "_" + version::channelCommandId(kBakeActionSuffix);
}

bool BakeRequest::operator==(const BakeRequest& o) const {
    return instanceGuid == o.instanceGuid && stagedFilePath == o.stagedFilePath &&
           sourceSampleId == o.sourceSampleId &&
           sourceRelativePath == o.sourceRelativePath &&
           sourceDisplayName == o.sourceDisplayName && ownUsageKey == o.ownUsageKey &&
           rootNote == o.rootNote && generation == o.generation;
}

bool BakeOutcome::operator==(const BakeOutcome& o) const {
    return status == o.status && sampleId == o.sampleId &&
           relativePath == o.relativePath && displayName == o.displayName &&
           rootNote == o.rootNote && channelCount == o.channelCount &&
           replaced == o.replaced && message == o.message && generation == o.generation;
}

std::string encodeBakeRequest(const BakeRequest& req) {
    std::string out = kRequestMagic;
    putField(out, req.instanceGuid);
    putField(out, req.stagedFilePath);
    putField(out, req.sourceSampleId);
    putField(out, req.sourceRelativePath);
    putField(out, req.sourceDisplayName);
    putField(out, req.ownUsageKey);
    putField(out, std::to_string(req.rootNote));
    putField(out, std::to_string(req.generation));
    return out;
}

std::optional<BakeRequest> decodeBakeRequest(const std::string& wire) {
    Cursor cur(wire);
    if (!cur.literal(kRequestMagic)) return std::nullopt;

    BakeRequest req;
    if (!cur.field(req.instanceGuid)) return std::nullopt;
    if (!cur.field(req.stagedFilePath)) return std::nullopt;
    if (!cur.field(req.sourceSampleId)) return std::nullopt;
    if (!cur.field(req.sourceRelativePath)) return std::nullopt;
    if (!cur.field(req.sourceDisplayName)) return std::nullopt;
    if (!cur.field(req.ownUsageKey)) return std::nullopt;
    if (!cur.fieldInt(req.rootNote)) return std::nullopt;
    if (!cur.fieldInt64(req.generation)) return std::nullopt;

    if (!cur.ok() || !cur.atEnd()) return std::nullopt;
    return req;
}

std::string encodeBakeOutcome(const BakeOutcome& outcome) {
    std::string out = kOutcomeMagic;
    putField(out, std::to_string(static_cast<int>(outcome.status)));
    putField(out, outcome.sampleId);
    putField(out, outcome.relativePath);
    putField(out, outcome.displayName);
    putField(out, std::to_string(outcome.rootNote));
    putField(out, std::to_string(outcome.channelCount));
    putField(out, outcome.replaced ? "1" : "0");
    putField(out, outcome.message);
    putField(out, std::to_string(outcome.generation));
    return out;
}

std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire) {
    Cursor cur(wire);
    if (!cur.literal(kOutcomeMagic)) return std::nullopt;

    BakeOutcome outcome;
    int rawStatus = 0;
    std::string replaced;
    if (!cur.fieldInt(rawStatus)) return std::nullopt;
    if (!cur.field(outcome.sampleId)) return std::nullopt;
    if (!cur.field(outcome.relativePath)) return std::nullopt;
    if (!cur.field(outcome.displayName)) return std::nullopt;
    if (!cur.fieldInt(outcome.rootNote)) return std::nullopt;
    if (!cur.fieldInt(outcome.channelCount)) return std::nullopt;
    if (!cur.field(replaced)) return std::nullopt;
    if (!cur.field(outcome.message)) return std::nullopt;
    if (!cur.fieldInt64(outcome.generation)) return std::nullopt;

    if (!cur.ok() || !cur.atEnd()) return std::nullopt;
    if (replaced != "0" && replaced != "1") return std::nullopt;
    outcome.status = statusFromWire(rawStatus);
    outcome.replaced = (replaced == "1");
    return outcome;
}

BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
                              const BakeRequest& sent) {
    BakeAnswer answer;
    // `raw` folds three distinct bridge outcomes into one — absent, explicitly
    // cleared, and an unreadable/oversized read — because the bridge cannot label
    // which occurred. A caller's message for `Cleared` must not claim more than that.
    if (!raw || raw->empty()) {
        answer.kind = BakeAnswerKind::Cleared;
        return answer;
    }
    if (std::optional<BakeOutcome> outcome = decodeBakeOutcome(*raw)) {
        answer.kind = outcome->generation == sent.generation
                          ? BakeAnswerKind::Answered
                          : BakeAnswerKind::ForeignOutcome;
        answer.outcome = std::move(outcome);
        return answer;
    }
    // Still a request: whether it is OURS is what separates "nothing read this key" from
    // "another instance overwrote it" — a persisted instanceGuid is copyable, so two
    // instances CAN name one key.
    if (const std::optional<BakeRequest> req = decodeBakeRequest(*raw)) {
        answer.kind =
            *req == sent ? BakeAnswerKind::Unanswered : BakeAnswerKind::ForeignRequest;
        return answer;
    }
    answer.kind = BakeAnswerKind::Undecodable;
    return answer;
}

const BakeOutcome* answeredOutcome(const BakeAnswer& answer) {
    if (answer.kind != BakeAnswerKind::Answered || !answer.outcome) return nullptr;
    return &*answer.outcome;
}

BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
                                 std::int64_t nowSec) {
    // Listed by the enumerator but not returned whole by the reader. Distinct from the
    // next case even though both leave the key alone: this one is the extension failing
    // to read a request that may well be there, and it is invisible from the other end.
    if (!key.readable) return BakeScanVerdict::IgnoreUnreadable;

    // Not a request: an outcome the writing instance has not collected yet, or a value
    // from a build we do not read. The writer owns clearing its own key.
    if (!key.decoded) return BakeScanVerdict::IgnoreNotARequest;

    // Either direction, so a clock moved backwards is caught too.
    const std::int64_t age = nowSec - key.generation;
    if (age > kMaxRequestAgeSeconds || age < -kMaxRequestAgeSeconds)
        return BakeScanVerdict::ClearStale;

    // Two facts have to agree before anything may land: the loaded project is the one a
    // persist would write into (loadedProjectIsActive), and this key's tab IS that loaded
    // project (matchesLoadedProject — false by construction whenever no project is
    // loaded, which is what folds the former three-way check into two).
    const bool landable = session.loadedProjectIsActive && key.matchesLoadedProject;
    return landable ? BakeScanVerdict::Land : BakeScanVerdict::RefuseWrongProject;
}

std::string describeBakeScan(const BakeScanTally& t) {
    // `answered` is pass-wide and counts a QUEUED write, so it cannot stand alone: a pass
    // that answered somebody else's key while skipping ours, or that queued our answer and
    // had the write rejected, would otherwise print nothing at all and leave the asking
    // instance with a no-answer and a silent console.
    const int unanswered = t.unreadable + t.notARequest + t.staleCleared;
    if (t.answered > 0 && unanswered == 0 && t.writeFailed == 0 && t.writeUnproven == 0)
        return {};

    std::string s = "ReaSampler resample: the landing action ran and scanned " +
                    countOf(t.tabsScanned, "project tab") + ". ";
    if (t.keysFound == 0)
        return s +
               "No rsbake_ key was visible to it in any of them. That fits a request "
               "published into a project this scan did not enumerate, a key cleared "
               "before the scan reached it, or a key set this session that the "
               "enumeration cannot yet see -- this pass cannot tell which apart.\n";

    s += "It found " + countOf(t.keysFound, "rsbake_ key") +
         " (every such key, not pending requests only), " +
         std::to_string(t.activeTabKeys) + " of them in REAPER's active tab. It answered " +
         std::to_string(t.answered);
    if (unanswered > 0) {
        s += " and left " + std::to_string(unanswered) + " unanswered";
        if (t.unreadable > 0)
            s += "; " + std::to_string(t.unreadable) + " could not be read back";
        if (t.notARequest > 0)
            s += "; " + std::to_string(t.notARequest) +
                 " held something other than a request";
        // "were cleared" would assert a write this count cannot see: whether each clear
        // actually took is a per-KEY read-back, and describeBakeKey is where it is said.
        if (t.staleCleared > 0)
            s += "; " + std::to_string(t.staleCleared) +
                 " were past the age bound, so no reader was left to answer";
    }
    s += ".";
    if (t.writeFailed > 0)
        s += " " + countOf(t.writeFailed, "answer") +
             " could not be written back into the project, so whoever asked for them sees "
             "no answer at all.";
    if (t.writeUnproven > 0)
        s += " " + countOf(t.writeUnproven, "answer") +
             " could not be checked after writing, so whether they reached the project is "
             "unknown.";
    return s + "\n";
}

BakeLanding bakeLandingAfterPersist(BakeLanding landing, bool persisted) {
    if (landing == BakeLanding::Unpersisted && persisted) return BakeLanding::Banked;
    return landing;
}

namespace {

// The Land verdict's own clause, with its own fail-closed default — an unnamed enumerator
// must not fall out empty and get reported by the caller as an unnamed VERDICT, which is a
// different gap in a different enum.
std::string describeLanding(BakeLanding landing) {
    switch (landing) {
        case BakeLanding::Banked:
            // Bounded by what `persisted` actually observed (see BakeLanding): a saved
            // project was active and the write was issued. The trailing clause is an
            // instruction, not a claim about the .rpp's current contents.
            return "landed into the bank, and this pass issued its bank write into the "
                   "saved project -- save the project to keep it";
        case BakeLanding::Unpersisted:
            // Says nothing about what the project holds: the persist may never have run at
            // all, and a dedup hit's target may have been in the project since long before
            // this pass.
            return "landed into the bank IN MEMORY ONLY -- this pass's persist did not "
                   "report success, so nothing here can say the project carries it";
        case BakeLanding::Partial:
            return "the landing failed after it had begun writing -- it may have left a "
                   "file in the bank folder and an entry in memory";
        case BakeLanding::Refused:
            return "the landing was refused";
    }
    return "a landing state this build has no word for";
}

// The write-back's own clause, tail of every line that made one.
std::string describeProof(BakeWriteProof proof) {
    switch (proof) {
        case BakeWriteProof::Confirmed:
            return ". The answer was written back.\n";
        case BakeWriteProof::Rejected:
            return ". The answer could NOT be written back -- the instance that asked will "
                   "report no answer.\n";
        case BakeWriteProof::Unknown:
            break;
    }
    return ". Whether the answer reached the key is unknown -- the write-and-check itself "
           "failed.\n";
}

}  // namespace

std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcome) {
    const std::string head = "ReaSampler resample: " + key + " -- ";
    std::string clause;  // set only by the verdicts that write something back
    switch (outcome.verdict) {
        case BakeScanVerdict::IgnoreUnreadable:
            return head + (outcome.oversized
                               ? "skipped: its value was too large to read back whole.\n"
                               : "skipped: the enumerator listed it but it read back "
                                 "empty.\n");
        case BakeScanVerdict::IgnoreNotARequest:
            return head +
                   "skipped: it holds something other than a pending request -- an answer "
                   "nobody has collected, or a wire this build does not read.\n";
        case BakeScanVerdict::ClearStale: {
            // The clear is a write like any other, so it is claimed only where it was read
            // back: an unconfirmed one leaves a value standing for the next pass.
            const std::string aged = head + "past the age bound with no reader left";
            switch (outcome.proof) {
                case BakeWriteProof::Confirmed:
                    return aged + ", so it was cleared unanswered.\n";
                case BakeWriteProof::Rejected:
                    // Not "could not be read back": Rejected also covers a clear that was
                    // never issued, where no read happened at all. What holds either way is
                    // the consequence.
                    return aged +
                           ", but the clear did NOT take -- the key still holds a value and "
                           "the next pass will see it again.\n";
                case BakeWriteProof::Unknown:
                    break;
            }
            return aged +
                   "; whether the clear took is unknown -- the write-and-check itself "
                   "failed.\n";
        }
        case BakeScanVerdict::RefuseWrongProject:
            clause = "refused";
            break;
        case BakeScanVerdict::Land:
            clause = describeLanding(outcome.landing);
            break;
    }
    // Fails closed on a verdict this build has no word for, the same reason
    // answeredOutcome exists: no -Wswitch is configured, so an appended enumerator would
    // otherwise fall straight into the trailing clause and print half a sentence.
    if (clause.empty()) return head + "a verdict this build has no word for.\n";
    std::string s = head + clause;
    if (!outcome.detail.empty()) s += " (" + outcome.detail + ")";
    return s + describeProof(outcome.proof);
}

}  // namespace reasampler::wire
