#pragma once
// bake_wire — the resample bake's request/outcome pair, on ONE per-instance ext-state key
// ("rsbake_<instanceGuid>"). Pure: no REAPER/VST3/SWELL/vendor includes.
//
// A call and a return in one UI tick — deliberately NOT the poller/nonce handshake this
// seam once spiked; nothing here may grow a claim protocol. `generation` is a wall-clock
// stamp: it distinguishes a repeat bake from a leftover, and dates a request the extension
// must refuse rather than answer to nobody.

#include <cstdint>
#include <optional>
#include <string>

namespace reasampler::wire {

// The extension action the instrument invokes, as its command-id suffix. FOREVER-STABLE
// per channel like every other registered suffix. Both artifacts read this one symbol.
inline constexpr const char* kBakeActionSuffix = "RESAMPLE_BAKE";

// The NamedCommandLookup spelling of that action: a LEADING UNDERSCORE the
// rec->Register("command_id", …) string itself does not carry. The one place that
// underscore is written.
std::string bakeActionLookupName();

// What the instrument asks for. Every path is the instrument's own knowledge: it staged
// the file, it knows which capture it was resampling, and it knows its own usage key.
struct BakeRequest {
    std::string  instanceGuid;        // the asking instance ("rsbake_<guid>" names the key)
    std::string  stagedFilePath;      // ABSOLUTE, outside the bank folder; the instrument deletes it
    std::string  sourceSampleId;      // the capture being resampled (lineage parent)
    std::string  sourceRelativePath;  // its project-relative path — the tie query's subject
    std::string  sourceDisplayName;   // what the new entry's name is derived from
    std::string  ownUsageKey;         // "rsusage_<guid>" — excluded from the tie scan
    int          rootNote = 60;
    std::int64_t generation = 0;

    bool operator==(const BakeRequest& o) const;
    bool operator!=(const BakeRequest& o) const { return !(*this == o); }
};

// Why a bake did not land. Values are WIRE INTEGERS — never renumber, only append; an
// unrecognized value decodes as `Failed` so a newer extension's vocabulary cannot make an
// older instrument read a failure as a success.
enum class BakeStatus : int {
    Ok = 0,
    Failed = 1,        // generic / unrecognized
    NoProject = 2,     // unsaved project: the bank has no location
    StagedMissing = 3, // the staged file was gone or unreadable
    NoSource = 4,      // the source capture is not in any bank
    IndexRejected = 5, // the bank refused the add
    WrongProject = 6,  // the request belongs to a project tab this extension has not loaded
};

// What the extension did. On Ok the four sample fields describe the landed entry, so the
// instance can adopt it without a bank read — the same self-contained discipline the
// SampleRefs table exists for.
struct BakeOutcome {
    BakeStatus   status = BakeStatus::Failed;
    std::string  sampleId;
    std::string  relativePath;
    std::string  displayName;
    int          rootNote = 60;
    int          channelCount = 0;
    bool         replaced = false;  // false = a distinct entry was added
    std::string  message;           // human-readable, for the console
    std::int64_t generation = 0;    // echoes the request's

    bool operator==(const BakeOutcome& o) const;
    bool operator!=(const BakeOutcome& o) const { return !(*this == o); }
};

// Length-prefixed fields behind a magic+version tag, the house idiom, so arbitrary bytes
// in a path or a name round-trip whole.
std::string encodeBakeRequest(const BakeRequest& req);
std::string encodeBakeOutcome(const BakeOutcome& outcome);

// std::nullopt on malformed/truncated/trailing-garbage input, and on a tag this build does
// not read — a future version's record is refused rather than half-parsed. Round-trips.
std::optional<BakeRequest> decodeBakeRequest(const std::string& wire);
std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire);

// --- Reading the shared key, from either end -----------------------------------------
//
// One key carries both records, so what it holds after the action returned is the ONLY
// evidence either side gets. The two classifiers below are that reading, stated once.

