#include "cypress.h"
#include <saas/library/rtyt/lib/cypress/tree.pb.h>
#include <library/cpp/yson/node/node_io.h>

#include <google/protobuf/descriptor.pb.h>
#include <mapreduce/yt/library/cypress_path/cypress_path.h>

using namespace NRTYT;

namespace {
    TVector<TString> ParsePathToTokens(const NYT::TYPath& path) {
        NYT::TCypressPath currentNode(path);
        TVector<TString> nodesByPath;
        while (currentNode.GetParent() != currentNode) {
            nodesByPath.push_back(currentNode.GetBasename().GetPath());
            currentNode = currentNode.GetParent();
        }
        Reverse(nodesByPath.begin(), nodesByPath.end());
        return nodesByPath;
    }
    template <class T>
    TString JoinPath(T begin, T end) {
        NYT::TCypressPath currentNode;
        while (begin != end) {
            currentNode /= *begin;
            begin++;
        }
        return currentNode.GetPath();
    }
}


void TCypressClient::Save(NTree::TTree* treeProto, const NYT::TYPath& pathPrefix, TCypressNodePtr node) {
    if (node->IsMountPoint() && node != Root) {
        return; // do not save mounted clients
    }
    auto newNode = treeProto->MutableNodes()->Add();
    node->Save(newNode, pathPrefix);
    if (node->GetType() == NYT::ENodeType::NT_MAP) {
        auto newPrefix = pathPrefix + node->GetName();

        for (NYT::TNodeId id : node->ListChildren()) {
            Save(treeProto, newPrefix, NodeTree[id].Get());
        }
    }
}


void TCypressClient::Sync(TMaybe<NYT::TNodeId> mountId, bool needFlush) {
    if (mountId.Empty()) {
        Y_ENSURE(StorageRoot.Defined(), "trying to save memory-only cypress");
        TFsPath path = StorageRoot.GetRef() / MetaFileName;
        TFile file(path.GetPath(), CreateAlways | WrOnly);
        NTree::TTree proto;
        Save(&proto, "/", Root);
        Y_PROTOBUF_SUPPRESS_NODISCARD proto.SerializeToFileDescriptor(file.GetHandle());
        if (needFlush) {
            file.Flush();
        }
    } else {
        NYT::TNodeId id = mountId.GetRef();
        Y_ENSURE(MountPoints.contains(id), "No such mount point: " << GetGuidAsString(id));
        MountPoints[id]->Sync(Nothing(), needFlush);
    }
}


void TCypressClient::Mount(
            const TFsPath& cypressStorageRoot,
            const NYT::TYPath& mountPoint,
            bool failIfNotExist) {
    NYT::TCypressPath path(mountPoint);
    TCypressNodePtr parentNode = nullptr;
    Y_ENSURE(GetNode(path.GetParent().GetPath(), parentNode), "While mount " << 
                                    mountPoint << ": parent node does not exist");

    Y_ENSURE((parentNode->GetType() == NYT::ENodeType::NT_MAP), "While mount " <<
                                    mountPoint << ": parent node is not a map_node");

    Y_ENSURE(!parentNode->HasChild(path.GetBasename()), "While mount " <<
                                    mountPoint << ": destination node already exists");

    THolder<TCypressClient> newClient(new TCypressClient(cypressStorageRoot, failIfNotExist));
    
    NYT::TNodeId id = newClient->Root->GetId();
    THolder<TCypressNode> mountNode = MakeHolder<TCypressNode>(id, path.GetBasename(), newClient.Get());
    parentNode->AddChild(mountNode.Get());
    MountPoints[id] = std::move(newClient);
    NodeTree[id] = std::move(mountNode);
}

void TCypressClient::Umount(NYT::TNodeId mountedCypressId) {
    TCypressNodePtr node = NodeTree.at(mountedCypressId).Get();
    Y_ENSURE(node->IsMountPoint(), "Trying to umount node " << GetGuidAsString(mountedCypressId) << " but it is not a mount point");
    Y_ENSURE(MountPoints.contains(mountedCypressId), "can't find mount point with id " << GetGuidAsString(mountedCypressId));

    MountPoints[mountedCypressId]->Sync();

    auto parent = node->GetParent();
    Y_ENSURE(parent, "somewhat this node (" << node->GetName() << "; " << 
                        GetGuidAsString(mountedCypressId) <<
                        ") has no parent, are you trying to unmount root cypress?");
    parent->RemoveChild(node->GetName());
    NodeTree.erase(mountedCypressId);
    MountPoints.erase(mountedCypressId);
}

