// session.cpp — the ReaSamplerSession lifecycle half of the persist seam (see
// session.h for the identity-transition design and the TU map).
//
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
// pointers; here they are extern (CLAUDE.md §contract).
//
// classifyProjectTransition (pure, capture_paths) takes a `sameProjectObject`
// bool so the decision stays REAPER-free and testable; poll() executes the
// verdict. REAPER exposes no stable per-project GUID (GetSetProjectInfo_String
// has no PROJECT_GUID desc; GetProjectStateChangeCount is a session-local
// counter, not cross-open identity), so we mint one (genGuid/guidToString)
// under kProjExtGuidKey; Save-As copies the whole .rpp including our ext
// state, so the new project initially shares the old GUID, and poll()
// re-GUIDs it after relocating (or on the forked-sibling Load branch).
//
// Division of labour for undo/redo: the identity-transition poll (this file)
// owns open/tab-switch/new/forked-sibling/Save-As. The `projectconfig` hook
// (main.cpp, BeginLoadProjectState with isUndo) owns undo/redo, where identity
// is unchanged but ext state rolled back/forward on disk — the identity poll
// would see NoOp there, so the hook requests a reload that poll() drains next
// tick, once REAPER has restored the <EXTSTATE> block. The hook fires on
// undo, redo, AND normal open, but the reload flag is set only for isUndo, so
// a normal open never double-loads.

#include "shell/persist/session.h"

#include <string>

#include "shell/persist/ext_state_io.h"
#include "shell/persist/persist_internal.h"

#include "core/capture/capture_paths.h"  // classifyProjectTransition / deriveRelocationPlan (pure)

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_MarkProjectDirty
#define REAPERAPI_WANT_SetProjExtState
#define REAPERAPI_WANT_ShowConsoleMsg
#include "reaper_plugin_functions.h"

