/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsWindowsPackageManager.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/Logging.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#ifndef __MINGW32__
#  include "nsProxyRelease.h"
#  include <comutil.h>
#  include <wrl.h>
#  include <windows.applicationmodel.store.h>
#  include <windows.management.deployment.h>
#  include <windows.services.store.h>
#endif  // __MINGW32__
#include "nsError.h"
#include "nsString.h"
#include "nsISupportsPrimitives.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsXPCOMCID.h"
#include "json/json.h"

#ifndef __MINGW32__  // WinRT headers not yet supported by MinGW
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Management;
using namespace ABI::Windows::Services::Store;
#endif

// Campaign IDs are stored in a JSON data structure under this key
// for installs done without the user being signed in to the Microsoft
// store.
#define CAMPAIGN_ID_JSON_FIELD_NAME "customPolicyField1"

namespace mozilla {
namespace toolkit {
namespace system {

NS_IMPL_ISUPPORTS(nsWindowsPackageManager, nsIWindowsPackageManager)

NS_IMETHODIMP
nsWindowsPackageManager::FindUserInstalledPackages(
    const nsTArray<nsString>& aNamePrefixes, nsTArray<nsString>& aPackages) {
#ifdef __MINGW32__
  return NS_ERROR_NOT_IMPLEMENTED;
#else
  ComPtr<IInspectable> pmInspectable;
  ComPtr<Deployment::IPackageManager> pm;
  HRESULT hr = RoActivateInstance(
      HStringReference(
          RuntimeClass_Windows_Management_Deployment_PackageManager)
          .Get(),
      &pmInspectable);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }
  hr = pmInspectable.As(&pm);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }

  ComPtr<Collections::IIterable<ApplicationModel::Package*> > pkgs;
  hr = pm->FindPackagesByUserSecurityId(NULL, &pkgs);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }
  ComPtr<Collections::IIterator<ApplicationModel::Package*> > iterator;
  hr = pkgs->First(&iterator);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }
  boolean hasCurrent;
  hr = iterator->get_HasCurrent(&hasCurrent);
  while (SUCCEEDED(hr) && hasCurrent) {
    ComPtr<ApplicationModel::IPackage> package;
    hr = iterator->get_Current(&package);
    if (!SUCCEEDED(hr)) {
      return NS_ERROR_FAILURE;
    }
    ComPtr<ApplicationModel::IPackageId> packageId;
    hr = package->get_Id(&packageId);
    if (!SUCCEEDED(hr)) {
      return NS_ERROR_FAILURE;
    }
    HString rawName;
    hr = packageId->get_FamilyName(rawName.GetAddressOf());
    if (!SUCCEEDED(hr)) {
      return NS_ERROR_FAILURE;
    }
    unsigned int tmp;
    nsString name(rawName.GetRawBuffer(&tmp));
    for (uint32_t i = 0; i < aNamePrefixes.Length(); i++) {
      if (name.Find(aNamePrefixes.ElementAt(i)) != kNotFound) {
        aPackages.AppendElement(name);
        break;
      }
    }
    hr = iterator->MoveNext(&hasCurrent);
  }
  return NS_OK;
#endif  // __MINGW32__
}

NS_IMETHODIMP
nsWindowsPackageManager::GetInstalledDate(uint64_t* ts) {
#ifdef __MINGW32__
  return NS_ERROR_NOT_IMPLEMENTED;
#else
  ComPtr<ApplicationModel::IPackageStatics> pkgStatics;
  HRESULT hr = RoGetActivationFactory(
      HStringReference(RuntimeClass_Windows_ApplicationModel_Package).Get(),
      IID_PPV_ARGS(&pkgStatics));
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }

  ComPtr<ApplicationModel::IPackage> package;
  hr = pkgStatics->get_Current(&package);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }

  ComPtr<ApplicationModel::IPackage3> package3;
  hr = package.As(&package3);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }

  DateTime installedDate;
  hr = package3->get_InstalledDate(&installedDate);
  if (!SUCCEEDED(hr)) {
    return NS_ERROR_FAILURE;
  }

  *ts = installedDate.UniversalTime;
  return NS_OK;
