#include "altay_reader.h"
#include "common.h"
#include "names.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <yandex/maps/wiki/common/yt.h>

#include <sprav/protos/export.pb.h>
#include <mapreduce/yt/interface/client.h>
#include <library/cpp/protobuf/yql/descriptor.h>

#include <util/system/env.h>
#include <util/string/strip.h>

using namespace NYT;

namespace maps::wiki::merge_poi {
namespace {
const TString YT_REALM = "hahn";

const TString TAG_HAS_VERIFIED_OWNER = "has_verified_owner";

const TString GEOPRODUCT_IDS_URL = "//home/geoadv/export/production/priority_company_permanent_id";
const TString GEOSEARCH_SNAPSHOT_ATTR_PATH = "//home/sprav/altay/prod/geosearch_snapshot/@export-snapshot-path";
const std::string SNAPSHOT_FOLDER = "/snapshot";
const std::string MAPPING_FOLDER = "/snapshot/nyak_mapping";
const std::string MAPPING_UNKNOWN_FOLDER = "/snapshot/nyak_mapping_unknown";
const std::string INDOOR_COORDINATES_FOLDER = "/nyak/indoor_candidates";

TString
langName(NSprav::NLanguage::Language lang)
{
    const google::protobuf::EnumDescriptor* languageProtoEnumDescriptor =
        NSprav::NLanguage::Language_descriptor();
    auto langValue = languageProtoEnumDescriptor->FindValueByNumber(lang);
    ASSERT(langValue);
    return to_lower(langValue->name());
}

poi_feed::FeedObjectData
createFeedObjectData(
    const NSpravExport::TExportedCompany& company,
    const std::string& nmapsIdStr,
    poi_feed::PermalinkId permalink,
    std::uint64_t revision,
    const std::unordered_set<std::string>& supportedNmapsLangs)
{

    poi_feed::FeedObjectData data;
    if (!nmapsIdStr.empty()) {
        data.setNmapsId(boost::lexical_cast<poi_feed::ObjectId>(nmapsIdStr));
    }
    data.setPermalink(permalink);
    data.setToDelete(!company.HasUnreliable() &&
        company.HasClosedKind() && !company.GetClosedKind());
    if (revision) {
        data.setActualizationDate(
            chrono::sinceEpochToTimePoint<std::chrono::milliseconds>(revision));
    }
    if (company.HasIsAdvert()) {
        data.setIsAdvert(
            company.GetIsAdvert()
                ? poi_feed::FeedObjectData::IsAdvert::Yes
                : poi_feed::FeedObjectData::IsAdvert::No);
    }
    if (company.HasIsPlatinum()) {
        data.setIsPlatinum(
            company.GetIsPlatinum()
                ? poi_feed::FeedObjectData::IsPlatinum::Yes
                : poi_feed::FeedObjectData::IsPlatinum::No);
    }
    if (company.HasGeo()) {
        data.setPosition(poi_feed::FeedObjectData::Position {
            company.GetGeo().GetLocation().GetPos().GetLon(),
            company.GetGeo().GetLocation().GetPos().GetLat()
        });
    }
    std::set<std::string> langs;
    for (const auto& name : company.GetName()) {
        const auto lang = langName(name.GetLang());
        if (!supportedNmapsLangs.count(lang)) {
            WARN() << "Unsupported lang: " << lang << " Permalink: " << permalink;
            continue;
        }
        if (langs.count(lang)) {
            continue;
        }
        langs.insert(lang);
        data.addName({lang, name.GetValue()});
    }
    std::set<std::string> langsShort;
    for (const auto& name : company.GetShortName()) {
        const auto langShort = langName(name.GetLang());
        if (!supportedNmapsLangs.count(langShort)) {
            WARN() << "Unsupported lang: " << langShort << " Permalink: " << permalink;
            continue;
        }
        if (langsShort.count(langShort)) {
            continue;
        }
        langsShort.insert(langShort);
        data.addShortName({langShort, name.GetValue()});
    }
    for (const auto& rubric : company.GetRubric()) {
        if (rubric.HasIsMain() && rubric.GetIsMain()) {
            data.setRubricId(rubric.GetId());
            break;
        }
    }
    for (const auto& tag : company.GetTag()) {
        if (tag.GetId() == TAG_HAS_VERIFIED_OWNER) {
            data.setHasOwner(poi_feed::FeedObjectData::HasOwner::Yes);
        }
    }
    return data;
}

NYT::TTableReaderPtr<NYT::TNode>
createYTReader(NYT::IClientPtr& client, const std::string& url, size_t limit)
{
    return
        limit == 0
            ? client->CreateTableReader<NYT::TNode>(TString(url))
            : client->CreateTableReader<NYT::TNode>(TRichYPath(TString(url)).AddRange(
                    TReadRange()
                        .LowerLimit(TReadLimit().RowIndex(0))
                        .UpperLimit(TReadLimit().RowIndex(limit))));
}
} // namespace


chrono::TimePoint
altayDataDateISO(const std::string& nyakMappingYtUrl)
{
    auto client = common::yt::createYtClient(YT_REALM);
    return
        maps::chrono::parseIsoDateTime(
            client->Get(TString(nyakMappingYtUrl + "/@modification_time"s)).AsString());
}

poi_feed::FeedObjectDataVector
readAltayCompanies(
    const std::string& nyakMappingYtUrl,
    const std::unordered_set<std::string>& supportedNmapsLangs,
    size_t limit/* = 0*/)
{
    NYT::JoblessInitialize();
    auto client = common::yt::createYtClient(YT_REALM);
    auto rowCount = client->Get(TString(nyakMappingYtUrl + "/@row_count"s)).AsInt64();
    auto reader = createYTReader(client, nyakMappingYtUrl, limit);
    poi_feed::FeedObjectDataVector exportedCompanies;
    exportedCompanies.reserve(rowCount);
    std::unordered_set<TString> originalIds;
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& row = reader->GetRow();
        const auto originalId = row["original_id"].AsString();
        if (originalIds.count(originalId)) {
            WARN() << "Skipping duplicate original_id: " << originalId;
            continue;
        }
        if (row["permalink"].IsNull() || row["revision"].IsNull() ||
            row["export_proto"].IsNull() ||
            row["revision"].IntCast<uint64_t>() == 0)
        {
            WARN() << "Skipping incomplete record original_id: " << originalId;
            continue;
        }
        NSpravExport::TExportedCompany exportProto;
        Y_PROTOBUF_SUPPRESS_NODISCARD exportProto.ParseFromString(row["export_proto"].AsString());
        if (!exportProto.HasGeo()) {
            WARN() << "Skipping original_id without geometry: " << originalId;
            continue;
        }
        exportedCompanies.emplace_back(
            createFeedObjectData(
                exportProto,
                originalId,
                row["permalink"].IntCast<poi_feed::PermalinkId>(),
                row["revision"].IntCast<uint64_t>(),
                supportedNmapsLangs));
        originalIds.insert(originalId);
    }
    return exportedCompanies;
}

