#pragma once
// persist — the REAPER-facing bridge between the in-memory BankIndex and project
// ext state (CLAUDE.md §load-bearing split; CONTEXT.md §Persistence & paths).
//
// Save: serialize the BankIndex JSON -> SetProjExtState under namespace
//   "reasampler" (ext state lives inside the .rpp, so the index travels with the
//   project for free).
// Load: on project load, GetProjExtState -> bank_model::deserialize -> in-memory
//   BankIndex, then resolve each entry's bank file against the CURRENT project
//   dir (project-relative resolution — a project opened from a new location still
//   finds its bank).
// Save-As: when the project path changes, relocate the physical bank folder so
//   the wavs end up under the new .rpp (the index's relative paths stay valid).
//
// The header is REAPER-free (no SDK types leak here): callers interact through a
// ReaSamplerSession that owns the bank and the persist lifecycle. All REAPER API
// calls live in persist.cpp. It depends on bank_model (pure) for JSON round-trip
// and capture_paths (pure) for the path arithmetic it drives.

#include <cstdint>
#include <string>

#include "app_version.h"
#include "bank_book.h"
#include "bank_model.h"
#include "ext_keys.h"
#include "owned_manifest.h"
#include "prune_reconcile.h"
#include "tail_control.h"
#include "view_mode_model.h"

namespace reasampler {

// The ext-state namespace + the WIRE-SHARED key names are the contract between this
// extension (writer) and the VST3 instrument (reader), so they live in ext_keys.h
// (pure, REAPER-free) and are included above — not duplicated here. The namespace is
// CHANNEL-DERIVED (Phase V, V4): ext_keys.h's kProjExtNamespace / this projExtNamespace()
// both delegate to app_version's extStateNamespace() — "reasampler" on stable (byte-
// identical to the pre-V4 build) or "reasampler_beta" on the isolated beta build. Both
// artifacts read the ONE app_version symbol, so the instrument reads exactly the namespace
// the extension writes, per channel. Beta reads/writes ONLY its own namespace — a project
// saved by stable shows empty/default state in beta and vice versa; that isolation is the
// accepted V4 safety property (no cross-namespace read, migration, or fallback), not a bug.
// The per-key semantics persist relies on (spellings owned by ext_keys.h):
//   * kProjExtBanksKey  : the whole serialized BankBook (pool + named banks).
//     AUTHORITATIVE going forward; the VST reads this key to see the live bank.
//   * kProjExtIndexKey  : RETIRED legacy single-bank key. No longer WRITTEN (cleared
//     on save); READ once on load to migrate a legacy project into the pool.
//   * kProjExtViewKey   : the Design-View ViewModeModel JSON.
//   * kProjExtTailKey   : the docked panel's TailSetting JSON.
//   * kProjExtGuidKey   : the per-project minted GUID (content-based identity; poll()
//     tells a Save-As from a recycled-pointer project switch by it).
// All are FOREVER-STABLE once shipped: changing any strands every already-saved
// project's stored state under that key.
//
// The accessor form of the namespace: ext_keys.h's kProjExtNamespace is the value; this
// is the const char* the SetProjExtState/GetProjExtState calls in persist.cpp pass. Kept
// as an accessor (not a literal) because the string is channel-derived at build time.
inline const char* projExtNamespace() { return extStateNamespace().c_str(); }

// The two EXTENSION-ONLY keys — NOT part of the VST wire contract (the instrument
// reads only banks/view/tail/guid), so they stay here rather than in ext_keys.h:
//
// owned_files — the owned-file manifest JSON (project-relative files the capture path
// itself created; Phase B B-cap seam, consumed by Phase R prune to tell the bank system's
// own orphans from hand-dropped files). A SIBLING key alongside banks/view/tail — NOT
// folded into `banks`, so it stays decoupled from membership. FOREVER-STABLE: changing it
// strands every saved project's ownership record (prune falls back to an empty manifest —
// graceful, but the attribution safety net is lost until the next capture rebuilds it).
inline constexpr const char* kProjExtOwnedKey = "owned_files";

// version — the ReaSampler version that last WROTE this project (Phase V, V1). Written on
// every save, so every saved .rpp records which build produced its state — the seam a
// future within-channel forward migration keys off. An absent key is the explicit
// pre-versioning case, read silently, never an error. FOREVER-STABLE key string.
inline constexpr const char* kProjExtVersionKey = "version";

// Owns the session's BankBook (Phase B: pool + named banks) and drives persistence
// against the active REAPER project. One instance lives for the extension's
// lifetime (main.cpp). It tracks
// the project identity it last saw so the timer tick can detect a project load
// (a different project became active) and a Save-As (SAME project, path changed):
//
//   * project load       -> load the index from ext state, resolve bank paths
//   * Save-As (new dir)   -> relocate the bank folder under the new .rpp
//
// Identity is layered GUID-PRIMARY: the minted GUID (content-based identity of
// record, immune to REAPER recycling a closed project's ReaProject* address) is
// checked FIRST, and the live pointer disambiguates only the same-GUID case — a
// forked sibling (same GUID, different object -> Load) vs a genuine Save-As (same
// GUID, same object, new path -> relocate). GUID-first catches pointer recycling
// (a reopened/new project reusing the previous address with a different GUID — the
// W12 defect that stopped the bank reloading); the pointer catches forks (Save-As
// copies our GUID onto a distinct object — the W10 defect that clobbered a bank).
//
// The book itself is exposed for the capture/action layer to mutate; persist
// only reads it on save and replaces it on load.
class ReaSamplerSession {
public:
    ReaSamplerSession() = default;

