/*
 * SPDX-FileCopyrightText: 2026 Oak Ridge National Laboratory and Contributors
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "FileAWSSDK.h"
#include "adios2/core/ADIOS.h"
#include "adios2/helper/adiosLog.h"
#include "adios2/helper/adiosString.h"
#include "adios2/helper/adiosSystem.h"

#include <algorithm> // sort
#include <cstdio>    // remove
#include <cstring>   // strerror
#include <errno.h>   // errno
#include <fcntl.h>   // open
#include <regex>
#include <sys/stat.h>  // open, fstat
#include <sys/types.h> // open
#include <unistd.h>    // write, close, ftruncate

/// \cond EXCLUDE_FROM_DOXYGEN
#include <ios> //std::ios_base::failure
/// \endcond

namespace adios2
{
namespace transport
{

FileAWSSDK::FileAWSSDK(helper::Comm const &comm)
: Transport("File", "AWSSDK", comm) /*, m_Impl(&m_ImplSingleton)*/
{
}

FileAWSSDK::~FileAWSSDK()
{
    if (m_IsOpen)
    {
        Close();
    }
}

void FileAWSSDK::SetParameters(const Params &params)
{
    // Parameters are set from config parameters if present
    // Otherwise, they are set from environment if present
    // Otherwise, they remain at their default value

    helper::SetParameterValue("endpoint", params, m_Endpoint);
    if (m_Endpoint.empty())
    {
        if (const char *epEnv = std::getenv("AWS_ENDPOINT"))
        {
            m_Endpoint = std::string(epEnv);
        }
    }

    helper::SetParameterValue("bucket", params, m_BucketPrefix);
    if (m_BucketPrefix.empty())
    {
        if (const char *bucketEnv = std::getenv("ADIOS2_AWS_BUCKET"))
        {
            m_BucketPrefix = std::string(bucketEnv);
        }
    }

    helper::SetParameterValue("cache", params, m_CachePath);
    if (m_CachePath.empty())
    {
        if (const char *Env = std::getenv("AWS_CACHE"))
        {
            m_CachePath = std::string(Env);
        }
    }

    helper::SetParameterValue("accesskeyid", params, m_accessKeyID);
    if (m_accessKeyID.empty())
    {
        if (const char *Env = std::getenv("AWS_ACCESS_KEY_ID"))
        {
            m_accessKeyID = std::string(Env);
        }
    }
    helper::SetParameterValue("secretkey", params, m_secretKey);
    if (m_secretKey.empty())
    {
        if (const char *Env = std::getenv("AWS_SECRET_KEY"))
        {
            m_secretKey = std::string(Env);
        }
    }
    helper::SetParameterValue("sessiontoken", params, m_sessionToken);
    if (m_sessionToken.empty())
    {
        if (const char *Env = std::getenv("AWS_SESSION_TOKEN"))
        {
            m_sessionToken = std::string(Env);
        }
    }

    helper::SetParameterValueInt("verbose", params, m_Verbose, "");

    // S3 object mode: "multi" (default) or "single"
    std::string objectModeStr;
    helper::SetParameterValue("s3_object_mode", params, objectModeStr);
    if (!objectModeStr.empty())
    {
        if (objectModeStr == "single")
        {
            m_MultiObjectMode = false;
        }
        else if (objectModeStr == "multi")
        {
            m_MultiObjectMode = true;
        }
        else
        {
            helper::Throw<std::invalid_argument>(
                "Toolkit", "transport::file::FileAWSSDK", "SetParameters",
                "s3_object_mode must be 'multi' or 'single', got '" + objectModeStr + "'");
        }
    }

    helper::SetParameterValue("filenameintar", params, m_FileNameInTar);

    std::string recheckStr = "true";
    helper::SetParameterValue("recheck_metadata", params, recheckStr);
    m_RecheckMetadata = helper::StringTo<bool>(recheckStr, "");

    // Part size limits for multipart uploads (accepts human-readable sizes like "64MB", "1GB")
    std::string minPartSizeStr, maxPartSizeStr;
    helper::SetParameterValue("min_part_size", params, minPartSizeStr);
    helper::SetParameterValue("max_part_size", params, maxPartSizeStr);

    if (!minPartSizeStr.empty())
    {
        size_t minPartSize = helper::StringToByteUnits(minPartSizeStr, "min_part_size");
        if (minPartSize < S3_MIN_PART_SIZE)
        {
            helper::Throw<std::invalid_argument>(
                "Toolkit", "transport::file::FileAWSSDK", "SetParameters",
                "min_part_size must be at least 5MB (S3 requirement), got " + minPartSizeStr);
        }
        m_MinPartSize = minPartSize;
    }

    if (!maxPartSizeStr.empty())
    {
        size_t maxPartSize = helper::StringToByteUnits(maxPartSizeStr, "max_part_size");
        if (maxPartSize > S3_MAX_PART_SIZE)
        {
            helper::Throw<std::invalid_argument>(
                "Toolkit", "transport::file::FileAWSSDK", "SetParameters",
                "max_part_size must be at most 5GB (S3 limit), got " + maxPartSizeStr);
        }
        if (maxPartSize < m_MinPartSize)
        {
            helper::Throw<std::invalid_argument>(
                "Toolkit", "transport::file::FileAWSSDK", "SetParameters",
                "max_part_size (" + maxPartSizeStr + ") must be >= min_part_size (" +
                    minPartSizeStr + ")");
        }
        m_MaxPartSize = maxPartSize;
    }

    std::string directThresholdStr;
    helper::SetParameterValue("direct_upload_threshold", params, directThresholdStr);
    if (!directThresholdStr.empty())
    {
        m_DirectUploadThreshold =
            helper::StringToByteUnits(directThresholdStr, "direct_upload_threshold");
    }

    std::string numSubFilesStr;
    helper::SetParameterValue("s3_num_subfiles", params, numSubFilesStr);
    if (!numSubFilesStr.empty())
    {
        m_NumSubFiles = std::stoi(numSubFilesStr);
    }

    core::ADIOS::Global_init_AWS_API();

    s3ClientConfig = new Aws::S3::S3ClientConfiguration;
    s3ClientConfig->endpointOverride = m_Endpoint;
    s3ClientConfig->useVirtualAddressing = false;
    s3ClientConfig->enableEndpointDiscovery = false;
    s3ClientConfig->region = "us-east-1"; // Required for signature calculation
    // Disable automatic checksum calculation which uses aws-chunked encoding
    // that strips Content-Length — incompatible with MinIO and other S3-compatible stores.
    // checksumConfig was added in AWS SDK 1.11.486; older versions don't need this
    // because they don't have the aws-chunked behavior.
#if defined(AWS_SDK_VERSION_MAJOR) &&                                                              \
    (AWS_SDK_VERSION_MAJOR > 1 ||                                                                  \
     (AWS_SDK_VERSION_MAJOR == 1 &&                                                                \
      (AWS_SDK_VERSION_MINOR > 11 ||                                                               \
       (AWS_SDK_VERSION_MINOR == 11 && AWS_SDK_VERSION_PATCH >= 486))))
    s3ClientConfig->checksumConfig.requestChecksumCalculation =
        Aws::Client::RequestChecksumCalculation::WHEN_REQUIRED;
#endif
    // Use UNSIGNED-PAYLOAD instead of computing SHA256 over the request body.
    // Avoids stream-seek issues and is required for S3-compatible stores.
    s3ClientConfig->payloadSigningPolicy =
        Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never;

    // Fall back to environment variables if credentials not set as ADIOS2 parameters
    if (m_accessKeyID.empty())
    {
        const char *env = std::getenv("AWS_ACCESS_KEY_ID");
        if (env)
        {
            m_accessKeyID = env;
        }
    }
    if (m_secretKey.empty())
    {
        const char *env = std::getenv("AWS_SECRET_ACCESS_KEY");
        if (env)
        {
            m_secretKey = env;
        }
    }
    if (m_sessionToken.empty())
    {
        const char *env = std::getenv("AWS_SESSION_TOKEN");
        if (env)
        {
            m_sessionToken = env;
        }
    }

    Aws::Auth::AWSCredentials aws_credentials;
    if (!m_sessionToken.empty())
    {
        aws_credentials = {m_accessKeyID, m_secretKey, m_sessionToken};
    }
    else
    {
        aws_credentials = {m_accessKeyID, m_secretKey};
    }

    if (m_accessKeyID.empty() && m_secretKey.empty() && m_sessionToken.empty())
    {
        // last hope user set AWS_PROFILE (CampaignReader does this)
        s3Client = new Aws::S3::S3Client(*s3ClientConfig);
    }
    else
    {
        s3Client =
            new Aws::S3::S3Client(aws_credentials, *s3ClientConfig,
                                  Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never, false);
    }
    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::SetParameters: AWS Transport created with endpoint = '"
                  << m_Endpoint << "' bucket = '" << m_BucketPrefix
                  << "' object_mode = " << (m_MultiObjectMode ? "multi" : "single")
                  << " recheck_metadata = " << m_RecheckMetadata
                  << " min_part_size = " << m_MinPartSize << " max_part_size = " << m_MaxPartSize
                  << " direct_upload_threshold = " << m_DirectUploadThreshold << std::endl;
    }
}

