// Copyright 2023 Huawei Cloud Computing Technology Co., Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef BOOTSTRAP_BUILD_TOOL

#include "src/buildtool/serve_api/remote/source_tree_client.hpp"

#include <utility>

#include <grpcpp/grpcpp.h>

#include "justbuild/just_serve/just_serve.pb.h"
#include "src/buildtool/common/artifact_digest_factory.hpp"
#include "src/buildtool/common/bazel_types.hpp"
#include "src/buildtool/common/remote/client_common.hpp"
#include "src/buildtool/logging/log_level.hpp"

namespace {

auto StringToArchiveType(std::string const& type) noexcept
    -> justbuild::just_serve::ServeArchiveTreeRequest_ArchiveType {
    using ServeArchiveType =
        justbuild::just_serve::ServeArchiveTreeRequest_ArchiveType;
    return type == "zip"
               ? ServeArchiveType::ServeArchiveTreeRequest_ArchiveType_ZIP
               : ServeArchiveType::ServeArchiveTreeRequest_ArchiveType_TAR;
}

auto PragmaSpecialToSymlinksResolve(
    std::optional<PragmaSpecial> const& resolve_symlinks)
    -> justbuild::just_serve::ServeArchiveTreeRequest_SymlinksResolve {
    using ServeSymlinksResolve =
        justbuild::just_serve::ServeArchiveTreeRequest_SymlinksResolve;
    if (not resolve_symlinks) {
        return ServeSymlinksResolve::
            ServeArchiveTreeRequest_SymlinksResolve_NONE;
    }
    switch (resolve_symlinks.value()) {
        case PragmaSpecial::Ignore: {
            return ServeSymlinksResolve::
                ServeArchiveTreeRequest_SymlinksResolve_IGNORE;
        }
        case PragmaSpecial::ResolvePartially: {
            return ServeSymlinksResolve::
                ServeArchiveTreeRequest_SymlinksResolve_PARTIAL;
        }
        case PragmaSpecial::ResolveCompletely: {
            return ServeSymlinksResolve::
                ServeArchiveTreeRequest_SymlinksResolve_COMPLETE;
        }
    }
    // default return, to avoid [-Werror=return-type] error
    return ServeSymlinksResolve::ServeArchiveTreeRequest_SymlinksResolve_NONE;
}

}  // namespace

SourceTreeClient::SourceTreeClient(
    ServerAddress const& address,
    HashFunction hash_function,
    gsl::not_null<RemoteContext const*> const& remote_context) noexcept
    : hash_function_{hash_function} {
    stub_ =
        justbuild::just_serve::SourceTree::NewStub(CreateChannelWithCredentials(
            address.host, address.port, remote_context->auth));
}

auto SourceTreeClient::ServeCommitTree(std::string const& commit_id,
                                       std::string const& subdir,
                                       bool sync_tree) const noexcept
    -> result_t {
    justbuild::just_serve::ServeCommitTreeRequest request{};
    request.set_commit(commit_id);
    request.set_subdir(subdir);
    request.set_sync_tree(sync_tree);

    grpc::ClientContext context;
    justbuild::just_serve::ServeCommitTreeResponse response;
    grpc::Status status = stub_->ServeCommitTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() !=
        ::justbuild::just_serve::ServeCommitTreeResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ServeCommitTree response returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{
            response.status() !=
                    ::justbuild::just_serve::ServeCommitTreeResponse::NOT_FOUND
                ? GitLookupError::Fatal
                : GitLookupError::NotFound};
    }
    TreeResult result = {response.tree(), std::nullopt};
    // if asked to sync, get digest from response
    if (sync_tree) {
        auto digest = ArtifactDigestFactory::FromBazel(hash_function_.GetType(),
                                                       response.digest());
        if (not digest) {
            logger_.Emit(LogLevel::Debug, std::move(digest).error());
            return unexpected{GitLookupError::Fatal};
        }
        result.digest = *std::move(digest);
    }
    return result;  // success
}

