#include "watchdog_opts.h"

#include <library/cpp/watchdog/lib/factory.h>
#include <library/cpp/logger/global/global.h>

#include <util/folder/path.h>
#include <util/stream/file.h>
#include <util/string/strip.h>
#include <util/string/vector.h>
#include <util/system/mutex.h>


IWatchdogOptionSubscriber::~IWatchdogOptionSubscriber() {
}


IWatchdogOptionsUpdater::~IWatchdogOptionsUpdater() {
}


IWatchdogOptions::~IWatchdogOptions() {
}

void IWatchdogOptions::Destroy(TSubscriberInfo* t) noexcept {
    if (t->GetOwner())
        t->GetOwner()->DoDestroy(t);
    else
        TDelete::Destroy(t);
}


TWatchdogOptionsCollection::TWatchdogOptionsCollection()
    : Mutex(new TMutex())
    , Updating(false)
{
}

TWatchdogOptionsCollection::~TWatchdogOptionsCollection() {
    Release();
}

void TWatchdogOptionsCollection::MergeMaps(TData &data, const TData &patch) {
    for (auto kv : patch) {
        data[kv.first] = kv.second;
    }
}

void TWatchdogOptionsCollection::DoUpdate(const TData& newData) {
    // (newData is allowed to be a reference to this->Data)
    TGuard<TMutex> g(*Mutex);
    CHECK_WITH_LOG(!Updating);

    if (Data.empty() && newData.empty() && Overrides.empty())
        return;

    // Apply realtime overrides (i.e. from controller)
    TData mergedData(newData);
    MergeMaps(mergedData, Overrides);

    // Apply changes and notify the subscribers
    TData removedKeys(Data);
    for (auto kv : mergedData) {
        removedKeys.erase(kv.first);
        DoUpdate(kv.first, kv.second);
    }

    for (auto kv : removedKeys) {
        DoUpdate(kv.first, TString());
        Data.erase(kv.first);
    }
}


void TWatchdogOptionsCollection::DoUpdate(const TString& key, const TString& value) {
    TGuard<TMutex> g(*Mutex);
    CHECK_WITH_LOG(!Updating);

    if (Data[key] != value) {
        INFO_LOG << "Watchdog option to apply: " << key << "=" << value << Endl;
        Data[key] = value;
        DoNotify(key, value);
    }
}

void TWatchdogOptionsCollection::DoNotify(const TString& key, const TString& value) {
    TGuard<TMutex> g(*Mutex);
    Updating = true;

    for (const TSubscriberInfo* s : Subscribers) {
        Y_ASSERT(!!s && !!s->Subscriber);

        if (!s->Options.empty() && s->Options.find(key) == s->Options.end())
            continue;

        if (!s->Owner)
            continue; //only happens if the subscriber was destroyed by another subscriber while we were iterating

        //Note: the subscriber is permitted to change subscription or die while handling the event
        DoNotify(*s->Subscriber, key, value);
    }

    Subscribers.remove_if([](TSubscriberInfo* sub) {
        bool hasNoOwner = !sub->Owner;
        if (hasNoOwner) {
            TDelete::Destroy(sub);
        }
        return hasNoOwner;
    });
    Updating = false;
}

void TWatchdogOptionsCollection::DoNotify(IWatchdogOptionSubscriber& subscriber, const TString& key, const TString& value) {
    try {
        subscriber.OnWatchdogOption(key, value);
    } catch(...) {
        ERROR_LOG << "OnWatchdogOption handler threw an exception: " << CurrentExceptionMessage() << Endl;
    }
}

//
// IWatchdogOptionsUpdater implementation
//    The protected methods below allow some components, like Controller, to override or simulate
//    application of an ITS option
//
void TWatchdogOptionsCollection::Override(const TString& key, const TMaybe<TString>& value) {
    TGuard<TMutex> g(*Mutex);
    CHECK_WITH_LOG(!Updating);

    if (value.Defined()) {
        Overrides[key] = value.GetRef(); // may be empty
    } else {
        Overrides.erase(key); // switch back to the ITS value
    }
}

