#include "export_helpers.h"
#include "measurable_task.h"

#include <maps/libs/concurrent/include/scoped_guard.h>
#include <yandex/maps/shell_cmd.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/std/map.h>
#include <maps/libs/process/include/process.h>
#include <yandex/maps/wiki/common/retry_duration.h>

namespace maps::wiki::exporter {

namespace {

const std::string TAR_BINARY = "/bin/tar";
const size_t PORT = 443;
const auto SCHEMA = mds::Schema::HTTPS;

} //anonymous namespace

Dataset publishMdsDataset(
    const ExportConfig& exportCfg,
    const Region& region,
    IsTested tested,
    mds_dataset::DatasetStatus status,
    const std::vector<std::string>& files)
{
    namespace ds = mds_dataset;

    const ds::ExportMetadata metadata(
        ds::BasicMetadata(exportCfg.tag(), region, status),
        exportCfg.subset(),
        tested);

    auto mdsConfig = exportCfg.mdsConfig(region);

    INFO() << "Publishing dataset with metadata: " << metadata
         << "Dataset files: {" << common::join(files, ", ") << "}";

    auto dataset = common::retryDuration([&] {
        mdsConfig.setReadPort(PORT);
        mds::Mds mdsClient(mdsConfig);
        ds::DatasetWriter<ds::ExportMetadata> writer(mdsClient, exportCfg.mdsPool());

        try {
             return writer.createDataset(metadata, files, SCHEMA);
        } catch (const ds::MdsDatasetError& ex) {
            throw common::RetryDurationCancel() << ex.what();
        }
    });

    INFO() << "Created MDS dataset: " << dataset;
    return dataset;
}


Dataset publishMdsErrorDataset(
    const ExportConfig& exportCfg,
    const std::vector<std::string>& files)
{
    return publishMdsDataset(
        exportCfg,
        NO_REGION,
        IsTested::No,
        mds_dataset::DatasetStatus::Incomplete,
        files);
}

std::optional<Dataset> makeDataset(
    const ExportConfig& exportCfg,
    const ExportFiles& exportFiles,
    const Region& region)
{
    std::optional<Dataset> dataset;
    const std::string forRegion = (region == NO_REGION) ? "" : " for region '" + region + "'";
    if (exportCfg.mdsUploadEnabled()) {
        runMeasurable(
            [&]{
                dataset = publishMdsDataset(
                    exportCfg,
                    region,
                    exportCfg.tested(),
                    mds_dataset::DatasetStatus::Available,
                    resultFiles(exportFiles, exportCfg.subset(), region));
            },
            exportCfg.logger(), "saving result in MDS" + forRegion);
    } else {
        WARN() <<
            "Not publishing mds dataset for " << exportCfg.subset() << " " <<
            forRegion << " because upload is disabled";
    }
    return dataset;
}


std::string files(const Dataset& dataset)
{
    return common::join(
        dataset.fileLinks(),
        [](const mds_dataset::FileLink& fileLink) { return fileLink.readingUrl(); },
        " , ");
}

void tar(const std::string& source, const std::string& output) {
    /*
     * We need to create tar archive with _plain_ structure:
     * it should contain only files, without any directories.
     * The easiest way to do it is to use --directory key and
     * pass --filenames relative to this directory.
     */
    process::Command tarCmd{
        TAR_BINARY,
        "--directory", source,
        "--create",
        "--file", output,
    };

    const fs::path sourcePath(source);
    ASSERT(fs::is_directory(sourcePath));

    auto end = fs::recursive_directory_iterator();
    for (fs::recursive_directory_iterator itr(sourcePath); itr != end; ++itr) {
        if (fs::is_regular_file(itr->status())) {
            auto relativePath = fs::relative(itr->path(), sourcePath);
            tarCmd.addArgument(relativePath.string());
        }
    }

    INFO() << "Executing " << tarCmd;
    process::runAndCheck(tarCmd);
    INFO() << "tar archive created successfully: " << output;
}

void tarGz(const std::string& source, const std::string& output)
{
    fs::path sourcePath(source);
    ASSERT(fs::exists(sourcePath));

    //will contain space-separated list of files to be archived (without directory prefixed!)
    std::string filenames;
    std::string dirPath = source;
    if (fs::is_directory(sourcePath)) {
        for (fs::directory_iterator itr(sourcePath); itr != fs::directory_iterator(); ++itr) {
            REQUIRE(
                fs::is_regular_file(itr->path()),
                "Can not put non-regular file at " << itr->path() << " into tar archive"
            );
            filenames += itr->path().filename().string();
            filenames += " ";
        }
    } else {
        //Used when creating json dump of service attributes.
        filenames = sourcePath.filename();
        dirPath = sourcePath.parent_path().string();
    }

    //see note in tar() method about --directory argument
    std::string tarGzPipelinedCmd = (
        "tar --directory " + dirPath + " --create " + filenames + " | " +
        "pigz --stdout > " + output
    );
    INFO() << "Executing " << tarGzPipelinedCmd;
    auto res = shell::runCmd(tarGzPipelinedCmd);
    REQUIRE(
        res.exitCode == 0,
        "tar.gz command '" << tarGzPipelinedCmd << "'\n"
        "failed: " << res.stdErr
    );

    INFO() << "tar.gz archive created: " << output;
}

void gzTar(const std::string& source, const std::string& output)
{
    fs::path sourcePath(source);
    REQUIRE(fs::exists(sourcePath), "Source path " << sourcePath << " does not exists");
    REQUIRE(fs::is_directory(sourcePath), "Source path " << sourcePath << " is not a directory");
    const auto end = fs::directory_iterator();

    for (fs::directory_iterator itr(sourcePath); itr != end; ++itr) {
        std::string inputFile = itr->path().string();
        std::string inputFileBasename = itr->path().filename().string();
        //will create source/path.gz file while keeping source/path in place
        std::string pigzCommand = "pigz --keep " + inputFile;
        INFO() << "Executing " << pigzCommand;
        auto pigzResult = shell::runCmd(pigzCommand);
        REQUIRE(
            pigzResult.exitCode == 0,
            "pigz command '" << pigzCommand << "' failed: " << pigzResult.stdErr
        );

        std::string gzippedFile = inputFile + GZ_SUFFIX;
        std::string gzippedFileBasename = inputFileBasename + GZ_SUFFIX;
        ASSERT(fs::exists(gzippedFile));
        /*
         * We need to create tar archive with _plain_ structure:
         * it should contain only files, without any directories.
         * The easiest way to do it is to use --directory key and
         * pass FILE argument relative to this directory.
         */
        std::string tarCommand = "tar --directory " + source + " --append --file " + output + " " + gzippedFileBasename;
        INFO() << "Executing " << tarCommand;
        auto tarResult = shell::runCmd(tarCommand);
        REQUIRE(
            tarResult.exitCode == 0,
            "tar command '" << tarCommand << "'\n"
            "failed: " << tarResult.stdErr
        );

        /*
         * In order to leave the directory in the same state,
         * as it was given to us, we have to remove gzippedFile created during pigz call.
         *
         * FIXME: this is only needed because we intend to pack the same folder
         * into .tar.gz archive immediately after packing it into .gz.tar archive.
         *
         * This logic SHOULD BE removed along with removal of this duplication.
         */
        fs::remove(gzippedFile);
    }

    INFO() << "gz.tar archive created: " << output;
}

void gzipFile(const std::string& source, const std::string& output)
{
    REQUIRE(fs::exists(source), "Bad source file " << source);

    auto pigzCmd = "pigz --keep --stdout " + source + " > " + output;

    auto res = shell::runCmd(pigzCmd);
    REQUIRE(
        res.exitCode == 0,
        "pigz command '" << pigzCmd << "'\n"
        "failed: " << res.stdErr);

    INFO() << "gz archive created: " << output;
}

void copySingleFile(const std::string& source, const std::string& output)
{
    REQUIRE(fs::exists(source), "Bad source file " << source);

    auto cpCmd = "cp " + source + " " + output;

    auto res = shell::runCmd(cpCmd);
    REQUIRE(
        res.exitCode == 0,
        "cp command '" << cpCmd << "'\n"
        "failed: " << res.stdErr);

    INFO() << "File " << output << " created";
}

void saveResult(
    const ExportConfig& exportCfg,
    pgpool3::Pool& dbPool,
    const Datasets& datasets)
{
    if (datasets.empty()) {
        return;
    }
    std::map<std::string, std::string> datasetFiles;
    for(const auto& dataset: datasets) {
        const std::string regionPrefix = (dataset.region() == NO_REGION)
            ? ""
            : (dataset.region() + ".");
        for (const auto& fileLink : dataset.fileLinks()) {
            datasetFiles[regionPrefix + fileLink.name()] = fileLink.readingUrl();
        }
    }

    json::Builder resJson;
    resJson << datasetFiles;

    common::retryDuration([&] {
        auto txn = dbPool.masterWriteableTransaction();

        auto query =
            "INSERT INTO service.export_result\n"
            "(task_id, message, dataset_id)\n"
            "VALUES (" + std::to_string(exportCfg.taskId()) + ", " +
            txn->quote(resJson.str()) + ", " +
            txn->quote(exportCfg.tag()) + ")";

        txn->exec(query);
        txn->commit();
    });
}

} // namespace maps::wiki::exporter

