#include "yt_publisher.h"
#include "mapreduce/yt/interface/fwd.h"

#include <saas/library/index_snapshot/snapshot_manager.h>
#include <saas/library/searchmap/parsers/parser.h>
#include <saas/library/searchmap/searchmap.h>
#include <saas/library/yt/common/yt_blob.h>

#include <library/cpp/svnversion/svnversion.h>

#include <util/string/builder.h>
#include <util/string/printf.h>

namespace {
    class TShardsSetCreator: public NSearchMapParser::ISearchMapProcessor {
    public:
        TShardsSetCreator(TPublishCommand::TShardIdSet& shardsSet)
            : ShardsSet(shardsSet)
        {
        }

        void Do(const NSearchMapParser::TServiceSpecificOptions& /*options*/,
                        const NSearchMapParser::TReplicaSpecificOptions& /*rso*/,
                        const TInterval<TShardIndex>& /*interval*/,
                        const NSearchMapParser::TSearchInformation& si
        ) override {
            ShardsSet.insert(si.Shards);
        }

        const TPublishCommand::TShardIdSet& Get() const {
            return ShardsSet;
        }

    private:
        TPublishCommand::TShardIdSet &ShardsSet;
    };
    struct TSegmentKeyColumns {
        ui32 ShardMin;
        ui32 ShardMax;
        TMaybe<ui32> SegmentId;
        TSegmentKeyColumns() = default;
        TSegmentKeyColumns(ui32 shardMin, ui32 shardMax)
            : ShardMin(shardMin)
            , ShardMax(shardMax)
            {
        }

        TSegmentKeyColumns(ui32 shardMin, ui32 shardMax, ui32 segmentId)
            : ShardMin(shardMin)
            , ShardMax(shardMax)
            , SegmentId(segmentId)
            {
        }

        TSegmentKeyColumns(const NYT::TNode::TListType& keysList)
            : ShardMin(keysList[0].AsInt64())
            , ShardMax(keysList[1].AsInt64())
            {
            if (keysList.size() > 2) {
                SegmentId = keysList[2].AsInt64();
            }
        }
        bool operator==(const TSegmentKeyColumns& other) const {
            return ShardMin == other.ShardMin && ShardMax == other.ShardMax && SegmentId == other.SegmentId;
        }
    };

    TString GetShardResourceTitle(const NRTYServer::TShardResource& resource, const TString& serviceName, bool isDelta) {
        TStringBuilder builder;
        builder << "standalone_indexer_" << serviceName << "_"
                << resource.GetTimestamp() << "_"
                << resource.GetShardMin() << "_"
                << resource.GetShardMax() << "_";
        if (isDelta) {
            builder << "delta#" << resource.GetSegmentId() << "_" << resource.GetTimestampEx();
        } else {
            builder << "full";
        }
        return builder;
    }
}

template <>
struct THash<TSegmentKeyColumns> {
    size_t operator()(const TSegmentKeyColumns& key) const {
        std::tuple<ui32, ui32, ui32> tupleToHash(key.ShardMin, key.ShardMax, key.SegmentId.GetOrElse(Max<ui32>()));
        return THash<std::tuple<ui32, ui32, ui32>>()(tupleToHash);
    }
};

TPublishCommand::TPublishCommand(TInputs inputs, TOutputs outputs, bool verbose, NSaas::TYTLaunchReport& report, TContext context)
        : TYTCommand(std::move(inputs), std::move(outputs), "TPublishCommand", verbose, report, {})
        , Context(std::move(context))
{
}

TPublishCommand::~TPublishCommand() = default;

void TPublishCommand::DoPrepareSpec() {
    THolder<NSearchMapParser::ISearchMapParser> parser = NSearchMapParser::OpenSearchMap(Context.SearchMapText, {Context.ServiceName});
    THolder<NSearchMapParser::TSearchMap> SearchMap(new NSearchMapParser::TSearchMap(parser->GetSearchMap()));
    auto serviceIt = SearchMap->GetServiceMap().find(Context.ServiceName);
    Y_ENSURE(serviceIt != SearchMap->GetServiceMap().end(), "Cannot find " + Context.ServiceName + " in searchmap");

    const NSearchMapParser::TSearchCluster* Service = &serviceIt->second;
    TShardsSetCreator ssc(ShardsSet);
    Service->ProcessSearchMap(ssc);
    Y_ENSURE(!ShardsSet.empty(), "Empty list of shards in searchmap for service " + Context.ServiceName);

    SnapshotManager.Reset(IIndexSnapshotManager::TFactory::Construct(Context.SnapshotManager, Context.SnapshotManagerContext));
    Y_ENSURE(SnapshotManager, "Cannot construct SnapshotManager: " + Context.SnapshotManager.Quote());

    Y_ENSURE(Inputs.size() <= 2, "No more than two paths should be provided as input: table with index and sorted shard resources"); // do we need old publish?
}

