#include "core/model/owned_manifest.h"

#include <cctype>

#include "core/json/json.h"

// owned_manifest implementation. JSON shape is a single object with one string
// array:
//
//     {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]}

namespace reasampler::model {

// -- path invariant (mirror of bank_model's isAbsolutePath) ----------------

namespace {

// Any leading '/' or '\' (POSIX root / UNC), or a leading <alpha>: (Windows drive,
// incl. drive-relative "C:foo") is absolute — same rejection bank_model applies to
// Sample.relativePath; the manifest holds the same kind of path, so the invariant
// must match exactly.
bool isAbsolutePath(const std::string& p) {
    if (p.empty()) return false;
    if (p[0] == '/' || p[0] == '\\') return true;
    if (p.size() >= 2 && std::isalpha(static_cast<unsigned char>(p[0])) && p[1] == ':')
        return true;
    return false;
}

}  // namespace

// -- mutation / query -------------------------------------------------------

ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) {
    if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath;
    if (isAbsolutePath(relativePath)) return ManifestAddResult::RejectedAbsolutePath;
    if (contains(relativePath)) return ManifestAddResult::AlreadyPresent;
    paths_.push_back(relativePath);
    return ManifestAddResult::Added;
}

bool OwnedFileManifest::contains(const std::string& relativePath) const {
    for (const auto& p : paths_)
        if (p == relativePath) return true;
    return false;
}

// -- JSON writer --------------------------------------------------------

std::string OwnedFileManifest::serialize() const {
    std::string out = "{\"owned\":[";
    for (std::size_t i = 0; i < paths_.size(); ++i) {
        if (i) out += ',';
        json::writeEscaped(out, paths_[i]);
    }
    out += "]}";
    return out;
}

// JSON parser: string-array-only grammar. Tolerates unknown keys and requires
// the "owned" value to be an array of strings.

namespace {

bool parseManifest(json::Reader& r, OwnedFileManifest& out) {
    if (!r.consume('{')) return false;
    r.skipWs();
    if (r.consume('}')) return true;  // empty object -> empty manifest
    for (;;) {
        std::string key;
        if (!r.parseKey(key)) return false;
        if (key == "owned") {
            std::vector<std::string> paths;
            if (!r.parseStringArray(paths)) return false;
            for (auto& p : paths) {
                // Feed through add() 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 into the manifest.
                out.add(p);
            }
        } else {
            if (!r.skipValue()) return false;  // forward-compat: tolerate unknown keys
        }
        r.skipWs();
        if (r.consume(',')) continue;
        if (r.consume('}')) return true;
        return false;
    }
}

}  // namespace

std::optional<OwnedFileManifest> OwnedFileManifest::deserialize(const std::string& blob) {
    OwnedFileManifest m;
    json::Reader r(blob);
    if (!parseManifest(r, m)) return std::nullopt;
    return m;
}

} // namespace reasampler::model
