// export_bank.cpp — see export_bank.h for the contract.
//
// wav_codec is called for hashBytes ONLY. Payload bytes are copied and hashed, never
// rebuilt, trimmed, normalized or collapsed — the capture path's mono collapse must
// not reach an export.

#include "shell/package/export_bank.h"

#include <cstddef>
#include <optional>
#include <utility>

#include "core/capture/capture_paths.h"  // resolveBankFile — the index's relative -> absolute
#include "core/capture/wav_codec.h"      // hashBytes
#include "core/model/bank_book.h"
#include "shell/package/package_io.h"
#include "shell/persist/session.h"  // ReaSamplerSession — read through its inline book() only

namespace reasampler {

namespace {

package::SourceFileState stateOf(const std::string& absPath) {
    switch (fileStatus(absPath)) {
        case FileStatus::Present:    return package::SourceFileState::Present;
        case FileStatus::Unreadable: return package::SourceFileState::Unreadable;
        case FileStatus::Absent:     break;
    }
    return package::SourceFileState::Missing;
}

std::vector<std::string> absoluteSources(const std::string& projectDir,
                                         const std::vector<std::string>& relativePaths) {
    std::vector<std::string> out;
    out.reserve(relativePaths.size());
    for (const std::string& rel : relativePaths)
        out.push_back(capture::resolveBankFile(projectDir, rel));
    return out;
}

}  // namespace

ExportSurvey surveyBankExport(const ReaSamplerSession& session,
                              const std::string& projectDir,
                              const std::string& bankId) {
    ExportSurvey survey;
    const Bank* bank = session.book().bank(bankId);
    if (!bank) return survey;
    survey.bankFound = true;

    package::ExportInputs inputs;
    inputs.bankDisplayName = bank->displayName;
    inputs.slots = bank->slots;
    for (const model::Sample& s : bank->index.all()) {
        package::ExportCandidate c;
        c.sample = s;
        c.fileState = stateOf(capture::resolveBankFile(projectDir, s.relativePath));
        inputs.candidates.push_back(std::move(c));
    }
    survey.plan = package::planExport(inputs);
    return survey;
}

bool digestSources(package::PackageManifest& manifest,
                   const std::vector<std::string>& sourceAbsPaths,
                   std::string& outFailedName) {
    outFailedName.clear();
    if (sourceAbsPaths.size() != manifest.entries.size()) return false;
    for (std::size_t i = 0; i < manifest.entries.size(); ++i) {
        const PayloadBuffer payload = readFilePayload(sourceAbsPaths[i]);
        if (payload.empty()) {
            outFailedName = manifest.entries[i].fileName;
            return false;
        }
        manifest.entries[i].byteLength = payload.size();
        manifest.entries[i].byteHash = capture::hashBytes(payload.data(), payload.size());
    }
    return true;
}

ExportOutcome writePackageFile(const package::EncodedPackage& encoded,
                               const package::PackageManifest& manifest,
                               const std::vector<std::string>& sourceAbsPaths,
                               const std::string& destAbsPath) {
    ExportOutcome out;
    if (sourceAbsPaths.size() != manifest.entries.size() ||
        encoded.layout.size() != manifest.entries.size()) {
        out.status = ExportStatus::EncodeFailed;
        return out;
    }

    // Every early return below abandons the writer through its destructor, which
    // removes the temp and leaves the destination untouched.
    PackageFileWriter writer(destAbsPath);
    if (!writer.ok() || !writer.appendRaw(encoded.prefix.data(), encoded.prefix.size())) {
        out.status = ExportStatus::WriteFailed;
        return out;
    }
    std::uint64_t written = encoded.prefix.size();

    for (std::size_t i = 0; i < manifest.entries.size(); ++i) {
        const package::PackageEntry& entry = manifest.entries[i];
        const PayloadBuffer payload = readFilePayload(sourceAbsPaths[i]);
        if (payload.empty()) {
            out.status = ExportStatus::SourceReadFailed;
            out.offendingName = entry.fileName;
            return out;
        }
        if (payload.size() != entry.byteLength ||
            capture::hashBytes(payload.data(), payload.size()) != entry.byteHash) {
            out.status = ExportStatus::SourceChanged;
            out.offendingName = entry.fileName;
            return out;
        }
        if (!writer.appendPayload(payload)) {
            out.status = ExportStatus::WriteFailed;
            return out;
        }
        written += payload.size();
    }

    if (written != encoded.totalSize || !writer.commit()) {
        out.status = ExportStatus::WriteFailed;
        return out;
    }
    out.status = ExportStatus::Written;
    out.entriesWritten = manifest.entries.size();
    out.bytesWritten = written;
    return out;
}

ExportOutcome exportBank(const ReaSamplerSession& session, const ExportRequest& req) {
    ExportOutcome out;
    if (req.projectDir.empty()) {
        out.status = ExportStatus::NoProjectDir;
        return out;
    }

    const ExportSurvey survey = surveyBankExport(session, req.projectDir, req.bankId);
    if (!survey.bankFound) {
        out.status = ExportStatus::NoSuchBank;
        return out;
    }
    out.bankDisplayName = survey.plan.manifest.bankDisplayName;
    out.excluded = survey.plan.excluded;

    if (survey.plan.verdict == package::ExportVerdict::Refused) {
        out.status = ExportStatus::RefusedUnrepresentable;
        return out;
    }
    if (survey.plan.verdict == package::ExportVerdict::Incomplete && !req.allowIncomplete) {
        out.status = ExportStatus::RefusedIncomplete;
        return out;
    }
    // The save dialog's own overwrite confirm covered the path the USER chose, which
    // is not necessarily the path handed here (the picker re-appends `.rsbank`), so
    // consent for the real target is re-taken by the skin.
    if (!req.allowOverwrite && fileStatus(req.destAbsPath) == FileStatus::Present) {
        out.status = ExportStatus::RefusedDestinationExists;
        return out;
    }

    package::PackageManifest manifest = survey.plan.manifest;
    manifest.exportTimestamp = req.exportTimestamp;
    const std::vector<std::string> sources =
        absoluteSources(req.projectDir, survey.plan.sourceRelativePaths);

    if (!digestSources(manifest, sources, out.offendingName)) {
        out.status = ExportStatus::SourceReadFailed;
        return out;
    }
    const std::optional<package::EncodedPackage> encoded = package::encodePackage(manifest);
    if (!encoded) {
        out.status = ExportStatus::EncodeFailed;
        return out;
    }

    ExportOutcome written = writePackageFile(*encoded, manifest, sources, req.destAbsPath);
    written.bankDisplayName = out.bankDisplayName;
    written.excluded = std::move(out.excluded);
    return written;
}

}  // namespace reasampler