void FileAWSSDK::WaitForOpen()
{
    if (m_IsOpening)
    {
        if (m_OpenFuture.valid())
        {
            // m_FileDescriptor = m_OpenFuture.get();
        }
        m_IsOpening = false;
        CheckFile("couldn't open file " + m_Name + ", in call to AWSSDK open");
        m_IsOpen = true;
    }
}

void FileAWSSDK::SetUpCache()
{
    if (!m_CachePath.empty())
    {
        if (helper::EndsWith(m_ObjectName, "md.idx") || helper::EndsWith(m_ObjectName, "md.0") ||
            helper::EndsWith(m_ObjectName, "mmd.0"))
        {
            m_CachingThisFile = true;
        }
    }
    if (!m_CachePath.empty())
    {
        std::string path = m_ObjectName;
        if (!m_FileNameInTar.empty())
            path = m_FileNameInTar;

        if (helper::EndsWith(path, "md.idx"))
        {
            m_CachingThisFile = true;
            m_CacheFilePath = m_CachePath + "/md.idx";
        }
        else if (helper::EndsWith(path, "mmd.0"))
        {
            m_CachingThisFile = true;
            m_CacheFilePath = m_CachePath + "/mmd.0";
        }
        else if (helper::EndsWith(path, "md.0"))
        {
            m_CachingThisFile = true;
            m_CacheFilePath = m_CachePath + "/md.0";
        }
    }

    if (m_CachingThisFile)
    {
        if (!m_RecheckMetadata)
        {
            m_CacheFileRead = new FileFStream(m_Comm);
            try
            {
                m_CacheFileRead->Open(m_CacheFilePath, Mode::Read);
                m_Size = m_CacheFileRead->GetSize();
                m_IsCached = true;
                m_CachingThisFile = false;
                if (m_Verbose > 0)
                {
                    std::cout << "FileAWSSDK::SetUpCache: Already cached " << m_CacheFilePath
                              << ", size = " << m_Size << std::endl;
                }
            }
            catch (std::ios_base::failure &)
            {
                delete m_CacheFileRead;
                m_IsCached = false;
            }
        }
    }
}

void FileAWSSDK::CheckCache(const size_t fileSize)
{
    if (m_CachingThisFile)
    {
        /* Check if cache file exists and size equals the cloud version*/
        m_CacheFileRead = new FileFStream(m_Comm);
        try
        {
            m_CacheFileRead->Open(m_CacheFilePath, Mode::Read);
            size_t cacheSize = m_CacheFileRead->GetSize();
            if (cacheSize == fileSize)
            {
                m_IsCached = true;
                m_CachingThisFile = false;
                if (m_Verbose > 0)
                {
                    std::cout << "FileAWSSDK::CheckCache: Already cached " << m_CacheFilePath
                              << ", full size = " << cacheSize << std::endl;
                }
            }
            else
            {
                std::cout << "FileAWSSDK::CheckCache: Partially cached " << m_CacheFilePath
                          << ", cached size = " << cacheSize << " full size = " << fileSize
                          << std::endl;
            }
        }
        catch (std::ios_base::failure &)
        {
            delete m_CacheFileRead;
        }

        if (m_CachingThisFile)
        {
            /* Create output file for caching this data later */
            const auto lastPathSeparator(m_CacheFilePath.find_last_of(PathSeparator));
            if (lastPathSeparator != std::string::npos)
            {
                const std::string dirpath(m_CacheFilePath.substr(0, lastPathSeparator));
                helper::CreateDirectory(dirpath);
            }
            m_CacheFileWrite = new FileFStream(m_Comm);
            m_CacheFileWrite->Open(m_CacheFilePath, Mode::Write);
            if (m_Verbose > 0)
            {
                std::cout << "FileAWSSDK::CheckCache: Caching turn on for " << m_CacheFilePath
                          << std::endl;
            }
        }
    }
}

