/*
  ==============================================================================

   This file is part of the JUCE library.
   Copyright (c) 2022 - Raw Material Software Limited

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

   JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
   EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
   DISCLAIMED.

  ==============================================================================
*/

#pragma once

//==============================================================================
namespace LicenseHelpers
{
    inline LicenseState::Type licenseTypeForString (const String& licenseString)
    {
        if (licenseString == "juce-pro")       return LicenseState::Type::pro;
        if (licenseString == "juce-indie")     return LicenseState::Type::indie;
        if (licenseString == "juce-edu")       return LicenseState::Type::educational;
        if (licenseString == "juce-personal")  return LicenseState::Type::personal;

        jassertfalse; // unknown type
        return LicenseState::Type::none;
    }

    using LicenseVersionAndType = std::pair<int, LicenseState::Type>;

    inline LicenseVersionAndType findBestLicense (std::vector<LicenseVersionAndType>&& licenses)
    {
        if (licenses.size() == 1)
            return licenses[0];

        auto getValueForLicenceType = [] (LicenseState::Type type)
        {
            switch (type)
            {
                case LicenseState::Type::pro:          return 4;
                case LicenseState::Type::indie:        return 3;
                case LicenseState::Type::educational:  return 2;
                case LicenseState::Type::personal:     return 1;
                case LicenseState::Type::gpl:
                case LicenseState::Type::none:
                default:                               return -1;
            }
        };

        std::sort (licenses.begin(), licenses.end(),
                   [getValueForLicenceType] (const LicenseVersionAndType& l1, const LicenseVersionAndType& l2)
                   {
                       if (l1.first > l2.first)
                           return true;

                       if (l1.first == l2.first)
                           return getValueForLicenceType (l1.second) > getValueForLicenceType (l2.second);

                       return false;
                   });

        auto findFirstLicense = [&licenses] (bool isPaid)
        {
            auto iter = std::find_if (licenses.begin(), licenses.end(),
                                      [isPaid] (const LicenseVersionAndType& l)
                                      {
                                          auto proOrIndie = (l.second == LicenseState::Type::pro || l.second == LicenseState::Type::indie);
                                          return isPaid ? proOrIndie : ! proOrIndie;
                                      });

            return iter != licenses.end() ? *iter
                                          : LicenseVersionAndType();
        };

        auto newestPaid = findFirstLicense (true);
        auto newestFree = findFirstLicense (false);

        if (newestPaid.first >= projucerMajorVersion || newestPaid.first >= newestFree.first)
            return newestPaid;

        return newestFree;
    }
}

//==============================================================================
class LicenseQueryThread
{
public:
    enum class ErrorType
    {
        busy,
        cancelled,
        connectionError,
        webResponseError
    };

    using ErrorMessageAndType = std::pair<String, ErrorType>;
    using LicenseQueryCallback = std::function<void (ErrorMessageAndType, LicenseState)>;

    //==============================================================================
    LicenseQueryThread() = default;

    void checkLicenseValidity (const LicenseState& state, LicenseQueryCallback completionCallback)
    {
        if (jobPool.getNumJobs() > 0)
        {
            completionCallback ({ {}, ErrorType::busy }, {});
            return;
        }

        jobPool.addJob ([this, state, completionCallback]
        {
            auto updatedState = state;

            auto result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), updatedState);

            WeakReference<LicenseQueryThread> weakThis (this);
            MessageManager::callAsync ([weakThis, result, updatedState, completionCallback]
            {
                if (weakThis != nullptr)
                    completionCallback (result, updatedState);
            });
        });
    }

    void doSignIn (const String& email, const String& password, LicenseQueryCallback completionCallback)
    {
        cancelRunningJobs();

        jobPool.addJob ([this, email, password, completionCallback]
        {
            LicenseState state;

            auto result = runTask (std::make_unique<UserLogin> (email, password), state);

            if (result == ErrorMessageAndType())
                result = runTask (std::make_unique<UserLicenseQuery> (state.authToken), state);

            if (result != ErrorMessageAndType())
                state = {};

            WeakReference<LicenseQueryThread> weakThis (this);
            MessageManager::callAsync ([weakThis, result, state, completionCallback]
            {
                if (weakThis != nullptr)
                    completionCallback (result, state);
            });
        });
    }

    void cancelRunningJobs()
    {
        jobPool.removeAllJobs (true, 500);
    }

