#include "core/package/package_manifest.h"

#include <unordered_set>
#include <utility>

#include "core/json/json.h"
#include "core/package/package_format.h"

namespace reasampler::package {

namespace {

using json::numToStr;
using ObjWriter = json::Writer;

// Shared by serializeManifest and deserializeManifest — see this directory's
// CLAUDE.md for why duplicate names are rejected both ways. Equivalence is the
// format's, not std::string's: entryNameKey is sameEntryName's ASCII-case fold.
//
// A set, not the pairwise scan this replaced: under kMaxManifestBytes a hostile
// package can declare hundreds of thousands of minimal entries, and O(n^2) over that
// is a multi-minute hang on the decode path an import drives.
bool duplicateName(const std::vector<PackageEntry>& entries) {
    std::unordered_set<std::string> seen;
    seen.reserve(entries.size());
    for (const auto& e : entries)
        if (!seen.insert(entryNameKey(e.fileName)).second) return true;
    return false;
}

// The one-sample BankModel image of `s` — bank_model's own writer, verbatim, so
// the per-sample shape has exactly one owner. nullopt when add() would reject
// the record (its guards are the format's guards too).
std::optional<std::string> nestSample(const model::Sample& s) {
    model::BankModel one;
    if (one.add(s) != model::AddResult::Added) return std::nullopt;
    return one.serialize();
}

}  // namespace

bool PackageEntry::operator==(const PackageEntry& o) const {
    return fileName == o.fileName && byteLength == o.byteLength &&
           byteHash == o.byteHash && sample == o.sample;
}

bool PackageManifest::operator==(const PackageManifest& o) const {
    return bankDisplayName == o.bankDisplayName && exportTimestamp == o.exportTimestamp &&
           entries == o.entries && slots == o.slots;
}

std::optional<std::string> serializeManifest(const PackageManifest& m) {
    for (const auto& e : m.entries) {
        if (!isValidEntryName(e.fileName)) return std::nullopt;
        if (!isValidNestedSamplePath(e.sample.relativePath)) return std::nullopt;
        // Cross-module contract with src/shell/package — see this directory's
        // CLAUDE.md.
        if (e.byteLength == 0) return std::nullopt;
    }
    if (duplicateName(m.entries)) return std::nullopt;

    std::string out;
    {
        ObjWriter root(out);
        root.keyStr("bankName", m.bankDisplayName);
        root.keyRaw("exported", numToStr(m.exportTimestamp));

        root.keyBegin("entries");
        out += '[';
        for (std::size_t i = 0; i < m.entries.size(); ++i) {
            const auto& e = m.entries[i];
            auto nested = nestSample(e.sample);
            if (!nested) return std::nullopt;
            if (i) out += ',';
            ObjWriter w(out);
            w.keyStr("name", e.fileName);
            // byteLength rides as a signed decimal; 2^63 bytes is beyond any file.
            w.keyRaw("length", numToStr(static_cast<std::int64_t>(e.byteLength)));
            w.keyStr("hash", e.byteHash);
            w.keyRaw("index", *nested);
        }
        out += ']';

        root.keyBegin("slots");
        out += m.slots.serialize();
    }  // root closes here (NRVO note in json::Writer)
    return out;
}

namespace {

// Mirrors bank_book_json's private slots parser: [{id, slot}, ...] pairs handed
// to SlotMap::fromEntries, which owns the defensive repair rules. Deliberately
// does NOT reject a repeated "id"/"slot" key the way the root and entry parsers
// below reject theirs — this grammar belongs to core/model's bank_book_json, and
// diverging here would give one wire shape two behaviours in two files. The
// stakes differ too: a repeated "name" decides which file an entry lands on,
// while a repeated "id" here still feeds SlotMap::fromEntries's deterministic
// first-wins/never-double-occupy repair, so no ambiguity survives. Do not
// "finish" the repeat-key rejection here to match the parsers below.
bool parseSlots(json::Reader& r, model::SlotMap& out) {
    std::vector<std::pair<std::string, int>> pairs;
    if (!r.consume('[')) return false;
    r.skipWs();
    if (r.consume(']')) {
        out = model::SlotMap::fromEntries(pairs);
        return true;
    }
    do {
        if (!r.consume('{')) return false;
        std::string id;
        int slot = 0;
        bool haveId = false, haveSlot = false;
        do {
            std::string k;
            if (!r.parseKey(k)) return false;
            if (k == "id") { if (!r.parseString(id)) return false; haveId = true; }
            else if (k == "slot") { if (!r.parseInt(slot)) return false; haveSlot = true; }
            else { if (!r.skipValue()) return false; }
        } while (r.consume(','));
        if (!r.consume('}')) return false;
        if (!haveId || !haveSlot) return false;
        pairs.emplace_back(std::move(id), slot);
    } while (r.consume(','));
    if (!r.consume(']')) return false;
    out = model::SlotMap::fromEntries(pairs);
    return true;
}

bool parseEntry(json::Reader& r, PackageEntry& e) {
    if (!r.consume('{')) return false;
    r.skipWs();
    if (r.consume('}')) return false;  // an entry needs all four fields

    bool haveName = false, haveLength = false, haveHash = false, haveSample = false;
    do {
        std::string key;
        if (!r.parseKey(key)) return false;

        // A repeated key is rejected here exactly as at the root — same format
        // question, one level down.
        if (key == "name") {
            if (haveName || !r.parseString(e.fileName)) return false;
            haveName = true;
        } else if (key == "length") {
            std::int64_t v = 0;
            if (haveLength || !r.parseInt64(v)) return false;
            if (v < 0) return false;
            e.byteLength = static_cast<std::uint64_t>(v);
            haveLength = true;
        } else if (key == "hash") {
            if (haveHash || !r.parseString(e.byteHash)) return false;
            haveHash = true;
        } else if (key == "index") {
            if (haveSample) return false;
            std::string raw;
            if (!r.captureValue(raw)) return false;
            auto idx = model::BankModel::deserialize(raw);
            // Exactly one sample: add()'s silent drop (rejected record) or a
            // multi-sample blob both fail the entry rather than half-parse.
            if (!idx || idx->size() != 1) return false;
            e.sample = idx->all().front();
            haveSample = true;
        } else {
            if (!r.skipValue()) return false;  // forward-compat unknown keys
        }
    } while (r.consume(','));

    if (!r.consume('}')) return false;
    if (!haveName || !haveLength || !haveHash || !haveSample) return false;
    return isValidEntryName(e.fileName) && isValidNestedSamplePath(e.sample.relativePath);
}

bool parseManifest(json::Reader& r, PackageManifest& m) {
    if (!r.consume('{')) return false;
    r.skipWs();
    // "Which duplicate keys are legal" is a format contract, so it is answered
    // for every root key rather than only for the one that would accumulate:
    // a repeated key is rejected, never last-wins. Unknown keys may repeat —
    // they are skipped, and a future format must stay free to add them.
    bool haveBankName = false, haveExported = false, haveEntries = false, haveSlots = false;
    const auto firstTime = [](bool& seen) { const bool ok = !seen; seen = true; return ok; };
    if (!r.consume('}')) {  // not the empty-object shortcut: parse the members
        do {
            std::string key;
            if (!r.parseKey(key)) return false;

            if (key == "bankName") {
                if (!firstTime(haveBankName)) return false;
                if (!r.parseString(m.bankDisplayName)) return false;
            } else if (key == "exported") {
                if (!firstTime(haveExported)) return false;
                if (!r.parseInt64(m.exportTimestamp)) return false;
            } else if (key == "entries") {
                if (!firstTime(haveEntries)) return false;
                if (!r.consume('[')) return false;
                r.skipWs();
                if (!r.consume(']')) {
                    do {
                        PackageEntry e;
                        if (!parseEntry(r, e)) return false;
                        m.entries.push_back(std::move(e));
                    } while (r.consume(','));
                    if (!r.consume(']')) return false;
                }
            } else if (key == "slots") {
                if (!firstTime(haveSlots)) return false;
                if (!parseSlots(r, m.slots)) return false;
            } else {
                if (!r.skipValue()) return false;  // forward-compat unknown keys
            }
        } while (r.consume(','));

        if (!r.consume('}')) return false;
    }
    r.skipWs();
    if (!r.eof()) return false;  // trailing garbage — even after an empty object
    return !duplicateName(m.entries);
}

}  // namespace

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

}  // namespace reasampler::package