void FileAWSSDK::Open(const std::string &name, const Mode openMode, const bool async,
                      const bool directio)
{
    // If bucket is specified via parameter, use it directly and name becomes the object key
    if (!m_BucketPrefix.empty())
    {
        m_BucketName = m_BucketPrefix;
        m_ObjectName = name;
        // Strip leading slashes from object key (S3 keys shouldn't start with /)
        // Also strip leading dots, those lead to authentication issues
        while (!m_ObjectName.empty() &&
               (m_ObjectName[0] == PathSeparator || m_ObjectName[0] == '.'))
        {
            m_ObjectName = m_ObjectName.substr(1);
        }
        m_Name = m_BucketName + PathSeparator + m_ObjectName;
    }
    else
    {
        // Original behavior: parse bucket/object from the path
        std::string fullPath = name;
        m_Name = fullPath;

        size_t pos = fullPath.find(PathSeparator);
        if (pos == std::string::npos)
        {
            helper::Throw<std::invalid_argument>("Toolkit", "transport::file::FileAWSSDK", "Open",
                                                 "invalid 'bucket/object' name " + fullPath);
        }
        m_BucketName = fullPath.substr(0, pos);
        m_ObjectName = fullPath.substr(pos + 1);
    }

    m_OpenMode = openMode;
    switch (m_OpenMode)
    {

    case Mode::Write:
        ProfilerStart("open");
        m_WriteBuffer.clear();
        m_TotalBytesWritten = 0;

        if (m_MultiObjectMode)
        {
            // Multi-object mode: save base object name, upload individual objects
            m_BaseObjectName = m_ObjectName;
            m_NextObjectNumber = 0;
            // Each rank deletes its own stale sub-objects (e.g. data.3.0, data.3.1, ...)
            DeleteStaleSubObjects(m_ObjectName);
            // Also delete the bare single-object key (e.g. "data.0") left by a
            // prior single-object mode run.  DeleteObject is a no-op if the key
            // doesn't exist, so this is always safe.
            {
                Aws::S3::Model::DeleteObjectRequest delReq;
                delReq.SetBucket(m_BucketName);
                delReq.SetKey(m_ObjectName);
                s3Client->DeleteObject(delReq);
            }
            // Subfile 0 also cleans up objects from orphaned subfiles
            // (prior runs that used more subfiles than the current run)
            if (m_NumSubFiles > 0)
            {
                size_t lastDot = m_ObjectName.rfind('.');
                if (lastDot != std::string::npos && m_ObjectName.substr(lastDot + 1) == "0")
                {
                    DeleteOrphanedSubObjects();
                }
            }
        }
        else
        {
            // Single-object mode: one multipart upload per file
            // First, clean up stale sub-objects from a prior multi-object mode run
            // (e.g. "data.0.0", "data.0.1", ... left when switching from multi to single)
            DeleteStaleSubObjects(m_ObjectName);
            // Subfile 0 also cleans up objects from orphaned subfiles
            if (m_NumSubFiles > 0)
            {
                size_t lastDot = m_ObjectName.rfind('.');
                if (lastDot != std::string::npos && m_ObjectName.substr(lastDot + 1) == "0")
                {
                    DeleteOrphanedSubObjects();
                }
            }
            m_WriteBuffer.reserve(m_MinPartSize);
            m_CompletedParts.clear();
            m_CurrentPartNumber = 0;
            InitiateMultipartUpload();
        }

        m_IsOpen = true;
        ProfilerStop("open");
        break;

    case Mode::Append:
        if (m_MultiObjectMode)
        {
            m_BaseObjectName = m_ObjectName;
            // Find the highest existing segment number to continue from
            m_NextObjectNumber = 0;
            m_TotalBytesWritten = 0;
            Aws::String continuationToken;
            bool hasMore = true;
            std::string prefix = m_ObjectName + ".";
            while (hasMore)
            {
                Aws::S3::Model::ListObjectsV2Request listReq;
                listReq.SetBucket(m_BucketName);
                listReq.SetPrefix(prefix);
                listReq.SetMaxKeys(1000);
                if (!continuationToken.empty())
                {
                    listReq.SetContinuationToken(continuationToken);
                }
                auto listOutcome = s3Client->ListObjectsV2(listReq);
                if (!listOutcome.IsSuccess())
                {
                    break;
                }
                const auto &result = listOutcome.GetResult();
                for (const auto &obj : result.GetContents())
                {
                    // Extract segment number from key suffix after last '.'
                    std::string key(obj.GetKey().c_str());
                    size_t lastDot = key.rfind('.');
                    if (lastDot != std::string::npos)
                    {
                        unsigned long segNum = std::stoul(key.substr(lastDot + 1));
                        if (segNum + 1 > m_NextObjectNumber)
                        {
                            m_NextObjectNumber = segNum + 1;
                        }
                    }
                    m_TotalBytesWritten += obj.GetSize();
                }
                hasMore = result.GetIsTruncated();
                if (hasMore)
                {
                    continuationToken = result.GetNextContinuationToken();
                }
            }
            if (m_Verbose > 0)
            {
                std::cout << "FileAWSSDK::Open(Append): " << m_Name << " found "
                          << m_NextObjectNumber << " existing segments, " << m_TotalBytesWritten
                          << " bytes" << std::endl;
            }
            m_IsOpen = true;
        }
        else
        {
            helper::Throw<std::ios_base::failure>(
                "Toolkit", "transport::file::FileAWSSDK", "Open",
                "Append mode not supported for single-object S3 " + m_Name);
        }
        break;

    case Mode::Read: {
        ProfilerStart("open");
        errno = 0;

        SetUpCache();
        // m_IsCached=true if already found in cache and m_RecheckMetadata=false
        // m_CachingThisFile=true if we want caching and m_IsCached=false
        // m_CacheFilePath is the path to the local file in cache

        if (!m_IsCached)
        {
            Aws::S3::Model::HeadObjectRequest head_object_request;
            head_object_request.SetBucket(m_BucketName);
            head_object_request.SetKey(m_ObjectName);

            if (m_Verbose > 0)
            {
                std::cout << "FileAWSSDK::Open: S3 HeadObjectRequests bucket='"
                          << head_object_request.GetBucket() << "'  object = '"
                          << head_object_request.GetKey() << "'" << std::endl;
            }
            head_object = s3Client->HeadObject(head_object_request);
            if (head_object.IsSuccess())
            {
                // Single object exists — use it directly
                m_Size = head_object.GetResult().GetContentLength();
                CheckCache(m_Size);
            }
            else
            {
                // Object not found — try multi-object layout (data.0.0, data.0.1, ...)
                DiscoverSubObjects();
                if (m_IsMultiObjectLayout)
                {
                    m_Size = m_TotalVirtualSize;
                    if (m_Verbose > 0)
                    {
                        std::cout << "FileAWSSDK::Open: Multi-object layout for '" << m_ObjectName
                                  << "': " << m_SubObjects.size()
                                  << " objects, total size = " << m_Size << std::endl;
                    }
                }
                else
                {
                    helper::Throw<std::invalid_argument>(
                        "Toolkit", "transport::file::FileAWSSDK", "Open",
                        "'bucket/object'  " + m_Name + " does not exist ");
                }
            }

            m_Errno = errno;
        }
        ProfilerStop("open");
        break;
    }
    default:
        CheckFile("unknown open mode for file " + m_Name + ", in call to AWSSDK open");
    }

    if (!m_IsOpening && !m_IsOpen)
    {
        // Only check for read mode - write mode already set m_IsOpen = true
        CheckFile("couldn't open file " + m_Name + ", in call to AWSSDK open");
        m_IsOpen = true;
    }
}