void TWatchdogOptionsCollection::NotifyNow() {
    TGuard<TMutex> g(*Mutex);
    CHECK_WITH_LOG(!Updating);

    DoUpdate(Data);
}

//
// Subscribe / unsubscribe routines
//     Code below imitates the weak_ptr behaviour for TSubscriberInfo references in collection:
//     when client dies, it is automatically unsubscribed; however, the unsubscription is done by parent
//
TWatchdogOptionsCollection::TSubscription TWatchdogOptionsCollection::Subscribe(IWatchdogOptionSubscriber* subscriber, const TSet<TString>& options) {
    CHECK_WITH_LOG(!!subscriber);
    TGuard<TMutex> g(*Mutex);

    TSubscription sub(new TSubscriberInfo());
    sub->Owner = this;
    sub->Subscriber = subscriber;
    sub->Options = options;

    for (const auto& kv : Data) {
        if (options.empty() || options.find(kv.first) != options.end())
            DoNotify(*subscriber, kv.first, kv.second);
    }

    // store a weak reference
    Subscribers.push_back(sub.Get());

    return sub;
}


void TWatchdogOptionsCollection::DoDestroy(TSubscriberInfo* t) {
    Y_ASSERT(t->RefCount() == 0);
    TGuard<TMutex> g(*Mutex);

    try {
        t->Owner = nullptr;
        if (!Updating) { //if we are not on the watchdog thread
            Subscribers.remove(t);
            TDelete::Destroy(t);
        }
    } catch (...) {
        Y_ASSERT(0);
    }
}

void TWatchdogOptionsCollection::Release() {
    TGuard<TMutex> g(*Mutex);
    for (TSubscriberInfo* subscriberInfo : Subscribers) {
        //We're killed while a client is alive? Let the client own what remains.
        Y_ASSERT(!!subscriberInfo->RefCount());
        subscriberInfo->Owner = nullptr;
    }
    Subscribers.clear();
}

// Dump
void TWatchdogOptionsCollection::DumpData(TStringOutput& o, const TString& sep) const {
    TGuard<TMutex> g(*Mutex);
    for (const auto& kv : Data) {
        o << kv.first << '=' << kv.second;

        if (sep.size())
            o << sep;
        else
            o << Endl;
    }

    if (!sep)
        o.Flush();
}

TWatchdogOptionsCollection::TData TWatchdogOptionsCollection::GetData() const {
    TGuard<TMutex> g(*Mutex);
    return Data;
};


TWatchdogOptionsHandle::TWatchdogOptionsHandle(const TVector<TString>& filenames, TDuration period)
        : IWatchDogHandleFreq((ui32)period.Seconds())
        , FileNames(filenames)
        , Period(period)
        , Active(false)
{
    CHECK_WITH_LOG(period.Seconds() > 0);
    DoCheck(TInstant::Now());
}

TWatchdogOptionsHandle::TWatchdogOptionsHandle(const TString& filename, TDuration frequency)
        : TWatchdogOptionsHandle(TVector<TString>{filename}, frequency)
{
}

TWatchdogOptionsHandle::~TWatchdogOptionsHandle() {
    Disable();
}

TDuration TWatchdogOptionsHandle::GetPeriod() const {
    return Period;
}

void TWatchdogOptionsHandle::Enable()
{
    if (!Active) {
        WatchDogHandle.Reset(Singleton<TWatchDogFactory>()->RegisterWatchDogHandle(this));
        Active = true;
    }
}

void TWatchdogOptionsHandle::Disable()
{
    if (Active) {
        WatchDogHandle.Reset();
        Active = false;
    }
}

void TWatchdogOptionsHandle::DoCheck(TInstant) {
    TData newData;

    for (const TString& fileName : FileNames) {
        try {
            if (TFsPath(fileName).Exists()) {
                TUnbufferedFileInput file(fileName);

                for (TString line; file.ReadLine(line);) {
                    line = StripInPlace(line);
                    if (!line)
                        continue;

                    TVector<TString> v = SplitString(line, "=", 2, KEEP_EMPTY_TOKENS);
                    if (v.size() < 2)
                        continue;

                    TString &key = StripInPlace(v[0]);
                    TString &value = StripInPlace(v[1]);
                    newData[key] = value;
                }
            }

        } catch (...) {
            ERROR_LOG << "Failed to read and parse " << fileName << " : " << CurrentExceptionMessage() << Endl;
            return;
        }
    }
    DoUpdate(newData);
}

