#include "../include/query_builder.h"

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

namespace maps::wiki::views {

namespace {

const std::set<std::string> ALL_VIEW_TABLES {
    TABLE_OBJECTS,
    TABLE_OBJECTS_G,

    TABLE_OBJECTS_C,
    TABLE_OBJECTS_P,
    TABLE_OBJECTS_L,
    TABLE_OBJECTS_A,
    TABLE_OBJECTS_R
};

const std::string BRANCH_MASK_ALIAS = "bm";
const std::string WITH_CLAUSE_TABLE_NAME_STUB = "";

} // namespace


QueryBuilder::QueryBuilder(revision::DBID branchId)
    : branchId_(branchId)
{
    if (branchId_ != revision::TRUNK_BRANCH_ID) {
        aliases_.insert(BRANCH_MASK_ALIAS);
    }
}

void QueryBuilder::with(const std::string& alias, const std::string& query)
{
    with(alias, {}, query);
}

void QueryBuilder::with(
    const std::string& alias,
    const std::string& fields,
    const std::string& query)
{
    ASSERT(!alias.empty());
    ASSERT(!query.empty());
    REQUIRE(aliases_.insert(alias).second,
            "Alias " << alias << " already used");

    withData_.push_back({alias, fields, query});
}

void QueryBuilder::selectFields(std::string str)
{
    ASSERT(selectFields_.empty());
    selectFields_ = std::move(str);
}

void QueryBuilder::fromWith(const std::string& alias)
{
    ASSERT(!alias.empty());

    auto checkAlias = [&]() {
        for (const auto& wd : withData_) {
            if (wd.alias == alias) {
                return true;
            }
        }
        return false;
    };
    REQUIRE(checkAlias(), "Unknown with alias " << alias);

    auto& aliases = fromTables_[WITH_CLAUSE_TABLE_NAME_STUB];
    auto res = aliases.insert(alias);
    ASSERT(res.second);
}

void QueryBuilder::whereClause(std::string str)
{
    ASSERT(whereClause_.empty());
    whereClause_ = std::move(str);
}

std::string QueryBuilder::query() const
{
    ASSERT(!selectFields_.empty());
    ASSERT(!fromTables_.empty());
    ASSERT(!whereClause_.empty());

    auto needBmClause =
        branchId_ != revision::TRUNK_BRANCH_ID && !fromTables_.count(TABLE_OBJECTS);

    std::string bmClause;
    std::string tables;
    for (const auto& pair : fromTables_) {
        const auto& tableName = pair.first;
        for (const auto& alias : pair.second) {
            tables += tables.empty() ? "" : ", ";
            if (tableName == WITH_CLAUSE_TABLE_NAME_STUB) {
                tables += alias;
            } else if (needBmClause) {
                tables += "vrevisions_stable." + tableName + " " + alias;
                bmClause += alias + ".branch_mask_id IN "
                    "(SELECT branch_mask_id FROM " + BRANCH_MASK_ALIAS + ") AND ";
            } else {
                tables += tableName + " " + alias;
            }
        }
    }

    std::string result;
    auto addWith = [&](const WithData& wd) {
        result += result.empty() ? "WITH " : ", ";
        result += wd.alias;
        if (!wd.fields.empty()) {
            result += "(" + wd.fields + ")";
        }
        result += " AS (" + wd.query + ") ";
    };

    if (needBmClause) {
        addWith(WithData{
            BRANCH_MASK_ALIAS,
            {},
            "SELECT branch_mask_id FROM vrevisions_stable.branch_mask"
            " WHERE branches ? '" + std::to_string(branchId_) + "'"
        });
    }
    for (const auto& wd : withData_) {
        addWith(wd);
    }

    result += "SELECT " + selectFields_ + " FROM " + tables + " WHERE ";
    if (bmClause.empty()) {
        result += whereClause_;
    } else {
        result += bmClause + "(" + whereClause_ + ")";
    }
    return result;
}

void QueryBuilder::fromTableInternal(
    const std::string& tableName,
    const std::set<std::string>& aliases)
{
    ASSERT(!tableName.empty());
    ASSERT(!aliases.empty());

    REQUIRE(
        (branchId_ == revision::TRUNK_BRANCH_ID && tableName == TABLE_CONTOUR_OBJECTS_GEOM) ||
        ALL_VIEW_TABLES.count(tableName),
        "Unsupported table " << tableName);

    for (const auto& alias : aliases) {
        ASSERT(!alias.empty());
        REQUIRE(aliases_.insert(alias).second,
                "Table " << tableName << ". Alias " << alias << " already used");

        auto& aliases = fromTables_[tableName];
        auto res = aliases.insert(alias);
        ASSERT(res.second);
    }
}

} // namespace maps::wiki::views