    // The multi-bank book (Phase B): the pool + named banks, each wrapping a
    // BankIndex, plus the active-bank id. The action layer (B3) creates / renames /
    // reorders / deletes banks and moves samples here; the panel (B4) reads it;
    // persist serializes it under the `banks` key on save and replaces it on load.
    BankBook& book() { return book_; }
    const BankBook& book() const { return book_; }

    // The capture add-target: the ACTIVE bank's BankIndex (defaults to the pool).
    // The capture path adds a captured Sample through this seam, so a capture lands
    // in whichever bank is active — the single behavioural change B2 wires in over
    // M7/M8 (the capture backends are untouched; only the target index moved). The
    // panel/insert readers that displayed the single index continue to read it here
    // unchanged; today it resolves to the pool (default active), matching prior
    // single-bank behaviour, until B3/B4 let the user switch the active bank.
    BankIndex& bank() { return book_.activeIndex(); }
    const BankIndex& bank() const { return book_.activeIndex(); }

    // The in-memory Design-View model. The view/action layer mutates it (tag,
    // toggle, snapshot); persist serializes it on save and replaces it on project
    // load — exactly as it treats the bank. D3 persists MODEL STATE only; applying
    // visibility/processing (reapply-on-open) is D4's job, not this member's.
    ViewModeModel& view() { return view_; }
    const ViewModeModel& view() const { return view_; }

    // The docked panel's tail setting (mode + manualMs), authoritative here — NOT in
    // panel state — so it travels inside the .rpp: persist serializes it on save and
    // replaces it on project load exactly as it treats the bank and view model. The
    // panel reads/writes it through this seam (bank_panel holds the session), and the
    // capture actions read it via bankPanelTailSetting. Default None / 2 s manual for
    // an unsaved or pre-feature project (no stored key -> this default survives load).
    TailSetting& tail() { return tail_; }
    const TailSetting& tail() const { return tail_; }

    // The owned-file manifest (Phase B B-cap): the set of project-relative files the
    // capture path itself created. The capture add-path records each created file here
    // (main.cpp, alongside the bank add), exactly as it adds the Sample to the active
    // bank; persist serializes it under the `owned_files` key on save and replaces it on
    // project load / undo-reload — peer to book_/view_/tail_. Phase R prune CONSUMES it;
    // B-cap only writes and persists it (no prune logic here).
    OwnedFileManifest& owned() { return owned_; }
    const OwnedFileManifest& owned() const { return owned_; }

