#pragma once
// origin_ledger — the one persisted record family behind file tracking: for every
// file the system itself created, what act created it and what it derives from.
// Supersedes the path-only owned manifest (same ext-state key, widened shape).
// Nothing here decides anything; tracking_authority does.

#include <cstddef>
#include <optional>
#include <string>
#include <vector>

namespace reasampler::tracking {

// The act that created the file. `Unknown` is the never-recorded case — a record
// lifted from a legacy path-only manifest, or one whose creator did not know.
// PERSISTED AS INTEGERS: never renumber an existing value, only append.
enum class OriginKind {
    Unknown       = 0,
    Capture       = 1,
    Ingest        = 2,
    Recapture     = 3,  // regenerated in place from its recorded source recipe
    Resample      = 4,  // baked from an instrument's own processing chain
    PackageImport = 5,  // package-sourced vs Ingest's user-picked; unrecoverable once merged
};

// One system-created file's birth record. `relativePath` is the key and is ALWAYS
// project-relative (the same invariant as Sample.relativePath); an absolute path is
// rejected rather than relativized, because the pure model has no project root and
// guessing one could point at the wrong file.
struct OriginRecord {
    std::string relativePath;
    OriginKind  kind = OriginKind::Unknown;
    // The bank id as of birth; "" when never recorded. NOT unique across records: a
    // recapture writes a new file under the SAME bank id, so the ledger legitimately
    // holds two records sharing a sampleId with different paths. `relativePath` is
    // the only key; a by-sampleId lookup is ambiguous by construction.
    std::string sampleId;
    std::string parentSampleId;  // the capture this derives from; "" = root / none

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

// Outcome of a record(). The op reports what happened rather than silently mutating
// on a bad request (mirrors BankModel::AddResult).
enum class RecordResult {
    Recorded,
    AlreadyPresent,        // dedup no-op; the existing record is NOT overwritten
    RejectedEmptyPath,
    RejectedAbsolutePath,
};

// Insertion-ordered, path-keyed, deduplicated. Insertion order is preserved so
// serialize() round-trips byte-identically (deterministic ext-state).
//
// NOT a mirror of the bank index: removing or moving an index entry leaves the
// record alone. The ledger tracks files *created*; prune reconciles it against the
// index later.
class OriginLedger {
public:
    OriginLedger() = default;

    // A path already present is AlreadyPresent and leaves the stored record
    // untouched — the no-backfill rule, enforced here rather than at call sites.
    RecordResult record(const OriginRecord& rec);

    // nullptr when the path was never recorded. Distinguishing that from an
    // unreadable ledger is the loader's job (see LedgerStatus).
    const OriginRecord* find(const std::string& relativePath) const;
    bool contains(const std::string& relativePath) const;

    // Prune's `owned` input, in insertion order.
    std::vector<std::string> ownedPaths() const;

    const std::vector<OriginRecord>& records() const { return records_; }
    std::size_t size() const { return records_.size(); }
    bool empty() const { return records_.empty(); }

    bool operator==(const OriginLedger& o) const { return records_ == o.records_; }

    // Lossless round-trip: deserialize(serialize(x)) == x. std::nullopt on malformed
    // input AND on a blob whose "v" is newer than this build can read — the caller
    // must treat either as unreadable, never as empty. loadLedger tells them apart.
    std::string serialize() const;
    static std::optional<OriginLedger> deserialize(const std::string& json);

private:
    std::vector<OriginRecord> records_;
};

// The states of a stored ledger. `Fresh` (absent key) and `Loaded` are the two
// usable ones. The other two are distinguished only so the operator gets the right
// recovery advice — clearing a corrupt blob is repair, clearing a newer build's blob
// is destruction.
enum class LedgerStatus { Fresh, Loaded, Unreadable, FutureVersion };

// The one degraded-state test, deliberately serving BOTH axes — "may I answer a
// destructive question?" and "may I overwrite the stored blob?" — because today
// every degraded status answers no to both. Split them only when a status needs one
// without the other.
inline bool ledgerDegraded(LedgerStatus s) {
    return s == LedgerStatus::Unreadable || s == LedgerStatus::FutureVersion;
}

struct LedgerLoad {
    LedgerStatus status = LedgerStatus::Fresh;
    OriginLedger ledger;  // empty unless status == Loaded
};

// Classifies a raw stored value. An empty string is the absent key (Fresh), not an
// error — an empty string is not valid JSON, so the two cases cannot be told apart
// by the parser alone.
LedgerLoad loadLedger(const std::string& stored);

}  // namespace reasampler::tracking
