// import_landing.cpp — see import_landing.h for the contract. REAPER-free: standard
// filesystem only, so every property this file decides is unit-testable.

#include "shell/package/import_landing.h"

#include <cassert>
#include <cstdint>
#include <filesystem>
#include <system_error>
#include <utility>
#include <vector>

#include "core/capture/wav_codec.h"  // hashBytes — the digest the manifest records
#include "core/package/bank_package.h"
#include "shell/package/package_io.h"
#include "shell/package/package_path.h"

namespace reasampler {

namespace fs = std::filesystem;
using package::EntryAction;
using package::PackageEntrySpan;

namespace {

ImportLanding refusal(ImportOutcome outcome, const package::PackageHeader& header) {
    ImportLanding out;
    out.outcome = outcome;
    out.header = header;
    return out;
}

// Reads the package head incrementally: requiredPrefixSize may grow its answer as
// fields arrive, so ask, read to the count, ask again. False means these bytes can
// never frame a package, or the file is shorter than its own header claims.
bool readPrefix(PackageFileReader& reader, std::vector<std::uint8_t>& prefix) {
    for (;;) {
        const auto need = package::requiredPrefixSize(prefix);
        if (!need) return false;
        if (prefix.size() >= *need) return true;
        PayloadBuffer head = reader.readRange(0, *need);
        if (head.size() != *need) return false;
        prefix.assign(head.data(), head.data() + head.size());
    }
}

}  // namespace

ImportLanding landPackage(const std::string& packageAbsPath,
                          const std::string& projectDir,
                          const BankBook& destination,
                          const std::string& uniqueTag,
                          LandedFileJournal& journal) {
    const package::PackageHeader noHeader;
    if (projectDir.empty()) return refusal(ImportOutcome::NoProject, noHeader);

    PackageFileReader reader(packageAbsPath);
    if (!reader.ok()) return refusal(ImportOutcome::Unreadable, noHeader);

    std::vector<std::uint8_t> prefix;
    if (!readPrefix(reader, prefix)) return refusal(ImportOutcome::Malformed, noHeader);

    const package::DecodedPackage dec = package::decodePackage(prefix, reader.fileSize());
    if (dec.status == package::PackageReadability::TooNew)
        return refusal(ImportOutcome::TooNew, dec.header);
    if (dec.status != package::PackageReadability::Readable)
        return refusal(ImportOutcome::Malformed, dec.header);

    const std::string bankDir = package::bankFolderDir(projectDir);

    ImportLanding out;
    out.header = dec.header;
    out.plan = package::planImport(dec.manifest, destination, projectDir,
                                   listFolderFileNames(bankDir), uniqueTag);

    // Integrity first, over EVERY declared entry — including one the plan collapses,
    // since a package that fails its own digest is refused whole rather than partly
    // trusted. Nothing is on disk yet, so a failure here needs no rollback.
    for (std::size_t i = 0; i < dec.layout.size(); ++i) {
        const PackageEntrySpan& span = dec.layout[i];
        // The format refuses a zero-length entry on encode; one arriving anyway cannot
        // be told from a failed read at this seam, so it is not well-formed input.
        if (span.length == 0) return refusal(ImportOutcome::Malformed, dec.header);

        PayloadBuffer payload = reader.readRange(span.offset, span.length);
        if (payload.size() != span.length) {
            out.outcome = ImportOutcome::IntegrityFailed;
            out.failedEntryName = span.name;
            return out;
        }
        if (capture::hashBytes(payload.data(), payload.size()) !=
            dec.manifest.entries[i].byteHash) {
            out.outcome = ImportOutcome::IntegrityFailed;
            out.failedEntryName = span.name;
            return out;
        }
    }

    std::error_code ec;
    fs::create_directories(utf8Path(bankDir), ec);  // idempotent; the write reports failure

    for (const package::PlannedEntry& e : out.plan.entries) {
        if (e.action != EntryAction::Land) continue;
        const PackageEntrySpan& span = dec.layout[e.manifestIndex];
        PayloadBuffer payload = reader.readRange(span.offset, span.length);
        if (payload.size() == span.length &&
            journal.writeLandedFile(bankDir + "/" + e.destFileName, payload)) {
            continue;
        }
        out.outcome = ImportOutcome::WriteFailed;
        out.failedEntryName = e.destFileName;
        out.rollback = journal.rollback();
        return out;
    }

    out.outcome = ImportOutcome::Landed;
    return out;
}

bool applyImportedBank(BankBook& book, const std::string& bankId,
                       const package::ImportPlan& plan, const RecordBirth& recordBirth) {
    // An empty std::function throws std::bad_function_call on invoke; every real caller
    // supplies one, so an empty one here is a caller bug, not a runtime condition to
    // recover from — enforce the contract rather than let it surface as an uncaught
    // exception out of an extension action.
    assert(recordBirth && "applyImportedBank: RecordBirth must not be empty");
    if (!book.createBank(bankId, plan.bankDisplayName)) return false;

    BankModel* index = book.index(bankId);
    for (const package::PlannedEntry& e : plan.entries) {
        if (e.action != EntryAction::Land) continue;
        const AddResult added = index->add(e.sample);
        // planImport already deduped Land entries by hash against an empty destination
        // bank (this same freshly-created one), so a Collapsed add here would mean the
        // plan and the book disagree — that would silently undercount reportSuccess's
        // landedCount rather than fail loudly.
        assert(added == AddResult::Added && "planImport's Land entries must not collapse");
        (void)added;
        // Unconditional on the add's outcome: the file exists either way, and an
        // unrecorded file is permanently unreclaimable.
        recordBirth(e.sample);
    }

    book.bank(bankId)->slots = plan.slots;
    book.reconcileSlots();
    return true;
}

}  // namespace reasampler
