#include <processargs.h>

#include <buildboxcommon_commandlinetypes.h>
#include <buildboxcommon_connectionoptions_commandline.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_logging_commandline.h>
#include <buildboxcommon_permissions.h>

#include <cstdlib>
#include <cstring>
#include <thread>

namespace casupload {

constexpr unsigned int DEFAULT_MAX_NUM_UPLOAD_THREADS = 32;
constexpr unsigned int DEFAULT_MAX_NUM_DIGEST_THREADS = 16;

using ArgumentSpec = buildboxcommon::CommandLineTypes::ArgumentSpec;
using DataType = buildboxcommon::CommandLineTypes::DataType;
using TypeInfo = buildboxcommon::CommandLineTypes::TypeInfo;
using DefaultValue = buildboxcommon::CommandLineTypes::DefaultValue;

const std::string usageText =
    "Uploads the given files and directories to CAS, then prints the digest\n"
    "hash and size of the corresponding Directory messages.\n"
    "\n"
    "The files are placed in CAS subdirectories corresponding to their\n"
    "paths. For example, 'casupload file1.txt subdir/file2.txt' would\n"
    "create a CAS directory containing file1.txt and a subdirectory called\n"
    "'subdir' containing file2.txt.\n"
    "\n"
    "The directories will be uploaded individually as merkle trees.\n"
    "The merkle tree for a directory will contain all of the content\n"
    "within the directory.\n"
    "\n"
    "Example usage: casupload --remote=http://localhost:50051 foo.c foo.h\n";

ProcessedArgs processArgs(int argc, char *argv[])
{
    ProcessedArgs args = {};

    // If this tool gets to use another API endpoint, this has to be changed
    // to be a common configuration, and separate CAS configuration added.
    //
    // NB: Third argument is not supplied because there is an obsolete
    // --cas-server argument that can be used instead of --remote.
    // Once --cas-server is removed, make --remote required by using
    // a three-argument constructor here.
    auto connectionOptionsCommandLine =
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "");

    std::vector<buildboxcommon::CommandLineTypes::ArgumentSpec> spec;

    const auto &connectionOptionsSpec = connectionOptionsCommandLine.spec();

    spec.insert(spec.end(), connectionOptionsSpec.cbegin(),
                connectionOptionsSpec.cend());