namespace reasampler {

using persist_detail::ensureProjectGuid;
using persist_detail::genProjectGuidString;
using persist_detail::getProjExtStateString;
using persist_detail::projectDirOf;
using persist_detail::readActiveProject;
using persist_detail::relocateBankFolder;

void ReaSamplerSession::recordCreated(const model::Sample& sample,
                                      tracking::OriginKind kind) {
    tracking::OriginRecord rec;
    rec.relativePath = sample.relativePath;
    rec.kind = kind;
    rec.sampleId = sample.id;
    // The Sample's own provenance is where the parent was resolved; reading it here
    // rather than re-deriving keeps one source for the lineage fact. Absent
    // provenance is a root capture, not a gap.
    if (sample.provenance) rec.parentSampleId = sample.provenance->parentSampleId;

    const tracking::RecordResult result = tracking_.record(rec);
    // A rejection means a file exists that nothing attributes to us — invisible
    // otherwise, and exactly the gap this ledger exists to close. AlreadyPresent is
    // the normal dedup outcome, not a gap. Written as "anything but the two OK
    // outcomes" rather than a rejection whitelist, so a future RecordResult value
    // is reported by default instead of silently passing through unrecognized.
    if (result != tracking::RecordResult::Recorded &&
        result != tracking::RecordResult::AlreadyPresent) {
        ShowConsoleMsg(("ReaSampler: could not record the origin of '" +
                        sample.relativePath +
                        "' -- the path is empty or absolute. The file is NOT tracked and "
                        "prune will treat it as a foreign file.\n").c_str());
    }
}

bool ReaSamplerSession::consumeLoadSignal() {
    const bool pending = loadPending_;
    loadPending_ = false;
    return pending;
}

void ReaSamplerSession::requestReload() {
    // Set-only; poll() drains it next tick. Idempotent — multiple undo/redo
    // callbacks before the next tick collapse to one reload.
    reloadRequested_ = true;
}

void ReaSamplerSession::poll() {
    std::string rppPath;
    void* proj = readActiveProject(rppPath);
    const std::string currentGuid =
        proj ? getProjExtStateString(proj, projExtNamespace(), kProjExtGuidKey)
             : std::string{};

    if (!primed_) {
        // First observation: adopt current identity and load its index, without
        // treating it as a "change" (avoids a spurious relocation on startup).
        primed_      = true;
        loadFromProject(proj, projectDirOf(rppPath));
        lastProject_ = proj;
        lastGuid_    = ensureProjectGuid(proj, rppPath, currentGuid);
        lastRppPath_ = rppPath;
        reloadRequested_ = false;  // priming already loaded — a co-tick request is moot
        return;
    }

    // Undo/redo reload, owned by the projectconfig hook, not the identity
    // classifier below: an undo/redo keeps the same project identity, so
    // classifyProjectTransition would return NoOp and never re-read ext
    // state. By now REAPER has finished restoring the <EXTSTATE> block, so
    // GetProjExtState returns the post-undo value. Reload and identity-adopt
    // (no relocation — path unchanged). This is the ONLY undo/redo reload
    // path — the timer never polls ext-state content to detect an undo.
    if (reloadRequested_) {
        reloadRequested_ = false;
        loadFromProject(proj, projectDirOf(rppPath));
        lastProject_ = proj;
        lastGuid_    = ensureProjectGuid(proj, rppPath, currentGuid);
        lastRppPath_ = rppPath;
        return;
    }

    // Pointer identity is the primary signal: a genuine Save-As keeps the SAME
    // ReaProject* (one object saved elsewhere); a tab-switch/open is a different
    // object. Passing the bool (not the pointer) keeps the classifier pure.
    const bool sameProjectObject = (proj == lastProject_);
    const capture::ProjectTransition transition = capture::classifyProjectTransition(
        sameProjectObject, lastGuid_, lastRppPath_, currentGuid, rppPath);

    switch (transition) {
        case capture::ProjectTransition::NoOp:
            return;

        case capture::ProjectTransition::Load: {
            // A different project of record is active. Load its index; never relocate.
            //
            // Forked-sibling re-GUID: gate on `!sameProjectObject` so this fires
            // only for the fork case (same GUID, different object) — a Save-As
            // fork that copied our GUID and never re-saved. A recycled-pointer
            // Load (currentGuid != lastGuid_) must NOT re-GUID — it is already
            // a distinct identity; currentGuid == lastGuid_ can only hold here
            // when that case did not fire.
            if (proj && !sameProjectObject && !currentGuid.empty() &&
                currentGuid == lastGuid_ && !rppPath.empty()) {
                const std::string fresh = genProjectGuidString();
                SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
                                kProjExtGuidKey, fresh.c_str());
                MarkProjectDirty(static_cast<ReaProject*>(proj));
                loadFromProject(proj, projectDirOf(rppPath));
                lastProject_ = proj;
                lastGuid_    = fresh;
                lastRppPath_ = rppPath;
                return;
            }

            // Normal load: establish identity the same way prime does.
            loadFromProject(proj, projectDirOf(rppPath));
            lastProject_ = proj;
            lastGuid_    = ensureProjectGuid(proj, rppPath, currentGuid);
            lastRppPath_ = rppPath;
            return;
        }

        case capture::ProjectTransition::SaveAsRelocate: {
            // Same project object, new .rpp path: a genuine Save-As. Relocate
            // the bank folder so the wavs sit under the new .rpp and the
            // index's relative paths still resolve. Keep the in-memory bank
            // as-is (Save-As copied our ext state) — do NOT reload.
            const std::string oldDir = projectDirOf(lastRppPath_);
            const std::string newDir = projectDirOf(rppPath);
            const capture::BankRelocation plan =
                capture::deriveRelocationPlan(oldDir, newDir);
            if (plan.needed) {
                relocateBankFolder(plan.oldBankDir, plan.newBankDir);
            }

            // Save-As duplicated our ext state, so the new project shares the
            // old GUID; mint a fresh one and mark dirty so it flushes on the
            // next save, and A/B no longer collide on identity when reopened.
            const std::string fresh = genProjectGuidString();
            if (proj) {
                SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
                                kProjExtGuidKey, fresh.c_str());
                MarkProjectDirty(static_cast<ReaProject*>(proj));
            }
            lastProject_ = proj;   // unchanged (same object) — set for symmetry
            lastGuid_    = fresh;
            lastRppPath_ = rppPath;
            return;
        }
    }
}

}  // namespace reasampler
