#include "client.h"

#include <maps/libs/common/include/exception.h>

#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-core/include/aws/core/Aws.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentialsProviderChain.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-core/include/aws/core/client/ClientConfiguration.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/AbortMultipartUploadRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/CreateBucketRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/CreateMultipartUploadRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/CompleteMultipartUploadRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/DeleteBucketRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/DeleteObjectRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/GetObjectRequest.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/ListObjectsV2Request.h>
#include <contrib/libs/aws-sdk-cpp/aws-cpp-sdk-s3/include/aws/s3/model/UploadPartRequest.h>

#include <atomic>

namespace maps::fw_updater::storage::s3 {

namespace {

std::atomic<int> g_initCount = 0;

struct Api {
    static void init()
    {
        if (g_initCount++ > 0) {
            return;
        }
        Aws::InitAPI(Aws::SDKOptions{});
    }

    static void shutdown()
    {
        if (--g_initCount > 0) {
            return;
        }
        Aws::ShutdownAPI({});
    }

    Api() { init(); }
    ~Api() { shutdown(); }
};

} // namespace

S3Client::S3Client(const config::S3Config& s3Config,
                   RetryStrategyPtr retryStrategy)
    : publicReadHost_(s3Config.publicReadHost())
    , bucket_(s3Config.bucket())
{
    Api::init();

    Aws::Client::ClientConfiguration config{};
    config.scheme = Aws::Http::Scheme::HTTP;
    config.endpointOverride = s3Config.host();
    config.connectTimeoutMs = s3Config.connectTimeoutMs();
    config.requestTimeoutMs = s3Config.requestTimeoutMs();
    config.retryStrategy = retryStrategy;

    auto auth = std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(
        s3Config.accessKeyId(), s3Config.accessKeySecret());

    client_ = std::make_shared<Aws::S3::S3Client>(auth, config,
        Aws::Client::AWSAuthV4Signer::PayloadSigningPolicy::Never,
        /*useVirtualAddressing = */false);
}

UploadId S3Client::createMultipartUpload(const std::string& key)
{
    Aws::S3::Model::CreateMultipartUploadRequest request;
    request.WithBucket(bucket_)
           .WithKey(key)
           .WithACL(Aws::S3::Model::ObjectCannedACL::public_read);

    auto outcome = client_->CreateMultipartUpload(request);

    REQUIRE(outcome.IsSuccess(),
            "Failed to create multipart upload: " << outcome.GetError());

    return outcome.GetResult().GetUploadId();
}

Aws::S3::Model::CompletedPart S3Client::uploadPart(
    const std::string& key,
    const UploadId& uploadId,
    int partNumber,
    std::streambuf& content)
{
    auto body = std::make_shared<std::iostream>(&content);

    Aws::S3::Model::UploadPartRequest request;
    request.WithBucket(bucket_)
           .WithKey(key)
           .WithUploadId(uploadId)
           .WithPartNumber(partNumber)
           .SetBody(body);

    auto outcome = client_->UploadPart(request);
    REQUIRE(outcome.IsSuccess(),
            "Failed to upload part: " << outcome.GetError());

    return Aws::S3::Model::CompletedPart{}
        .WithETag(outcome.GetResult().GetETag())
        .WithPartNumber(partNumber);
}

void S3Client::completeMultipartUpload(
    const std::string& key,
    const UploadId& uploadId,
    std::vector<Aws::S3::Model::CompletedPart> completedParts)
{
    Aws::S3::Model::CompleteMultipartUploadRequest request;

    request.WithBucket(bucket_)
           .WithKey(key)
           .WithUploadId(uploadId)
           .WithMultipartUpload(
               Aws::S3::Model::CompletedMultipartUpload{}
                   .WithParts(std::move(completedParts)));

    auto outcome = client_->CompleteMultipartUpload(request);
    REQUIRE(outcome.IsSuccess(),
            "Failed to complete multipart upload: " << outcome.GetError());
}

void S3Client::abortMultipartUpload(const std::string& key, const UploadId& uploadId)
{
    Aws::S3::Model::AbortMultipartUploadRequest request;

    request.WithBucket(bucket_)
           .WithKey(key)
           .WithUploadId(uploadId);

    auto outcome = client_->AbortMultipartUpload(request);
    REQUIRE(outcome.IsSuccess(),
            "Failed to abort multipart upload: " << outcome.GetError());
}

ObjectHolder S3Client::getObject(const std::string& key)
{
    Aws::S3::Model::GetObjectRequest request;

    request.WithBucket(bucket_).WithKey(key);

    auto outcome = client_->GetObject(request);
    REQUIRE(outcome.IsSuccess(),
            "Failed to get object: " << outcome.GetError());

    return ObjectHolder(outcome.GetResultWithOwnership());
}

void S3Client::deleteObject(const std::string& key)
{
    Aws::S3::Model::DeleteObjectRequest request;
    request.WithBucket(bucket_).WithKey(key);
    auto outcome = client_->DeleteObject(request);
    REQUIRE(outcome.IsSuccess(),
            "Failed to delete object " << key << outcome.GetError());
}


void S3Client::createBucket()
{
    Aws::S3::Model::CreateBucketRequest request;
    request.WithBucket(bucket_)
           .WithACL(Aws::S3::Model::BucketCannedACL::public_read);

    auto outcome = client_->CreateBucket(request);
    REQUIRE(outcome.IsSuccess(),
            "Failed to create bucket " << bucket_ << ": " << outcome.GetError());
}


std::string S3Client::makeReadingUrl(const std::string& key) const
{
    return "https://" + bucket_ + "." + publicReadHost_ + "/" + key;
}

} // maps::fw_updater::storage::s3