#endif  // __MINGW32__
}

static HRESULT RejectOnMainThread(
    const char* aName, nsMainThreadPtrHandle<dom::Promise> promiseHolder,
    nsresult result) {
  DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
      aName, [promiseHolder = std::move(promiseHolder), result]() {
        promiseHolder.get()->MaybeReject(result);
      }));
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");

  return S_OK;
}

#ifndef __MINGW32__
// forward declarations
static void GetCampaignIdFromStoreProductOnBackgroundThread(
    ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr,
    ComPtr<IStoreContext> storeContext,
    nsMainThreadPtrHandle<dom::Promise> promiseHolder);
static void GetCampaignIdFromLicenseOnBackgroundThread(
    ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal,
    nsMainThreadPtrHandle<dom::Promise> promiseHolder,
    nsAutoString aCampaignId);
#endif  // __MINGW32__

static std::tuple<nsMainThreadPtrHolder<dom::Promise>*, nsresult>
InitializePromise(JSContext* aCx, dom::Promise** aPromise) {
  *aPromise = nullptr;

  nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!global)) {
    return std::make_tuple(nullptr, NS_ERROR_FAILURE);
  }

  ErrorResult erv;
  RefPtr<dom::Promise> outer = dom::Promise::Create(global, erv);
  if (NS_WARN_IF(erv.Failed())) {
    return std::make_tuple(nullptr, erv.StealNSResult());
  }

  auto promiseHolder = new nsMainThreadPtrHolder<dom::Promise>(
      "nsWindowsPackageManager::CampaignId Promise", outer);

  outer.forget(aPromise);

  return std::make_tuple(promiseHolder, NS_OK);
}

