// package_io.cpp — see package_io.h for the seam's contract. Non-throwing at the
// boundary about filesystem_error: every filesystem call uses the error_code form.
// Allocation can still throw bad_alloc — readRange's sanity ceiling exists to keep
// that surface small, not to remove it.

#include "shell/package/package_io.h"

#include <algorithm>
#include <atomic>
#include <limits>
#include <utility>

#include "shell/package/package_path.h"

#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#include <share.h>
#include <sys/stat.h>
#else
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#endif

namespace reasampler {

namespace fs = std::filesystem;

namespace {
std::atomic<int> g_alivePayloads{0};
std::atomic<int> g_peakAlivePayloads{0};
}

// ---------------------------------------------------------------------------
// PayloadBuffer

PayloadBuffer::PayloadBuffer(std::vector<std::uint8_t> bytes)
    : bytes_(std::move(bytes)), counted_(!bytes_.empty()) {
    if (counted_) {
        const int now = g_alivePayloads.fetch_add(1, std::memory_order_relaxed) + 1;
        int peak = g_peakAlivePayloads.load(std::memory_order_relaxed);
        while (now > peak && !g_peakAlivePayloads.compare_exchange_weak(
                                 peak, now, std::memory_order_relaxed)) {
        }
    }
}

PayloadBuffer::~PayloadBuffer() { release(); }

PayloadBuffer::PayloadBuffer(PayloadBuffer&& other) noexcept
    : bytes_(std::move(other.bytes_)), counted_(other.counted_) {
    // The count transfers with the bytes — a move must never double-count.
    other.bytes_.clear();
    other.counted_ = false;
}

PayloadBuffer& PayloadBuffer::operator=(PayloadBuffer&& other) noexcept {
    if (this != &other) {
        release();
        bytes_ = std::move(other.bytes_);
        counted_ = other.counted_;
        other.bytes_.clear();
        other.counted_ = false;
    }
    return *this;
}

int PayloadBuffer::alive() { return g_alivePayloads.load(std::memory_order_relaxed); }
int PayloadBuffer::highWaterMark() { return g_peakAlivePayloads.load(std::memory_order_relaxed); }

void PayloadBuffer::release() {
    if (counted_) g_alivePayloads.fetch_sub(1, std::memory_order_relaxed);
    counted_ = false;
    bytes_.clear();
}

// ---------------------------------------------------------------------------
// PackageFileWriter

PackageFileWriter::PackageFileWriter(const std::string& destAbsPath)
    : destPath_(utf8Path(destAbsPath)), tempPath_(destPath_) {
    tempPath_ += ".rsbanktmp";  // += concatenates; / would make it a child
    out_.open(tempPath_, std::ios::binary | std::ios::trunc);
    ok_ = static_cast<bool>(out_);
}

PackageFileWriter::~PackageFileWriter() {
    if (!done_) abort();
}

bool PackageFileWriter::appendRaw(const std::uint8_t* data, std::size_t len) {
    if (!ok_ || done_) return false;
    if (len == 0) return true;
    out_.write(reinterpret_cast<const char*>(data),
               static_cast<std::streamsize>(len));
    ok_ = static_cast<bool>(out_);
    return ok_;
}

bool PackageFileWriter::appendPayload(const PayloadBuffer& payload) {
    if (payload.empty()) {
        ok_ = false;  // the stream is now short of what the framing will claim
        return false;
    }
    return appendRaw(payload.data(), payload.size());
}

bool PackageFileWriter::commit() {
    if (done_) return false;
    if (ok_) {
        out_.flush();
        ok_ = static_cast<bool>(out_);
    }
    out_.close();
    if (!ok_) {
        abort();
        return false;
    }
    // rename() replaces the destination in one step (the mono-collapse precedent):
    // prior contents survive until the replacement is known-complete, and a failed
    // rename self-cleans the temp rather than littering it.
    std::error_code ec;
    fs::rename(tempPath_, destPath_, ec);
    if (ec) {
        fs::remove(tempPath_, ec);
        done_ = true;
        ok_ = false;
        return false;
    }
    done_ = true;
    return true;
}

void PackageFileWriter::abort() {
    if (done_) return;
    out_.close();
    std::error_code ec;
    fs::remove(tempPath_, ec);
    done_ = true;
    ok_ = false;
}

// ---------------------------------------------------------------------------
// PackageFileReader

PackageFileReader::PackageFileReader(const std::string& srcAbsPath) {
    const fs::path path = utf8Path(srcAbsPath);
    std::error_code ec;
    const std::uintmax_t sz = fs::file_size(path, ec);
    if (ec) return;
    in_.open(path, std::ios::binary);
    if (!in_) return;
    size_ = static_cast<std::uint64_t>(sz);
    ok_ = true;
}

PayloadBuffer PackageFileReader::readRange(std::uint64_t offset, std::uint64_t length) {
    // Overflow-safe range check: length is capped by the real file size before any
    // allocation happens, so a hostile offset/length pair cannot demand the moon.
    // Also rejected here rather than truncated: a length that would not fit in
    // size_t (possible on a 32-bit build, where streamsize below stays 64-bit and
    // so would read past a truncated allocation) and a length past the sanity
    // ceiling, which exists so a merely large-but-real file size can't still hand
    // std::vector a multi-gigabyte demand.
    //
    // `length > size_` short-circuits before the two branches below ever see a real
    // file, so neither is reachable without a genuine >4 GiB fixture — this guard
    // ships unexercised by test_package_io.cpp, which covers past-the-end,
    // starts-at-the-end, and zero-length only. The ordering (cheap size check first)
    // is deliberate and correct; it is not reordered to make the branch testable.
    constexpr std::uint64_t kMaxReadRangeBytes = std::uint64_t{4} << 30;  // 4 GiB
    if (!ok_ || length == 0 || length > size_ || offset > size_ - length ||
        length > kMaxReadRangeBytes ||
        length > static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max())) {
        return PayloadBuffer{};
    }
    in_.clear();  // a prior failed read must not poison this one
    in_.seekg(static_cast<std::streamoff>(offset));
    if (!in_) return PayloadBuffer{};
    std::vector<std::uint8_t> bytes(static_cast<std::size_t>(length));
    in_.read(reinterpret_cast<char*>(bytes.data()),
             static_cast<std::streamsize>(length));
    if (static_cast<std::uint64_t>(in_.gcount()) != length) return PayloadBuffer{};
    return PayloadBuffer(std::move(bytes));
}

