#include "core/model/bank_book.h"

#include <algorithm>
#include <unordered_set>

#include "core/util/ascii_ws.h"

// bank_book implementation — the registry RULES half: construction, pool
// privileges, bank lifecycle, active bank, sample movement/removal, slot order,
// and the reference queries. The JSON round-trip half lives in bank_book_json.cpp,
// compiled into the same target. The one symbol both halves share is the private
// static BankBook::nameKey display-name folding rule (declared in bank_book.h).

namespace reasampler {

// -- construction + bank lookup ----------------------------------------------

BankBook::BankBook() {
    Bank pool;
    pool.id = kPoolBankId;
    pool.displayName = kPoolBankName;
    pool.ordinal = 0;
    banks_.push_back(std::move(pool));
    activeBankId_ = kPoolBankId;
}

Bank* BankBook::bank(const std::string& id) {
    for (auto& b : banks_)
        if (b.id == id) return &b;
    return nullptr;
}

const Bank* BankBook::bank(const std::string& id) const {
    for (const auto& b : banks_)
        if (b.id == id) return &b;
    return nullptr;
}

BankModel* BankBook::index(const std::string& id) {
    Bank* b = bank(id);
    return b ? &b->index : nullptr;
}

const BankModel* BankBook::index(const std::string& id) const {
    const Bank* b = bank(id);
    return b ? &b->index : nullptr;
}

Bank& BankBook::pool() {
    // The pool is seeded on construction and is un-deletable, so it always exists.
    return *bank(kPoolBankId);
}

const Bank& BankBook::pool() const {
    return *bank(kPoolBankId);
}

// -- Ordinal normalization ----------------------------------------------------

void BankBook::normalizeOrdinals() {
    // Stable-sort by ordinal with the pool pinned first, then rewrite ordinals to a
    // contiguous 0..N-1. Stability preserves the caller's relative order among banks
    // that share (or, after a reorder shuffle, tie on) an ordinal.
    std::stable_sort(banks_.begin(), banks_.end(), [](const Bank& a, const Bank& b) {
        if (a.isPool() != b.isPool()) return a.isPool();  // pool always first
        return a.ordinal < b.ordinal;
    });
    for (std::size_t i = 0; i < banks_.size(); ++i)
        banks_[i].ordinal = static_cast<int>(i);
}

// -- Display-name uniqueness (trimmed + case-insensitive, ASCII) --------------

// Folds a display name to its uniqueness key: strip leading/trailing ASCII
// whitespace, lower-case ASCII letters — so "Drums"/"drums"/" Drums " share one
// key. ASCII-only by design — the pure core carries no locale facility; bank names
// are short user labels, not Unicode case-folding candidates. Private static: the
// one folding rule shared with bank_book_json.cpp's parse-time coalesce.
std::string BankBook::nameKey(const std::string& s) {
    std::size_t b = 0, e = s.size();
    while (b < e && util::isAsciiWs(s[b])) ++b;
    while (e > b && util::isAsciiWs(s[e - 1])) --e;
    std::string out;
    out.reserve(e - b);
    for (std::size_t i = b; i < e; ++i) {
        char c = s[i];
        if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
        out += c;
    }
    return out;
}

// True if any bank OTHER than `exceptId` already carries `name`'s uniqueness key. The
// exception lets renameBank accept a bank keeping (or re-casing/-spacing) its own name.
bool BankBook::displayNameTaken(const std::string& name, const std::string& exceptId) const {
    const std::string key = nameKey(name);
    for (const auto& b : banks_)
        if (b.id != exceptId && nameKey(b.displayName) == key) return true;
    return false;
}

// -- Bank lifecycle ------------------------------------------------------------

bool BankBook::createBank(const std::string& id, const std::string& displayName) {
    if (id.empty()) return false;              // ids key the registry
    if (id == kPoolBankId) return false;       // reserved pool id
    if (bank(id) != nullptr) return false;     // duplicate id
    // Display names are unique (trimmed + case-insensitive); the pool's "Pool" is a
    // reserved name and is caught here like any other collision.
    if (displayNameTaken(displayName, /*exceptId=*/id)) return false;

    Bank b;
    b.id = id;
    b.displayName = displayName;
    b.ordinal = static_cast<int>(banks_.size());  // append; normalize compacts it
    banks_.push_back(std::move(b));
    normalizeOrdinals();
    return true;
}

// Runs behind displayNameTaken so the probe and the create/rename check can never
// disagree about what "already used" means. exceptId is deliberately "" — no bank can
// carry an empty id, so nothing is excluded from the scan.
std::string BankBook::uniqueDisplayName(const std::string& seed) const {
    if (!displayNameTaken(seed, /*exceptId=*/std::string{})) return seed;
    for (int n = 2;; ++n) {
        std::string candidate = seed + " " + std::to_string(n);
        if (!displayNameTaken(candidate, /*exceptId=*/std::string{})) return candidate;
    }
}

bool BankBook::renameBank(const std::string& id, const std::string& displayName) {
    if (id == kPoolBankId) return false;  // pool is un-renamable
    Bank* b = bank(id);
    if (b == nullptr) return false;
    // Reject a name already used by a DIFFERENT bank. Renaming a bank to its own
    // current name (or a case/space variant of it) is a no-op success, not a
    // rejection — exceptId=id excludes the bank itself from the collision scan.
    if (displayNameTaken(displayName, /*exceptId=*/id)) return false;
    b->displayName = displayName;
    return true;
}

bool BankBook::deleteBank(const std::string& id) {
    if (id == kPoolBankId) return false;  // pool is un-deletable
    auto it = std::find_if(banks_.begin(), banks_.end(),
                           [&](const Bank& b) { return b.id == id; });
    if (it == banks_.end()) return false;

    banks_.erase(it);
    // If the active bank was the one deleted, fall back to the pool (invariant: the
    // active id always names a live bank).
    if (activeBankId_ == id) activeBankId_ = kPoolBankId;
    normalizeOrdinals();
    return true;
}

bool BankBook::reorderBank(const std::string& id, int newOrdinal) {
    if (id == kPoolBankId) return false;  // pool is pinned at ordinal 0
    if (bank(id) == nullptr) return false;

    // Work on the named banks as an ordered list (banks_ is already ordinal-sorted
    // with the pool first, so named banks are banks_[1..]). Pull the target out and
    // re-insert it at the requested position, clamped into the named-bank range
    // [1..N], then rewrite ordinals contiguously. This is O(N) and obviously correct.
    std::vector<Bank> named;
    named.reserve(banks_.size());
    for (auto& b : banks_)
        if (!b.isPool()) named.push_back(std::move(b));

    auto it = std::find_if(named.begin(), named.end(),
                           [&](const Bank& b) { return b.id == id; });
    Bank moved = std::move(*it);
    named.erase(it);

    // Named ordinals are 1..N; convert to a 0-based insertion index into `named`.
    const int hi = static_cast<int>(named.size());  // insert-at range is [0..size]
    int insertAt = std::max(0, std::min(newOrdinal - 1, hi));
    named.insert(named.begin() + insertAt, std::move(moved));

    // Rebuild banks_: pool first, then the reordered named banks. Assign ordinals
    // directly by position here — NOT via normalizeOrdinals(), whose stable_sort keys
    // on the (now stale) ordinals and would undo the reinsertion order.
    std::vector<Bank> rebuilt;
    rebuilt.reserve(named.size() + 1);
    rebuilt.push_back(std::move(pool()));
    for (auto& b : named) rebuilt.push_back(std::move(b));
    banks_ = std::move(rebuilt);
    for (std::size_t i = 0; i < banks_.size(); ++i)
        banks_[i].ordinal = static_cast<int>(i);
    return true;
}

bool BankBook::evacuate(const std::string& id) {
    if (id == kPoolBankId) return false;  // pool is un-evacuable (it is the target)
    Bank* src = bank(id);
    if (src == nullptr) return false;

    // Move every member into the pool, index-only, observing destination collapse.
    // Snapshot the members first, then clear the source — BankModel has no bulk move,
    // and adding into the pool must not alias the vector we are draining.
    BankModel& poolIndex = pool().index;
    const std::vector<Sample> members = src->index.all();  // copy
    for (const auto& s : members)
        poolIndex.add(s);  // Added or Collapsed; either way the pool now holds the hash
    src->index = BankModel{};  // leave the evacuated bank empty
    return true;
}

// -- Active bank ----------------------------------------------------------------

bool BankBook::setActiveBank(const std::string& id) {
    if (bank(id) == nullptr) return false;  // unknown id never corrupts state
    activeBankId_ = id;
    return true;
}

BankModel& BankBook::activeIndex() {
    // activeBankId_ always names a live bank; it falls back to the pool on delete.
    return bank(activeBankId_)->index;
}

const BankModel& BankBook::activeIndex() const {
    return bank(activeBankId_)->index;
}

// -- Sample movement (index-only) -----------------------------------------------

namespace {

// Adds `s` to `dest` and maps the BankModel outcome onto the transfer outcome for
// the "gained a NEW entry" case (`gained`) vs the collapse case. Rejected outcomes
// (absolute path / empty id) cannot occur here: the sample already passed add() on
// the source side, so its path and id are already valid.
TransferResult applyDestAdd(BankModel& dest, const Sample& s, TransferResult gained) {
    return dest.add(s) == AddResult::Collapsed ? TransferResult::Collapsed : gained;
}

} // namespace

TransferResult BankBook::moveSample(const std::string& sampleId,
                                    const std::string& fromBankId,
                                    const std::string& toBankId) {
    Bank* from = bank(fromBankId);
    Bank* to   = bank(toBankId);
    if (from == nullptr || to == nullptr) return TransferResult::RejectedUnknownBank;
    if (fromBankId == toBankId) return TransferResult::RejectedSameBank;

    const Sample* s = from->index.query(sampleId);
    if (s == nullptr) return TransferResult::RejectedSampleAbsent;

    // Copy the sample out before removing it: query returns a pointer into the
    // source vector that remove() invalidates.
    const Sample moved = *s;
    from->index.remove(sampleId);  // source loses the entry unconditionally on a move
    return applyDestAdd(to->index, moved, TransferResult::Moved);
}

TransferResult BankBook::copySample(const std::string& sampleId,
                                    const std::string& fromBankId,
                                    const std::string& toBankId) {
    Bank* from = bank(fromBankId);
    Bank* to   = bank(toBankId);
    if (from == nullptr || to == nullptr) return TransferResult::RejectedUnknownBank;
    if (fromBankId == toBankId) return TransferResult::RejectedSameBank;

    const Sample* s = from->index.query(sampleId);
    if (s == nullptr) return TransferResult::RejectedSampleAbsent;

    const Sample copy = *s;  // source entry is left intact
    return applyDestAdd(to->index, copy, TransferResult::Copied);
}

// -- Sample removal (index-only) + the last-reference query ---------------------

RemoveResult BankBook::removeSample(const std::string& sampleId,
                                    const std::string& fromBankId,
                                    RemoveScope scope) {
    if (scope == RemoveScope::AllBanks) {
        // Latent seam: purge the id from every bank that holds it. fromBankId is
        // ignored (the id is dropped book-wide). Removed iff at least one drop landed.
        bool any = false;
        for (auto& b : banks_)
            if (b.index.remove(sampleId)) {
                b.slots.remove(sampleId);  // keep SlotMap in sync: leave an empty gap
                any = true;
            }
        return any ? RemoveResult::Removed : RemoveResult::RejectedSampleAbsent;
    }

    // ThisBank (default, the only surfaced verb): drop from the one named source bank.
    Bank* from = bank(fromBankId);
    if (from == nullptr) return RemoveResult::RejectedUnknownBank;
    if (!from->index.remove(sampleId)) return RemoveResult::RejectedSampleAbsent;
    from->slots.remove(sampleId);  // keep SlotMap in sync: the removed sample's slot becomes a gap
    return RemoveResult::Removed;
}

bool BankBook::updateSampleInPlace(const std::string& sampleId, const Sample& updated) {
    for (auto& b : banks_)
        if (b.index.query(sampleId) != nullptr)
            return b.index.updateInPlace(sampleId, updated);
    return false;  // no bank holds the id
}

// -- Sample display order — SlotMap driven, index membership untouched -----------

namespace {

// The bank's live sample ids in INDEX (insertion) order — the reconcile/migration seed.
std::vector<std::string> indexIds(const BankModel& idx) {
    std::vector<std::string> ids;
    for (const auto& s : idx.all()) ids.push_back(s.id);
    return ids;
}

// Squares one bank's SlotMap with its index membership. A map with NO overlap with
// the index (a bank with no persisted slot data, or freshly constructed) is seeded
// dense from insertion order; an existing map is reconciled (drop stale, append unmapped).
void reconcileBankSlots(Bank& b) {
    const std::vector<std::string> live = indexIds(b.index);
    if (b.slots.empty()) {
        b.slots.resetDense(live);  // migration / first-population default: dense, no gaps
        return;
    }
    b.slots.reconcile(live);  // partial map: keep positions, drop stale, append new
}

}  // namespace

void BankBook::reconcileSlots() {
    for (auto& b : banks_) reconcileBankSlots(b);
}

std::vector<std::string> BankBook::orderedSampleIds(const std::string& bankId) {
    Bank* b = bank(bankId);
    if (b == nullptr) return {};
    reconcileBankSlots(*b);            // ensure the map covers all live members
    return b->slots.orderedIds();
}

bool BankBook::reorderSample(const std::string& id, const std::string& bankId,
                             int targetSlot) {
    Bank* b = bank(bankId);
    if (b == nullptr) return false;
    if (b->index.query(id) == nullptr) return false;  // bank does not hold the sample
    reconcileBankSlots(*b);                            // complete the target space first
    return b->slots.reorder(id, targetSlot);           // gap-preserving; index untouched
}

bool BankBook::replaceSample(const std::string& newId, const std::string& oldId,
                             const std::string& bankId) {
    if (newId == oldId) return false;
    Bank* b = bank(bankId);
    if (b == nullptr) return false;
    // Both the dragged sample and the occupant must live in this bank.
    if (b->index.query(newId) == nullptr) return false;
    if (b->index.query(oldId) == nullptr) return false;

    reconcileBankSlots(*b);  // complete the map so oldId's slot is known

    // Capture the target slot BEFORE any mutation so the position survives the removal.
    const int targetSlot = b->slots.slotOf(oldId);
    if (targetSlot < 0) return false;  // occupant not positioned (shouldn't happen post-reconcile)

    // POOL GUARD (settled): the occupant's index-removal must pass the SAME guard the
    // remove verb applies. Commit the removal FIRST so a rejection is a true no-op (no
    // slot mutation happened yet). removeSample(ThisBank) permits per-sample removal from
    // any bank incl. the pool (per-sample remove is not a pool privilege), so it succeeds
    // whenever the occupant exists — which we verified — but routing through it means a
    // future pool-floor guard added to remove governs replace identically, one rule.
    const RemoveResult r = removeSample(oldId, bankId, RemoveScope::ThisBank);
    if (r != RemoveResult::Removed) return false;  // guard rejected -> nothing changed

    // Occupant gone from the index; now update the slot markers. Drop oldId's now-dangling
    // marker to free the target slot, then move newId onto it. reorder onto an EMPTY slot
    // places newId there exactly and empties newId's own (source) slot — the slot position
    // is preserved and only its occupant changed, exactly the replace contract.
    b->slots.remove(oldId);
    b->slots.reorder(newId, targetSlot);
    return true;
}

bool BankBook::hashReferencedElsewhere(const std::string& hash,
                                       const std::string& exceptBankId) const {
    if (hash.empty()) return false;  // empty hashes never dedup (mirror findByHash)
    for (const auto& b : banks_) {
        if (b.id == exceptBankId) continue;         // the removed-from bank is excluded
        if (b.index.findByHash(hash) != nullptr) return true;
    }
    return false;
}

std::vector<std::string> BankBook::referencedPaths() const {
    // Union across the whole book (pool first, then named banks in ordinal order —
    // banks_ is kept ordinal-sorted). De-duplicate by exact string so a file a copy
    // put in two banks appears once. Skip empty paths (they reference no file).
    std::vector<std::string> paths;
    std::unordered_set<std::string> seen;
    for (const auto& b : banks_) {
        for (const auto& s : b.index.all()) {
            if (s.relativePath.empty()) continue;
            if (seen.insert(s.relativePath).second)
                paths.push_back(s.relativePath);
        }
    }
    return paths;
}

// ===========================================================================
// JSON — serialize / deserialize / adoptBanks live in bank_book_json.cpp
// (Q-W5 extraction; byte-identical format, golden-literal-pinned by the Q-W1
// test). loadFromPersisted stays here: it is the load-source PRECEDENCE rule
// (banks-blob vs legacy vs empty), not the codec.
// ===========================================================================

// ---------------------------------------------------------------------------
// Active-bank cycle ordering (pure, free function — mirror of nextModeId)
// ---------------------------------------------------------------------------

std::string nextBankId(const std::vector<std::string>& orderedBankIds,
                       const std::string& currentBankId) {
    if (orderedBankIds.empty()) return {};  // nothing to cycle to
    for (std::size_t i = 0; i < orderedBankIds.size(); ++i) {
        if (orderedBankIds[i] == currentBankId)
            return orderedBankIds[(i + 1) % orderedBankIds.size()];  // wrap past the last
    }
    // Active id not in the list (stale/unknown) — jump to the first id as a sane
    // home rather than returning "" (matches nextModeId's fallback).
    return orderedBankIds.front();
}

BankBook BankBook::loadFromPersisted(const std::string& banksJson,
                                     const std::string& legacyJson) {
    // Precedence 1: the authoritative `banks` blob. A present-but-malformed blob is
    // an error, not an absence — degrade to an empty book rather than falling through
    // to a stale legacy key (which would resurrect superseded single-bank state).
    if (!banksJson.empty()) {
        auto book = deserialize(banksJson);
        return book ? std::move(*book) : BankBook{};
    }
    // Precedence 2: no `banks` yet, but a legacy `bank_index` — one-way pool migration
    // (deserialize's parse-time legacy path promotes it into the pool). A malformed
    // legacy blob likewise degrades to empty.
    if (!legacyJson.empty()) {
        auto book = deserialize(legacyJson);
        return book ? std::move(*book) : BankBook{};
    }
    // Precedence 3: a brand-new / never-captured project — a fresh empty book.
    return BankBook{};
}

} // namespace reasampler
