#pragma once

#include "cypress_node.h"
#include <google/protobuf/descriptor.h>
#include <mapreduce/yt/interface/cypress.h>
#include <mapreduce/yt/library/cypress_path/cypress_path.h>
namespace NRTYT {

class TCypressClient: public NYT::ICypressClient, public TAtomicRefCount<TCypressClient> {
    static constexpr const char* MetaFileName = "cypress_meta";

    using TYPath = NYT::TYPath;
    using TRichYPath = NYT::TRichYPath;
    using TNode = NYT::TNode;
    using ENodeType = NYT::ENodeType;
    using TNodeId = NYT::TNodeId;
    
    TMaybe<TFsPath> StorageRoot;
    THashMap<TNodeId, THolder<TCypressNode>> NodeTree;
    THashMap<TNodeId, THolder<TCypressClient>> MountPoints;

    TCypressNodePtr Root;

    void CreateDirPath(const NYT::TCypressPath& path);
    THolder<TCypressNode> CreateNode(const TString& name, NYT::ENodeType type, NYT::TNode attrs = NYT::TNode::CreateMap());
    THolder<TCypressNode> CreateCopyNode(TCypressNodePtr node, const TString& name);

    bool GetNode(const NYT::TYPath& path, TCypressNodePtr& nodeToStore);
    TCypressClient* GetLowestMountPoint(const NYT::TYPath& path);

    void RemoveNode(NYT::TNodeId id);
    void Save(NTree::TTree* treeProto, const NYT::TYPath& pathPrefix, TCypressNodePtr node);

protected:
    friend class TCypressNode;
    TCypressNode* GetRoot();

public:
    TCypressClient(TMaybe<TFsPath> storageRoot = Nothing(), bool failIfNotExist = false)
    : StorageRoot(std::move(storageRoot)) {
        NYT::TNodeId rootId;
        CreateGuid(&rootId);
        THolder<TCypressNode> root = CreateNode("", NYT::NT_MAP);
        Root = root.Get();
        NodeTree[rootId] = std::move(root);

        if (StorageRoot.Defined()) {
            TFsPath metaFilePath = StorageRoot.GetRef() / MetaFileName;
            Y_ENSURE(!failIfNotExist || metaFilePath.Exists(), 
                    "There is no cypress by such path: " << StorageRoot);
            StorageRoot->MkDirs();
            if (metaFilePath.Exists()) {
                TFile file(metaFilePath.GetPath(), RdOnly);
                NTree::TTree proto;
                Y_PROTOBUF_SUPPRESS_NODISCARD proto.ParseFromFileDescriptor(file.GetHandle());
                auto nodes = proto.GetNodes();
                for (const auto& node : nodes) {
                    NYT::TCypressPath path(node.GetPath());
                    NYT::ENodeType type = FromString<NYT::ENodeType>(node.GetType());
                    NYT::TNode attrs = NYT::NodeFromYsonString(node.GetAttributes());
                    Create(path.GetPath(), type, 
                            NYT::TCreateOptions()
                                .Recursive(true)
                                .IgnoreExisting(true)
                                .Attributes(attrs)
                    );
                }
            }
        }
    }

    TString GetTableStorage(const TYPath& tablePath);

    const ::google::protobuf::Descriptor* RestoreProtoFromAttrs(const NYT::TYPath& tablePath);

    void Sync(TMaybe<NYT::TNodeId> mountedCypressId = Nothing(), bool needFlush = true);

    void Mount(
        const TFsPath& cypressStorageRoot,
        const NYT::TYPath& mountPoint,
        bool failIfNotExist = false);

    void Umount(NYT::TNodeId mountedCypressId);
    NYT::TNodeId GetNodeId(const NYT::TYPath& path);

public: // NYT::ICypressClient
    virtual TNodeId Create(
        const TYPath& path,
        ENodeType type,
        const NYT::TCreateOptions& options = NYT::TCreateOptions()) override;

    virtual void Remove(
        const TYPath& path,
        const NYT::TRemoveOptions& options = NYT::TRemoveOptions()) override;

    virtual bool Exists(
        const TYPath& path,
        const NYT::TExistsOptions& options = NYT::TExistsOptions()) override;

    virtual TNode Get(
        const TYPath& path,
        const NYT::TGetOptions& options = NYT::TGetOptions()) override;

    virtual void Set(
        const TYPath& path,
        const TNode& value,
        const NYT::TSetOptions& options = NYT::TSetOptions()) override;

    virtual void MultisetAttributes(
        const TYPath& path,
        const TNode::TMapType& value,
        const NYT::TMultisetAttributesOptions& options = NYT::TMultisetAttributesOptions()) override;

    virtual TNode::TListType List(
        const TYPath& path,
        const NYT::TListOptions& options = NYT::TListOptions()) override;

    virtual TNodeId Copy(
        const TYPath& sourcePath,
        const TYPath& destinationPath,
        const NYT::TCopyOptions& options = NYT::TCopyOptions()) override;

    virtual TNodeId Move(
        const TYPath& sourcePath,
        const TYPath& destinationPath,
        const NYT::TMoveOptions& options = NYT::TMoveOptions()) override;

    virtual TNodeId Link(
        const TYPath& targetPath,
        const TYPath& linkPath,
        const NYT::TLinkOptions& options = NYT::TLinkOptions()) override;

    virtual void Concatenate(
        const TVector<TRichYPath>& sourcePaths,
        const TRichYPath& destinationPath,
        const NYT::TConcatenateOptions& options = NYT::TConcatenateOptions()) override;

    virtual NYT::TRichYPath CanonizeYPath(const NYT::TRichYPath& path) override;

    virtual TVector<NYT::TTableColumnarStatistics> GetTableColumnarStatistics(
        const TVector<NYT::TRichYPath>& paths,
        const NYT::TGetTableColumnarStatisticsOptions& options) override;

    // Get a file with given md5 from Cypress file cache located at 'cachePath'.
    virtual TMaybe<TYPath> GetFileFromCache(
        const TString& md5Signature,
        const TYPath& cachePath,
        const NYT::TGetFileFromCacheOptions& options = NYT::TGetFileFromCacheOptions()) override;

    // Put a file 'filePath' to Cypress file cache located at 'cachePath'.
    // The file must have "md5" attribute and 'md5Signature' must match its value.
    virtual TYPath PutFileToCache(
        const TYPath& filePath,
        const TString& md5Signature,
        const TYPath& cachePath,
        const NYT::TPutFileToCacheOptions& options = NYT::TPutFileToCacheOptions()) override;

private:
    class IWalkFunctor {
        bool Finished = false;

    protected:
        void Finish() {
            Finished = true;
        }

    public:
        bool IsFinished() const {
            return Finished;
        }
        virtual void ProcessNode(TCypressClient* client, TCypressNodePtr node, const TString& child) = 0;
        virtual void OnExit(TCypressClient* client, TCypressNodePtr node) = 0;
    };

    std::pair<TCypressClient*, TCypressNodePtr> WalkByPath(
            const TYPath& path,
            std::function<bool(TCypressClient*, TCypressNodePtr, const TString&)> callback);

    static bool CreateDirPathCallback(TCypressClient* client, TCypressNodePtr node, const TString& child);
    static bool CheckIfDirExistsCallback(TCypressClient* client, TCypressNodePtr node, const TString& child);
};
} // namespace NRTYT
