#pragma once
// package_format — the RSBK bank-package contract: magic, the version ladder,
// the readability classification, and the three naming rules below. Pure:
// standard library only. The framing codec that acts on this contract is
// bank_package; the manifest grammar is package_manifest.

#include <cstdint>
#include <string>

namespace reasampler::package {

// Version ladder for the RSBK container (read-and-validate, like the origin
// ledger's "v"):
//
//   format 1 (current)  magic "RSBK" | u32 formatVersion | u32 minReaderVersion
//                       | u32 len + writer semver | u32 len + JSON manifest
//                       | payloads concatenated in manifest entry order.
//                       All integers little-endian.
//
// Two integers, two jobs: formatVersion is what the writer emitted (monotonic,
// bumped on ANY change); minReaderVersion is the oldest reader that can read the
// package safely (bumped only on a STRUCTURAL change — a field's meaning shifts,
// a section is removed, framing moves; an additive change — a new optional
// manifest key, a new enum value with a defined degrade — leaves it alone). The
// reader's whole rule: read iff minReaderVersion <= kPackageFormatVersion.
// formatVersion beyond that is message text and log material only.
//
// FROZEN FOR ALL FUTURE VERSIONS: the fields through the writer semver. A
// too-new package must still yield the writer's version so the refusal can name
// what to install — a structural change may rearrange anything after the semver,
// never before it.
inline constexpr char kPackageMagic[4] = {'R', 'S', 'B', 'K'};
inline constexpr std::uint32_t kPackageFormatVersion = 1;
inline constexpr std::uint32_t kPackageMinReaderVersion = 1;

// Hostile-input allocation caps (error signaled, never a multi-gigabyte
// allocation off a forged length field). Generous against real content: a
// semver is ~10 bytes; a manifest for hundreds of samples is well under 1 MB.
inline constexpr std::uint32_t kMaxWriterVersionBytes = 64;
inline constexpr std::uint32_t kMaxManifestBytes = 64u * 1024u * 1024u;
inline constexpr std::size_t kMaxEntryNameBytes = 255;

// The three-way read verdict (the FutureVersion precedent): TooNew refuses the
// whole package before anything is produced; Malformed is a header no honest
// writer emits. Also the status of a full prefix decode in bank_package.
enum class PackageReadability {
    Readable,
    TooNew,
    Malformed,
};

// Classify a stored header pair against THIS build's ladder. minReaderVersion
// above kPackageFormatVersion is TooNew; a zero version or minReader >
// formatVersion is Malformed (a writer cannot require a reader newer than what
// it wrote).
PackageReadability classifyPackageVersion(std::uint32_t formatVersion,
                                          std::uint32_t minReaderVersion);

// The entry-name rule: a bare file name only. Rejects empty, ".", the exact
// ".." component (a name can only ever be one component, since separators are
// banned below — a substring scan would over-reject legal names like
// "take..final.wav"), any control byte (NUL included — truncates at the first
// filesystem call and collides two distinct manifest entries onto one file) or
// 0x7F, any '/', '\\' or ':' (which also bans every absolute form — drive, UNC,
// rooted), any Windows-reserved character (`*?|<>"`), a trailing dot or space
// (silently stripped at file creation, so "a.wav " and "a.wav" would collide),
// a DOS device name (CON/PRN/AUX/NUL/COM1-9/LPT1-9 plus the superscript
// COM/LPT 1-3 forms, case-insensitive, with or without an extension), names
// over kMaxEntryNameBytes, and any byte sequence that is not well-formed UTF-8.
bool isValidEntryName(const std::string& name);

// The format's name-equivalence rule: two entry names that differ only by ASCII
// case are ONE name. Windows and macOS's default APFS are case-insensitive, so
// "Kick.wav" and "kick.wav" would extract onto a single file — and a bank
// authored on a case-sensitive filesystem produces that pair honestly. Non-ASCII
// bytes compare exactly (see this directory's CLAUDE.md on NFC/NFD).
bool sameEntryName(const std::string& a, const std::string& b);

// sameEntryName's fold made explicit: the ASCII-lower-cased bytes, so
// entryNameKey(a) == entryNameKey(b) exactly when sameEntryName(a, b). For a caller
// holding many names at once — folding them into a set is what turns an O(n^2)
// pairwise scan into a linear one.
std::string entryNameKey(const std::string& name);

// The one field in the format that CAN express a path: a nested Sample's
// relativePath, which is bank-relative by design. Rejects every absolute form
// (the shared util::isAbsolutePath test) and any ".." component — BankModel::add
// checks only the former, so traversal reaches the format without this.
bool isValidNestedSamplePath(const std::string& path);

// The fixed header, informational semver included. writerVersion is
// version::stampVersion() on the write side — it exists so a TooNew refusal can
// tell the user which build to install; it never gates. Defaults are 0/0, NOT
// the current ladder pair, so a header that never parsed reads as obviously
// unset rather than as a plausible 1/1. The fields are meaningful whenever they
// are non-zero, not only on success: a decode that got past the header and
// failed later (a corrupt manifest) reports the real pair alongside Malformed.
struct PackageHeader {
    std::uint32_t formatVersion = 0;
    std::uint32_t minReaderVersion = 0;
    std::string writerVersion;

    bool operator==(const PackageHeader& o) const {
        return formatVersion == o.formatVersion &&
               minReaderVersion == o.minReaderVersion &&
               writerVersion == o.writerVersion;
    }
};

}  // namespace reasampler::package