NS_IMETHODIMP
nsWindowsPackageManager::CampaignId(JSContext* aCx, dom::Promise** aPromise) {
  NS_ENSURE_ARG_POINTER(aPromise);

#ifdef __MINGW32__
  return NS_ERROR_NOT_IMPLEMENTED;
#else

  // This is only relevant for MSIX packaged builds.
  if (!mozilla::HasPackageIdentity()) {
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  auto [unwrappedPromiseHolder, result] = InitializePromise(aCx, aPromise);

  NS_ENSURE_SUCCESS(result, result);

  nsMainThreadPtrHandle<dom::Promise> promiseHolder(unwrappedPromiseHolder);

  ComPtr<IStoreContextStatics> scStatics = nullptr;
  HRESULT hr = RoGetActivationFactory(
      HStringReference(RuntimeClass_Windows_Services_Store_StoreContext).Get(),
      IID_PPV_ARGS(&scStatics));
  /* Per
   * https://docs.microsoft.com/en-us/windows/uwp/publish/create-a-custom-app-promotion-campaign#programmatically-retrieve-the-custom-campaign-id-for-an-app
   * there are three ways to find a campaign ID.
   * 1) If the user was logged into the Microsoft Store when they installed
   *    it will be available in a SKU.
   * 2) If they were not logged in, it will be available through the
   *    StoreAppLicense
   *
   * There's also a third way, in theory, to retrieve it on very old versions of
   * Windows 10 (1511 or earlier). However, these versions don't appear to be
   * able to use the Windows Store at all anymore - so there's no point in
   * supporting that scenario.
   *
   */
  if (!SUCCEEDED(hr) || scStatics == nullptr) {
    promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
    return NS_OK;
  }

  ComPtr<IStoreContext> storeContext = nullptr;
  hr = scStatics->GetDefault(&storeContext);
  if (!SUCCEEDED(hr) || storeContext == nullptr) {
    promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
    return NS_OK;
  }

  /* Despite the documentation not saying otherwise, these don't work
   * consistently when called from the main thread. I tried the two scenarios
   * described above multiple times, and couldn't consistently get the campaign
   * id when running this code async on the main thread. So instead, this
   * dispatches to a background task to do the work, and then dispatches to the
   * main thread to call back into the Javascript asynchronously
   *
   */
  result = NS_DispatchBackgroundTask(
      NS_NewRunnableFunction(
          __func__,
          [storeContext = std::move(storeContext),
           promiseHolder = std::move(promiseHolder)]() -> void {
            ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr = nullptr;

            auto hr =
                storeContext->GetStoreProductForCurrentAppAsync(&asyncSpr);
            if (!SUCCEEDED(hr) || asyncSpr == nullptr) {
              RejectOnMainThread(__func__, std::move(promiseHolder),
                                 NS_ERROR_FAILURE);
              return;
            }

            auto callback =
                Callback<IAsyncOperationCompletedHandler<StoreProductResult*> >(
                    [promiseHolder, asyncSpr,
                     storeContext = std::move(storeContext)](
                        IAsyncOperation<StoreProductResult*>* asyncInfo,
                        AsyncStatus status) -> HRESULT {
                      bool asyncOpSucceeded = status == AsyncStatus::Completed;
                      if (!asyncOpSucceeded) {
                        return RejectOnMainThread(__func__,
                                                  std::move(promiseHolder),
                                                  NS_ERROR_FAILURE);
                      }

                      GetCampaignIdFromStoreProductOnBackgroundThread(
                          std::move(asyncSpr), std::move(storeContext),
                          std::move(promiseHolder));

                      return S_OK;
                    });

            hr = asyncSpr->put_Completed(callback.Get());
            if (!SUCCEEDED(hr)) {
              RejectOnMainThread(__func__, std::move(promiseHolder),
                                 NS_ERROR_FAILURE);
              return;
            }
          }),
      NS_DISPATCH_EVENT_MAY_BLOCK);

  if (NS_FAILED(result)) {
    promiseHolder.get()->MaybeReject(NS_ERROR_FAILURE);
    return NS_OK;
  }

  return NS_OK;
#endif  // __MINGW32__
}

#ifndef __MINGW32__
static void GetCampaignIdFromStoreProductOnBackgroundThread(
    ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr,
    ComPtr<IStoreContext> storeContext,
    nsMainThreadPtrHandle<dom::Promise> promiseHolder) {
  ComPtr<IStoreProductResult> productResult = nullptr;

  auto hr = asyncSpr->GetResults(&productResult);
  if (!SUCCEEDED(hr) || productResult == nullptr) {
    RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
    return;
  }

  nsAutoString campaignId;

  ComPtr<IStoreProduct> product = nullptr;
  hr = productResult->get_Product(&product);
  if (SUCCEEDED(hr) && (product != nullptr)) {
    ComPtr<Collections::IVectorView<StoreSku*> > skus = nullptr;
    hr = product->get_Skus(&skus);
    if (!SUCCEEDED(hr) || skus == nullptr) {
      RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
      return;
    }

    unsigned int size;
    hr = skus->get_Size(&size);
    if (!SUCCEEDED(hr)) {
      RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
      return;
    }

    for (unsigned int i = 0; i < size; i++) {
      ComPtr<IStoreSku> sku = nullptr;
      hr = skus->GetAt(i, &sku);
      if (!SUCCEEDED(hr) || sku == nullptr) {
        RejectOnMainThread(__func__, std::move(promiseHolder),
                           NS_ERROR_FAILURE);
        return;
      }

      boolean isInUserCollection = false;
      hr = sku->get_IsInUserCollection(&isInUserCollection);
      if (!SUCCEEDED(hr) || !isInUserCollection) continue;

      ComPtr<IStoreCollectionData> scd = nullptr;
      hr = sku->get_CollectionData(&scd);
      if (!SUCCEEDED(hr) || scd == nullptr) continue;

      HString msCampaignId;
      hr = scd->get_CampaignId(msCampaignId.GetAddressOf());
      if (!SUCCEEDED(hr)) continue;

      unsigned int tmp;
      campaignId.Assign(msCampaignId.GetRawBuffer(&tmp));
      if (campaignId.Length() > 0) {
        campaignId.AppendLiteral("&msstoresignedin=true");
      }
    }
  }

  if (!campaignId.IsEmpty()) {
    // If we got here, it means that campaignId has been processed and can be
    // sent back via the promise
    DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
        __func__, [promiseHolder = std::move(promiseHolder),
                   campaignId = std::move(campaignId)]() {
          promiseHolder.get()->MaybeResolve(campaignId);
        }));

    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");

    return;
  }

  // There's various points above that could exit without a failure.
  // If we get here without a campaignId we may as well just check
  // the AppStoreLicense.
  ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal = nullptr;

  hr = storeContext->GetAppLicenseAsync(&asyncSal);
  if (!SUCCEEDED(hr) || asyncSal == nullptr) {
    RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
    return;
  }

  auto callback = Callback<IAsyncOperationCompletedHandler<StoreAppLicense*> >(
      [asyncSal, promiseHolder, campaignId = std::move(campaignId)](
          IAsyncOperation<StoreAppLicense*>* asyncInfo,
          AsyncStatus status) -> HRESULT {
        bool asyncOpSucceeded = status == AsyncStatus::Completed;
        if (!asyncOpSucceeded) {
          return RejectOnMainThread(__func__, std::move(promiseHolder),
                                    NS_ERROR_FAILURE);
        }

        GetCampaignIdFromLicenseOnBackgroundThread(std::move(asyncSal),
                                                   std::move(promiseHolder),
                                                   std::move(campaignId));

        return S_OK;
      });

  hr = asyncSal->put_Completed(callback.Get());
  if (!SUCCEEDED(hr)) {
    RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
    return;
  }
}