private:
    //==============================================================================
    struct AccountEnquiryBase
    {
        virtual ~AccountEnquiryBase() = default;

        virtual bool isPOSTLikeRequest() const = 0;
        virtual String getEndpointURLSuffix() const = 0;
        virtual StringPairArray getParameterNamesAndValues() const = 0;
        virtual String getExtraHeaders() const = 0;
        virtual int getSuccessCode() const = 0;
        virtual String errorCodeToString (int) const = 0;
        virtual bool parseServerResponse (const String&, LicenseState&) = 0;
    };

    struct UserLogin final : public AccountEnquiryBase
    {
        UserLogin (const String& e, const String& p)
            : userEmail (e), userPassword (p)
        {
        }

        bool isPOSTLikeRequest() const override       { return true; }
        String getEndpointURLSuffix() const override  { return "/authenticate/projucer"; }
        int getSuccessCode() const override           { return 200; }

        StringPairArray getParameterNamesAndValues() const override
        {
            StringPairArray namesAndValues;
            namesAndValues.set ("email", userEmail);
            namesAndValues.set ("password", userPassword);

            return namesAndValues;
        }

        String getExtraHeaders() const override
        {
            return "Content-Type: application/json";
        }

        String errorCodeToString (int errorCode) const override
        {
            switch (errorCode)
            {
                case 400:  return "Please enter your email and password to sign in.";
                case 401:  return "Your email and password are incorrect.";
                case 451:  return "Access denied.";
                default:   return "Something went wrong, please try again.";
            }
        }

        bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override
        {
            auto json = JSON::parse (serverResponse);

            licenseState.authToken = json.getProperty ("token", {}).toString();
            licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString();

            return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty());
        }

        String userEmail, userPassword;
    };

    struct UserLicenseQuery final : public AccountEnquiryBase
    {
        UserLicenseQuery (const String& authToken)
            : userAuthToken (authToken)
        {
        }

        bool isPOSTLikeRequest() const override       { return false; }
        String getEndpointURLSuffix() const override  { return "/user/licences/projucer"; }
        int getSuccessCode() const override           { return 200; }

        StringPairArray getParameterNamesAndValues() const override
        {
            return {};
        }

        String getExtraHeaders() const override
        {
            return "x-access-token: " + userAuthToken;
        }

        String errorCodeToString (int errorCode) const override
        {
            switch (errorCode)
            {
                case 401:  return "User not found or could not be verified.";
                default:   return "User licenses info fetch failed (unknown error).";
            }
        }

        bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override
        {
            auto json = JSON::parse (serverResponse);

            if (auto* licensesJson = json.getArray())
            {
                std::vector<LicenseHelpers::LicenseVersionAndType> licenses;

                for (auto& license : *licensesJson)
                {
                    auto version = license.getProperty ("product_version", {}).toString().trim();
                    auto type    = license.getProperty ("licence_type", {}).toString();
                    auto status  = license.getProperty ("status", {}).toString();

                    if (status == "active" && type.isNotEmpty() && version.isNotEmpty())
                        licenses.push_back ({ version.getIntValue(), LicenseHelpers::licenseTypeForString (type) });
                }

                if (! licenses.empty())
                {
                    auto bestLicense = LicenseHelpers::findBestLicense (std::move (licenses));

                    licenseState.version = bestLicense.first;
                    licenseState.type = bestLicense.second;
                }

                return true;
            }

            return false;
        }

        String userAuthToken;
    };

    //==============================================================================
    static String postDataStringAsJSON (const StringPairArray& parameters)
    {
        DynamicObject::Ptr d (new DynamicObject());

        for (auto& key : parameters.getAllKeys())
            d->setProperty (key, parameters[key]);

        return JSON::toString (var (d.get()));
    }

    static ErrorMessageAndType runTask (std::unique_ptr<AccountEnquiryBase> accountEnquiryTask, LicenseState& state)
    {
        const ErrorMessageAndType cancelledError ("Cancelled.", ErrorType::cancelled);
        const String endpointURL ("https://api.juce.com/api/v1");

        URL url (endpointURL + accountEnquiryTask->getEndpointURLSuffix());

        auto isPOST = accountEnquiryTask->isPOSTLikeRequest();

        if (isPOST)
            url = url.withPOSTData (postDataStringAsJSON (accountEnquiryTask->getParameterNamesAndValues()));

        if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit())
            return cancelledError;

        int statusCode = 0;
        auto urlStream = url.createInputStream (URL::InputStreamOptions (isPOST ? URL::ParameterHandling::inPostData
                                                                                : URL::ParameterHandling::inAddress)
                                                  .withExtraHeaders (accountEnquiryTask->getExtraHeaders())
                                                  .withConnectionTimeoutMs (5000)
                                                  .withStatusCode (&statusCode));

        if (urlStream == nullptr)
            return { "Failed to connect to the web server.", ErrorType::connectionError };

        if (statusCode != accountEnquiryTask->getSuccessCode())
            return { accountEnquiryTask->errorCodeToString (statusCode), ErrorType::webResponseError };

        if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit())
            return cancelledError;

        String response;

        for (;;)
        {
            char buffer [8192] = "";
            auto num = urlStream->read (buffer, sizeof (buffer));

            if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit())
                return cancelledError;

            if (num <= 0)
                break;

            response += buffer;
        }

        if (ThreadPoolJob::getCurrentThreadPoolJob()->shouldExit())
            return cancelledError;

        if (! accountEnquiryTask->parseServerResponse (response, state))
            return { "Failed to parse server response.", ErrorType::webResponseError };

        return {};
    }

    //==============================================================================
    ThreadPool jobPool { ThreadPoolOptions{}.withNumberOfThreads (1) };

    //==============================================================================
    JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread)
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseQueryThread)
};