void FileAWSSDK::OpenChain(const std::string &name, Mode openMode, const helper::Comm &chainComm,
                           const bool async, const bool directio)
{
    int token = 1;
    if (chainComm.Rank() > 0)
    {
        chainComm.Recv(&token, 1, chainComm.Rank() - 1, 0, "Chain token in FileAWSSDK::OpenChain");
    }

    Open(name, openMode, async, directio);

    if (chainComm.Rank() < chainComm.Size() - 1)
    {
        chainComm.Isend(&token, 1, chainComm.Rank() + 1, 0,
                        "Sending Chain token in FileAWSSDK::OpenChain");
    }
}

void FileAWSSDK::Write(const char *buffer, size_t size, size_t start)
{
    WaitForOpen();

    if (m_OpenMode != Mode::Write && m_OpenMode != Mode::Append)
    {
        helper::Throw<std::ios_base::failure>("Toolkit", "transport::file::FileAWSSDK", "Write",
                                              "File not opened for writing: " + m_Name);
    }

    // S3 only supports sequential writes
    if (start != MaxSizeT && start != m_TotalBytesWritten)
    {
        helper::Throw<std::ios_base::failure>(
            "Toolkit", "transport::file::FileAWSSDK", "Write",
            "S3 only supports sequential writes. "
            "Requested start: " +
                std::to_string(start) +
                ", current position: " + std::to_string(m_TotalBytesWritten) +
                ". Hint: Set StripeSize=0 when using S3 storage.");
    }

    ProfilerStart("write");

    if (m_MultiObjectMode)
    {
        // Multi-object mode: buffer data, with zero-copy for large writes
        if (size >= m_DirectUploadThreshold && !m_WriteBuffer.empty())
        {
            // Large write: flush current buffer as one object, then upload
            // the large buffer directly as another (zero-copy)
            FlushWriteBuffer();
        }

        if (size >= m_DirectUploadThreshold)
        {
            // Upload large data directly without copying into m_WriteBuffer
            std::string key = m_BaseObjectName + "." + std::to_string(m_NextObjectNumber);
            m_NextObjectNumber++;
            UploadObject(key, buffer, size);
        }
        else
        {
            // Small write: accumulate in buffer
            m_WriteBuffer.insert(m_WriteBuffer.end(), buffer, buffer + size);
        }
    }
    else
    {
        // Single-object mode: original multipart upload buffering
        size_t offset = 0;

        // Step 1: If we have buffered data, fill it up to target size first
        if (!m_WriteBuffer.empty())
        {
            size_t spaceInBuffer = m_MinPartSize - m_WriteBuffer.size();
            size_t toBuffer = std::min(size, spaceInBuffer);

            m_WriteBuffer.insert(m_WriteBuffer.end(), buffer, buffer + toBuffer);
            offset = toBuffer;

            if (m_WriteBuffer.size() >= m_MinPartSize)
            {
                UploadBufferedPart();
            }
        }

        // Step 2: Handle remaining data
        size_t remaining = size - offset;

        // Upload large chunks directly (but respect max part size)
        while (remaining >= m_MinPartSize)
        {
            size_t partSize = std::min(remaining, m_MaxPartSize);
            UploadPart(buffer + offset, partSize);
            offset += partSize;
            remaining -= partSize;
        }

        // Buffer any small remainder for later
        if (remaining > 0)
        {
            m_WriteBuffer.insert(m_WriteBuffer.end(), buffer + offset, buffer + size);
        }
    }

    m_TotalBytesWritten += size;
    ProfilerWriteBytes(size);
    ProfilerStop("write");
}

