#include "core/tracking/origin_ledger.h"

#include <utility>

#include "core/json/json.h"
#include "core/util/relative_path.h"

// Version ladder for the stored blob, under the FOREVER-STABLE "owned_files" key:
//
//   v1 (legacy, path-only)  {"owned":["reasampler_bank/a.wav"]}
//   v2 (current)            {"v":2,"records":[{"path":"...","kind":1,
//                                              "sample":"id","parent":"pid"}]}
//
// v1 blobs lift to v2 records with kind Unknown and empty ids — a pre-existing bank
// keeps every protection it had (the paths are still owned) and gains no invented
// lineage. Both shapes parse; only v2 is written.
//
// "v" is READ and validated, not just written: a v3 record shape parsed by these v2
// rules would yield a plausible-but-partial ledger, and the next save would overwrite
// the v3 blob with that truncation. A version above kLedgerVersion is therefore its
// own degraded status, never a Loaded ledger.

namespace reasampler::tracking {

namespace {

using util::isAbsolutePath;

// The version serialize() writes, and the highest this build can read.
constexpr int kLedgerVersion = 2;

// An unrecognized persisted integer degrades to Unknown rather than failing the
// parse: a newer channel's kind must not make the whole ledger unreadable, which
// would halt prune on nothing worse than a vocabulary gap.
OriginKind kindFromInt(int v) {
    switch (v) {
        case 1: return OriginKind::Capture;
        case 2: return OriginKind::Ingest;
        case 3: return OriginKind::Recapture;
        case 4: return OriginKind::Resample;
        case 5: return OriginKind::PackageImport;
        default: return OriginKind::Unknown;
    }
}

}  // namespace

bool OriginRecord::operator==(const OriginRecord& o) const {
    return relativePath == o.relativePath && kind == o.kind &&
           sampleId == o.sampleId && parentSampleId == o.parentSampleId;
}

RecordResult OriginLedger::record(const OriginRecord& rec) {
    if (rec.relativePath.empty()) return RecordResult::RejectedEmptyPath;
    if (isAbsolutePath(rec.relativePath)) return RecordResult::RejectedAbsolutePath;
    if (contains(rec.relativePath)) return RecordResult::AlreadyPresent;
    records_.push_back(rec);
    return RecordResult::Recorded;
}

const OriginRecord* OriginLedger::find(const std::string& relativePath) const {
    for (const OriginRecord& r : records_)
        if (r.relativePath == relativePath) return &r;
    return nullptr;
}

bool OriginLedger::contains(const std::string& relativePath) const {
    return find(relativePath) != nullptr;
}

std::vector<std::string> OriginLedger::ownedPaths() const {
    std::vector<std::string> out;
    out.reserve(records_.size());
    for (const OriginRecord& r : records_) out.push_back(r.relativePath);
    return out;
}

std::string OriginLedger::serialize() const {
    std::string out = "{\"v\":" + json::numToStr(kLedgerVersion) + ",\"records\":[";
    for (std::size_t i = 0; i < records_.size(); ++i) {
        if (i) out += ',';
        const OriginRecord& r = records_[i];
        out += "{\"path\":";
        json::writeEscaped(out, r.relativePath);
        out += ",\"kind\":" + json::numToStr(static_cast<int>(r.kind));
        out += ",\"sample\":";
        json::writeEscaped(out, r.sampleId);
        out += ",\"parent\":";
        json::writeEscaped(out, r.parentSampleId);
        out += '}';
    }
    out += "]}";
    return out;
}

namespace {

bool parseRecord(json::Reader& r, OriginRecord& out) {
    if (!r.consume('{')) return false;
    r.skipWs();
    if (r.consume('}')) return true;
    for (;;) {
        std::string key;
        if (!r.parseKey(key)) return false;
        if (key == "path") {
            if (!r.parseString(out.relativePath)) return false;
        } else if (key == "kind") {
            int k = 0;
            if (!r.parseInt(k)) return false;
            out.kind = kindFromInt(k);
        } else if (key == "sample") {
            if (!r.parseString(out.sampleId)) return false;
        } else if (key == "parent") {
            if (!r.parseString(out.parentSampleId)) return false;
        } else if (!r.skipValue()) {
            return false;
        }
        r.skipWs();
        if (r.consume(',')) continue;
        if (r.consume('}')) return true;
        return false;
    }
}

bool parseRecordArray(json::Reader& r, OriginLedger& out) {
    if (!r.consume('[')) return false;
    r.skipWs();
    if (r.consume(']')) return true;
    for (;;) {
        OriginRecord rec;
        if (!parseRecord(r, rec)) return false;
        // Through record() so the persisted invariants (dedup, reject empty/absolute)
        // are re-asserted on load: a hand-edited or corrupt blob cannot smuggle an
        // absolute or duplicate path in.
        out.record(rec);
        r.skipWs();
        if (r.consume(',')) continue;
        if (r.consume(']')) return true;
        return false;
    }
}

// `versionOut` stays 0 when no "v" key was present — the legacy path-only shape, or
// an empty object. Read regardless of key order, so it is validated after the close.
// A duplicate "v" key (never emitted by serialize()) is last-wins, same as every
// other duplicate key here — not reachable except by hand-editing the blob.
bool parseLedger(json::Reader& r, OriginLedger& out, int& versionOut) {
    if (!r.consume('{')) return false;
    r.skipWs();
    if (r.consume('}')) return true;
    for (;;) {
        std::string key;
        if (!r.parseKey(key)) return false;
        if (key == "v") {
            if (!r.parseInt(versionOut)) return false;
        } else if (key == "records") {
            if (!parseRecordArray(r, out)) return false;
        } else if (key == "owned") {
            std::vector<std::string> paths;
            if (!r.parseStringArray(paths)) return false;
            for (const std::string& p : paths) out.record(OriginRecord{p});
        } else if (!r.skipValue()) {
            return false;
        }
        r.skipWs();
        if (r.consume(',')) continue;
        if (r.consume('}')) return true;
        return false;
    }
}

enum class ParseOutcome { Ok, Malformed, FutureVersion };

// The one parse; the two public entry points differ only in how much of the outcome
// they can express. `out` is written only on Ok — never a partial ledger.
ParseOutcome parseStored(const std::string& text, OriginLedger& out) {
    OriginLedger parsed;
    int version = 0;
    ::reasampler::json::Reader r(text);
    if (!parseLedger(r, parsed, version)) {
        // A future record shape (e.g. v3 retyping "kind" or "records") fails these v2
        // parse rules on the very field the version bump changed. "v" is read before
        // any record parsing in our own writer's key order, so `version` already holds
        // it here: report FutureVersion, not Malformed, or the operator gets the
        // corrupt-blob "clear it" advice against a newer build's ledger.
        return version > kLedgerVersion ? ParseOutcome::FutureVersion : ParseOutcome::Malformed;
    }
    // Trailing garbage means the blob is not what it claims; accepting it would turn
    // a detectably-corrupt value into a silently-partial ledger.
    r.skipWs();
    if (!r.eof()) return ParseOutcome::Malformed;
    if (version < 0) return ParseOutcome::Malformed;
    if (version > kLedgerVersion) return ParseOutcome::FutureVersion;
    out = std::move(parsed);
    return ParseOutcome::Ok;
}

}  // namespace

std::optional<OriginLedger> OriginLedger::deserialize(const std::string& json) {
    OriginLedger ledger;
    if (parseStored(json, ledger) != ParseOutcome::Ok) return std::nullopt;
    return ledger;
}

LedgerLoad loadLedger(const std::string& stored) {
    LedgerLoad load;
    if (stored.empty()) return load;  // absent key -> Fresh, not an error
    switch (parseStored(stored, load.ledger)) {
        case ParseOutcome::Ok:            load.status = LedgerStatus::Loaded; break;
        case ParseOutcome::Malformed:     load.status = LedgerStatus::Unreadable; break;
        case ParseOutcome::FutureVersion: load.status = LedgerStatus::FutureVersion; break;
    }
    return load;
}

}  // namespace reasampler::tracking
