#pragma once

#include "doc_hash_iterator.h"

#include <saas/library/mapping/mapping.h>

#include <library/cpp/logger/global/global.h>
#include <library/cpp/streams/special/buffered_throttled_file.h>
#include <library/cpp/streams/special/throttle.h>

#include <util/generic/algorithm.h>
#include <util/folder/path.h>
#include <util/system/filemap.h>

namespace NSaas {
    template<class TId>
    struct TDocHashWithIdT {
        TDocHash Hash;
        TId Id;

        TDocHashWithIdT(std::pair<TDocHash, TId> hashWithId)
            : Hash(hashWithId.first)
            , Id(hashWithId.second)
        {
        }
    };

    using TDocHashWithId = TDocHashWithIdT<ui32>;

    template<class TId>
    struct IDocHashStorageT {
        virtual ~IDocHashStorageT() = default;
        virtual ui64 GetEstimatedSize() const = 0;
        virtual THolder<IDocHashIteratorT<TId>> CreateDocHashIterator() const = 0;
    };
    using IDocHashStorage = IDocHashStorageT<ui32>;


    template<class TId, const TId* NF = nullptr>
    struct THashToDocIdT : public IDocHashStorageT<TId> {
    private:
        template<const TId* Ptr>
        struct TValueOrMax {
            static constexpr TId Value = *Ptr;
        };

        template<>
        struct TValueOrMax<nullptr> {
            static constexpr TId Value = Max<TId>();
        };

    public:
        static constexpr TId NotFound = TValueOrMax<NF>::Value;
        using THashWithId = TDocHashWithIdT<TId>;
        THashToDocIdT() = default;
        THashToDocIdT(bool useGlobalMapping)
            : UseGlobalMapping(useGlobalMapping)
        {}

        bool IsOpen() const {
            return !!Data;
        }

        bool CheckPath(const TFsPath& path, ui64 expectedCount = Max<ui64>()) const {
            if (!path.Exists()) {
                NOTICE_LOG << "File " << path << " does not exist" << Endl;
                return false;
            }
            ui32 version = 0;
            ui64 hashCount = 0;
            {
                TFile file(path, OpenExisting | RdOnly);
                if (file.Read(&version, sizeof(version)) != sizeof(version) || version != CurrentVersion) {
                    ERROR_LOG << "Wrong version of " << path << Endl;
                    return false;
                }
                if (file.Read(&hashCount, sizeof(hashCount)) != sizeof(hashCount)) {
                    ERROR_LOG << "Cannot read element count from " << path << Endl;
                    return false;
                }
                if (static_cast<ui64>(file.GetLength()) != ExpectedFileSize(hashCount)) {
                    ERROR_LOG << path << " is corrupted" << Endl;
                    return false;
                }
                //TODO(yrum, SAAS-5392): restore check
                Y_UNUSED(expectedCount);
            }
            return true;
        }

        bool Open(TFsPath path) {
            if (IsOpen()) {
                Close();
            }
            CHECK_WITH_LOG(!Mapping);

            Path = std::move(path);
            if (!CheckPath(Path)) {
                return false;
            }
            Mapping = MapIndexFile(Path, UseGlobalMapping);
            if (!Mapping) {
                return false;
            }
            Data = reinterpret_cast<const THashWithId*>(Mapping->Ptr());
            Size = Mapping->MappedSize() / sizeof(THashWithId);
            return true;
        }