void FileAWSSDK::Read(char *buffer, size_t size, size_t start)
{
    WaitForOpen();

    if (start != MaxSizeT)
    {
        if (start >= m_Size)
        {
            helper::Throw<std::ios_base::failure>(
                "Toolkit", "transport::file::FileAWSSDK", "Read",
                "couldn't move to start position " + std::to_string(start) +
                    " beyond the size of " + m_Name + " which is " + std::to_string(m_Size));
        }
        m_SeekPos = start;
        errno = 0;
        m_Errno = errno;
    }

    if (m_SeekPos + size > m_Size)
    {
        helper::Throw<std::ios_base::failure>("Toolkit", "transport::file::FileAWSSDK", "Read",
                                              "can't read " + std::to_string(size) +
                                                  " bytes from position " +
                                                  std::to_string(m_SeekPos) + " from " + m_Name +
                                                  " whose size is " + std::to_string(m_Size));
    }

    if (m_IsCached)
    {
        m_CacheFileRead->Read(buffer, size, m_SeekPos);
        if (m_Verbose > 0)
        {
            std::cout << "FileAWSSDK::Read: Read from cache " << m_CacheFileRead->m_Name
                      << " start = " << m_SeekPos << " size = " << size << std::endl;
        }
        return;
    }

    if (m_IsMultiObjectLayout)
    {
        // Multi-object read: find the sub-object(s) that contain the requested range
        size_t readPos = m_SeekPos;
        size_t bytesRemaining = size;
        size_t bufferOffset = 0;

        while (bytesRemaining > 0)
        {
            // Binary search: find the sub-object containing readPos
            size_t lo = 0, hi = m_SubObjects.size();
            while (lo + 1 < hi)
            {
                size_t mid = (lo + hi) / 2;
                if (m_SubObjects[mid].cumulativeOffset <= readPos)
                {
                    lo = mid;
                }
                else
                {
                    hi = mid;
                }
            }

            const SubObject &sub = m_SubObjects[lo];
            size_t offsetInObject = readPos - sub.cumulativeOffset;
            size_t availableInObject = sub.size - offsetInObject;
            size_t toRead = std::min(bytesRemaining, availableInObject);

            // Fetch range from this sub-object
            Aws::S3::Model::GetObjectRequest request;
            request.SetBucket(m_BucketName);
            request.SetKey(sub.key);
            std::stringstream range;
            range << "bytes=" << offsetInObject << "-" << offsetInObject + toRead - 1;
            request.SetRange(range.str());

            auto outcome = s3Client->GetObject(request);
            if (!outcome.IsSuccess())
            {
                const Aws::S3::S3Error &err = outcome.GetError();
                helper::Throw<std::invalid_argument>(
                    "Toolkit", "transport::file::FileAWSSDK", "Read",
                    "multi-object read failed for '" + sub.key + "' range " + range.str() + ": " +
                        err.GetExceptionName() + ": " + err.GetMessage());
            }

            auto *body = outcome.GetResult().GetBody().rdbuf();
            body->sgetn(buffer + bufferOffset, static_cast<std::streamsize>(toRead));

            readPos += toRead;
            bufferOffset += toRead;
            bytesRemaining -= toRead;
        }
    }
    else
    {
        // Single-object read
        Aws::S3::Model::GetObjectRequest request;
        request.SetBucket(m_BucketName);
        request.SetKey(m_ObjectName);
        std::stringstream range;
        range << "bytes=" << m_SeekPos << "-" << m_SeekPos + size - 1;
        request.SetRange(range.str());

        Aws::S3::Model::GetObjectOutcome outcome = s3Client->GetObject(request);

        if (!outcome.IsSuccess())
        {
            const Aws::S3::S3Error &err = outcome.GetError();
            helper::Throw<std::invalid_argument>(
                "Toolkit", "transport::file::FileAWSSDK", "Read",
                "'bucket/object'  " + m_Name + ", range " + range.str() +
                    "GetObject: " + err.GetExceptionName() + ": " + err.GetMessage());
        }
        else
        {
            if (m_Verbose > 0)
            {
                std::cout << "FileAWSSDK::Read: Successfully retrieved '" << m_ObjectName
                          << "' from '" << m_BucketName << "'."
                          << "\nObject length = " << outcome.GetResult().GetContentLength()
                          << "\nRange requested = " << range.str() << std::endl;
            }
            auto *body = outcome.GetResult().GetBody().rdbuf();
            body->sgetn(buffer, size);

            /* Save to cache */
            if (m_CachingThisFile)
            {
                m_CacheFileWrite->Write(buffer, size, m_SeekPos);
                m_CacheFileWrite->Flush();
                if (m_Verbose > 0)
                {
                    std::cout << "FileAWSSDK::Read: Written to cache " << m_CacheFileWrite->m_Name
                              << " start = " << m_SeekPos << " size = " << size << std::endl;
                }
            }
        }
    }
}

size_t FileAWSSDK::GetSize()
{
    WaitForOpen();
    switch (m_OpenMode)
    {
    case Mode::Write:
        return m_TotalBytesWritten;
    case Mode::Append:
        return m_TotalBytesWritten;
    case Mode::Read:
        return m_Size;
    default:
        return 0;
    }
}

void FileAWSSDK::Flush()
{
    // No-op for S3.  Write() already uploads parts when the buffer fills,
    // and partial buffers can't be uploaded (S3 minimum part size = 5 MB).
    // Data is finalized via FinalizeSegment() (multi-object) or Close() (single-object).
}

void FileAWSSDK::FinalizeSegment()
{
    if (m_OpenMode != Mode::Write && m_OpenMode != Mode::Append)
    {
        return;
    }

    if (m_MultiObjectMode)
    {
        // Upload buffered data as the next numbered object
        FlushWriteBuffer();
    }
    // Single mode: no-op (multipart upload continues across segments)
}

void FileAWSSDK::Close()
{
    WaitForOpen();
    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::Close(" << m_Name << ") Enter" << std::endl;
    }
    ProfilerStart("close");
    errno = 0;
    m_Errno = errno;

    if (m_OpenMode == Mode::Write || m_OpenMode == Mode::Append)
    {
        if (m_MultiObjectMode)
        {
            // Multi-object mode: flush remaining buffered data as final object
            FlushWriteBuffer();
            if (m_Verbose > 0)
            {
                std::cout << "FileAWSSDK::Close: Multi-object mode, wrote " << m_NextObjectNumber
                          << " objects for " << m_Name << std::endl;
            }
        }
        else
        {
            // Single-object mode: finalize multipart upload
            // Upload any remaining data as the final part
            // (Final part can be < 5MB, unlike other parts)
            if (!m_WriteBuffer.empty())
            {
                UploadBufferedPart();
            }

            // Complete or abort the multipart upload
            if (!m_CompletedParts.empty())
            {
                CompleteMultipartUpload();
            }
            else if (!m_UploadId.empty())
            {
                // No parts uploaded (empty file) - abort the upload
                AbortMultipartUpload();
                if (m_Verbose > 0)
                {
                    std::cout << "FileAWSSDK::Close: Aborted empty multipart upload for " << m_Name
                              << std::endl;
                }
            }
        }
    }

    if (s3Client)
    {
        delete s3Client;
        s3Client = nullptr;
    }
    if (s3ClientConfig)
    {
        delete s3ClientConfig;
        s3ClientConfig = nullptr;
    }
    if (m_CachingThisFile)
    {
        m_CacheFileWrite->Close();
        delete m_CacheFileWrite;
    }
    if (m_IsCached)
    {
        m_CacheFileRead->Close();
        delete m_CacheFileRead;
    }

    m_IsOpen = false;
    ProfilerStop("close");
}

void FileAWSSDK::Delete()
{
    WaitForOpen();
    if (m_IsOpen)
    {
        Close();
    }
    std::remove(m_Name.c_str());
}

void FileAWSSDK::CheckFile(const std::string hint) const
{
    if (!m_IsCached && !m_IsMultiObjectLayout && !head_object.IsSuccess())
    {
        helper::Throw<std::ios_base::failure>("Toolkit", "transport::file::FileAWSSDK", "CheckFile",
                                              hint);
    }
}

void FileAWSSDK::SeekToEnd() { m_SeekPos = MaxSizeT; }