// What the instrument found under its own key once invokeExtensionAction returned. The
// distinction that matters: `Unanswered` is nothing having WRITTEN over the key, which is
// a DIFFERENT fault from every refusal — a refusal is an outcome. It does NOT mean the
// landing never read the key: a scan that skipped this key, and a scan whose answer write
// was rejected, both leave the request sitting here identically.
enum class BakeAnswerKind {
    Answered,        // a decodable outcome echoing this request's generation
    ForeignOutcome,  // a decodable outcome, but for another generation
    Unanswered,      // this request, unchanged: nothing wrote an outcome over it
    ForeignRequest,  // a request that is not ours — another instance shares this key
    Cleared,         // the key holds nothing — absent, explicitly cleared, or a read
                     // failure at the bridge; these are not distinguishable from here
    Undecodable,     // neither record — a build whose wire this one does not read
};

struct BakeAnswer {
    BakeAnswerKind kind = BakeAnswerKind::Cleared;
    std::optional<BakeOutcome> outcome;  // set iff Answered or ForeignOutcome
};

// `raw` is the key's value after the invocation (nullopt = absent/empty).
BakeAnswer classifyBakeAnswer(const std::optional<std::string>& raw,
                              const BakeRequest& sent);

// The outcome iff `answer.kind == Answered`; nullptr in every other state. The ONE place
// the Answered-implies-outcome-is-set contract is enforced, so a caller can fail closed
// on a state this switch does not (yet) name instead of dereferencing `answer.outcome` on
// the strength of switch exhaustiveness alone — exhaustiveness a future BakeAnswerKind
// enumerator (BakeAnswerKind is documented append-only, like its BakeStatus neighbor)
// would silently break with no compiler diagnostic (no -Wswitch/-Werror configured).
const BakeOutcome* answeredOutcome(const BakeAnswer& answer);

// The whole chain is a call and a return inside ONE editor tick, so a request older than
// this has no reader left. Landing one would bank it for nobody and leave an outcome
// nobody collects in the .rpp forever. The bound is only meaningful because both ends
// read the same wall clock (std::time) AND the instrument stamps `generation`
// immediately before publishing the request — after staging the WAV, so that write
// never eats into the budget.
inline constexpr std::int64_t kMaxRequestAgeSeconds = 30;

// The extension's per-key verdict on one scanned `rsbake_*` key.
enum class BakeScanVerdict {
    Land,               // land it into the loaded project's bank
    RefuseWrongProject, // answer WrongProject — the book in memory belongs to another tab
    ClearStale,         // no reader left: clear the key, never answer it
    IgnoreUnreadable,   // the enumerator listed it, the reader could not return it whole
    IgnoreNotARequest,  // read whole, but an uncollected outcome or a wire we do not read
};

// The session's side of the verdict: whether the loaded project is the one REAPER will
// persist into. Whether a project is loaded at ALL is folded into
// BakeScanKey::matchesLoadedProject below rather than carried as a second flag here — a
// key can only ever match a project that is loaded, so a standalone "session has a
// project" flag on this side could disagree with the key's and describe a state the
// shell can never actually produce.
struct BakeScanContext {
    bool loadedProjectIsActive = false;  // the loaded project is REAPER's active tab
};

// The scanned key's side — per-TAB, which is what makes a request found in a background
// tab decidable without any REAPER type crossing into this module.
struct BakeScanKey {
    bool readable = false;      // the enumerated key's value came back WHOLE
    bool decoded = false;       // that value decoded as a BakeRequest (implies readable)
    std::int64_t generation = 0;
    bool matchesLoadedProject = false;  // this key's tab IS the session's loaded project
                                        // — false whenever the session has no loaded
                                        // project, by construction (see BakeScanContext)
};

BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key,
                                 std::int64_t nowSec);

// What ONE scan pass actually saw, accumulated by the shell as it applies the verdicts
// above. Counts only, so the report below is provable without a DAW. It exists because
// every non-answering verdict leaves the asking instance with the same evidence — its own
// request, untouched — and only these counts say which of them happened.
struct BakeScanTally {
    int tabsScanned = 0;
    int keysFound = 0;      // EVERY `rsbake_*` key enumerated across every open tab —
                            // uncollected outcomes and unreadable values included, not
                            // pending requests only
    int activeTabKeys = 0;  // of those, in REAPER's active tab. NOT "the tab the bake was
                            // fired against": the action is invoked with the instance's
                            // own project as `proj`, and whether REAPER makes that project
                            // current for the action's duration is `[verify — DAW]`
    int unreadable = 0;     // IgnoreUnreadable
    int notARequest = 0;    // IgnoreNotARequest
    int staleCleared = 0;   // ClearStale
    int answered = 0;       // an outcome was QUEUED for write-back (a landing OR a
                            // refusal) — pass-wide, not per key, and not proof the write
                            // reached the project; `writeFailed`/`writeUnproven` are that
    int writeFailed = 0;    // of `answered`, the ones whose value read back as something
                            // other than what was written — an asking instance sees these
                            // as no answer at all
    int writeUnproven = 0;  // of `answered`, the ones whose write-and-check itself failed:
                            // NOT known to have landed and NOT known to have failed
};