auto SourceTreeClient::ServeArchiveTree(
    std::string const& content,
    std::string const& archive_type,
    std::string const& subdir,
    std::optional<PragmaSpecial> const& resolve_symlinks,
    bool sync_tree) const noexcept -> result_t {
    justbuild::just_serve::ServeArchiveTreeRequest request{};
    request.set_content(content);
    request.set_archive_type(StringToArchiveType(archive_type));
    request.set_subdir(subdir);
    request.set_resolve_symlinks(
        PragmaSpecialToSymlinksResolve(resolve_symlinks));
    request.set_sync_tree(sync_tree);

    grpc::ClientContext context;
    justbuild::just_serve::ServeArchiveTreeResponse response;
    grpc::Status status = stub_->ServeArchiveTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() !=
        ::justbuild::just_serve::ServeArchiveTreeResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ServeArchiveTree response returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{
            response.status() !=
                    ::justbuild::just_serve::ServeArchiveTreeResponse::NOT_FOUND
                ? GitLookupError::Fatal
                : GitLookupError::NotFound};
    }
    TreeResult result = {response.tree(), std::nullopt};
    // if asked to sync, get digest from response
    if (sync_tree) {
        auto digest = ArtifactDigestFactory::FromBazel(hash_function_.GetType(),
                                                       response.digest());
        if (not digest) {
            logger_.Emit(LogLevel::Debug, std::move(digest).error());
            return unexpected{GitLookupError::Fatal};
        }
        result.digest = *std::move(digest);
    }
    return result;  // success
}

auto SourceTreeClient::ServeDistdirTree(
    std::shared_ptr<std::unordered_map<std::string, std::string>> const&
        distfiles,
    bool sync_tree) const noexcept -> result_t {
    justbuild::just_serve::ServeDistdirTreeRequest request{};
    for (auto const& [k, v] : *distfiles) {
        auto* distfile = request.add_distfiles();
        distfile->set_name(k);
        distfile->set_content(v);
        distfile->set_executable(false);
    }
    request.set_sync_tree(sync_tree);

    grpc::ClientContext context;
    justbuild::just_serve::ServeDistdirTreeResponse response;
    grpc::Status status = stub_->ServeDistdirTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() !=
        ::justbuild::just_serve::ServeDistdirTreeResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ServeDistdirTree response returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{
            response.status() !=
                    ::justbuild::just_serve::ServeDistdirTreeResponse::NOT_FOUND
                ? GitLookupError::Fatal
                : GitLookupError::NotFound};
    }
    TreeResult result = {response.tree(), std::nullopt};
    // if asked to sync, get digest from response
    if (sync_tree) {
        auto digest = ArtifactDigestFactory::FromBazel(hash_function_.GetType(),
                                                       response.digest());
        if (not digest) {
            logger_.Emit(LogLevel::Debug, std::move(digest).error());
            return unexpected{GitLookupError::Fatal};
        }
        result.digest = *std::move(digest);
    }
    return result;  // success
}

auto SourceTreeClient::ServeForeignFileTree(const std::string& content,
                                            const std::string& name,
                                            bool executable) const noexcept
    -> result_t {
    justbuild::just_serve::ServeDistdirTreeRequest request{};
    auto* distfile = request.add_distfiles();
    distfile->set_name(name);
    distfile->set_content(content);
    distfile->set_executable(executable);

    grpc::ClientContext context;
    justbuild::just_serve::ServeDistdirTreeResponse response;
    grpc::Status status = stub_->ServeDistdirTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() !=
        ::justbuild::just_serve::ServeDistdirTreeResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ServeDistdirTree called for foreign file response "
                     "returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{
            response.status() !=
                    ::justbuild::just_serve::ServeDistdirTreeResponse::NOT_FOUND
                ? GitLookupError::Fatal
                : GitLookupError::NotFound};
    }
    return TreeResult{response.tree(), std::nullopt};  // success
}

auto SourceTreeClient::ServeContent(std::string const& content) const noexcept
    -> expected<ArtifactDigest, GitLookupError> {
    justbuild::just_serve::ServeContentRequest request{};
    request.set_content(content);

    grpc::ClientContext context;
    justbuild::just_serve::ServeContentResponse response;
    grpc::Status status = stub_->ServeContent(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() !=
        ::justbuild::just_serve::ServeContentResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ServeContent response returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{
            response.status() !=
                    ::justbuild::just_serve::ServeContentResponse::NOT_FOUND
                ? GitLookupError::Fatal
                : GitLookupError::NotFound};
    }
    auto digest = ArtifactDigestFactory::FromBazel(hash_function_.GetType(),
                                                   response.digest());
    if (not digest) {
        logger_.Emit(LogLevel::Debug, std::move(digest).error());
        return unexpected{GitLookupError::Fatal};
    }
    return *std::move(digest);  // success
}

