// Workaround for boost::filesystem with boost version <= 1.40
#include <boost/version.hpp>
#if (BOOST_VERSION <= 104000)
#include <boost/config.hpp>
#undef BOOST_HAS_RVALUE_REFS
#define BOOST_NO_RVALUE_REFERENCES
#endif

#include "magic_strings.h"
#include "sql.h"
#include <yandex/maps/wiki/mds_dataset/dataset_gateway.h>
#include <yandex/maps/wiki/mds_dataset/export_metadata.h>
#include <maps/libs/pgpool/include/pgpool3.h>

#include <boost/filesystem.hpp>

#include <algorithm>
#include <fstream>
#include <sstream>
#include <vector>

namespace fs = boost::filesystem;

namespace maps {
namespace wiki {
namespace mds_dataset {

typedef pqxx::transaction_base Txn;

namespace {

void validateFiles(const std::vector<std::string>& filePaths)
{
    if (filePaths.empty()) {
        throw MdsDatasetError() << "Can't create dataset without files";
    }
    for (const auto& filePath : filePaths) {
        const fs::path p(filePath);
        if (!fs::exists(p) || fs::is_directory(p)) {
            throw InvalidDatasetFile() << "Invalid dataset file: " << p;
        }
    }
}

} // namespace


// Dataset Writer impl

template <typename MetaData>
class DatasetWriter<MetaData>::Impl
{
public:
    Impl(mds::Mds& mdsClient, pgpool3::Pool& pgPool)
        : mdsClient_(mdsClient)
        , pgPool_(pgPool)
    {}

    ~Impl() = default;

    void createDataset(
            const MetaData& metadata,
            const std::vector<std::string>& filePaths,
            mds::Schema schema)
    {
        validateMetadata(metadata);
        validateFiles(filePaths);

        const auto datasetPath =
            metadata.id() + "/"
            + (metadata.region().empty() ? "" : (metadata.region() + "/"));

        std::vector<FileLink> uploadedFiles;

        try {
            // First upload all files to MDS
            for (const auto& path : filePaths) {
                auto fileName = fs::path(fs::path(path).filename()).string();
                std::ifstream file(path);
                auto resp = mdsClient_.post(datasetPath + fileName, file);
                uploadedFiles.push_back(FileLink{
                    std::move(resp.key()),
                    std::move(fileName),
                    mdsClient_.makeReadUrl(resp.key(), schema)
                });
            }

            // Now store metadata in postgres
            auto txn = pgPool_.masterWriteableTransaction();
            sql::writeMetadata(metadata, *txn);
            sql::writeFileLinks<MetaData>(metadata.id(), metadata.region(), uploadedFiles, *txn);
            txn->commit();
        } catch (const std::exception&) {
            // Delete all uploaded files from MDS
            for (const auto& fileLink: uploadedFiles) {
                mdsClient_.del(fileLink.mdsKey());
            }
            throw;
        }
    }

    void deleteDataset(const DatasetID& id, const Region& region)
    {
        {
            auto txn = pgPool_.masterWriteableTransaction();
            sql::setDatasetStatus<MetaData>(id, region, DatasetStatus::Deleting, *txn);
            txn->commit();
        }

        auto txn = pgPool_.masterWriteableTransaction();

        auto fileLinks = sql::loadFileLinks<MetaData>(id, region, *txn);
        for (const auto& fileLink : fileLinks) {
            mdsClient_.del(fileLink.mdsKey());
        }

        sql::deleteFileLinks<MetaData>(id, region, *txn);
        sql::setDatasetStatus<MetaData>(id, region, DatasetStatus::Deleted, *txn);
        txn->commit();
    }

    pgpool3::Pool& pool() { return pgPool_; }

private:
    mds::Mds& mdsClient_;
    pgpool3::Pool& pgPool_;
};

template <typename MetaData>
DatasetWriter<MetaData>::DatasetWriter(
    mds::Mds& mdsClient,
    pgpool3::Pool& pgPool)
        : pimpl_(new Impl(mdsClient, pgPool))
{}

template <typename MetaData>
DatasetWriter<MetaData>::~DatasetWriter()
{}

template <typename MetaData>
Dataset<MetaData> DatasetWriter<MetaData>::createDataset(
        const MetaData& metadata,
        const std::vector<std::string>& filePaths,
        mds::Schema schema)
{
    try {
        pimpl_->createDataset(metadata, filePaths, schema);
    } catch (const pqxx::unique_violation&) {
        throw DuplicateDataset(metadata.id(), metadata.region())
                << "Dataset with id = " << metadata.id()
                << " and region = '" << metadata.region() << "' already exists";
    }
    // Use master transaction to avoid race condition
    // when reading the just written dataset
    auto txn = pimpl_->pool().masterReadOnlyTransaction();
    return DatasetReader<MetaData>::dataset(*txn, metadata.id(), metadata.region());
}

template <typename MetaData>
void DatasetWriter<MetaData>::deleteDataset(const DatasetID& id, const Region& region)
{
    pimpl_->deleteDataset(id, region);
}

template <typename MetaData>
void DatasetWriter<MetaData>::deleteDataset(const DatasetID& id)
{
    deleteDataset(id, NO_REGION);
}

// Dataset Reader impl

template <typename MetaData>
class DatasetReader<MetaData>::Impl
{
public:
    Impl() = delete;
};

template <typename MetaData>
Dataset<MetaData> DatasetReader<MetaData>::dataset(
        Txn& txn,
        const DatasetID& id,
        const Region& region)
{
    typename MetaData::FilterType filter(txn);
    filter.byId(id);
    filter.byRegion(region);
    auto clause = filter.toSql();
    auto datasets = sql::loadDatasets<MetaData>(txn, clause);

    if (datasets.empty()) {
        throw DatasetNotFound(id, region) << "Dataset with id = " << id << " and region = '" << region << "' not found";
    }
    REQUIRE(datasets.size() == 1,
        "Multiple datasets with id = " << id << " and region = '" << region << "'");
    return std::move(datasets[0]);
}

template <typename MetaData>
Dataset<MetaData> DatasetReader<MetaData>::dataset(
        Txn& txn,
        const DatasetID& id)
{
    return dataset(txn, id, NO_REGION);
}

template <typename MetaData>
std::vector<Dataset<MetaData>> DatasetReader<MetaData>::datasets(Txn& txn)
{
    return sql::loadDatasets<MetaData>(txn);
}

template <typename MetaData>
std::vector<Dataset<MetaData>> DatasetReader<MetaData>::datasets(
        Txn& txn,
        const FilterType& filter)
{
    return sql::loadDatasets<MetaData>(txn, filter.toSql());
}

template <typename MetaData>
std::vector<Dataset<MetaData>> DatasetReader<MetaData>::datasets(
        Txn& txn,
        const FilterType& filter,
        size_t limit,
        size_t offset)
{
    std::ostringstream limitClause;
    limitClause << "LIMIT " << limit << " OFFSET " << offset;

    return sql::loadDatasets<MetaData>(txn, filter.toSql(), limitClause.str());
}

// Explicit instantiation
#define INSTANTIATE_CLASSES(MetaData)      \
template class Dataset<MetaData>;          \
template class DatasetWriter<MetaData>;    \
template class DatasetReader<MetaData>;

INSTANTIATE_CLASSES(ExportMetadata)

} // mds_dataset
} // wiki
} // maps