IndoorCandidatesData::IndoorCandidatesData(
    const std::string& nyakIndoorCandidatesYTURL,
    const EditorCfg& editorCfg,
    const std::unordered_set<std::string>& supportedNmapsLangs,
    size_t limit/* = 0*/)
{
    NYT::JoblessInitialize();
    auto client = common::yt::createYtClient(YT_REALM);
    auto rowCount = client->Get(TString(nyakIndoorCandidatesYTURL + "/@row_count"s)).AsInt64();
    auto reader = createYTReader(client, nyakIndoorCandidatesYTURL, limit);
    candidates.reserve(rowCount);
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& row = reader->GetRow();
        if (row["permalink"].IsNull() ||
            row["export_proto"].IsNull())
        {
            continue;
        }
        NSpravExport::TExportedCompany exportProto;
        Y_PROTOBUF_SUPPRESS_NODISCARD exportProto.ParseFromString(row["export_proto"].AsString());
        if (!exportProto.HasGeo()) {
            WARN() << "Skipping indoor candidate without geometry: "
                << row["permalink"].IntCast<poi_feed::PermalinkId>();
            continue;
        }
        if (exportProto.GetUrl().empty() &&
                !exportProto.HasIsPlatinum() &&
                !exportProto.HasIsAdvert()) {
            WARN() << "Skipping indoor candidate without URL/Platinum/Advert: "
                << row["permalink"].IntCast<poi_feed::PermalinkId>();
            continue;
        }

        auto isNameEqualsToRubricName = false;
        for (const auto& name : exportProto.GetName()) {
            if (langName(name.GetLang()) == "ru" &&
                isRubricName({langName(name.GetLang()), name.GetValue()}, editorCfg)) {
                    isNameEqualsToRubricName = true;
                    break;
            }
        }
        for (const auto& shortName : exportProto.GetShortName()) {
            if (langName(shortName.GetLang()) == "ru" &&
                isRubricName({langName(shortName.GetLang()), shortName.GetValue()}, editorCfg)) {
                    isNameEqualsToRubricName = true;
                    break;
            }
        }
        if (isNameEqualsToRubricName) {
            WARN() << "Skipping indoor candidate with name equals to rubric name: "
                << row["permalink"].IntCast<poi_feed::PermalinkId>();
            continue;
        }
        const auto permalinkId = row["permalink"].IntCast<poi_feed::PermalinkId>();
        const auto locatedAt = row["located_at"].IntCast<poi_feed::PermalinkId>();
        candidatePermalinkToLocatedAt
            .emplace(permalinkId, locatedAt);
        candidates.emplace_back(
            createFeedObjectData(
                exportProto,
                {},
                permalinkId,
                0,
                supportedNmapsLangs));
    }
}