//
// Some binding for the shared handles (because the author of daemon_base/module did not know about OOD and Dependency Injection,
// there may be no way to share watchdog handle between several IDaemonModule )
//
namespace {
    template <class TRegistryPtr, class T>
    struct TRemover {
        TRemover(T& instance, TRegistryPtr registry)
            : Instance_(instance)
            , Registry_(registry)
        {
        }

        T& Instance_;
        TRegistryPtr Registry_;

        ~TRemover() {
            Registry_->Drop(&Instance_);
        }
    };

    template <class TItem>
    class TWeakIntrusiveRegistry final {
    public:
        using TPtr = TWeakIntrusiveRegistry<TItem>*;
        using TWeakRef = TItem*;
        using TIntrusiveRef = typename TItem::TPtr;
        using TRegistry = TMap<TString, TWeakRef>;
    public:
        //inline TIntrusiveRef Get(const TString& objectId) {
        //    TGuard<TMutex> g(Lock_);
        //    return TIntrusiveRef(GetWeak(objectId));
        //};

        ~TWeakIntrusiveRegistry() {
            TGuard<TMutex> g(Lock_);
            Registry_.clear();
        }

        template <typename CreatorFunc>
        inline TIntrusiveRef GetOrCreate(const TString& objectId, CreatorFunc creatorFunc) {
            TGuard<TMutex> g(Lock_);
            TWeakRef obj = GetWeak(objectId);
            if (!obj) {
                obj = SetWeak(objectId, creatorFunc());
            }
            return TIntrusiveRef(obj);
        };

        void Drop(const TWeakRef ptr) {
            TGuard<TMutex> g(Lock_);
            DropWeak(ptr);
        }
    private:
        TWeakRef GetWeak(const TString& objectId) const {
            auto iter = Registry_.find(objectId);
            return (iter != Registry_.end()) ? iter->second : TWeakRef();
        }
        TWeakRef SetWeak(const TString& objectId, TWeakRef object) {
            Registry_[objectId] = object;
            return object;
        }
        void DropWeak(const TWeakRef& object) {
            for (auto it = Registry_.begin(); it != Registry_.end();) {
                if (it->second == object) {
                    it = Registry_.erase(it);
                } else {
                    it++;
                }
            }
        }
    private:
        TRegistry Registry_;
        TMutex Lock_;
    };

    using TSharedWatchdogRegistry = TWeakIntrusiveRegistry<TWatchdogOptionsHandle>;

    class TSharedWatchdogHandle final : public TWatchdogOptionsHandle {
    private:
        TRemover<TSharedWatchdogRegistry::TPtr, TWatchdogOptionsHandle> Remover_;
    public:
        TSharedWatchdogHandle(TSharedWatchdogRegistry::TPtr reg, const TVector<TString>& filenames, TDuration period)
                : TWatchdogOptionsHandle(filenames, period)
                , Remover_(*this, reg)
        {
        }
    };
}

TWatchdogOptionsHandle::TPtr TWatchdogOptionsHandle::CreateShared(const TString& filename, TDuration period) {
    return CreateShared(TVector<TString>{filename}, period);
}

TWatchdogOptionsHandle::TPtr TWatchdogOptionsHandle::CreateShared(const TVector<TString>& filenames, TDuration period) {
    TDuration adjustedPeriod = period ? period : TDuration::Seconds(5);

    auto* registry = Singleton<TSharedWatchdogRegistry>();
    TString objectId = JoinStrings(filenames,";");
    TWatchdogOptionsHandle::TPtr object = registry->GetOrCreate(objectId, [&](){ return new TSharedWatchdogHandle(registry, filenames, adjustedPeriod); });
    Y_ENSURE(!period || object->GetPeriod() <= adjustedPeriod); // if existing object created with a lower frequency, throw
    return object;
}