    // The ReaSampler version that last WROTE the active project, recovered from its
    // ext-state stamp on load (Phase V, V1). PreVersioning when the project carries no
    // stamp (saved before this feature), Unknown for a malformed stamp, Stamped with the
    // exact stored string otherwise — all silent, never an error. Replaced on every load
    // path (peer-symmetry with bank_/view_/tail_); default PreVersioning for an unsaved
    // or never-loaded session. Exposed so a future migration step (or diagnostics) can
    // reason about the origin build without re-reading ext state.
    const WritingVersion& writingVersion() const { return writingVersion_; }

    // The S9 bank-generation counter (the value stamped under `bank_generation`). Monotonic
    // per project: recovered on load (so it continues from the stored value rather than
    // resetting), bumped by bank-content mutations via bumpBankGeneration(), and written on
    // every saveToActiveProject(). Exposed const for the writer sites to read/log.
    std::int64_t bankGeneration() const { return bankGeneration_; }

    // Bump the S9 bank-generation counter — call at every bank-CONTENT mutation that changes
    // what a live instance would PLAY (capture add, re-capture-in-place, sample remove,
    // move/copy affecting banks, ingest import). NOT the pure-organizational verbs (create /
    // rename / activate / reorder a bank), which change no existing (bankId, sampleId) ->
    // content mapping. The bumped value is persisted by the NEXT saveToActiveProject() call
    // the same mutation already makes (the counter rides the persist blob, so there is no
    // separate write). In-memory only here — cheap and REAPER-free; the persist is the write.
    // Over-bumping is safe (a reload that finds unchanged content atomically re-installs the
    // same instrument, no glitch); under-bumping misses a hands-free refresh, so the sites err
    // toward bumping. Idempotent per logical op — call once per mutation, before the persist.
    void bumpBankGeneration() { ++bankGeneration_; }

    // Serialize the current book (under the `banks` key), view model, and tail setting
    // to the active project's ext state (namespace "reasampler"), and clear the retired
    // legacy `bank_index` key. Non-destructive beyond writing our own ext-state keys.
    // Safe to call when there is no active/saved project (it no-ops).
    //
    // Returns true iff a persist actually happened (an active, SAVED project existed);
    // false when it no-op'd (no active project, or an unsaved one with no .rpp). Lets a
    // caller wrapping this in an undo block skip the block when nothing was written, so
    // no dangling no-effect undo entry is opened on an unsaved project.
    bool saveToActiveProject();

    // Compute the Phase R prune dry-run for the ACTIVE project (Wave 2 — REPORT ONLY,
    // deletes nothing). Enumerates the resolved CURRENT bank folder (the SAME M4 project-
    // relative machinery the index/persist use — never a stale absolute path, so it is
    // correct across a Save-As relocation), spells every enumerated entry with the index's
    // own convention (bankRelativeForName — byte-identical to the capture path's spelling),
    // and feeds the R1 pure core with (present, book().referencedPaths(), owned().paths()).
    // Returns the orphan count + reclaimable bytes + the (possibly display-truncated) file
    // list. The decision stays in the pure core — this method only enumerates, resolves,
    // and stats. READ-ONLY across the whole persist seam: it writes NO ext-state, calls no
    // save / MarkProjectDirty, and mutates neither the book, the manifest, nor any file.
    //
    // Yields an empty report (count 0) when there is no active/saved project or no bank
    // folder on disk yet — an unsaved or never-captured project has nothing to reclaim.
    PruneReport pruneDryRun() const;

    // The FULL (untruncated) prune orphan set for the ACTIVE project — the same fresh
    // enumerate + pure-core compute pruneDryRun() runs, but returning EVERY orphan (no
    // 64-cap display clip) as project-relative index-spelled paths, in enumeration order.
    // The R3 action calls this to obtain the exact set it will CONFIRM and then delete
    // (pruneDryRun's truncated list is for the console readout; the delete set must be
    // complete). READ-ONLY — no ext-state, no save, no file mutation. Empty when there is
    // no active/saved project or no bank folder yet.
    std::vector<std::string> pruneOrphanSet() const;