void FileAWSSDK::SeekToBegin() { m_SeekPos = 0; }

void FileAWSSDK::Seek(const size_t start)
{
    if (start != MaxSizeT)
    {
        m_SeekPos = start;
    }
    else
    {
        SeekToEnd();
    }
}

void FileAWSSDK::Truncate(const size_t length)
{
    if (!m_MultiObjectMode)
    {
        helper::Throw<std::ios_base::failure>("Toolkit", "transport::file::FileAWSSDK", "Truncate",
                                              "does not support truncating " + m_Name);
    }

    if (length == 0)
    {
        // Truncate to 0 = delete everything — reuse DeleteStaleObjects logic
        // but scoped to this subfile only
        std::string prefix = m_BaseObjectName + ".";
        Aws::String continuationToken;
        bool hasMore = true;
        while (hasMore)
        {
            Aws::S3::Model::ListObjectsV2Request listReq;
            listReq.SetBucket(m_BucketName);
            listReq.SetPrefix(prefix);
            listReq.SetMaxKeys(1000);
            if (!continuationToken.empty())
            {
                listReq.SetContinuationToken(continuationToken);
            }
            auto listOutcome = s3Client->ListObjectsV2(listReq);
            if (!listOutcome.IsSuccess())
            {
                break;
            }
            const auto &result = listOutcome.GetResult();
            if (!result.GetContents().empty())
            {
                Aws::S3::Model::Delete deleteObj;
                for (const auto &obj : result.GetContents())
                {
                    deleteObj.AddObjects(Aws::S3::Model::ObjectIdentifier().WithKey(obj.GetKey()));
                }
                Aws::S3::Model::DeleteObjectsRequest deleteReq;
                deleteReq.SetBucket(m_BucketName);
                deleteReq.SetDelete(std::move(deleteObj));
                s3Client->DeleteObjects(deleteReq);
            }
            hasMore = result.GetIsTruncated();
            if (hasMore)
            {
                continuationToken = result.GetNextContinuationToken();
            }
        }
        m_NextObjectNumber = 0;
        m_TotalBytesWritten = 0;
        if (m_Verbose > 0)
        {
            std::cout << "FileAWSSDK::Truncate(0): deleted all segments for " << m_BaseObjectName
                      << std::endl;
        }
        return;
    }

    // List segments, sorted by number, to find where `length` falls
    std::string prefix = m_BaseObjectName + ".";
    struct SegInfo
    {
        std::string key;
        size_t segNum;
        size_t size;
    };
    std::vector<SegInfo> segs;

    Aws::String continuationToken;
    bool hasMore = true;
    while (hasMore)
    {
        Aws::S3::Model::ListObjectsV2Request listReq;
        listReq.SetBucket(m_BucketName);
        listReq.SetPrefix(prefix);
        listReq.SetMaxKeys(1000);
        if (!continuationToken.empty())
        {
            listReq.SetContinuationToken(continuationToken);
        }
        auto listOutcome = s3Client->ListObjectsV2(listReq);
        if (!listOutcome.IsSuccess())
        {
            break;
        }
        const auto &result = listOutcome.GetResult();
        for (const auto &obj : result.GetContents())
        {
            std::string key(obj.GetKey().c_str());
            std::string suffix = key.substr(prefix.length());
            bool isNumeric = !suffix.empty();
            for (char c : suffix)
            {
                if (c < '0' || c > '9')
                {
                    isNumeric = false;
                    break;
                }
            }
            if (isNumeric)
            {
                segs.push_back({key, std::stoul(suffix), static_cast<size_t>(obj.GetSize())});
            }
        }
        hasMore = result.GetIsTruncated();
        if (hasMore)
        {
            continuationToken = result.GetNextContinuationToken();
        }
    }

    std::sort(segs.begin(), segs.end(),
              [](const SegInfo &a, const SegInfo &b) { return a.segNum < b.segNum; });

    // Walk segments, find which ones fall beyond `length`
    size_t cumSize = 0;
    std::vector<Aws::S3::Model::ObjectIdentifier> toDelete;
    size_t keepCount = 0;
    for (const auto &seg : segs)
    {
        if (cumSize >= length)
        {
            toDelete.push_back(
                Aws::S3::Model::ObjectIdentifier().WithKey(Aws::String(seg.key.c_str())));
        }
        else
        {
            ++keepCount;
        }
        cumSize += seg.size;
    }

    // Delete the excess segments
    if (!toDelete.empty())
    {
        // Batch delete (up to 1000 at a time)
        for (size_t i = 0; i < toDelete.size(); i += 1000)
        {
            Aws::S3::Model::Delete deleteObj;
            size_t end = std::min(i + 1000, toDelete.size());
            for (size_t j = i; j < end; ++j)
            {
                deleteObj.AddObjects(toDelete[j]);
            }
            Aws::S3::Model::DeleteObjectsRequest deleteReq;
            deleteReq.SetBucket(m_BucketName);
            deleteReq.SetDelete(std::move(deleteObj));
            s3Client->DeleteObjects(deleteReq);
        }
        if (m_Verbose > 0)
        {
            std::cout << "FileAWSSDK::Truncate(" << length << "): kept " << keepCount
                      << " segments, deleted " << toDelete.size() << " for " << m_BaseObjectName
                      << std::endl;
        }
    }

    m_NextObjectNumber = keepCount;
    m_TotalBytesWritten = length;
}

void FileAWSSDK::MkDir(const std::string &fileName) {}

