#pragma once

#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/strings.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/mpro/include/region.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/factory/include/release.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/yt_utils/include/rows_count.h>

#include <maps/libs/chrono/include/time_point.h>

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

#include <mapreduce/yt/util/temp_table.h>
#include <mapreduce/yt/util/ypath_join.h>
#include <mapreduce/yt/interface/client.h>

#include <set>
#include <optional>
#include <unordered_map>

namespace maps::wiki::autocart::pipeline {

struct ProcessedIssue {
    uint64_t issueId;
    chrono::TimePoint date;
};

struct Execution {
    BldRecognitionRegion region;
    uint64_t issueId;
    chrono::TimePoint date;
};

class YTStorageClient {
public:
    YTStorageClient(NYT::IClientPtr client, const TString& storagePath);

    // Satellite releases

    bool hasReleases() const;

    // throw exception if there is no releases
    Release getLastRelease() const;

    void addRelease(const Release& release, const ReleaseAttrs& attrs);

    std::map<Release, ReleaseAttrs> getAllReleasesAttrs() const;

    // Execution history

    void saveExecution(const BldRecognitionRegion& region, uint64_t issueId);

    std::vector<Execution> getExecutionHistory() const;

    // Satellite coverage

    void updateCoverage(const ReleasesCoverage& coverage);

    geolib3::MultiPolygon2 updateCoverage(
        const std::map<Release, ReleaseAttrs>& releaseToAttrs,
        size_t zoom);

    std::optional<ReleasesCoverage> getCoverageAtZoom(int zoom) const;

    // AOI and issueId map methods

    std::unordered_map<TString, ProcessedIssue> getRegionToIssueMap() const;

    std::optional<ProcessedIssue> getIssue(const TString& region) const;

    void updateIssue(const TString& region, uint64_t issueId);

    // Results methods

    template <typename Result>
    bool isResultsExists(const TString& region, uint64_t issueId) const
    {
        NYT::ITransactionPtr txn = client_->StartTransaction();
        NYT::ILockPtr lock = txn->Lock(
            resultsTablePath<Result>(),
            NYT::ELockMode::LM_SNAPSHOT,
            NYT::TLockOptions().Waitable(true)
        );
        lock->Wait(waitLockDuration_);
        NYT::TNodeId nodeId = lock->GetLockedNodeId();
        TString nodePath = "#" + GetGuidAsString(nodeId);
        return isResultsExists(txn, nodePath, region, issueId);
    }

    template <typename Result>
    void getResults(
        const TString& region, uint64_t issueId, std::vector<Result>* results) const
    {
        NYT::ITransactionPtr txn = client_->StartTransaction();
        NYT::ILockPtr lock = txn->Lock(
            resultsTablePath<Result>(),
            NYT::ELockMode::LM_SNAPSHOT,
            NYT::TLockOptions().Waitable(true)
        );
        lock->Wait(waitLockDuration_);
        NYT::TNodeId nodeId = lock->GetLockedNodeId();
        TString nodePath = "#" + GetGuidAsString(nodeId);
        REQUIRE(isResultsExists(txn, nodePath, region, issueId),
                Result::getName() + " results do not exist");
        NYT::TRichYPath rangePath = resultsRangePath(nodePath, region, issueId);
        NYT::TTableReaderPtr<NYT::TNode> reader
            = txn->CreateTableReader<NYT::TNode>(rangePath);
        for (; reader->IsValid(); reader->Next()) {
            const NYT::TNode& row = reader->GetRow();
            results->push_back(Result::fromYTNode(row));
        }
    }

    template <typename Result>
    size_t getResultsCount() const {
        NYT::ITransactionPtr txn = client_->StartTransaction();
        NYT::ILockPtr lock = txn->Lock(
            resultsTablePath<Result>(),
            NYT::ELockMode::LM_SNAPSHOT,
            NYT::TLockOptions().Waitable(true)
        );
        lock->Wait(waitLockDuration_);
        NYT::TNodeId nodeId = lock->GetLockedNodeId();
        TString nodePath = "#" + GetGuidAsString(nodeId);
        return getRowsCount(txn, nodePath);
    }