// The console summary for one pass. Empty ONLY when the pass answered somebody AND left no
// key unanswered AND every answer was read back from its own key — so the summary's absence
// means nothing went wrong, never that the action did not run. A silent console is NOT
// evidence the landing never ran; nothing here can observe that, and no caller may say it.
std::string describeBakeScan(const BakeScanTally& tally);

// How far a `Land` verdict actually got. `Banked` is reachable ONLY through
// bakeLandingAfterPersist below — a landing may not claim the word on its own, because
// nothing a landing can see says whether the pass's persist ran. What `Banked` claims is
// exactly its `persisted` input and nothing more; this is that claim's one home.
enum class BakeLanding {
    Refused,      // nothing was written and the book is untouched
    Partial,      // it threw AFTER it had begun writing — a file and/or an entry may exist
    Unpersisted,  // it reached the in-memory book; the pass's persist did not report success
    Banked,       // in the book, and the pass issued its bank write into a saved project
};

// The ONE route to `Banked`: every Land verdict's landing is assigned `Unpersisted` and
// then passed through here. The special case this replaces — a dedup hit answered `Banked`
// directly on the grounds that it changed nothing — was wrong precisely when the entry it
// deduped against was one the SAME pass had just added and then failed to persist.
//
// `persisted` is ReaSamplerSession::saveToActiveProject's return: a saved project was
// active and the bank write was ISSUED into it. That is the whole claim — not that REAPER
// took the value, not that any sibling key was written, and not that the .rpp on disk holds
// it, which REAPER writes on the project's own save.
//
// The `Unpersisted` limb survives that narrowing: under a Land verdict the persist's own
// two refusals are already excluded, so what reaches it is a persist that did not RUN —
// bake_land assigns the landing inside a guarded scan and sets its `persisted` local only
// in the block after it, so a throw between the two leaves a landed entry with the flag
// still false.
BakeLanding bakeLandingAfterPersist(BakeLanding landing, bool persisted);

// Whether the write a verdict required actually took. Three states, not two: the read-back
// that proves it can itself fail, and "we could not check" is a different thing to tell the
// user from "it did not land".
enum class BakeWriteProof {
    Confirmed,  // the key read back as exactly the bytes written (absent/empty for a clear)
    Rejected,   // the write did not take: it read back as something else, or it was never
                // issued at all
    Unknown,    // the write was issued and the check itself failed; neither claim available
};

// What ONE scanned key ended the pass in — the per-key half of the tally above, which
// counts but cannot name. Every field but the verdict is verdict-conditional; the phrasing
// function is the one place that pairing is spelled out.
struct BakeKeyOutcome {
    BakeScanVerdict verdict = BakeScanVerdict::IgnoreUnreadable;
    bool oversized = false;  // IgnoreUnreadable only: the value exceeded the read ceiling
                             // rather than reading back empty
    BakeLanding landing = BakeLanding::Refused;  // Land only
    BakeWriteProof proof = BakeWriteProof::Unknown;  // Land / RefuseWrongProject /
                                                     // ClearStale: whether the write this
                                                     // verdict required took
    std::string detail;  // the outcome's own message, so one line per key is
                         // self-contained; empty for the verdicts without one, and empty
                         // wherever the landing clause already makes that same claim
};

// One console line naming a key and what the pass did with it, ends in '\n'. Printed for
// EVERY enumerated key, answered or not, because the counts above cannot tell an instance
// which key was its own — and the key carries the asking instance's guid.
std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcome);

}  // namespace reasampler::wire