NYT::TNodeId TCypressClient::GetNodeId(const NYT::TYPath& path) {
    TCypressNodePtr node = nullptr;
    Y_ENSURE(GetNode(path, node), "Node " << path << " does not exist");
    return node->GetId();
}

std::pair<TCypressClient*, TCypressNodePtr> TCypressClient::WalkByPath(const NYT::TYPath& path,
                std::function<bool(TCypressClient*, TCypressNodePtr, const TString&)> callback) {
    TVector<TString> dirs = ParsePathToTokens(path);
    TCypressNodePtr current = Root;
    for (size_t tokenId = 0; tokenId < dirs.size(); tokenId++) {
        TCypressNodePtr node = current;
        const TString& dir = dirs[tokenId];

        if (node->IsMountPoint()) {
            auto pathSuffix = JoinPath(dirs.begin() + tokenId, dirs.end());
            return node->AsMountedClient()->WalkByPath(pathSuffix, callback);
        }
        if (!callback(this, current, dir)) {
            return std::make_pair(this, nullptr);
        }
        current = NodeTree.at(node->GetChild(dir)).Get();
    }
    return std::make_pair(this, current);
}



void TCypressClient::CreateDirPath(
    const NYT::TCypressPath& cypressPath) {
    WalkByPath(cypressPath.GetPath(), CreateDirPathCallback);
}

THolder<TCypressNode> TCypressClient::CreateNode(const TString& name, NYT::ENodeType type, NYT::TNode attrs) {
    NYT::TNodeId id;
    CreateGuid(&id);
    Y_ENSURE(type != ENodeType::NT_TABLE || StorageRoot.Defined(), "Can't create table " << name << "; this cypress is in-memory mode");
    if (type == ENodeType::NT_TABLE) {
        attrs[StorageRootAttr] = StorageRoot->GetPath();
    }
    return MakeHolder<TCypressNode>(id, name, type, std::move(attrs));
}

THolder<TCypressNode> TCypressClient::CreateCopyNode(TCypressNodePtr node, const TString& name) {
    NYT::TNodeId id;
    CreateGuid(&id);
    return MakeHolder<TCypressNode>(id, name, node);
}


bool TCypressClient::GetNode(const NYT::TYPath& path, TCypressNodePtr& nodeToStore) {
    auto result = WalkByPath(path, CheckIfDirExistsCallback).second;
    if (result) {
        nodeToStore = result;
    }
    return result != nullptr;
}

TCypressClient* TCypressClient::GetLowestMountPoint(const NYT::TYPath& path) {
    return WalkByPath(path, CheckIfDirExistsCallback).first;
}

NYT::TNodeId TCypressClient::Create(
    const NYT::TYPath& path,
    NYT::ENodeType type,
    const NYT::TCreateOptions& options) {
    if (options.IgnoreExisting_ && options.Force_) {
        ythrow yexception() << "When creating file " << path << ": "
                            << "Cannot use options Force and IgnoreExisting simultaneously";
    }
    NYT::TCypressPath cypressPath(path);
    if (cypressPath.GetParent().GetPath() == cypressPath.GetPath()) { // Trying to create root node
        if (options.Force_) {
            ythrow yexception() << "Can't recreate root node, not implemented";
        } else if (options.IgnoreExisting_) {
            return Root->GetId();
        } else {
            ythrow yexception() << "When creating node '//' it already exists!";
        }
    }
    TCypressNodePtr parentNode = nullptr;
    if (!GetNode(cypressPath.GetParent().GetPath(), parentNode)) {
        if (!options.Recursive_) {
            ythrow yexception() << "When creating file " << path << ": "
                                << "There is no parent directory";
        }
        CreateDirPath(cypressPath.GetParent());
        GetNode(cypressPath.GetParent().GetPath(), parentNode);
    }
    auto mountedClient = GetLowestMountPoint(path);

    if (!options.IgnoreExisting_ && !options.Force_ && Exists(path)) {
        ythrow yexception() << "When creating node " << path << ": "
                            << "Node already exists!";
    }
    if (options.Force_ && Exists(path)) {
        Remove(path);
    } else if (options.IgnoreExisting_ && Exists(path)) {
        return parentNode->GetChild(cypressPath.GetBasename().GetPath());
    }
    THolder<TCypressNode> newNode =
            mountedClient->CreateNode(
                cypressPath.GetBasename().GetPath(),
                type,
                options.Attributes_.GetOrElse(NYT::TNode::CreateMap())
                );
    parentNode->AddChild(newNode.Get());
    auto id = newNode->GetId();
    mountedClient->NodeTree[id] = std::move(newNode);
    return id;
}