    template <typename Result>
    void saveResults(
        const TString& region, uint64_t issueId, const std::vector<Result>& results) const
    {
        NYT::ITransactionPtr txn = client_->StartTransaction();
        NYT::ILockPtr lock = txn->Lock(
            resultsTablePath<Result>(),
            NYT::ELockMode::LM_EXCLUSIVE,
            NYT::TLockOptions().Waitable(true)
        );
        lock->Wait(waitLockDuration_);
        REQUIRE(!isResultsExists(txn, resultsTablePath<Result>(), region, issueId),
                Result::getName() + " results is already exists");
        NYT::TTempTable tmpTable(txn);
        NYT::TTableWriterPtr<NYT::TNode> writer = txn->CreateTableWriter<NYT::TNode>(
            NYT::TRichYPath(tmpTable.Name()).Schema(Result::getTableSchema())
        );
        std::set<uint64_t> usedIds;
        TString dumpDate = TString(chrono::formatIsoDateTime(chrono::TimePoint::clock::now()));
        for (const Result& result : results) {
            REQUIRE(usedIds.count(result.id) == 0,
                    "There should be only one result with id " + std::to_string(result.id));
            usedIds.insert(result.id);
            NYT::TNode node = result.toYTNode();
            node[REGION] = region;
            node[ISSUE_ID] = issueId;
            node[DUMP_DATE] = dumpDate;
            writer->AddRow(node);
        }
        writer->Finish();
        txn->Merge(
            NYT::TMergeOperationSpec()
                .ForceTransform(true)
                .Mode(NYT::EMergeMode::MM_SORTED)
                .AddInput(resultsTablePath<Result>())
                .AddInput(tmpTable.Name())
                .Output(resultsTablePath<Result>())
        );
        tmpTable.Release();
        txn->Remove(tmpTable.Name(), NYT::TRemoveOptions().Recursive(true).Force(true));
        txn->Commit();
    }

    template <typename Result>
    void cloneResultsTable(NYT::IClientBasePtr client, const TString& cloneTablePath) const {
        NYT::ITransactionPtr txn = client->StartTransaction();

        NYT::ILockPtr lock = txn->Lock(
            resultsTablePath<Result>(),
            NYT::ELockMode::LM_SNAPSHOT,
            NYT::TLockOptions().Waitable(true)
        );
        lock->Wait(waitLockDuration_);
        NYT::TNodeId nodeId = lock->GetLockedNodeId();
        TString nodePath = "#" + GetGuidAsString(nodeId);
        txn->Copy(
            nodePath,
            cloneTablePath,
            NYT::TCopyOptions().Recursive(true).Force(true)
        );

        txn->Commit();
    }

private:
    template <typename Result>
    TString resultsTablePath() const {
        return NYT::JoinYPaths(storagePath_, Result::getName());
    }

    NYT::TRichYPath resultsRangePath(
        const TString& lockedNodePath,
        const TString& region, uint64_t issueId) const
    {
        NYT::TRichYPath range(lockedNodePath);
        range.AddRange(
            NYT::TReadRange().Exact(NYT::TReadLimit().Key({region, issueId}))
        );
        return range;
    }

    bool isResultsExists(
        NYT::ITransactionPtr txn,
        const TString& lockedNodePath,
        const TString& region, uint64_t issueId) const
    {
        NYT::TRichYPath path = resultsRangePath(lockedNodePath, region, issueId);
        NYT::TTableReaderPtr<NYT::TNode> reader
            = txn->CreateTableReader<NYT::TNode>(path);
        return reader->IsValid();
    }


    std::unordered_map<TString, ProcessedIssue>
    getRegionToIssueMapWithoutLock(
        NYT::ITransactionPtr txn, const TString& lockedNodePath) const;

    NYT::IClientPtr client_;
    const TString storagePath_;
    const TString issueIdTablePath_;
    const TString releaseTablePath_;
    const TString coverageTablePath_;
    const TString executionTablePath_;
    const TDuration waitLockDuration_;
};

void createYTStorage(
    NYT::ITransactionPtr txn,
    const TString& storagePath);

} // namespace maps::wiki::autocart::pipeline
