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

#include "assignment_request.h"

#include <cstddef>
#include <limits>

namespace reasampler {

namespace {

constexpr const char* kMagic = "rsassign1";

// Append one length-prefixed field:  <decimal-len> ':' <bytes>. Mirror of
// provenance's putField so the two seams share one wire idiom.
void putField(std::string& out, const std::string& field) {
    out += std::to_string(field.size());
    out += ':';
    out += field;
}

// Cursor over the encoded string. All reads are bounds-checked; a short read fails
// the whole parse (ok_ latches false). Mirror of provenance's Cursor, trimmed to the
// three field kinds this record needs.
class Cursor {
public:
    explicit Cursor(const std::string& s) : s_(s) {}

    bool ok() const { return ok_; }
    bool atEnd() const { return pos_ >= s_.size(); }

    // Reads one length-prefixed field into `out`. Fails on a missing ':', an empty or
    // non-numeric length, a length that overflows SIZE_MAX, or a length that runs past
    // the end. The digit count is capped at 20 (the decimal width of SIZE_MAX on a
    // 64-bit host) so a crafted 200-digit length cannot accumulate past SIZE_MAX via
    // repeated multiply. "never UB" promise from the header is upheld here.
    bool field(std::string& out) {
        if (!ok_) return false;
        const std::size_t colon = s_.find(':', pos_);
        if (colon == std::string::npos) return fail();
        if (colon == pos_) return fail();  // empty length token
        // Cap: SIZE_MAX fits in at most 20 decimal digits; a longer run is bogus.
        if (colon - pos_ > 20u) return fail();
        std::size_t len = 0;
        for (std::size_t i = pos_; i < colon; ++i) {
            const char c = s_[i];
            if (c < '0' || c > '9') return fail();
            const std::size_t digit = static_cast<std::size_t>(c - '0');
            // Overflow guard: if len would exceed SIZE_MAX after multiply+add, fail.
            if (len > (std::numeric_limits<std::size_t>::max() - digit) / 10u)
                return fail();
            len = len * 10u + digit;
        }
        const std::size_t start = colon + 1;
        // Guard: start may equal s_.size() (empty remainder), in which case only len==0
        // is valid; start > s_.size() cannot happen (colon < s_.size() by find()).
        // Use subtraction-first form to avoid start+len wrapping on a huge len.
        if (start > s_.size() || len > s_.size() - start) return fail();
        out.assign(s_, start, len);
        pos_ = start + len;
        return true;
    }

    // Reads a length-prefixed field and parses it as a signed 64-bit decimal (an
    // optional leading '-'). Fails on empty, non-digit, trailing bytes, or a value
    // that would overflow INT64_MAX / underflow INT64_MIN. The digit count is capped
    // at 19 (the decimal width of INT64_MAX, plus 1 for the optional sign = 20
    // characters maximum) so a crafted 21-digit field cannot accumulate UB. "never UB"
    // promise from the header is upheld: all arithmetic is done on positive digits
    // and capped before applying the sign.
    bool fieldInt64(std::int64_t& out) {
        std::string f;
        if (!field(f)) return false;
        if (f.empty()) return fail();
        std::size_t i = 0;
        bool neg = false;
        if (f[0] == '-') {
            neg = true;
            i = 1;
            if (f.size() == 1) return fail();  // bare "-"
        }
        // Cap at 19 digits (INT64_MAX = 9223372036854775807 — 19 digits). A 20-digit
        // positive value would overflow INT64_MAX; a 20-digit negative might be valid
        // (INT64_MIN = -9223372036854775808) but we conservatively reject it too: the
        // generation field is a unix timestamp, never near INT64 limits in practice.
        if (f.size() - i > 19u) return fail();
        std::int64_t v = 0;
        for (; i < f.size(); ++i) {
            const char c = f[i];
            if (c < '0' || c > '9') return fail();
            const std::int64_t digit = static_cast<std::int64_t>(c - '0');
            // Overflow guard: v * 10 + digit must not exceed INT64_MAX.
            if (v > (std::numeric_limits<std::int64_t>::max() - digit) / 10)
                return fail();
            v = v * 10 + digit;
        }
        out = neg ? -v : v;
        return true;
    }

    // Consumes an exact literal at the cursor (the magic tag). Fails if absent.
    bool literal(const char* lit) {
        if (!ok_) return false;
        std::size_t i = 0;
        for (; lit[i] != '\0'; ++i) {
            if (pos_ + i >= s_.size() || s_[pos_ + i] != lit[i]) return fail();
        }
        pos_ += i;
        return true;
    }

private:
    bool fail() {
        ok_ = false;
        return false;
    }

    const std::string& s_;
    std::size_t        pos_ = 0;
    bool               ok_ = true;
};

}  // namespace

std::string encodeAssignmentRequest(const AssignmentRequest& req) {
    std::string out = kMagic;
    putField(out, req.bankId);
    putField(out, req.sampleId);
    putField(out, std::to_string(req.generation));
    return out;
}

std::optional<AssignmentRequest> decodeAssignmentRequest(const std::string& wire) {
    Cursor cur(wire);
    if (!cur.literal(kMagic)) return std::nullopt;

    AssignmentRequest req;
    if (!cur.field(req.bankId)) return std::nullopt;
    if (!cur.field(req.sampleId)) return std::nullopt;
    if (!cur.fieldInt64(req.generation)) return std::nullopt;

    // Reject trailing garbage: a well-formed value ends exactly at the last field.
    if (!cur.ok() || !cur.atEnd()) return std::nullopt;
    return req;
}

}  // namespace reasampler
