/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/Logging.h"
#include "mozilla/WindowsVersion.h"
#include "mozilla/WinHeaderOnlyUtils.h"
#include "mozilla/mscom/EnsureMTA.h"
#ifndef __MINGW32__
#  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
  // The classes we're using are only available beginning with Windows 10
  if (!mozilla::IsWin10OrLater()) {
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  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
  // The classes we're using are only available beginning with Windows 10
  if (!mozilla::IsWin10OrLater()) {
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  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__
}

NS_IMETHODIMP
nsWindowsPackageManager::GetCampaignId(nsAString& aCampaignId) {
#ifdef __MINGW32__
  return NS_ERROR_NOT_IMPLEMENTED;
#else
  // The classes we're using are only available beginning with Windows 10,
  // and this is only relevant for MSIX packaged builds.
  if (!mozilla::IsWin10OrLater() || !mozilla::HasPackageIdentity()) {
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  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) return NS_ERROR_FAILURE;
  ComPtr<IStoreContext> storeContext = nullptr;
  hr = scStatics->GetDefault(&storeContext);
  if (!SUCCEEDED(hr) || storeContext == nullptr) return NS_ERROR_FAILURE;

  ComPtr<IAsyncOperation<StoreProductResult*> > asyncSpr = nullptr;

  {
    nsAutoHandle event(CreateEventW(nullptr, true, false, nullptr));
    bool asyncOpSucceeded = false;

    // Despite the documentation indicating otherwise, the async operations
    // and callbacks used here don't seem to work outside of a COM MTA.
    mozilla::mscom::EnsureMTA(
        [&event, &asyncOpSucceeded, &hr, &storeContext, &asyncSpr]() -> void {
          auto callback =
              Callback<IAsyncOperationCompletedHandler<StoreProductResult*> >(
                  [&asyncOpSucceeded, &event](
                      IAsyncOperation<StoreProductResult*>* asyncInfo,
                      AsyncStatus status) -> HRESULT {
                    asyncOpSucceeded = status == AsyncStatus::Completed;
                    return SetEvent(event.get());
                  });

          hr = storeContext->GetStoreProductForCurrentAppAsync(&asyncSpr);
          if (!SUCCEEDED(hr) || asyncSpr == nullptr) {
            asyncOpSucceeded = false;
            return;
          }
          hr = asyncSpr->put_Completed(callback.Get());
          if (!SUCCEEDED(hr)) {
            asyncOpSucceeded = false;
            return;
          }

          DWORD ret = WaitForSingleObject(event.get(), 30000);
          if (ret != WAIT_OBJECT_0) {
            asyncOpSucceeded = false;
          }
        });
    if (!asyncOpSucceeded) return NS_ERROR_FAILURE;
  }

  ComPtr<IStoreProductResult> productResult = nullptr;
  hr = asyncSpr->GetResults(&productResult);
  if (!SUCCEEDED(hr) || productResult == nullptr) return NS_ERROR_FAILURE;

  ComPtr<IStoreProduct> product = nullptr;
  hr = productResult->get_Product(&product);
  if (!SUCCEEDED(hr) || product == nullptr) return NS_ERROR_FAILURE;
  ComPtr<Collections::IVectorView<StoreSku*> > skus = nullptr;
  hr = product->get_Skus(&skus);
  if (!SUCCEEDED(hr) || skus == nullptr) return NS_ERROR_FAILURE;

  unsigned int size;
  hr = skus->get_Size(&size);
  if (!SUCCEEDED(hr)) return NS_ERROR_FAILURE;

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

    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 campaignId;
    hr = scd->get_CampaignId(campaignId.GetAddressOf());
    if (!SUCCEEDED(hr)) continue;

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

  // 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.
  if (aCampaignId.IsEmpty()) {
    ComPtr<IAsyncOperation<StoreAppLicense*> > asyncSal = nullptr;
    bool asyncOpSucceeded = false;
    nsAutoHandle event(CreateEventW(nullptr, true, false, nullptr));
    mozilla::mscom::EnsureMTA(
        [&event, &asyncOpSucceeded, &hr, &storeContext, &asyncSal]() -> void {
          auto callback =
              Callback<IAsyncOperationCompletedHandler<StoreAppLicense*> >(
                  [&asyncOpSucceeded, &event](
                      IAsyncOperation<StoreAppLicense*>* asyncInfo,
                      AsyncStatus status) -> HRESULT {
                    asyncOpSucceeded = status == AsyncStatus::Completed;
                    return SetEvent(event.get());
                  });

          hr = storeContext->GetAppLicenseAsync(&asyncSal);
          if (!SUCCEEDED(hr) || asyncSal == nullptr) {
            asyncOpSucceeded = false;
            return;
          }
          hr = asyncSal->put_Completed(callback.Get());
          if (!SUCCEEDED(hr)) {
            asyncOpSucceeded = false;
            return;
          }

          DWORD ret = WaitForSingleObject(event.get(), 30000);
          if (ret != WAIT_OBJECT_0) {
            asyncOpSucceeded = false;
          }
        });
    if (!asyncOpSucceeded) return NS_ERROR_FAILURE;

    ComPtr<IStoreAppLicense> license = nullptr;
    hr = asyncSal->GetResults(&license);
    if (!SUCCEEDED(hr) || license == nullptr) return NS_ERROR_FAILURE;

    HString extendedData;
    hr = license->get_ExtendedJsonData(extendedData.GetAddressOf());
    if (!SUCCEEDED(hr)) return NS_ERROR_FAILURE;

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

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

    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");
      }
    }
  }

  // No matter what happens in either block above, if they don't exit with a
  // failure we managed to successfully pull the campaignId from somewhere
  // (even if its empty).
  return NS_OK;

#endif  // __MINGW32__
}

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