static void GetCampaignIdFromLicenseOnBackgroundThread(
    ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal,
    nsMainThreadPtrHandle<dom::Promise> promiseHolder,
    nsAutoString aCampaignId) {
  ComPtr<IStoreAppLicense> license = nullptr;
  auto hr = asyncSal->GetResults(&license);
  if (!SUCCEEDED(hr) || license == nullptr) {
    RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
    return;
  }

  HString extendedData;
  hr = license->get_ExtendedJsonData(extendedData.GetAddressOf());
  if (!SUCCEEDED(hr)) {
    RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
    return;
  }

  Json::Value jsonData;
  Json::Reader jsonReader;

  unsigned int tmp;
  nsAutoString key(extendedData.GetRawBuffer(&tmp));
  if (!jsonReader.parse(NS_ConvertUTF16toUTF8(key).get(), jsonData, false)) {
    RejectOnMainThread(__func__, std::move(promiseHolder), NS_ERROR_FAILURE);
    return;
  }

  if (jsonData.isMember(CAMPAIGN_ID_JSON_FIELD_NAME) &&
      jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].isString()) {
    aCampaignId.Assign(
        NS_ConvertUTF8toUTF16(
            jsonData[CAMPAIGN_ID_JSON_FIELD_NAME].asString().c_str())
            .get());
    if (aCampaignId.Length() > 0) {
      aCampaignId.AppendLiteral("&msstoresignedin=false");
    }
  }

  DebugOnly<nsresult> rv = NS_DispatchToMainThread(NS_NewRunnableFunction(
      __func__, [promiseHolder = std::move(promiseHolder),
                 aCampaignId = std::move(aCampaignId)]() {
        promiseHolder.get()->MaybeResolve(aCampaignId);
      }));
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToMainThread failed");
}
#endif  // __MINGW32__

}  // namespace system
}  // namespace toolkit
}  // namespace mozilla
