#include "core/model/bank_book.h"

#include <algorithm>
#include <utility>
#include <vector>

#include "core/json/json.h"

// bank_book JSON round-trip — a sibling TU to bank_book.cpp, sharing its header
// and target. The registry RULES half stays in bank_book.cpp; the one shared
// symbol is the private static BankBook::nameKey folding rule — the parse-time
// duplicate-display-name coalesce below must fold names EXACTLY as create/rename
// uniqueness does, or a parsed book could violate the in-model invariant.
//
// The book blob nests one bank object per bank, each carrying that bank's
// BankModel serialized by bank_model's OWN writer, so per-bank sample
// serialization stays owned by bank_model and is not duplicated here. The book
// writer emits the bank envelope (id / displayName / ordinal) plus a raw "index"
// member whose value is the BankModel blob verbatim; the parser splits the book
// envelope, then hands each nested index blob straight to BankModel::deserialize.

namespace reasampler {

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

namespace {

std::string intToStr(int v) { return json::numToStr(v); }
using ObjWriter = json::Writer;

} // namespace

std::string BankBook::serialize() const {
    std::string out;
    {
        ObjWriter root(out);
        root.keyRaw("version", intToStr(1));
        root.keyStr("activeBank", activeBankId_);

        // banks: array of { id, displayName, ordinal, index: <BankModel blob> }.
        // The pool rides in as bank-zero, persisted identically to any named bank.
        root.keyBegin("banks");
        out += '[';
        for (std::size_t i = 0; i < banks_.size(); ++i) {
            if (i) out += ',';
            ObjWriter b(out);
            b.keyStr("id", banks_[i].id);
            b.keyStr("displayName", banks_[i].displayName);
            b.keyRaw("ordinal", intToStr(banks_[i].ordinal));
            // The nested index is bank_model's own JSON, emitted verbatim so the
            // per-sample shape stays owned by BankModel::serialize (not duplicated).
            b.keyRaw("index", banks_[i].index.serialize());
            // Display positions (gap-preserving). Absent on a pre-existing blob;
            // the parser defaults such a bank's slots from insertion order on load.
            b.keyRaw("slots", banks_[i].slots.serialize());
        }
        out += ']';
    }  // root closes here (see bank_model note on NRVO + deferred close)
    return out;
}

// -- JSON — parser (recursive descent; std::nullopt on malformed input, never UB) --

namespace {

// parseBank parses one bank object; parseSlots the "slots" array ([{id, slot},
// ...]) into (id, slot) pairs (empty array valid; the pair-level defensive
// repair — dupes/conflicts — lives in SlotMap::fromEntries); parseBook the root
// blob, distinguishing the legacy shape (a bare bank_index object: has
// "samples", no "banks") from the book shape (has "banks"): a legacy blob
// yields a single pool bank carrying the migrated index and an empty active id
// (⇒ pool). The member deserialize() adopts the result (duplicate-display-name
// coalesce + ordinal normalize + active resolve — the coalesce lives THERE, not
// here, because it folds through the private BankBook::nameKey these free
// functions cannot reach).
bool parseSlots(json::Reader& r, std::vector<std::pair<std::string, int>>& out);

bool parseBank(json::Reader& r, Bank& b) {
    if (!r.consume('{')) return false;
    r.skipWs();
    if (r.consume('}')) return false;  // a bank object must at least carry an id

    bool haveId = false;
    bool haveIndex = false;
    do {
        std::string key;
        if (!r.parseKey(key)) return false;

        if (key == "id") {
            if (!r.parseString(b.id)) return false;
            haveId = true;
        } else if (key == "displayName") {
            if (!r.parseString(b.displayName)) return false;
        } else if (key == "ordinal") {
            if (!r.parseInt(b.ordinal)) return false;
        } else if (key == "index") {
            std::string raw;
            if (!r.captureValue(raw)) return false;
            auto idx = BankModel::deserialize(raw);
            if (!idx) return false;  // a malformed nested index fails the whole parse
            b.index = std::move(*idx);
            haveIndex = true;
        } else if (key == "slots") {
            // Display positions. Absent on a pre-existing blob; when present it
            // drives the bank's SlotMap. reconcileSlots() (post-adopt) squares it
            // with membership.
            std::vector<std::pair<std::string, int>> pairs;
            if (!parseSlots(r, pairs)) return false;
            b.slots = SlotMap::fromEntries(pairs);
        } else {
            if (!r.skipValue()) return false;  // forward-compat unknown keys
        }
    } while (r.consume(','));

    if (!r.consume('}')) return false;
    if (!haveId || b.id.empty()) return false;   // id keys the registry
    if (!haveIndex) return false;                // every bank persists its index
    return true;
}

bool parseSlots(json::Reader& r, std::vector<std::pair<std::string, int>>& out) {
    out.clear();
    if (!r.consume('[')) return false;
    r.skipWs();
    if (r.consume(']')) return true;  // empty slot array — a bank with no positions yet
    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; }  // forward-compat
        } while (r.consume(','));
        if (!r.consume('}')) return false;
        if (!haveId || !haveSlot) return false;  // a slot entry needs both
        out.emplace_back(std::move(id), slot);
    } while (r.consume(','));
    return r.consume(']');
}