std::vector<poi_feed::UnknownMappingData>
readNyakMappingUnknown(const std::string& nyakMappingUnknownYTURL)
{
    NYT::JoblessInitialize();
    auto client = common::yt::createYtClient(YT_REALM);
    auto rowCount = client->Get(TString(nyakMappingUnknownYTURL + "/@row_count"s)).AsInt64();
    auto reader = client->CreateTableReader<NYT::TNode>(TString(nyakMappingUnknownYTURL));
    std::vector<poi_feed::UnknownMappingData> unknownCompanies;
    unknownCompanies.reserve(rowCount);
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& row = reader->GetRow();
        const auto originalId = row["original_id"].AsString();
        unknownCompanies.push_back({
            boost::lexical_cast<poi_feed::ObjectId>(row["original_id"].AsString()),
            row["permalink"].IntCast<poi_feed::PermalinkId>()
        });
    }
    return unknownCompanies;
}

NyakMappingLocations
getNyakExportLocation() try
{
    NYT::JoblessInitialize();
    auto ytclient = common::yt::createYtClient(YT_REALM);
    auto node = ytclient->Get(GEOSEARCH_SNAPSHOT_ATTR_PATH);
    const std::string snapshotPath = node.AsString();
    REQUIRE(snapshotPath.ends_with(SNAPSHOT_FOLDER),
        "Expected path should end with " + SNAPSHOT_FOLDER);
    const auto statePath =
        snapshotPath.substr(0, snapshotPath.length() - SNAPSHOT_FOLDER.length());
    return {
        .mainUrl = statePath + MAPPING_FOLDER,
        .unknownUrl = statePath + MAPPING_UNKNOWN_FOLDER,
        .nyakIndoorCandidatesYTURL = statePath + INDOOR_COORDINATES_FOLDER
    };
} catch (const std::exception& ex) {
    ERROR() << "Exception when trying to obtain nyak_export location: " << ex.what();
    return {};
}


std::set<poi_feed::PermalinkId>
readGeoproductPermalinks()
{
    NYT::JoblessInitialize();
    auto client = common::yt::createYtClient(YT_REALM);
    auto reader = createYTReader(client, GEOPRODUCT_IDS_URL, 0);
    std::set<poi_feed::PermalinkId> result;
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& row = reader->GetRow();
        if (row["is_protected"].As<bool>()) {
            result.insert(row["permanent_id"].IntCast<poi_feed::PermalinkId>());
        }
    }
    INFO() << "Unique geoproduct permalinks count: " << result.size();
    return result;
}
} // namespace maps::wiki::merge_poi