// ---------------------------------------------------------------------------

// Deliberately NOT core/util/file_bytes: that loader takes an unconverted narrow path
// (see package_path.h), and this seam's own reader already goes through utf8Path.
PayloadBuffer readFilePayload(const std::string& absPath) {
    PackageFileReader reader(absPath);
    return reader.readRange(0, reader.fileSize());
}

FileStatus fileStatus(const std::string& absPath) {
    const fs::path path = utf8Path(absPath);
    std::error_code ec;
    const fs::file_status st = fs::status(path, ec);
    // status() reports not_found through the type AND sets ec, so the type is the
    // discriminator; an ec with any other type is a real access failure.
    if (st.type() == fs::file_type::not_found) return FileStatus::Absent;
    if (ec || !fs::is_regular_file(st)) return FileStatus::Unreadable;
    std::ifstream probe(path, std::ios::binary);
    return probe ? FileStatus::Present : FileStatus::Unreadable;
}

bool writeFileExclusive(const std::string& absPath, const PayloadBuffer& payload) {
    if (payload.empty()) return false;
    const fs::path path = utf8Path(absPath);

    int fd = -1;
#ifdef _WIN32
    if (_wsopen_s(&fd, path.wstring().c_str(),
                  _O_WRONLY | _O_CREAT | _O_EXCL | _O_BINARY, _SH_DENYNO,
                  _S_IREAD | _S_IWRITE) != 0) {
        return false;
    }
#else
    fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644);
#endif
    if (fd < 0) return false;

    bool ok = true;
    std::size_t written = 0;
    while (written < payload.size()) {
        // Chunked because the Windows _write count is an unsigned int, not size_t.
        const std::size_t chunk =
            std::min<std::size_t>(payload.size() - written, 1u << 20);
#ifdef _WIN32
        const int n = _write(fd, payload.data() + written,
                             static_cast<unsigned int>(chunk));
#else
        const ssize_t n = ::write(fd, payload.data() + written, chunk);
        if (n < 0 && errno == EINTR) continue;  // a signal on the UI thread isn't a failure
#endif
        if (n <= 0) {
            ok = false;
            break;
        }
        written += static_cast<std::size_t>(n);
    }

#ifdef _WIN32
    _close(fd);
#else
    ::close(fd);
#endif

    if (!ok) {
        // Self-cleanup, not deletion authority: this call created the file moments
        // ago and nothing has ever referenced it (prune_fs.cpp's carve-out).
        std::error_code ec;
        fs::remove(path, ec);
    }
    return ok;
}

std::vector<std::string> listFolderFileNames(const std::string& dirAbsPath) {
    std::vector<std::string> names;
    std::error_code ec;
    // Manual iterator form (it.increment(ec)) keeps the loop non-throwing on a
    // mid-iteration failure, matching prune_fs's enumerate.
    fs::directory_iterator it(utf8Path(dirAbsPath), ec);
    for (; !ec && it != fs::directory_iterator{}; it.increment(ec)) {
        const auto& entry = *it;
        std::error_code reg_ec;
        if (!entry.is_regular_file(reg_ec)) continue;  // skip subdirs / specials
        names.push_back(pathToUtf8(entry.path().filename()));  // never .string(): ANSI
    }
    std::sort(names.begin(), names.end());
    return names;
}

}  // namespace reasampler