bool parseBook(json::Reader& r, const std::string& raw, std::vector<Bank>& banks,
               std::string& activeBank) {
    banks.clear();
    activeBank.clear();
    if (!r.consume('{')) return false;
    r.skipWs();
    if (r.consume('}')) return false;  // an empty object is neither shape → malformed

    // Decide the shape by which structural key we saw. A "banks" key ⇒ book shape; a
    // "samples" key with no "banks" ⇒ legacy shape (promote into the pool).
    std::vector<Bank> parsedBanks;
    bool sawBanks = false;
    bool sawSamples = false;

    do {
        std::string key;
        if (!r.parseKey(key)) return false;

        if (key == "banks") {
            sawBanks = true;
            if (!r.consume('[')) return false;
            r.skipWs();
            if (!r.consume(']')) {
                do {
                    Bank b;
                    if (!parseBank(r, b)) return false;
                    parsedBanks.push_back(std::move(b));
                } while (r.consume(','));
                if (!r.consume(']')) return false;
            }
        } else if (key == "activeBank") {
            if (!r.parseString(activeBank)) return false;
        } else if (key == "samples") {
            // Legacy marker; the legacy index is re-parsed from the whole input below.
            sawSamples = true;
            if (!r.skipValue()) return false;
        } else {
            if (!r.skipValue()) return false;  // version, or unknown
        }
    } while (r.consume(','));

    if (!r.consume('}')) return false;
    r.skipWs();
    if (!r.eof()) return false;  // trailing garbage

    // --- Legacy migration: a bare bank_index (samples, no banks) → pool. ---
    if (!sawBanks) {
        if (!sawSamples) return false;  // neither shape's marker → malformed
        auto legacy = BankModel::deserialize(raw);
        if (!legacy) return false;
        Bank pool;
        pool.id = kPoolBankId;
        pool.displayName = kPoolBankName;
        pool.ordinal = 0;
        pool.index = std::move(*legacy);
        banks.push_back(std::move(pool));  // { pool } with zero named banks
        activeBank.clear();                // ⇒ pool (default) after adoption
        return true;
    }

    // --- Book shape: the parsed banks ARE the book (pool folded in). ---
    // The pool must be present as bank-zero (serialize always emits it). Reject a
    // book blob that omits it rather than silently re-seeding — a book without its
    // pool is malformed, not a legacy blob.
    bool hasPool = std::any_of(parsedBanks.begin(), parsedBanks.end(),
                               [](const Bank& b) { return b.isPool(); });
    if (!hasPool) return false;

    // Reject duplicate bank ids (ids key the registry; a dup would corrupt lookup).
    for (std::size_t i = 0; i < parsedBanks.size(); ++i)
        for (std::size_t j = i + 1; j < parsedBanks.size(); ++j)
            if (parsedBanks[i].id == parsedBanks[j].id) return false;

    // Force the pool's fixed display name — it is not user-mutable, so we do not
    // trust a persisted override for it (keeps kPoolBankName authoritative).
    for (auto& b : parsedBanks)
        if (b.isPool()) b.displayName = kPoolBankName;

    banks = std::move(parsedBanks);
    return true;
}

} // namespace

void BankBook::adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank) {
    banks_ = std::move(banks);
    normalizeOrdinals();
    // Resolve the active bank defensively: fall back to the pool if the persisted id
    // names no bank, so a corrupt active id never leaves a dangling capture target.
    activeBankId_ = (bank(activeBank) != nullptr) ? activeBank : std::string(kPoolBankId);
}

std::optional<BankBook> BankBook::deserialize(const std::string& blob) {
    std::vector<Bank> banks;
    std::string activeBank;
    json::Reader r(blob);
    if (!parseBook(r, blob, banks, activeBank)) return std::nullopt;

    // --- Coalesce duplicate folded display names. --------------------------
    // The in-model create/rename path enforces unique display names under nameKey,
    // but a hand-edited .rpp blob can smuggle in two banks whose names fold to the
    // same key ("Drums" and " drums "). Rejecting the whole book over one collision
    // would degrade the user's entire library to empty, so instead we AUTO-
    // DISAMBIGUATE the later duplicate: scan in parse order, and the first time a
    // folded key repeats, suffix that bank's display name (" 2", " 3", …) until its
    // folded key is unique among names seen so far — the first bank to carry a key
    // keeps its name verbatim. No bank or sample is lost, ids are untouched, and the
    // pool's reserved "Pool" key is seeded first so a named bank folding to "pool"
    // is disambiguated away from it, never the reverse.
    //
    // Hosted here (not in the free parseBook) because it folds through the PRIVATE
    // BankBook::nameKey the create/rename uniqueness check also uses.
    {
        std::vector<std::string> seenKeys;
        seenKeys.reserve(banks.size());
        for (auto& b : banks) {
            if (b.isPool()) {                 // pool's name is fixed; reserve its key
                seenKeys.push_back(nameKey(b.displayName));
                continue;
            }
            const auto taken = [&](const std::string& k) {
                return std::find(seenKeys.begin(), seenKeys.end(), k) != seenKeys.end();
            };
            std::string key = nameKey(b.displayName);
            if (taken(key)) {
                // Suffix with an ascending integer until the folded key is free. Guard
                // against a pathological blob whose base name already ends in a number
                // by folding the candidate each attempt (nameKey normalizes it).
                const std::string base = b.displayName;
                for (int n = 2;; ++n) {
                    const std::string candidate = base + " " + std::to_string(n);
                    const std::string candKey = nameKey(candidate);
                    if (!taken(candKey)) {
                        b.displayName = candidate;
                        key = candKey;
                        break;
                    }
                }
            }
            seenKeys.push_back(key);
        }
    }

    BankBook book;
    book.adoptBanks(std::move(banks), activeBank);
    return book;
}

} // namespace reasampler