void TPublishCommand::DoRun(NYT::IClientBase* client) {
    TStringBuilder comment;
    for (ui32 i = 0; i < Context.SrcDocs.size(); i++) {
        NYT::TYPath srcPath = Context.SrcDocs[i].Path_;
        i64 srcRowCount = client->Get(srcPath + "/@row_count").AsInt64();
        TString srcMTime = client->Get(srcPath + "/@modification_time").AsString();
        comment << "SrcTable #" << i << ": " << srcPath.Quote() << ", " << srcRowCount << " rows, " << srcMTime << Endl;
    }
    NYT::TYPath indexPath = Inputs[0].Path_;
    i64 indexRowCount = client->Get(indexPath + "/@row_count").AsInt64();
    TString indexMTime = client->Get(indexPath + "/@modification_time").AsString();
    bool isDelta = false;
    if (client->Exists(indexPath + "/@is_delta") && client->Get(indexPath + "/@is_delta").AsBool()) {
        isDelta = true;
    }
    comment << "IndexTable: " << indexPath.Quote() << ", " << indexRowCount << " rows, " << indexMTime << Endl;
    comment << "CommandLine: " << Context.CommandLine.Quote() << Endl;
    comment << "Version: " << GetProgramSvnVersion() << Endl;
    IIndexSnapshotManager::TSnapshot snapshot;
    snapshot.Timestamp = Context.Timestamp;
    snapshot.Shards.SetComment(comment);
    Report.AddComment(comment);

    const TString namePrefix = Sprintf("standalone_indexer_%s_%lu", Context.ServiceName.data(), Context.Timestamp.Seconds());
    THashMap<TSegmentKeyColumns, TString> rbtorrents;
    if (Context.EnableSkyShare) {
        auto keyColumns = ShardIdKeyColumns;
        if (isDelta) {
            keyColumns.Add("segment_id");
        }
        NYT::TNode::TListType torrentList = client->GetParentClient()->SkyShareTable(std::vector<NYT::TYPath>{indexPath}, NYT::TSkyShareTableOptions().KeyColumns(keyColumns).EnableFastbone(true));
        for (const NYT::TNode& shardTorrent : torrentList) {
            TSegmentKeyColumns key(shardTorrent["key"].AsList());
            rbtorrents[key] = shardTorrent.ChildAsString("rbtorrent");
        }

    }
    if (Inputs.size() < 2 || !client->Exists(Inputs[1].Path_)) {
        // Fall back to legacy publish
        Y_VERIFY(!isDelta, "Missing table for delta publishing");
        for (auto&& shard : ShardsSet) {
            auto shardResource = snapshot.Shards.AddShard();
            shardResource->SetName(Sprintf("%s_%u-%u", namePrefix.data(), shard.GetMin(), shard.GetMax()));
            shardResource->SetTimestamp(Context.Timestamp.Seconds());
            shardResource->SetShardMin(shard.GetMin());
            shardResource->SetShardMax(shard.GetMax());
            shardResource->SetYTIndexTable(Sprintf("%s[(%uu,%uu)]", indexPath.data(), shard.GetMin(), shard.GetMax()));
            if (Context.EnableSkyShare) {
                shardResource->SetTorrent(rbtorrents[TSegmentKeyColumns(shard.GetMin(), shard.GetMax())]);
            }

        }
    } else {
        NYT::TYPath resourcePath = Inputs[1].Path_;
        TMap<NUtil::TInterval<ui32>, ui32> shardIntervalToSegmentId;
        NYT::TTableReaderPtr<NRTYServer::TShardResource> reader = client->CreateTableReader<NRTYServer::TShardResource>(resourcePath);
        for (; reader->IsValid(); reader->Next()) {
            const auto& row = reader->GetRow();
            NUtil::TInterval<ui32> currentInterval(row.GetShardMin(), row.GetShardMax());
            auto shardResource = snapshot.Shards.AddShard();
            shardResource->SetShardMin(row.GetShardMin());
            shardResource->SetShardMax(row.GetShardMax());
            ui32& currentSegmentId = shardIntervalToSegmentId[currentInterval];
            if (row.HasSegmentId()) {
                currentSegmentId = row.GetSegmentId();
                shardResource->SetSegmentId(currentSegmentId);
            }
            shardResource->SetName(GetShardResourceTitle(row, Context.ServiceName, isDelta));
            if (isDelta) {
                shardResource->SetTimestamp(row.GetTimestamp());
                shardResource->SetTimestampEx(row.GetTimestampEx());
                shardResource->SetYTIndexTable(Sprintf("%s[(%luu,%luu,%uu)]", indexPath.data(), row.GetShardMin(), row.GetShardMax(), currentSegmentId));
                if (Context.EnableSkyShare) {
                    shardResource->SetTorrent(rbtorrents[TSegmentKeyColumns(row.GetShardMin(), row.GetShardMax(), currentSegmentId)]);
                }
                currentSegmentId++;
            } else {
                shardResource->SetTimestamp(Context.Timestamp.Seconds());
                if (Context.EnableSkyShare) {
                    shardResource->SetTorrent(rbtorrents[TSegmentKeyColumns(row.GetShardMin(), row.GetShardMax())]);
                }
                shardResource->SetYTIndexTable(Sprintf("%s[(%luu,%luu)]", indexPath.data(), row.GetShardMin(), row.GetShardMax()));
            }
        }
    }
    SnapshotManager->PublishSnapshot(snapshot);
}