    spec.emplace_back("follow-symlinks", "Follow symlinks in the inputs",
                      TypeInfo(DataType::COMMANDLINE_DT_BOOL),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back("capture-mtime", "Capture mtime of files to be uploaded",
                      TypeInfo(DataType::COMMANDLINE_DT_BOOL),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back("capture-unix-mode",
                      "Capture Unix mode of files to be uploaded",
                      TypeInfo(DataType::COMMANDLINE_DT_BOOL),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back(
        "allow-chmod-to-read",
        "Allow temporary chmod to read unreadable files during upload",
        TypeInfo(DataType::COMMANDLINE_DT_BOOL), ArgumentSpec::O_OPTIONAL,
        ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back(
        "file-umask",
        "Bit mask in the octal format to turn off bits in captured "
        "unix modes of files, e.g. "
        "--file-umask=0222 makes all files non-writable",
        TypeInfo(DataType::COMMANDLINE_DT_STRING), ArgumentSpec::O_OPTIONAL,
        ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back("node-property",
                      "Node property attached to the uploaded path, e.g. "
                      "--node-property foo=bar",
                      TypeInfo(DataType::COMMANDLINE_DT_STRING_PAIR_ARRAY),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back("dry-run",
                      "Calculate and print digests, do not upload anything",
                      TypeInfo(DataType::COMMANDLINE_DT_BOOL),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back(
        "digest-function",
        "Set a custom digest function. Default: " +
            buildboxcommon::DigestFunction_Value_Name(
                BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE) +
            "\nSupported functions: " +
            buildboxcommon::DigestGenerator::supportedDigestFunctionsList(),
        TypeInfo(DataType::COMMANDLINE_DT_STRING), ArgumentSpec::O_OPTIONAL,
        ArgumentSpec::C_WITH_ARG);

    spec.emplace_back("output-digest-file",
                      "Write output root digest to the file in the form "
                      "\"<HASH>/<SIZE_BYTES>\"",
                      TypeInfo(DataType::COMMANDLINE_DT_STRING),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITH_ARG);

    spec.emplace_back("output-tree-digest-file",
                      "Write output tree digest to the file in the form "
                      "\"<HASH>/<SIZE_BYTES>\"",
                      TypeInfo(DataType::COMMANDLINE_DT_STRING),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITH_ARG);

    spec.emplace_back(
        "ignore-file",
        "Ignore files in the input directories that match the patterns in\n"
        "the specified file. Patterns format is as in .gitignore except\n"
        "! and ** are not supported.",
        TypeInfo(DataType::COMMANDLINE_DT_STRING), ArgumentSpec::O_OPTIONAL,
        ArgumentSpec::C_WITH_ARG);

    spec.emplace_back(
        "directory-format",
        "Set the format to upload directories in. '--directory-format=root' "
        "will upload individually and return a digest of the root directory."
        " '--directory-format=tree' will upload as a single tree message. "
        "'--directory-format=both' will do both.",
        TypeInfo(DataType::COMMANDLINE_DT_STRING), ArgumentSpec::O_OPTIONAL,
        ArgumentSpec::C_WITH_ARG);

    spec.emplace_back("num-upload-threads",
                      "The number of threads to upload blobs to remote CAS. "
                      "Default=min(hardware-concurrency*2,32)",
                      TypeInfo(DataType::COMMANDLINE_DT_INT),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITH_ARG);
    spec.emplace_back("num-digest-threads",
                      "The number of threads to hash files to digests. "
                      "Default=min(hardware-concurrency,16)",
                      TypeInfo(DataType::COMMANDLINE_DT_INT),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITH_ARG);

    spec.emplace_back("use-localcas",
                      "Use the LocalCAS API to capture directories. This "
                      "requires connecting to buildbox-casd.",
                      TypeInfo(DataType::COMMANDLINE_DT_BOOL),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITHOUT_ARG);
    spec.emplace_back(
        "bypass-local-cache",
        "Skip caching blobs in the LocalCAS storage when using "
        "the LocalCAS API. This has no effect if `--use-localcas` isn't set.",
        TypeInfo(DataType::COMMANDLINE_DT_BOOL), ArgumentSpec::O_OPTIONAL,
        ArgumentSpec::C_WITHOUT_ARG);

    spec.emplace_back("cas-server", "[Deprecated] Use --remote",
                      TypeInfo(DataType::COMMANDLINE_DT_STRING),
                      ArgumentSpec::O_OPTIONAL, ArgumentSpec::C_WITH_ARG);

    auto loggingSpec = buildboxcommon::loggingCommandLineSpec();
    spec.insert(spec.end(), loggingSpec.cbegin(), loggingSpec.cend());

    spec.emplace_back("", "", TypeInfo(&args.d_paths),
                      ArgumentSpec::O_REQUIRED,
                      ArgumentSpec::C_WITH_REST_OF_ARGS);

    auto cl = buildboxcommon::CommandLine(spec, usageText);

    if (!cl.parse(argc, argv)) {
        cl.usage();
        return args;
    }

    if (cl.exists("help") || cl.exists("version")) {
        args.d_processed = true;
        return args;
    }

    if (!connectionOptionsCommandLine.configureChannel(
            cl, "", &args.d_casConnectionOptions)) {
        return args;
    }

    if (cl.exists("cas-server")) {
        std::cerr << "WARNING\n"
                  << "WARNING --cas-server option is deprecated. Use --remote "
                     "instead.\n"
                  << "WARNING\n";

        if (args.d_casConnectionOptions.d_url != "") {
            std::cerr << "WARNING\n"
                      << "WARNING --cas-server and --remote options are "
                         "redundant, --cas-server value is ignored.\n"
                      << "WARNING\n";
        }
        else {
            args.d_casConnectionOptions.d_url = cl.getString("cas-server");
        }
    }

    if (args.d_casConnectionOptions.d_url == "") {
        std::cerr << "One of --cas-server and --remote is required\n";
        return args;
    }

    if (!buildboxcommon::parseLoggingOptions(cl, args.d_logLevel)) {
        return args;
    }

    args.d_digestFunctionValue =
        cl.exists("digest-function")
            ? buildboxcommon::DigestGenerator::stringToDigestFunction(
                  cl.getString("digest-function"))
            : static_cast<buildboxcommon::DigestFunction_Value>(
                  BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE);

    args.d_dryRunMode = cl.getBool("dry-run", false);
    args.d_followSymlinks = cl.getBool("follow-symlinks", false);
    args.d_captureMtime = cl.getBool("capture-mtime", false);
    args.d_captureUnixMode = cl.getBool("capture-unix-mode", false);
    args.d_allowChmodToRead = cl.getBool("allow-chmod-to-read", false);
    if (cl.exists("file-umask")) {
        if (!args.d_captureUnixMode) {
            std::cerr << "--file-umask can only be specified if "
                         "--capture-unix-mode is also used.";
            return args;
        }

        const auto umaskStr = cl.getString("file-umask");
        try {
            std::size_t endPos = 0;
            const mode_t umask =
                static_cast<mode_t>(std::stoul(umaskStr, &endPos, 8));
            if (endPos != umaskStr.size()) {
                throw std::invalid_argument("Invalid file umask option");
            }
            if (umask & (~PERMISSION_RWXALL_SPECIAL)) {
                throw std::out_of_range("File umask should be within 07777");
            }
            args.d_fileUmask = umask;
        }
        catch (std::exception &) {
            std::cerr << "Invalid file umask: " << umaskStr
                      << ". The option should be in octal format.";
            return args;
        }
    }

    for (const auto &propertyPair : cl.getVPS("node-property")) {
        args.d_nodeProperties[propertyPair.first] = propertyPair.second;
    }

    args.d_ignoreFile = cl.getString("ignore-file", "");

    args.d_outputRootDigestFile = cl.getString("output-digest-file", "");
    args.d_outputTreeDigestFile = cl.getString("output-tree-digest-file", "");

    std::string defaultDirectoryFormat = "root";
    if (!args.d_outputTreeDigestFile.empty() &&
        args.d_outputRootDigestFile.empty()) {
        defaultDirectoryFormat = "tree";
    }
    if (!args.d_outputTreeDigestFile.empty() &&
        !args.d_outputRootDigestFile.empty()) {
        defaultDirectoryFormat = "both";
    }

    std::string directoryFormat =
        cl.getString("directory-format", defaultDirectoryFormat);
    if (directoryFormat == "root") {
        args.d_directoryFormat = buildboxcommon::Command::DIRECTORY_ONLY;
    }
    else if (directoryFormat == "tree") {
        args.d_directoryFormat = buildboxcommon::Command::TREE_ONLY;
    }
    else if (directoryFormat == "both") {
        args.d_directoryFormat = buildboxcommon::Command::TREE_AND_DIRECTORY;
    }
    else {
        throw std::invalid_argument("directoryFormat is invalid please "
                                    "use 'root', 'tree' or 'both'");
    }

    //  Error message if incompatible options are specified.
    if (args.d_directoryFormat == buildboxcommon::Command::DIRECTORY_ONLY &&
        !args.d_outputTreeDigestFile.empty()) {
        BUILDBOX_LOG_ERROR("--output-tree-digest-file will be ignored as "
                           "--directory-format=root");
    }
    if (args.d_directoryFormat == buildboxcommon::Command::TREE_ONLY &&
        !args.d_outputRootDigestFile.empty()) {
        BUILDBOX_LOG_ERROR("--output-root-digest-file will be ignored as "
                           "--directory-format=tree");
    }

    const auto numThreadsError = "The number of threads should be >= 0";
    if (cl.exists("num-upload-threads")) {
        const int num = cl.getInt("num-upload-threads");
        if (num < 0) {
            std ::cerr << numThreadsError;
            return args;
        }
        args.d_numUploadThreads = num;
    }
    else {
        args.d_numUploadThreads =
            std::min(std::thread::hardware_concurrency() * 2,
                     DEFAULT_MAX_NUM_UPLOAD_THREADS);
    }
    if (cl.exists("num-digest-threads")) {
        const int num = cl.getInt("num-digest-threads");
        if (num < 0) {
            std ::cerr << numThreadsError;
            return args;
        }
        args.d_numDigestThreads = num;
    }
    else {
        args.d_numDigestThreads = std::min(std::thread::hardware_concurrency(),
                                           DEFAULT_MAX_NUM_DIGEST_THREADS);
    }

    args.d_localCasOption.d_useLocalCas = cl.getBool("use-localcas", false);
    args.d_localCasOption.d_bypassLocalCache =
        cl.getBool("bypass-local-cache", false);

    args.d_valid = true;
    return args;
} // namespace casupload

} // namespace casupload
