// shell/package/package_io — every filesystem act the export/import verbs need:
// streaming package read/write, whole-file payload read, folder listing, file status,
// and the exclusive create that lands one bank file. Bytes only — what a package
// contains is core/package's business. Blocking I/O: UI-thread actions only, never
// the audio thread.

#pragma once

#include <cstdint>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>

namespace reasampler {

// One entry's payload. Move-only, because a copy would silently double the bytes the
// seam promises to hold at most one of; alive() is the counter that makes that
// promise assertable instead of aspirational.
class PayloadBuffer {
public:
    PayloadBuffer() = default;
    explicit PayloadBuffer(std::vector<std::uint8_t> bytes);
    ~PayloadBuffer();
    PayloadBuffer(PayloadBuffer&& other) noexcept;
    PayloadBuffer& operator=(PayloadBuffer&& other) noexcept;
    PayloadBuffer(const PayloadBuffer&) = delete;
    PayloadBuffer& operator=(const PayloadBuffer&) = delete;

    const std::uint8_t* data() const { return bytes_.data(); }
    std::size_t size() const { return bytes_.size(); }
    bool empty() const { return bytes_.empty(); }

    // Buffers currently holding at least one byte, process-wide.
    static int alive();
    // The largest alive() has ever been, process-wide. A point-in-time alive() == 0
    // check after a call returns cannot fail on a whole-package-in-memory shape that
    // allocated N buffers and freed them all one at a time — highWaterMark() can,
    // since it is never reset.
    static int highWaterMark();

private:
    void release();

    std::vector<std::uint8_t> bytes_;
    bool counted_ = false;
};

// Streaming atomic writer; paths cross this seam as UTF-8 narrow strings and are held
// as fs::path internally. The temp sibling is created in the DESTINATION's own
// directory so commit()'s rename never crosses a volume — a cross-device rename
// degrades to a copy and stops being atomic. commit() REPLACES an existing
// destination (the deliberate asymmetry against LandedFileJournal; see CLAUDE.md).
class PackageFileWriter {
public:
    explicit PackageFileWriter(const std::string& destAbsPath);
    ~PackageFileWriter();
    PackageFileWriter(const PackageFileWriter&) = delete;
    PackageFileWriter& operator=(const PackageFileWriter&) = delete;

    bool ok() const { return ok_; }
    // Framing/header bytes. False on a failed or already-finished writer. A zero
    // length is accepted — framing has legitimate zero-length edges.
    bool appendRaw(const std::uint8_t* data, std::size_t len);
    // One entry's bytes. Also false — and the writer poisoned — on an EMPTY payload:
    // empty is this seam's one "nothing to work with" signal, so accepting it would
    // let a verb commit a package whose framing claims bytes nobody wrote.
    bool appendPayload(const PayloadBuffer& payload);
    // Flush, close, rename over the destination. False (and self-cleaning: the temp
    // is removed, the destination untouched) on any failure or on a second call.
    bool commit();
    // Close and remove the temp; the destination is never touched. Idempotent.
    void abort();

    const std::filesystem::path& destPath() const { return destPath_; }
    const std::filesystem::path& tempPath() const { return tempPath_; }

private:
    std::filesystem::path destPath_;
    std::filesystem::path tempPath_;
    std::ofstream out_;
    bool ok_ = false;
    bool done_ = false;
};

// Seek-and-read reader: exactly one payload is materialized per readRange call, and
// there is deliberately no read-whole-file entry point. Empty buffer on ANY failure —
// unopenable file, zero length, out of range, short read — so the caller has one
// "nothing to work with" branch. Use fileStatus() when the two must be told apart.
class PackageFileReader {
public:
    explicit PackageFileReader(const std::string& srcAbsPath);

    bool ok() const { return ok_; }
    std::uint64_t fileSize() const { return size_; }
    // Bytes [offset, offset+length). Range-checked against the real file size, so a
    // hostile layout can never demand an allocation past the file's end, and capped
    // against a 4 GiB sanity ceiling so a merely large-but-real file can't still
    // force a multi-gigabyte allocation out of one call.
    PayloadBuffer readRange(std::uint64_t offset, std::uint64_t length);

private:
    std::ifstream in_;
    std::uint64_t size_ = 0;
    bool ok_ = false;
};

// One source file read whole as one entry's payload — a bank file IS the streaming
// unit. Empty on any failure, per PackageFileReader — including a source file over
// readRange's 4 GiB ceiling, which reads as empty exactly like an unreadable file;
// fileStatus() cannot tell the two apart either, since it only checks openability.
PayloadBuffer readFilePayload(const std::string& absPath);

// Export must tell a missing indexed file from an unreadable one in its refusal
// message; readFilePayload deliberately cannot, since both fail to an empty buffer.
enum class FileStatus { Present, Absent, Unreadable };
FileStatus fileStatus(const std::string& absPath);

// Creates absPath and writes the payload, failing if ANYTHING already occupies the
// path. The create IS the existence check (O_EXCL / CREATE_NEW), so nothing can slip
// in between: an exists()-then-write pair would let a file created in that window be
// overwritten and then deleted by a rollback that believes it wrote it. Refuses an
// empty payload, and removes its own partial file on a mid-write failure. Not
// temp+rename — an exclusive rename has no portable spelling, and the debris a crash
// leaves here is unrecorded and unindexed either way.
bool writeFileExclusive(const std::string& absPath, const PayloadBuffer& payload);

// Bare file names (regular files only, never a path) in dirAbsPath, UTF-8, sorted so
// callers see a deterministic order; empty on a missing or unreadable folder.
std::vector<std::string> listFolderFileNames(const std::string& dirAbsPath);

}  // namespace reasampler