void FileAWSSDK::DiscoverSubObjects()
{
    // List objects with prefix "objectName." to find numbered sub-objects
    // e.g. for "data.0", look for "data.0.0", "data.0.1", ...
    std::string prefix = m_ObjectName + ".";

    m_SubObjects.clear();
    m_TotalVirtualSize = 0;

    Aws::String continuationToken;
    bool moreResults = true;

    while (moreResults)
    {
        Aws::S3::Model::ListObjectsV2Request request;
        request.SetBucket(m_BucketName);
        request.SetPrefix(prefix);
        if (!continuationToken.empty())
        {
            request.SetContinuationToken(continuationToken);
        }

        auto outcome = s3Client->ListObjectsV2(request);
        if (!outcome.IsSuccess())
        {
            if (m_Verbose > 0)
            {
                std::cout << "FileAWSSDK::DiscoverSubObjects: ListObjectsV2 failed for prefix '"
                          << prefix << "': " << outcome.GetError().GetMessage() << std::endl;
            }
            return; // Not multi-object layout
        }

        const auto &result = outcome.GetResult();
        for (const auto &object : result.GetContents())
        {
            std::string key = object.GetKey();
            // Extract the numeric suffix after the prefix
            std::string suffix = key.substr(prefix.length());
            // Verify it's a pure numeric suffix (no nested slashes or other chars)
            bool isNumeric = !suffix.empty();
            for (char c : suffix)
            {
                if (c < '0' || c > '9')
                {
                    isNumeric = false;
                    break;
                }
            }
            if (!isNumeric)
            {
                continue;
            }

            SubObject sub;
            sub.key = key;
            sub.size = static_cast<size_t>(object.GetSize());
            sub.cumulativeOffset = 0; // computed after sorting
            m_SubObjects.push_back(sub);
        }

        if (result.GetIsTruncated())
        {
            continuationToken = result.GetNextContinuationToken();
        }
        else
        {
            moreResults = false;
        }
    }

    if (m_SubObjects.empty())
    {
        return; // No sub-objects found
    }

    // Sort by numeric suffix (object number)
    std::sort(m_SubObjects.begin(), m_SubObjects.end(),
              [&prefix](const SubObject &a, const SubObject &b) {
                  size_t numA = std::stoull(a.key.substr(prefix.length()));
                  size_t numB = std::stoull(b.key.substr(prefix.length()));
                  return numA < numB;
              });

    // Build cumulative offset table
    size_t cumOffset = 0;
    for (auto &sub : m_SubObjects)
    {
        sub.cumulativeOffset = cumOffset;
        cumOffset += sub.size;
    }
    m_TotalVirtualSize = cumOffset;
    m_IsMultiObjectLayout = true;

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::DiscoverSubObjects: Found " << m_SubObjects.size()
                  << " sub-objects for prefix '" << prefix
                  << "', total size = " << m_TotalVirtualSize << std::endl;
    }
}

void FileAWSSDK::UploadObject(const std::string &key, const char *data, size_t size)
{
    Aws::Utils::Stream::PreallocatedStreamBuf streamBuf(
        reinterpret_cast<unsigned char *>(const_cast<char *>(data)), size);
    auto inputData = Aws::MakeShared<Aws::IOStream>("S3Upload", &streamBuf);

    Aws::S3::Model::PutObjectRequest request;
    request.SetBucket(m_BucketName);
    request.SetKey(key);
    request.SetBody(inputData);
    request.SetContentLength(static_cast<long long>(size));

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::UploadObject: bucket='" << m_BucketName << "' key='" << key
                  << "' size=" << size << std::endl;
    }

    auto outcome = s3Client->PutObject(request);
    // Release SDK's reference to the body stream before the stack-local
    // PreallocatedStreamBuf is destroyed.  This prevents potential use-after-free
    // if the SDK retains an internal reference (e.g. for retries or connection pooling).
    request.SetBody(nullptr);
    if (!outcome.IsSuccess())
    {
        helper::Throw<std::ios_base::failure>(
            "Toolkit", "transport::file::FileAWSSDK", "UploadObject",
            "Failed to upload object '" + key + "' to bucket '" + m_BucketName +
                "': " + outcome.GetError().GetMessage());
    }
}

void FileAWSSDK::FlushWriteBuffer()
{
    if (m_WriteBuffer.empty())
    {
        return;
    }

    std::string key = m_BaseObjectName + "." + std::to_string(m_NextObjectNumber);
    m_NextObjectNumber++;
    UploadObject(key, m_WriteBuffer.data(), m_WriteBuffer.size());
    m_WriteBuffer.clear();
}

void FileAWSSDK::DeleteStaleSubObjects(const std::string &base)
{
    // Delete sub-objects for one subfile, e.g. "data.3.0", "data.3.1", ...
    // Each rank calls this for its own subfile, so no cross-rank race.
    std::string prefix = base + ".";

    Aws::String continuationToken;
    bool hasMore = true;
    while (hasMore)
    {
        Aws::S3::Model::ListObjectsV2Request listReq;
        listReq.SetBucket(m_BucketName);
        listReq.SetPrefix(prefix);
        listReq.SetMaxKeys(1000);
        if (!continuationToken.empty())
        {
            listReq.SetContinuationToken(continuationToken);
        }

        auto listOutcome = s3Client->ListObjectsV2(listReq);
        if (!listOutcome.IsSuccess())
        {
            break; // non-fatal: proceed without cleanup
        }

        const auto &result = listOutcome.GetResult();
        const auto &objects = result.GetContents();
        if (objects.empty())
        {
            break;
        }

        for (const auto &obj : objects)
        {
            Aws::S3::Model::DeleteObjectRequest delReq;
            delReq.SetBucket(m_BucketName);
            delReq.SetKey(obj.GetKey());
            s3Client->DeleteObject(delReq);
        }

        if (m_Verbose > 0)
        {
            std::cout << "FileAWSSDK::DeleteStaleSubObjects: deleted " << objects.size()
                      << " stale objects with prefix '" << prefix << "'" << std::endl;
        }

        hasMore = result.GetIsTruncated();
        if (hasMore)
        {
            continuationToken = result.GetNextContinuationToken();
        }
    }
}

void FileAWSSDK::DeleteOrphanedSubObjects()
{
    // Called by subfile 0 only. Delete sub-objects belonging to subfiles
    // numbered >= m_NumSubFiles — leftovers from a prior run that used more
    // subfiles. Safe: no current rank writes to those subfiles.
    size_t lastSlash = m_ObjectName.rfind(PathSeparator);
    if (lastSlash == std::string::npos)
    {
        return;
    }
    // Prefix covers all data subfiles: "dir.bp/data."
    std::string dirPrefix = m_ObjectName.substr(0, lastSlash + 1) + "data.";

    Aws::String continuationToken;
    bool hasMore = true;
    while (hasMore)
    {
        Aws::S3::Model::ListObjectsV2Request listReq;
        listReq.SetBucket(m_BucketName);
        listReq.SetPrefix(dirPrefix);
        listReq.SetMaxKeys(1000);
        if (!continuationToken.empty())
        {
            listReq.SetContinuationToken(continuationToken);
        }

        auto listOutcome = s3Client->ListObjectsV2(listReq);
        if (!listOutcome.IsSuccess())
        {
            break;
        }

        const auto &result = listOutcome.GetResult();
        for (const auto &obj : result.GetContents())
        {
            // Key is like "dir.bp/data.5.3" (sub-object) or "dir.bp/data.5" (bare key)
            std::string key(obj.GetKey().c_str());
            std::string suffix = key.substr(dirPrefix.length());
            // suffix is "5.3" or "5" — subfile number is before the first dot (or the whole suffix)
            size_t dot = suffix.find('.');
            int subfileNum = -1;
            try
            {
                if (dot == std::string::npos)
                {
                    // Bare "data.N" key from single-object mode
                    subfileNum = std::stoi(suffix);
                }
                else
                {
                    // "N.M" sub-object from multi-object mode
                    subfileNum = std::stoi(suffix.substr(0, dot));
                }
            }
            catch (...)
            {
                continue;
            }
            if (subfileNum >= m_NumSubFiles)
            {
                Aws::S3::Model::DeleteObjectRequest delReq;
                delReq.SetBucket(m_BucketName);
                delReq.SetKey(obj.GetKey());
                s3Client->DeleteObject(delReq);
                if (m_Verbose > 0)
                {
                    std::cout << "FileAWSSDK::DeleteOrphanedSubObjects: deleted orphan '" << key
                              << "'" << std::endl;
                }
            }
        }

        hasMore = result.GetIsTruncated();
        if (hasMore)
        {
            continuationToken = result.GetNextContinuationToken();
        }
    }
}

