#pragma once
// bank_book — pure multi-bank registry: wraps N BankModel instances (bank_model
// itself is untouched — additive, no bankId on Sample). Movement between banks is
// index-only; files never relocate, banks are logical groupings over one shared pool.
//
// The pool is bank-zero (fixed id/name, ordinal 0), privileged and enforced HERE:
// always exists, un-deletable, un-renamable, un-evacuable (evacuate's destination
// only). createBank takes a caller-supplied id — REAPER GUID minting stays in the
// shell so this model stays pure and deterministic; the model still enforces
// non-empty/unique/not-reserved.

#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "core/model/bank_model.h"
#include "core/model/slot_map.h"

namespace reasampler {

// Interim: this module re-namespaces later; the model types it wraps live in
// reasampler::model.
using namespace model;

// The pool's fixed identity: createBank rejects this id; renameBank rejects this name.
inline constexpr const char* kPoolBankId   = "pool";
inline constexpr const char* kPoolBankName = "Pool";

// One bank: id/display/ordinal/BankModel/SlotMap. The pool is the bank whose
// id == kPoolBankId.
struct Bank {
    std::string id;           // stable, persisted; the pool's is kPoolBankId
    std::string displayName;  // mutable for named banks; fixed "Pool" for the pool
    int         ordinal = 0;  // display order; pool is 0, named banks 1..N
    BankModel   index;        // this bank's samples
    SlotMap     slots;        // display positions of this bank's samples (gap-preserving)

    bool isPool() const { return id == kPoolBankId; }

    bool operator==(const Bank& o) const {
        return id == o.id && displayName == o.displayName &&
               ordinal == o.ordinal && index == o.index && slots == o.slots;
    }
};

// Outcome of a cross-bank move/copy — reports what happened rather than mutating
// silently on a bad request.
//  - Moved / Copied:  transferred to the destination as a new entry.
//  - Collapsed:       destination already held the hash, collapsed onto it (move
//                     still removes the source; copy keeps it, as always).
//  - RejectedUnknownBank / RejectedSampleAbsent / RejectedSameBank: no-op guards.
enum class TransferResult {
    Moved,
    Copied,
    Collapsed,
    RejectedUnknownBank,
    RejectedSampleAbsent,
    RejectedSameBank,
};

// Scope of a sample-remove. ThisBank is the only behavior surfaced in the UI;
// AllBanks is a tested latent seam, not wired to any affordance.
//  - ThisBank: drop the entry from the one named source bank only (no cross-bank
//              cascade — dedup is per-bank).
//  - AllBanks: drop the sample's entry from every bank holding it ("purge from
//              the library").
enum class RemoveScope {
    ThisBank,
    AllBanks,
};

// Outcome of BankBook::removeSample — same honesty as TransferResult.
//  - Removed:              at least one index entry was dropped.
//  - RejectedUnknownBank:  the source bank id named no bank (ThisBank scope only).
//  - RejectedSampleAbsent: the sample id was in no bank in scope (nothing removed).
enum class RemoveResult {
    Removed,
    RejectedUnknownBank,
    RejectedSampleAbsent,
};

// An ordered registry of banks with the pool seeded as bank-zero, per-bank sample
// indices, an active-bank pointer, and lossless JSON round-trip.
class BankBook {
public:
    BankBook();  // seeds the pool (id kPoolBankId, name kPoolBankName, ordinal 0);
                 // active bank = pool; zero named banks.

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

    // Creates a named bank with a caller-supplied id/display name (next ordinal
    // assigned automatically). Rejects (false, no mutation) an empty/duplicate id,
    // the reserved pool id, or a duplicate display name (trimmed + case-insensitive,
    // ASCII — "Drums"/"drums"/" Drums " collide, including against the pool's "Pool").
    bool createBank(const std::string& id, const std::string& displayName);

    // Renames a named bank. Rejects (false, no mutation) an unknown id, the pool, or
    // a name already used by another bank. Renaming to its own current name is a
    // no-op success.
    bool renameBank(const std::string& id, const std::string& displayName);

    // The first name in the sequence `seed`, "seed 2", "seed 3", … whose fold is free
    // in this book — what a caller that must not be rejected (the package import) asks
    // for before createBank. First-FREE-ascending, not highest-plus-one, so it fills a
    // gap ("Drums" + "Drums 3" present yields "Drums 2") and is a pure function of the
    // current name set. The seed is returned verbatim when free and is NEVER re-parsed:
    // a bare trailing integer cannot be told from a user's own name, so "Kit 808" would
    // become "Kit 2" under a stripping rule. Terminates by pigeonhole (one of the first
    // N+1 candidates is free for N banks), so it needs no cap. A blank seed comes back
    // blank — what a missing name should become is the caller's policy, not the model's.
    std::string uniqueDisplayName(const std::string& seed) const;