        static void Rebuild(const IDocHashStorageT<TId>& docHashStorage, const TFsPath& path, const TThrottle::TOptions& writeOption) {
            INFO_LOG << "Rebuilding " << path << Endl;

            TVector<THashWithId> hashes;
            hashes.reserve(docHashStorage.GetEstimatedSize());

            auto iterator = docHashStorage.CreateDocHashIterator();
            CHECK_WITH_LOG(iterator);
            for (; iterator->IsValid(); iterator->Next()) {
                hashes.emplace_back(iterator->GetHashWithId());
            }
            SortBy(hashes.begin(), hashes.end(), [](const THashWithId& v) {return v.Hash; });
            {
                TBufferedThrottledFileOutputStream file(path, writeOption);
                const ui32 version = CurrentVersion;
                ui64 size = hashes.size();
                INFO_LOG << "rebuilding with size = " << size << " And expected size " << docHashStorage.GetEstimatedSize() << Endl;
                file.Write(&version, sizeof(version));
                file.Write(&size, sizeof(size));
                if (!hashes.empty()) {
                    file.Write(&hashes.front(), sizeof(THashWithId) * hashes.size());
                }
            }
            INFO_LOG << "Rebuilt " << path << Endl;
        }

        static void Rebuild(const IDocHashStorageT<TId>& docHashStorage, const TFsPath& path, ui32 WriteSpeedBytes) {
            Rebuild(docHashStorage, path, TThrottle::TOptions::FromMaxPerSecond(WriteSpeedBytes, TDuration::Seconds(1)));
        }

        void Close() {
            if (!IsOpen()) {
                return;
            }
            CHECK_WITH_LOG(Mapping);

            Mapping.Reset();
            if (UseGlobalMapping) {
                NRTYServer::ReleaseFileMapping(Path);
            }
            Data = nullptr;
        }

        TId GetIdByHash(TDocHash hash) const {
            CHECK_WITH_LOG(Data);
            auto it = LowerBoundBy(Data, Data + Size, hash, [](const THashWithId& v) {return v.Hash; });
            if (it == Data + Size || it->Hash != hash) {
                return NotFound;
            }
            return it->Id;
        }

        std::pair<const THashWithId*, const THashWithId*> GetByHash(TDocHash hash) const {
            CHECK_WITH_LOG(Data);
            THashWithId key({ hash, NotFound });
            auto cmp = [](const THashWithId& l, const THashWithId& r) {return l.Hash.operator < (r.Hash); };
            return EqualRange(Data, Data + Size, key, cmp);
        }

        // IDocHashStorage

        ui64 GetEstimatedSize() const override {
            CHECK_WITH_LOG(Data);
            return Size;
        }


        THolder<IDocHashIteratorT<TId>> CreateDocHashIterator() const override {
            CHECK_WITH_LOG(Data);
            return THolder(new TIterator(Data, Data + Size));
        }

    private:
        class TIterator : public IDocHashIteratorT<TId> {
        private:
            const THashWithId* Position;
            const THashWithId* End;
        public:
            TIterator(const THashWithId* begin, const THashWithId* end)
                : Position(begin)
                , End(end)
            {}

            virtual bool IsValid() const override {
                return Position != End;
            }

            virtual void Next() override {
                if (IsValid())
                    ++Position;
            }

            virtual std::pair<TDocHash, TId> GetHashWithId() const override {
                CHECK_WITH_LOG(IsValid());
                return std::make_pair(Position->Hash, Position->Id);
            }
        };

    private:
        static constexpr ui64 ExpectedFileSize(ui64 hashCount) {
            return sizeof(CurrentVersion) + sizeof(hashCount) + sizeof(THashWithId) * hashCount;
        }

        static THolder<TFileMap> MapIndexFile(const TFsPath& path, bool useGlobalMapping) {
            ui32 version = 0;
            ui64 hashCount = 0;
            TFile file(path, OpenExisting | RdOnly);
            file.Read(&version, sizeof(version));
            file.Read(&hashCount, sizeof(hashCount));
            THolder<TFileMap> mapping = NRTYServer::GetFileMapping(useGlobalMapping, path);
            mapping->Map(sizeof(version) + sizeof(hashCount), sizeof(THashWithId) * hashCount);
            return mapping;
        }

    private:
        TFsPath Path;
        THolder<TFileMap> Mapping;
        const THashWithId* Data = nullptr;
        ui64 Size = 0;
        bool UseGlobalMapping = false;
        static constexpr ui32 CurrentVersion = 4;
    };
    using THashToDocId = THashToDocIdT<ui32>;
}
