// package_import_action.cpp — see package_import_action.h for the contract.
// main.cpp owns the API pointers; this TU gets them extern.

#include "shell/actions/package_import_action.h"

#include <string>

#include "core/package/import_plan.h"
#include "core/package/package_format.h"
#include "core/version/app_version.h"
#include "shell/package/import_bank.h"
#include "shell/package/package_pickers.h"
#include "shell/persist/session.h"

#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_ShowMessageBox
#include "reaper_plugin_functions.h"

namespace reasampler {

namespace {

constexpr const char* kTitle = "ReaSampler: import bank package";

std::string quoted(const std::string& s) { return "\"" + s + "\""; }

// The message body itself is core/package::ledgerRefusalMessage — a pure
// (LedgerRefusal, namespace) -> string fold, testable without a DAW. This TU only
// supplies the channel-correct namespace and the console call.
void reportLedgerRefusal(package::LedgerRefusal refusal) {
    ShowConsoleMsg(
        package::ledgerRefusalMessage(refusal, version::extStateNamespace()).c_str());
}

// The refusal a user can act on names all three: what the package needs, what this
// build reads, and which build wrote it. Any two of them leave them stuck.
void reportTooNew(const ImportBankResult& r) {
    const bool knownWriter = !r.header.writerVersion.empty();
    const std::string writer =
        knownWriter ? "ReaSampler " + r.header.writerVersion : std::string("an unidentified build");
    std::string msg =
        "Cannot import this bank package.\n"
        "It was written by " + writer + " and needs package format " +
        std::to_string(r.header.minReaderVersion) + " or newer.\n"
        "This build (" + version::appVersion() + ") reads package format " +
        std::to_string(package::kPackageFormatVersion) + ".\n"
        "Nothing was imported. ";
    // "Install <writer> or newer" reads fine when writer is a real semver; it does not
    // when writer is the "unidentified build" filler, so that case gets its own sentence.
    msg += knownWriter ? "Install " + writer + " or newer and try again."
                       : "Install a newer version of ReaSampler and try again.";
    ShowMessageBox(msg.c_str(), kTitle, 0);
}

void reportSuccess(const ImportBankResult& r) {
    std::string detail = "ReaSampler import: imported " + std::to_string(r.landedCount) +
                         " sample(s) into a new bank: " + quoted(r.bankDisplayName);
    if (r.bankNameAdjusted)
        detail += " (a bank named " + quoted(r.seedBankName) +
                  " already exists in this project)";
    detail += ".\n";
    // Two distinct triggers (core/package::ImportPlan), reported as two counts rather
    // than folded into one ambiguous "already taken, or not spelled right" line.
    if (r.collisionRenameCount > 0) {
        detail += "  " + std::to_string(r.collisionRenameCount) +
                  " file(s) landed under a freshly minted name (the package's own name "
                  "was already taken in the bank folder). An existing bank file is "
                  "never overwritten.\n";
    }
    if (r.sanitizeRenameCount > 0) {
        detail += "  " + std::to_string(r.sanitizeRenameCount) +
                  " file(s) landed under a freshly minted name (not spelled the way "
                  "this bank spells a file).\n";
    }
    if (r.collapsedCount > 0) {
        // "Already present" here can only mean a duplicate BY CONTENT inside this same
        // package (Ε-F2: import never consults another bank's hashes) — deliberately
        // reworded from bank-package.md:448's "already present" phrasing, which reads
        // as "already in your project" and is misleading in this direction.
        detail += "  " + std::to_string(r.collapsedCount) +
                  " sample(s) duplicated another entry in this same package by content "
                  "and were written once.\n";
    }
    detail += "One undo removes the imported bank and its entries. It does NOT delete "
              "the imported files -- they stay in the bank folder, referenced by "
              "nothing, until a prune reclaims them.\n";
    ShowConsoleMsg(detail.c_str());

    // The console carries the copyable detail; the box makes the outcome unmissable.
    const std::string summary = "Imported " + std::to_string(r.landedCount) +
                                " sample(s) into a new bank: " +
                                quoted(r.bankDisplayName) + ".";
    ShowMessageBox(summary.c_str(), kTitle, 0);
}

void reportRollback(const RollbackResult& rollback, std::string& msg) {
    if (rollback.failedCount > 0) {
        msg += "\n" + std::to_string(rollback.failedCount) +
               " partly-imported file(s) could not be removed and are still in the bank "
               "folder. They are referenced by no bank; a prune will reclaim them.";
    }
}

void report(const ImportBankResult& r) {
    switch (r.outcome) {
        case ImportOutcome::Landed:
            reportSuccess(r);
            return;
        case ImportOutcome::TooNew:
            reportTooNew(r);
            return;
        case ImportOutcome::NoProject:
            ShowMessageBox("Save the project before importing a bank package -- an "
                           "unsaved project has no bank folder to import into.",
                           kTitle, 0);
            return;
        case ImportOutcome::Unreadable:
            ShowMessageBox("That file could not be opened. Nothing was imported.",
                           kTitle, 0);
            return;
        case ImportOutcome::Malformed:
            // Distinct from TooNew on purpose: the recoveries are opposite -- one is
            // "install a newer build", this one is "get an intact copy".
            //
            // bank-package.md:443 asks for a separate "This package is not well-formed"
            // message when an entry name carries a separator / ".." / an absolute form.
            // Not implemented: deserializeManifest returns one indistinguishable nullopt
            // for that and for ordinary corruption, so it folds into this generic box.
            // The binding spec (PLAN.md:2678) only requires Malformed != TooNew, which
            // this still satisfies -- that product-doc row is knowingly left open, not
            // silently missed.
            ShowMessageBox("This file is not a readable bank package (corrupt or "
                           "truncated). Nothing was imported.",
                           kTitle, 0);
            return;
        case ImportOutcome::IntegrityFailed: {
            std::string msg = "This bank package is damaged (entry " +
                              quoted(r.failedEntryName) +
                              " failed its integrity check). Nothing was imported.";
            ShowMessageBox(msg.c_str(), kTitle, 0);
            return;
        }
        case ImportOutcome::WriteFailed: {
            std::string msg = "Import failed and was rolled back. Nothing was added.";
            reportRollback(r.rollback, msg);
            ShowMessageBox(msg.c_str(), kTitle, 0);
            return;
        }
        case ImportOutcome::IndexRejected: {
            std::string msg = "The bank index rejected the import. Nothing was added.";
            reportRollback(r.rollback, msg);
            ShowMessageBox(msg.c_str(), kTitle, 0);
            return;
        }
    }
}

// FIRST, before the picker: making the user find and choose a file we have already
// decided to refuse is the wrong order.
bool ledgerPermits(ReaSamplerSession& session) {
    const package::LedgerRefusal refusal =
        package::importLedgerRefusal(session.ledgerStatus());
    if (refusal == package::LedgerRefusal::None) return true;
    reportLedgerRefusal(refusal);
    return false;
}

}  // namespace

std::string doImportBankPackage(ReaSamplerSession& session) {
    if (!ledgerPermits(session)) return {};
    std::string path;
    if (!pickPackageForImport(path) || path.empty()) return {};
    const ImportBankResult r = importBankPackage(session, path);
    report(r);
    return r.outcome == ImportOutcome::Landed ? r.bankId : std::string{};
}

std::string doImportBankPackageFile(ReaSamplerSession& session, const std::string& packageAbsPath) {
    if (packageAbsPath.empty()) return {};
    if (!ledgerPermits(session)) return {};
    const ImportBankResult r = importBankPackage(session, packageAbsPath);
    report(r);
    return r.outcome == ImportOutcome::Landed ? r.bankId : std::string{};
}

}  // namespace reasampler