void FileAWSSDK::InitiateMultipartUpload()
{
    Aws::S3::Model::CreateMultipartUploadRequest request;
    request.SetBucket(m_BucketName);
    request.SetKey(m_ObjectName);

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::InitiateMultipartUpload: bucket='" << m_BucketName << "' object='"
                  << m_ObjectName << "'" << std::endl;
    }

    auto outcome = s3Client->CreateMultipartUpload(request);
    if (!outcome.IsSuccess())
    {
        helper::Throw<std::ios_base::failure>("Toolkit", "transport::file::FileAWSSDK",
                                              "InitiateMultipartUpload",
                                              "Failed to initiate multipart upload for " + m_Name +
                                                  ": " + outcome.GetError().GetMessage());
    }

    m_UploadId = outcome.GetResult().GetUploadId();
    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::InitiateMultipartUpload: Upload ID = " << m_UploadId << std::endl;
    }
}

void FileAWSSDK::UploadPart(const char *data, size_t size)
{
    m_CurrentPartNumber++;

    // Zero-copy stream: reads directly from data buffer (no memcpy)
    // Buffer must stay valid until UploadPart returns (synchronous)
    Aws::Utils::Stream::PreallocatedStreamBuf streamBuf(
        reinterpret_cast<unsigned char *>(const_cast<char *>(data)), size);
    auto inputData = Aws::MakeShared<Aws::IOStream>("S3Upload", &streamBuf);

    Aws::S3::Model::UploadPartRequest request;
    request.SetBucket(m_BucketName);
    request.SetKey(m_ObjectName);
    request.SetPartNumber(m_CurrentPartNumber);
    request.SetUploadId(m_UploadId);
    request.SetBody(inputData);
    request.SetContentLength(static_cast<long long>(size));

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::UploadPart: Uploading part " << m_CurrentPartNumber
                  << ", size=" << size << " bytes" << std::endl;
    }

    auto outcome = s3Client->UploadPart(request);
    // Release SDK's reference to the body stream before the stack-local
    // PreallocatedStreamBuf is destroyed (same rationale as in UploadObject).
    request.SetBody(nullptr);
    if (!outcome.IsSuccess())
    {
        helper::Throw<std::ios_base::failure>(
            "Toolkit", "transport::file::FileAWSSDK", "UploadPart",
            "Failed to upload part " + std::to_string(m_CurrentPartNumber) + " for " + m_Name +
                ": " + outcome.GetError().GetMessage());
    }

    // Record completed part for final CompleteMultipartUpload call
    Aws::S3::Model::CompletedPart completedPart;
    completedPart.SetPartNumber(m_CurrentPartNumber);
    completedPart.SetETag(outcome.GetResult().GetETag());
    m_CompletedParts.push_back(completedPart);

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::UploadPart: Part " << m_CurrentPartNumber
                  << " uploaded, ETag=" << outcome.GetResult().GetETag() << std::endl;
    }
}

void FileAWSSDK::UploadBufferedPart()
{
    if (m_WriteBuffer.empty())
    {
        return;
    }

    UploadPart(m_WriteBuffer.data(), m_WriteBuffer.size());
    m_WriteBuffer.clear();
}

void FileAWSSDK::CompleteMultipartUpload()
{
    Aws::S3::Model::CompletedMultipartUpload completedUpload;
    completedUpload.SetParts(m_CompletedParts);

    Aws::S3::Model::CompleteMultipartUploadRequest request;
    request.SetBucket(m_BucketName);
    request.SetKey(m_ObjectName);
    request.SetUploadId(m_UploadId);
    request.SetMultipartUpload(completedUpload);

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::CompleteMultipartUpload: Completing upload for " << m_Name
                  << " with " << m_CompletedParts.size() << " parts" << std::endl;
    }

    auto outcome = s3Client->CompleteMultipartUpload(request);
    if (!outcome.IsSuccess())
    {
        helper::Throw<std::ios_base::failure>("Toolkit", "transport::file::FileAWSSDK",
                                              "CompleteMultipartUpload",
                                              "Failed to complete multipart upload for " + m_Name +
                                                  ": " + outcome.GetError().GetMessage());
    }

    if (m_Verbose > 0)
    {
        std::cout << "FileAWSSDK::CompleteMultipartUpload: Successfully uploaded " << m_Name << ", "
                  << m_CompletedParts.size() << " parts, " << m_TotalBytesWritten << " bytes total"
                  << std::endl;
    }

    m_UploadId.clear();
    m_CompletedParts.clear();
}

void FileAWSSDK::AbortMultipartUpload()
{
    if (m_UploadId.empty())
    {
        return;
    }

    Aws::S3::Model::AbortMultipartUploadRequest request;
    request.SetBucket(m_BucketName);
    request.SetKey(m_ObjectName);
    request.SetUploadId(m_UploadId);

    // Best effort - ignore errors on abort
    auto outcome = s3Client->AbortMultipartUpload(request);
    if (m_Verbose > 0)
    {
        if (outcome.IsSuccess())
        {
            std::cout << "FileAWSSDK::AbortMultipartUpload: Aborted upload for " << m_Name
                      << std::endl;
        }
        else
        {
            std::cout << "FileAWSSDK::AbortMultipartUpload: Failed to abort upload for " << m_Name
                      << ": " << outcome.GetError().GetMessage() << std::endl;
        }
    }

    m_UploadId.clear();
    m_CompletedParts.clear();
}

} // end namespace transport
} // end namespace adios2