    // Phase R (Reclaim), R3: DELETE the confirmed orphan set — the SOLE file-deletion path
    // in ReaSampler, callable ONLY after an explicit user confirm of a specific manifest.
    // Given the orphan set the user was shown and confirmed (`confirmed`, typically the
    // full pruneOrphanSet() captured moments earlier), this re-enumerates the folder, runs
    // the pure core FRESH, and deletes exactly `confirmed ∩ freshOrphans` (pruneDeletePlan)
    // so a file that vanished or became referenced between confirm and delete is skipped,
    // never wrongly deleted — and a newly-appeared orphan the user did NOT see is never
    // swept. Deletion routes to the OS trash where a portable move-to-trash is verified
    // (Windows Recycle Bin via SHFileOperation + FOF_ALLOWUNDO); elsewhere it falls back to
    // std::filesystem unlink behind this confirm guardrail (see persist.cpp for per-platform
    // routing). Non-throwing: every filesystem call uses error_code forms; a per-file
    // failure (locked, already gone) is recorded and skipped, never thrown across the C ABI.
    //
    // Does NOT modify the BankIndex/book (orphans are unreferenced by definition) and does
    // NOT modify the OwnedFileManifest (a reclaimed file drops out of the (owned ∩ present)
    // algebra naturally once it is off disk — no persist write, so no undo-point question
    // and no risk to the referenced/owned safety). Writes NO ext-state at all.
    //
    // No-ops (empty result) when there is no active/saved project, no bank folder, or the
    // delete plan is empty (everything went stale). The caller is responsible for having
    // shown the confirm; this method does NOT prompt.
    PruneDeletionResult pruneReclaim(const std::vector<std::string>& confirmed) const;

    // Write the S8 ingest ASSIGNMENT REQUEST to the active project's ext state (the
    // `assign_request` key, namespace "reasampler"): the extension telling the active
    // sampler instance "play THIS sample now." `wire` is the pure assignment_request
    // encoding (assignment_request.h); this method only routes the already-encoded value
    // to ext state + MarkProjectDirty — the (bankId, sampleId, generation) shaping and
    // the encode live in the ingest shell (the pure module) so persist stays a thin bridge.
    //
    // A SIBLING one-shot write, NOT part of saveToActiveProject's book/view/tail blob: an
    // assignment request is a transient "just assigned" signal the instrument reads and
    // acts on, so it rides its own key and is written only at ingest time, never on every
    // book save. Returns true iff written (an active, SAVED project existed); false on a
    // no-active / unsaved project (nothing to write into — the assign is dropped, matching
    // the book/manifest quiet-persist idiom the ingest add-path already tolerates).
    bool writeAssignmentRequest(const std::string& wire);

    // Poll the active project. Detects a project load (active project changed)
    // and a Save-As (active project's .rpp path changed) and reacts accordingly.
    // Intended to be driven by REAPER's "timer" register. Idempotent per tick.
    //
    // Also drains a pending undo/redo reload (requestReload): a Ctrl-Z / Ctrl-Shift-Z
    // keeps the SAME project identity (same ReaProject*/GUID/.rpp path), so the
    // identity classifier below reads it as NoOp and would never re-read ext state.
    // The projectconfig hook (main.cpp) raises the reload flag on an undo/redo state
    // restore; poll() honours it FIRST — reloading book_ + view_ + tail_ from the
    // (now-restored) ext state of the current project — before the identity check, so
    // the undo is reflected in-session without any content polling.
    void poll();

    // Request a reload of book_ + view_ + tail_ from the CURRENT active project's ext
    // state on the next poll() tick. Raised by the projectconfig hook (main.cpp) ONLY
    // on an undo/redo state restore (isUndo). Deferred (a flag, not an immediate read)
    // because the projectconfig callback fires BEFORE REAPER has restored the project's
    // <EXTSTATE> block — reading GetProjExtState synchronously there would return the
    // PRE-undo value. Draining it on the next timer tick reads the restored value. This
    // is REAPER-facing shell state; the request itself carries no REAPER types.
    void requestReload();