    // Deletes a named bank and its member entries (files untouched — a shell/prune
    // concern). Rejects (false, no mutation) an unknown id or the pool. Remaining
    // ordinals compact after; if the deleted bank was active, falls back to the pool.
    bool deleteBank(const std::string& id);

    // Reorders a named bank to newOrdinal (clamped into range, others shift to stay
    // contiguous). The pool is pinned at 0. Rejects an unknown id or the pool.
    bool reorderBank(const std::string& id, int newOrdinal);

    // Moves every member of a named bank into the pool (index-only, same
    // destination-collapse-by-hash as a move). Rejects an unknown id or the pool
    // (the pool is only ever a destination). Returns true even if already empty.
    bool evacuate(const std::string& id);

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

    // The active bank's id (the capture target). Defaults to the pool.
    const std::string& activeBankId() const { return activeBankId_; }

    // Sets the active bank. Rejects (false, no change) an id that names no bank.
    bool setActiveBank(const std::string& id);

    // The active bank's BankModel — always valid (falls back to the pool).
    BankModel&       activeIndex();
    const BankModel& activeIndex() const;

    // -- Sample movement (index-only; files never relocate) ------------------

    // Moves a sample by id between banks (destination collapse-by-hash observed).
    // See TransferResult for the full outcome set.
    TransferResult moveSample(const std::string& sampleId,
                              const std::string& fromBankId,
                              const std::string& toBankId);

    // Copies a sample by id between banks, source retained (destination
    // collapse-by-hash observed). Cross-bank dedup is NOT enforced — the same hash
    // may then live in both banks.
    TransferResult copySample(const std::string& sampleId,
                              const std::string& fromBankId,
                              const std::string& toBankId);

    // -- Sample removal (index-only; the file is NEVER touched — orphaned until prune) --

    // Drops a sample's index entry — non-destructive to the file (a last-reference
    // remove leaves it on disk, orphaned until prune reclaims it). See RemoveScope/
    // RemoveResult for scope and outcome. No mutation on any Rejected outcome.
    RemoveResult removeSample(const std::string& sampleId,
                              const std::string& fromBankId,
                              RemoveScope scope = RemoveScope::ThisBank);

    // -- Sample display order (index membership untouched) -------------------

    // The bank's sample ids in display (slot) order, reconciled against live index
    // membership first (drops stale markers, appends unmapped samples densely). An
    // unknown bank id yields an empty vector.
    std::vector<std::string> orderedSampleIds(const std::string& bankId);

    // Ensures every bank's SlotMap is consistent with its index membership (seeds a
    // dense order, or reconciles a partial map). Idempotent — call after deserialize
    // or any out-of-band membership change.
    void reconcileSlots();

    // Reorders sample `id` within `bankId` to targetSlot (gap-preserving; index/file/
    // metadata untouched). Returns false (no mutation) on an unknown bank or id.
    bool reorderSample(const std::string& id, const std::string& bankId, int targetSlot);

    // Alt-replace: the dragged `newId` (already a member of bankId) takes the slot of
    // `oldId`, and `oldId` is removed from the index (same semantics as removeSample
    // ThisBank). Position is preserved; only the occupant changes.
    //
    // Applies the same pool guard as removeSample — per-sample removal from the pool
    // is allowed (the pool's guards are un-delete/rename/evacuate, never per-sample
    // remove). Rejects (false, no mutation of either index or slots) an unknown bank,
    // a newId/oldId the bank doesn't hold, or newId == oldId.
    bool replaceSample(const std::string& newId, const std::string& oldId,
                       const std::string& bankId);

    // Refreshes a sample in place wherever it lives (re-capture): finds the bank
    // holding sampleId and replaces its entry with `updated` (order-preserving, no
    // dedup). Updates the FIRST holder in ordinal order if the id lives in multiple
    // banks via copy. Returns false (no mutation) if no bank holds the id or the
    // replacement's path is absolute.
    bool updateSampleInPlace(const std::string& sampleId, const Sample& updated);

