#pragma once
// bank_sync — PURE decision logic for the S9 bank-generation change-detection and the
// S8 instrument-side assignment-request consume. NO VST3, NO REAPER, NO SWELL, NO
// vendor/ includes. Standard library only. Unit-tested outside the DAW — the mirror of
// sample_map / bridge_marshal splitting the fiddly, testable arithmetic out of a
// host-facing shell.
//
// WHY IT EXISTS (S9/S8 reader seams). The instrument polls two "reasampler" ext-state
// keys off the audio thread: the S9 bank-generation counter (has the bank changed?) and
// the S8 assignment request (should I switch to a just-ingested sample?). The RAW string
// read crosses the bridge in the shell; every DECISION after — parse the generation
// stamp, decide whether it differs from what we last saw, decide whether a decoded
// assignment request is NEW-and-resolvable-and-worth-applying — is pure and lives here.
//
// The processor shell owns the cadence (a UI-thread timer, NEVER process) and the side
// effects (reloadFromBank, setSelectedSampleId); this module owns only the yes/no maths so
// the reader's rules are provable without a host. assignment_request.h owns the WIRE format
// (encode/decode); this module owns the CONSUME decision layered over a decoded request.

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

#include "assignment_request.h"  // AssignmentRequest (the decoded request this consumes)

namespace reasampler::vst {

// The S9 bank-generation "generation 0 = never stamped" default. A project saved before
// S9 shipped carries no bank_generation key; the bridge read yields an absent/empty value
// which parses to this, and the first real bump (>= 1) then reads as a change. Matches the
// writer's monotonic-from-1 counter (the extension bumps to 1 on the first mutation).
inline constexpr std::int64_t kBankGenerationAbsent = 0;

// Parse the raw bank-generation ext-state value the bridge read. The writer stamps a
// non-negative decimal integer (formatBankGeneration). Absent / empty / malformed / negative
// / overflowing all yield kBankGenerationAbsent (0) — the reader treats any unreadable stamp
// as "generation 0", so a pre-S9 or corrupt value is a clean default, never a crash and never
// a spurious reload storm (0 vs a previously-seen 0 is no change). Whole-string parse: trailing
// garbage after the digits rejects the value (returns 0), so a torn/partial write is ignored
// until the next clean poll (the read tolerates staleness by design — it reloads on the NEXT
// poll once the value is clean).
std::int64_t parseBankGeneration(const std::string& raw);

// Format a bank-generation counter for the ext-state stamp. The inverse of
// parseBankGeneration for a non-negative value: a plain decimal, no sign, no padding, so
// the stamp is byte-stable across writes of the same value.
std::string formatBankGeneration(std::int64_t generation);

// Has the bank generation changed since the reader last saw `seen`? True when `current`
// differs from `seen` — the reader then triggers a reload. Any difference counts (not just
// an increase): the writer is monotonic, but a project switch or reload can legitimately
// lower the value, and the reader should re-read the bank in that case too. `seen` starts at
// kBankGenerationAbsent so the first non-zero generation reads as a change (the pre-S9 /
// first-bump refresh the spec requires).
bool bankGenerationChanged(std::int64_t seen, std::int64_t current);

// The verdict of the S8 assignment-request consume decision (below). A pure value the
// processor shell acts on: apply the selection (or not) and advance the consumed marker
// (or not). Distinct booleans because the two are NOT the same event — a request may be
// consumed-as-seen (marker advances) without being applied (it named an unresolvable
// sample and was DROPPED per the reader requirement), so the shell must not re-evaluate it
// every poll.
struct AssignConsumeDecision {
    bool apply = false;                 // set this instance's selection to (bankId, sampleId) + reload
    std::string bankId;                 // the request's bank (valid only when apply)
    std::string sampleId;               // the request's sample (valid only when apply)
    std::int64_t consumedGeneration = 0;  // the marker to persist (== lastConsumed when nothing new)
};

// Decide whether to CONSUME a decoded assignment request (S8 instrument-side reader).
//
//   `request`         — the decoded assignment request (nullopt when the assign_request key
//                       is absent / malformed — nothing pending).
//   `lastConsumed`    — the generation this instance last consumed (persisted in component
//                       state so a re-open does not re-apply a request the user already got,
//                       then manually changed away from). Defaults to 0 for a fresh instance.
//   `resolves`        — whether the request's (bankId, sampleId) resolves to an existing bank
//                       sample RIGHT NOW (the shell computed this against the live bank blob).
//   `isFocusedTarget` — whether THIS instance is the assignment target under the shell's
//                       thundering-herd policy (e.g. only the focused-editor instance applies).
//                       The shell passes true when this instance should act; false suppresses
//                       consumption entirely so a non-target instance neither applies nor
//                       advances its marker (it stays eligible if it later becomes the target).
//
// RULES (all pure, order matters):
//   1. No request, or an OLDER/equal generation (<= lastConsumed): nothing new — do not
//      apply, marker unchanged. (Covers the re-open case: the persisted marker == the
//      request's generation, so it is not re-applied.)
//   2. A NEW request (generation > lastConsumed) but NOT this instance's target: do not
//      apply and do NOT advance the marker — a non-target instance must stay able to consume
//      the request if focus later lands on it. (No thundering herd: only the target acts.)
//   3. A NEW request, this instance IS the target, but the (bankId, sampleId) does NOT
//      resolve: DROP it silently (assignment_request.h reader requirement) — do not apply,
//      but DO advance the marker to the request's generation so a stale/unresolvable request
//      is consumed-as-seen and never re-evaluated (no error state, no selection change).
//   4. A NEW request, target, and resolvable: APPLY (selection <- (bankId, sampleId)) and
//      advance the marker to the request's generation.
//
// The shell then: if apply, setSelectedSampleId + reloadFromBank; always persist
// consumedGeneration into component state when it advanced.
AssignConsumeDecision consumeDecision(const std::optional<AssignmentRequest>& request,
                                      std::int64_t lastConsumed, bool resolves,
                                      bool isFocusedTarget);

}  // namespace reasampler::vst