void TCypressClient::RemoveNode(NYT::TNodeId id) {
    if (NodeTree[id]->GetType() == ENodeType::NT_MAP) {
        for (auto child : NodeTree[id]->ListChildren()) {
            RemoveNode(child);
        }
    }
    NodeTree[id]->ClearData();
    NodeTree.erase(id);
}

void TCypressClient::Remove(
    const TYPath& path,
    const NYT::TRemoveOptions& options) {
    TCypressNodePtr node = nullptr;
    if (!GetNode(path, node)) {
        if (!options.Force_) {
            ythrow yexception() << "When deleting node " << path << ": "
                                << "Node does not exist!";
        }
        return;
    }
    if (node->GetType() == ENodeType::NT_MAP) {
        if (!options.Recursive_) {
            ythrow yexception() << "When deleting node " << path << ": "
                                << "Can't delete map_node without recursive option!";
        }
    }
    TYPath pathSuffix = path;
    auto mountedClient = GetLowestMountPoint(pathSuffix);
    TCypressNodePtr parent = node->GetParent();
    parent->RemoveChild(node->GetName());
    mountedClient->RemoveNode(node->GetId());
}

bool TCypressClient::Exists(
    const TYPath& path,
    const NYT::TExistsOptions& /* options */) {
    TCypressNodePtr node = nullptr;
    return GetNode(path, node);
}

NYT::TNode TCypressClient::Get(
    const TYPath& path,
    const NYT::TGetOptions&) {
    NYT::TCypressPath nodePath(path);
    TCypressNodePtr node = nullptr;
    TYPath parentPath = nodePath.GetParent().GetPath();
    TYPath nodeName = nodePath.GetBasename().GetPath();
    nodeName.erase(nodeName.begin()); // delete a single slash
    if (nodeName[0] == '@') {
        if (!GetNode(parentPath, node)) {
            ythrow yexception() << parentPath << " Node does not exist!";
        }
        return node->GetAttributes()[nodeName];
    }
        
    if (!GetNode(path, node)) {
        ythrow yexception() << path << " Node does not exist!";
    }
    return node->GetAttributes();
}

void TCypressClient::Set(
    const TYPath& path,
    const NYT::TNode& value,
    const NYT::TSetOptions&) {
    Y_UNUSED(path);
    Y_UNUSED(value);
}

void TCypressClient::MultisetAttributes(
    const TYPath&,
    const NYT::TNode::TMapType&,
    const NYT::TMultisetAttributesOptions&) {
    ythrow yexception() << "MultisetAttributes is not implemented";
}

NYT::TNode::TListType TCypressClient::List(
    const TYPath& path,
    const NYT::TListOptions& options) {
    Y_UNUSED(path);
    Y_UNUSED(options);
    return TNode::TListType();
}

NYT::TNodeId TCypressClient::Copy(
    const TYPath& sourcePath,
    const TYPath& destinationPath,
    const NYT::TCopyOptions& options) {
    Y_UNUSED(options);
    TCypressNodePtr node = nullptr;
    if (!GetNode(sourcePath, node)) {
        ythrow yexception() << "When copying node " << sourcePath << ": "
                            << "Node does not exist!";
    }

    Y_ENSURE(!node->IsMountPoint(), "copying of mount point is not implemented");
    TCypressNodePtr destNode = nullptr;
    if (!GetNode(NYT::TCypressPath(destinationPath).GetParent().GetPath(), destNode)) {
        ythrow yexception() << "When copying node " << sourcePath << ": "
                            << "destinationPath does not exist!";
    }
    if (destNode->GetType() != NYT::NT_MAP) {
        ythrow yexception() << "When copying node " << sourcePath << ": "
                            << "destinationPath is not map_node!";
    }
    auto actualCypress = GetLowestMountPoint(destinationPath);
    THolder<TCypressNode> newNode = actualCypress->CreateCopyNode(node, NYT::TCypressPath(destinationPath).GetBasename().GetPath());
    auto id = newNode->GetId();
    destNode->AddChild(newNode.Get());
    actualCypress->NodeTree[id] = std::move(newNode);
    return id;
}