    // Does any bank other than exceptBankId still hold an entry whose contentHash
    // == hash? Backs the confirm-on-last-reference guardrail: two entries sharing a
    // hash share one file, so this answers "would removing here orphan the file."
    // An empty hash never matches (mirrors findByHash) — reads as
    // referenced-nowhere-else, the safe confirm-eliciting default.
    bool hashReferencedElsewhere(const std::string& hash,
                                 const std::string& exceptBankId) const;

    // Every project-relative path referenced by any bank (pool included) — the union
    // prune subtracts against. Paths are verbatim (no normalization), first-seen
    // order across banks in ordinal then insertion order, de-duplicated. An empty
    // relativePath is skipped.
    std::vector<std::string> referencedPaths() const;

    // -- Query ---------------------------------------------------------------

    // The bank with `id`, or nullptr. Pointer invalidated by any mutating call.
    Bank*       bank(const std::string& id);
    const Bank* bank(const std::string& id) const;

    // The bank's BankModel by id, or nullptr. Convenience over bank()->index.
    BankModel*       index(const std::string& id);
    const BankModel* index(const std::string& id) const;

    // The pool (always present). Never null.
    Bank&       pool();
    const Bank& pool() const;

    // All banks in ordinal order (pool first). The pool is always banks()[0].
    const std::vector<Bank>& banks() const { return banks_; }

    std::size_t size() const { return banks_.size(); }  // >= 1 (the pool)

    bool operator==(const BankBook& o) const {
        return banks_ == o.banks_ && activeBankId_ == o.activeBankId_;
    }

    // -- Persistence ---------------------------------------------------------

    // Serializes the whole book to JSON (lossless): pool as bank-zero + named banks
    // + per-bank indices + ordinals + active id. deserialize(serialize(x)) == x.
    std::string serialize() const;

    // Parses a book JSON produced by serialize(). std::nullopt on malformed input.
    //
    // A bare legacy bank_index JSON (pre-multi-bank shape: a "samples" array, no
    // "banks" key) is promoted into the pool's index — one-way, lossless — yielding
    // a book of { pool } with zero named banks.
    static std::optional<BankBook> deserialize(const std::string& json);

    // Resolves a BankBook from the two persisted ext-state values a project may
    // carry: the authoritative `banksJson` and the retired legacy `bank_index` blob.
    //   1. non-empty banksJson -> deserialize it. If malformed, do NOT fall back to
    //      legacy — a corrupt banks blob is an error, not an absence; returns an
    //      empty book so a stale legacy key can never resurrect superseded state.
    //   2. else non-empty legacyJson -> deserialize it (pool migration).
    //   3. else -> a fresh empty book (pool only).
    // Never returns nullopt — an unloadable input degrades to the empty book.
    static BankBook loadFromPersisted(const std::string& banksJson,
                                      const std::string& legacyJson);

private:
    std::vector<Bank> banks_;         // ordinal order; banks_[0] is always the pool
    std::string       activeBankId_;  // always names a live bank; defaults to pool

    // 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. Private
    // static because both halves of the split implementation (rules + JSON) need the
    // one folding rule; a drifted second copy would let a parsed book violate the
    // create/rename uniqueness invariant.
    static std::string nameKey(const std::string& s);

    // True if a bank other than exceptId already carries name's uniqueness key.
    // Backs the create/rename uniqueness check; pass exceptId=id to let a bank keep
    // (or re-case/-space) its own name.
    bool displayNameTaken(const std::string& name, const std::string& exceptId) const;

    // Re-sorts banks_ by ordinal (pool pinned first) and rewrites ordinals to a
    // contiguous 0..N-1. Called after any structural change (create/delete/reorder).
    void normalizeOrdinals();

    // Replaces the book's banks with a parsed set, normalizes ordinals, and resolves
    // the active bank (falling back to the pool if the id names no bank). Used only
    // by deserialize.
    void adoptBanks(std::vector<Bank>&& banks, const std::string& activeBank);
};

// The next bank id to activate when cycling the active bank forward, in ordinal
// order (pool -> named -> ... -> pool, wraps). Free function (not a member) so it's
// unit-testable against a bare id vector without a full book.
//   * empty list                 -> "" (nothing to cycle to)
//   * single id (pool-only)      -> that id (a one-bank book stays put)
//   * currentBankId not present  -> the first id (a sane home to jump to)
std::string nextBankId(const std::vector<std::string>& orderedBankIds,
                       const std::string& currentBankId);

} // namespace reasampler