    // Load signal for the D4 reapply-on-open glue. poll() raises this whenever it
    // (re)loads the view model from a project — prime, a project switch/open, or a
    // forked-sibling load. consumeLoadSignal() returns true ONCE per load and clears
    // it, so the integration layer (main.cpp) can react by reapplying the saved
    // active mode's visibility exactly once, then goes quiet on idle ticks.
    //
    // Signal-based seam by design: persist stays MODEL-ONLY (it never calls the view
    // shell), so there is no persist -> view dependency. main.cpp owns the glue —
    // it drives both persist.poll() and view::applyMode, so the reapply wiring lives
    // where those two already meet. D3 deliberately deferred exactly this to D4.
    bool consumeLoadSignal();

private:
    BankBook book_;

    // The Design-View model. Default-constructed = Arrange + Design seeded, active
    // = Arrange; loadFromProject leaves this default when a project has no stored
    // view_state (older project), so an absent key is graceful, not a crash.
    ViewModeModel view_;

    // The tail setting. Default None / kDefaultManualTailMs; loadFromProject resets it
    // to this default when a project has no stored tail_setting key (older / never-
    // adjusted project), so an absent key is graceful. Peer to bank_/view_.
    TailSetting tail_;

    // The owned-file manifest. Default empty; loadFromProject resets it to empty (or the
    // stored set) on EVERY load path (peer-symmetry with book_/view_/tail_): switching to
    // a project with no stored manifest must not inherit the previous project's ownership
    // record, and an undo that rolled back a capture must re-read the restored manifest so
    // the in-memory set matches disk. Absent key -> empty is graceful (older project).
    OwnedFileManifest owned_;

    // The writing-version stamp recovered on load (Phase V). Default PreVersioning;
    // loadFromProject replaces it on every load path (peer to bank_/view_/tail_), so
    // switching to a pre-versioning project reports PreVersioning rather than inheriting
    // the previous project's stamp. Read-only to consumers via writingVersion().
    WritingVersion writingVersion_;

    // The S9 bank-generation counter (peer to writingVersion_). Recovered on EVERY load path
    // from the stored `bank_generation` stamp (parseBankGeneration; absent -> 0), so it
    // continues monotonic from the persisted value across reopen and resets cleanly on a
    // project switch (a different project's counter, not the previous project's). bumped by
    // bumpBankGeneration() at bank-content mutations and stamped by saveToActiveProject().
    // Default 0 for an unsaved / never-loaded / pre-S9 session.
    std::int64_t bankGeneration_ = 0;

    // The project identity last observed by poll(), used to detect load/Save-As.
    // The GUID is the PRIMARY signal (a different stored GUID = a different project
    // of record = Load, immune to pointer recycling). The pointer disambiguates the
    // same-GUID case (different object = forked sibling -> Load; same object + new
    // path -> Save-As) and drives forked-sibling re-divergence; the path tells a
    // Save-As from an idle tick.
    // Held as void* so the header stays REAPER-free; it is a compared-only opaque
    // handle (never dereferenced), so a stale/recycled address is harmless.
    void*       lastProject_ = nullptr;   // last active ReaProject* (opaque; compare only)
    std::string lastGuid_;                // "" until the first saved project is seen
    std::string lastRppPath_;             // .rpp path last seen for lastProject_
    bool        primed_ = false;          // false until the first poll() observes state
    bool        loadPending_ = false;     // raised by loadFromProject; drained by consumeLoadSignal
    bool        reloadRequested_ = false; // raised by requestReload (projectconfig undo/redo); drained by poll

    // Load the book from the given project's ext state (the `banks` key, else the
    // legacy `bank_index` key migrated into the pool) and resolve bank paths against
    // projectDir at read time. Replaces the in-memory book. Also restores view_, tail_,
    // and owned_ from their sibling keys on every load path. projectDir empty -> the
    // book is reset to empty (unsaved project has no resolvable banks).
    void loadFromProject(void* proj, const std::string& projectDir);
};

} // namespace reasampler