NYT::TNodeId TCypressClient::Move(
    const TYPath& sourcePath,
    const TYPath& destinationPath,
    const NYT::TMoveOptions& options) {
    Y_UNUSED(options);
    TCypressNodePtr node = nullptr;
    TCypressNodePtr parentNode = nullptr;
    Y_ENSURE(GetNode(sourcePath, node), "While move " << sourcePath <<
                        " to " << destinationPath << ": Source node does not exist");

    Y_ENSURE(!node->IsMountPoint(), "While move " << sourcePath <<
                        " to " << destinationPath << ": Can't move mountpoint");
    parentNode = node->GetParent();

    auto id = Copy(sourcePath, destinationPath);
    parentNode->RemoveChild(node->GetName());
    NodeTree.erase(node->GetId());
    return id;
}

NYT::TNodeId TCypressClient::Link(
    const TYPath&,
    const TYPath&,
    const NYT::TLinkOptions&) {
    ythrow yexception() << "Link: Not implemented";
}

void TCypressClient::Concatenate(
    const TVector<NYT::TRichYPath>&,
    const NYT::TRichYPath&,
    const NYT::TConcatenateOptions&) {
    ythrow yexception() << "Concatenate: Not implemented";
}

NYT::TRichYPath TCypressClient::CanonizeYPath(const NYT::TRichYPath& path) {
    return path;
}

TVector<NYT::TTableColumnarStatistics> TCypressClient::GetTableColumnarStatistics(
    const TVector<NYT::TRichYPath>& paths,
    const NYT::TGetTableColumnarStatisticsOptions&) {
    return TVector<NYT::TTableColumnarStatistics>(paths.size());
}

// Get a file with given md5 from Cypress file cache located at 'cachePath'.
TMaybe<NYT::TYPath> TCypressClient::GetFileFromCache(
    const TString&,
    const NYT::TYPath&,
    const NYT::TGetFileFromCacheOptions&) {
    ythrow yexception() << "GetFileFromCache: Not implemented";
}
// Put a file 'filePath' to Cypress file cache located at 'cachePath'.
// The file must have "md5" attribute and 'md5Signature' must match its value.
NYT::TYPath TCypressClient::PutFileToCache(
    const NYT::TYPath&,
    const TString&,
    const NYT::TYPath&,
    const NYT::TPutFileToCacheOptions&) {
    ythrow yexception() << "PutFileToCache: Not implemented";
}

TString TCypressClient::GetTableStorage(const NYT::TYPath& tablePath) {
    TCypressNodePtr tableNode;
    if (!GetNode(tablePath, tableNode)) {
        ythrow yexception() << "There is no such table: " << tablePath;
    }
    if (tableNode->GetType() != NYT::ENodeType::NT_TABLE) {
        ythrow yexception() << "Node " << tablePath << "is not a table; it's " << tableNode->GetType();
    }
    auto result = TFsPath(tableNode->GetAttributes().ChildAsString(StorageRootAttr)) / tableNode->AsTable().DataPath;
    return std::move(result);
}

const ::google::protobuf::Descriptor* TCypressClient::RestoreProtoFromAttrs(const NYT::TYPath& tablePath) {
    TCypressNodePtr tableNode;
    if (!GetNode(tablePath, tableNode)) {
        ythrow yexception() << "There is no such table: " << tablePath;
    }
    if (tableNode->GetType() != NYT::ENodeType::NT_TABLE) {
        ythrow yexception() << "Node " << tablePath << "is not a table; it's " << tableNode->GetType();
    }
    if (!tableNode->AsTable().DescriptorPool.Get()) {
        tableNode->AsTable().DescriptorPool = new ::google::protobuf::DescriptorPool();
        tableNode->AsTable().DescriptorPool->AllowUnknownDependencies();
        TString serialization;
        try {
            serialization = tableNode->GetAttributes().ChildAsString("@_rtyt_file_descriptor");
        } catch (...) {
            ythrow yexception() << "Failed while restoring proto for node " << tablePath <<
                                " disk path: " << GetTableStorage(tablePath) <<
                                " " << CurrentExceptionMessage();
        }
        ::google::protobuf::FileDescriptorProto fileProto;
        Y_PROTOBUF_SUPPRESS_NODISCARD fileProto.ParseFromString(serialization);
        tableNode->AsTable().DescriptorPool->BuildFile(fileProto);
    }
    auto messageName = tableNode->GetAttributes().ChildAsString("@_rtyt_message_name");
    auto Pool = tableNode->AsTable().DescriptorPool.Get();
    return Pool->FindMessageTypeByName(messageName);
}

TCypressNode* TCypressClient::GetRoot() {
    return Root;
}