auto SourceTreeClient::ServeTree(std::string const& tree_id) const noexcept
    -> expected<ArtifactDigest, GitLookupError> {
    justbuild::just_serve::ServeTreeRequest request{};
    request.set_tree(tree_id);

    grpc::ClientContext context;
    justbuild::just_serve::ServeTreeResponse response;
    grpc::Status status = stub_->ServeTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() != ::justbuild::just_serve::ServeTreeResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ServeTree response returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{
            response.status() !=
                    ::justbuild::just_serve::ServeTreeResponse::NOT_FOUND
                ? GitLookupError::Fatal
                : GitLookupError::NotFound};
    }
    auto digest = ArtifactDigestFactory::FromBazel(hash_function_.GetType(),
                                                   response.digest());
    if (not digest) {
        logger_.Emit(LogLevel::Debug, std::move(digest).error());
        return unexpected{GitLookupError::Fatal};
    }
    return *std::move(digest);  // success
}

auto SourceTreeClient::CheckRootTree(std::string const& tree_id) const noexcept
    -> std::optional<bool> {
    justbuild::just_serve::CheckRootTreeRequest request{};
    request.set_tree(tree_id);

    grpc::ClientContext context;
    justbuild::just_serve::CheckRootTreeResponse response;
    grpc::Status status = stub_->CheckRootTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return std::nullopt;
    }
    if (response.status() !=
        ::justbuild::just_serve::CheckRootTreeResponse::OK) {
        if (response.status() !=
            ::justbuild::just_serve::CheckRootTreeResponse::NOT_FOUND) {
            logger_.Emit(LogLevel::Debug,
                         "CheckRootTree response returned with {}",
                         static_cast<int>(response.status()));
            return std::nullopt;  // tree lookup failed
        }
        return false;  // tree not found
    }
    return true;  // tree found
}

auto SourceTreeClient::GetRemoteTree(
    ArtifactDigest const& digest) const noexcept -> bool {
    justbuild::just_serve::GetRemoteTreeRequest request{};
    (*request.mutable_digest()) = ArtifactDigestFactory::ToBazel(digest);

    grpc::ClientContext context;
    justbuild::just_serve::GetRemoteTreeResponse response;
    grpc::Status status = stub_->GetRemoteTree(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return false;
    }
    if (response.status() !=
        ::justbuild::just_serve::GetRemoteTreeResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "GetRemoteTree response returned with {}",
                     static_cast<int>(response.status()));
        return false;  // retrieving tree or import-to-git failed
    }
    // success!
    return true;
}

auto SourceTreeClient::ComputeTreeStructure(ArtifactDigest const& tree)
    const noexcept -> expected<ArtifactDigest, GitLookupError> {
    justbuild::just_serve::ComputeTreeStructureRequest request{};
    request.set_tree(tree.hash());

    grpc::ClientContext context;
    justbuild::just_serve::ComputeTreeStructureResponse response;
    grpc::Status status =
        stub_->ComputeTreeStructure(&context, request, &response);

    if (not status.ok()) {
        LogStatus(&logger_, LogLevel::Debug, status);
        return unexpected{GitLookupError::Fatal};
    }
    if (response.status() !=
        ::justbuild::just_serve::ComputeTreeStructureResponse::OK) {
        logger_.Emit(LogLevel::Debug,
                     "ComputeTreeStructure response returned with {}",
                     static_cast<int>(response.status()));
        return unexpected{response.status() !=
                                  ::justbuild::just_serve::
                                      ComputeTreeStructureResponse::NOT_FOUND
                              ? GitLookupError::Fatal
                              : GitLookupError::NotFound};
    }
    // ComputeTreeStructure operates on git digests.
    auto digest = ArtifactDigestFactory::Create(HashFunction::Type::GitSHA1,
                                                response.tree_structure_hash(),
                                                /*size_unknown=*/0,
                                                /*is_tree=*/true);
    if (not digest) {
        logger_.Emit(LogLevel::Debug, std::move(digest).error());
        return unexpected{GitLookupError::Fatal};
    }
    return *std::move(digest);
}

#endif  // BOOTSTRAP_BUILD_TOOL
