// core/json — the ONE hand-rolled JSON lexical layer. Pure: standard library only
// — NO REAPER, NO SWELL, NO VST3.
//
// Owns the lexical half of the house JSON dialect: escape-aware string literals
// (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), bare scalar tokens, number
// parsing (strtod/strtoll, full-token + ERANGE rejection), key+':' consumption,
// unknown-value skipping, and the emit side (escaping, %.17g/%d/%lld rendering,
// the scoped object writer). Domain grammars — which keys exist, what shape each
// value takes — stay in the consumers (bank_model, bank_book, view_mode_model,
// origin_ledger, tail_control).
//
// Byte-compatibility contract (load-bearing): the emit helpers reproduce the prior
// per-module writers EXACTLY — writeEscaped's escape set, %.17g for doubles
// (shortest form that round-trips every IEEE-754 double bit-for-bit), plain
// decimal for ints — so a re-serialized blob is byte-identical to what the
// pre-extraction writers produced. Persisted .rpp ext-state must not shift by a byte.

#pragma once

#include <cstdint>
#include <string>
#include <vector>

namespace reasampler::json {

// -- emit helpers (writer side) ----------------------------------------------

// Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX
// for remaining control chars, everything else verbatim (UTF-8 passes through).
void writeEscaped(std::string& out, const std::string& s);

// Number rendering. %.17g is the shortest form that round-trips every IEEE-754
// double exactly, so deserialize(serialize(x)) == x holds bit-for-bit.
std::string numToStr(double v);
std::string numToStr(std::int64_t v);
std::string numToStr(int v);

// Flat homogeneous arrays: ["a","b"] / [1,2]. Empty vector -> "[]".
void writeStringArray(std::string& out, const std::vector<std::string>& v);
void writeIntArray(std::string& out, const std::vector<int>& v);

// Scoped object writer: appends '{' on construction and '}' on destruction, with
// comma separation handled internally. Nested values are written by keyBegin()
// followed by the caller emitting the value (e.g. a nested Writer scope or an
// array). NOTE the destructor-close means an enclosing scope must END (brace
// block) before the built string is returned — see the NRVO note in the
// consumers' serialize() implementations.
class Writer {
public:
    explicit Writer(std::string& out) : out_(out) { out_ += '{'; }
    ~Writer() { out_ += '}'; }

    Writer(const Writer&) = delete;
    Writer& operator=(const Writer&) = delete;

    // "key":<rawValue> — rawValue appended verbatim (numbers, bools, null,
    // pre-serialized nested blobs).
    void keyRaw(const char* key, const std::string& rawValue) {
        sep();
        writeEscaped(out_, key);
        out_ += ':';
        out_ += rawValue;
    }
    // "key":"value" — value escaped.
    void keyStr(const char* key, const std::string& value) {
        sep();
        writeEscaped(out_, key);
        out_ += ':';
        writeEscaped(out_, value);
    }
    // "key": — caller writes the value immediately after.
    void keyBegin(const char* key) {
        sep();
        writeEscaped(out_, key);
        out_ += ':';
    }

private:
    void sep() {
        if (first_) first_ = false; else out_ += ',';
    }
    std::string& out_;
    bool first_ = true;
};

// -- Reader — the lexical cursor (parser side) -------------------------------
//
// Every method returns false on malformed input and never reads out of bounds.
// Only the subset the house writers emit is supported. The reader borrows the
// input string — it must outlive the Reader.
class Reader {
public:
    explicit Reader(const std::string& s) : s_(s) {}

    bool eof() const { return pos_ >= s_.size(); }
    void skipWs();

    // Consumes `c` (after whitespace). False without advancing past `c` if the
    // next non-ws char differs.
    bool consume(char c);

    // JSON string literal (escapes + \uXXXX incl. surrogate pairs -> UTF-8).
    bool parseString(std::string& out);

    // Bare token (number / true / false / null) up to the next structural char.
    bool parseRawScalar(std::string& out);

    // Numbers: full-token parse; trailing bytes or ERANGE reject. parseInt
    // additionally rejects values outside [INT_MIN, INT_MAX].
    bool parseDouble(double& out);
    bool parseInt64(std::int64_t& out);
    bool parseInt(int& out);

    bool parseBool(bool& out);

    // Peeks for the `null` token; consumes it if present (wasNull=true),
    // otherwise leaves the position untouched (wasNull=false). Returns false
    // only on eof.
    bool expectNullOr(bool& wasNull);

    // An object member key + ':'.
    bool parseKey(std::string& key);

    // Homogeneous arrays. Appends to `out`; empty array is valid.
    bool parseStringArray(std::vector<std::string>& out);
    bool parseIntArray(std::vector<int>& out);

    // Skips one value of any shape (string / object / array / bare scalar) —
    // forward-compat for unknown keys.
    bool skipValue();

    // Captures the raw source text of one value verbatim (string-aware brace
    // matching), so a nested blob can be handed to its own parser — the
    // bank_book -> BankModel::deserialize seam.
    bool captureValue(std::string& raw);

private:
    const std::string& s_;
    std::size_t pos_ = 0;
};

}  // namespace reasampler::json
