/* 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 "ChannelClassifierService.h"
#include "Classifier.h"
#include "ContentClassifierService.h"
#include "HttpBaseChannel.h"
#include "mozilla/Components.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/FlowMarkers.h"
#include "mozilla/Maybe.h"
#include "mozilla/PerfStats.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/glean/UrlClassifierMetrics.h"
#include "mozilla/net/AsyncUrlChannelClassifier.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/net/UrlClassifierCommon.h"
#include "mozilla/net/UrlClassifierFeatureFactory.h"
#include "mozilla/net/UrlClassifierFeatureResult.h"
#include "nsContentUtils.h"
#include "nsIChannel.h"
#include "nsIHttpChannel.h"
#include "nsIUrlClassifierExceptionList.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsProxyRelease.h"
#include "nsServiceManagerUtils.h"
#include "nsUrlClassifierDBService.h"
#include "nsUrlClassifierUtils.h"
#include "mozilla/net/UrlClassifierCommon.h"

namespace mozilla {
namespace net {

namespace {

// Big picture comment
// -----------------------------------------------------------------------------
// nsUrlClassifierDBService::channelClassify() classifies a channel using a set
// of URL-Classifier features. This method minimizes the number of lookups and
// URI parsing and this is done using the classes here described.
//
// The first class is 'FeatureTask' which is able to retrieve the list of
// features for this channel using the feature-factory. See
// UrlClassifierFeatureFactory.
// For each feature, it creates a FeatureData object, which contains the
// entitylist and blocklist prefs and tables. The reason why we create
// FeatureData is because:
// - features are not thread-safe.
// - we want to store the state of the classification in the FeatureData
//   object.
//
// It can happen that multiple features share the same tables. In order to do
// the lookup just once, we have TableData class. When multiple features
// contain the same table, they have references to the same couple TableData +
// URIData objects.
//
// During the classification, the channel's URIs are fragmented. In order to
// create these fragments just once, we use the URIData class, which is pointed
// by TableData classes.
//
// The creation of these classes happens on the main-thread. The classification
// happens on the worker thread.

// URIData
// -----------------------------------------------------------------------------

// In order to avoid multiple URI parsing, we have this class which contains
// nsIURI and its fragments.
class URIData {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(URIData);

  static nsresult Create(nsIURI* aURI, nsIURI* aInnermostURI,
                         nsIUrlClassifierFeature::URIType aURIType,
                         URIData** aData);

  bool IsEqual(nsIURI* aURI) const;

  const nsTArray<nsCString>& Fragments();

  nsIURI* URI() const;

 private:
  URIData();
  ~URIData() = default;

  nsCOMPtr<nsIURI> mURI;
  nsCString mURISpec;
  nsTArray<nsCString> mFragments;
  nsIUrlClassifierFeature::URIType mURIType =
      nsIUrlClassifierFeature::blocklistURI;
};

/* static */
nsresult URIData::Create(nsIURI* aURI, nsIURI* aInnermostURI,
                         nsIUrlClassifierFeature::URIType aURIType,
                         URIData** aData) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aURI);
  MOZ_ASSERT(aInnermostURI);

  RefPtr<URIData> data = new URIData();
  data->mURI = aURI;
  data->mURIType = aURIType;

  nsUrlClassifierUtils* utilsService = nsUrlClassifierUtils::GetInstance();
  if (NS_WARN_IF(!utilsService)) {
    return NS_ERROR_FAILURE;
  }

  nsresult rv = utilsService->GetKeyForURI(aInnermostURI, data->mURISpec);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::URIData::Create new URIData created for spec "
       "%s [this=%p]",
       data->mURISpec.get(), data.get()));

  data.forget(aData);
  return NS_OK;
}

URIData::URIData() { MOZ_ASSERT(NS_IsMainThread()); }

bool URIData::IsEqual(nsIURI* aURI) const {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aURI);

  bool isEqual = false;
  nsresult rv = mURI->Equals(aURI, &isEqual);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  return isEqual;
}

const nsTArray<nsCString>& URIData::Fragments() {
  MOZ_ASSERT(!NS_IsMainThread());

  if (mFragments.IsEmpty()) {
    if (mURIType == nsIUrlClassifierFeature::pairwiseEntitylistURI) {
      LookupCache::GetLookupEntitylistFragments(mURISpec, &mFragments);
    } else {
      LookupCache::GetLookupFragments(mURISpec, &mFragments);
    }
  }

  return mFragments;
}

nsIURI* URIData::URI() const {
  MOZ_ASSERT(NS_IsMainThread());
  return mURI;
}

// TableData
// ----------------------------------------------------------------------------

// In order to avoid multiple lookups on the same table + URI, we have this
// class.
class TableData {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TableData);

  enum State {
    eUnclassified,
    eNoMatch,
    eMatch,
  };

  TableData(URIData* aURIData, const nsACString& aTable);

  nsIURI* URI() const;

  const nsACString& Table() const;

  const LookupResultArray& Result() const;

  State MatchState() const;

  bool IsEqual(URIData* aURIData, const nsACString& aTable) const;

  // Returns true if the table classifies the URI. This method must be called
  // on hte classifier worker thread.
  bool DoLookup(nsUrlClassifierDBServiceWorker* aWorkerClassifier);

 private:
  ~TableData();

  RefPtr<URIData> mURIData;
  State mState;

  nsCString mTable;
  LookupResultArray mResults;
};

TableData::TableData(URIData* aURIData, const nsACString& aTable)
    : mURIData(aURIData), mState(eUnclassified), mTable(aTable) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aURIData);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::TableData CTOR - new TableData created %s "
       "[this=%p]",
       PromiseFlatCString(aTable).get(), this));
}

TableData::~TableData() = default;

nsIURI* TableData::URI() const {
  MOZ_ASSERT(NS_IsMainThread());
  return mURIData->URI();
}

const nsACString& TableData::Table() const {
  MOZ_ASSERT(NS_IsMainThread());
  return mTable;
}

const LookupResultArray& TableData::Result() const {
  MOZ_ASSERT(NS_IsMainThread());
  return mResults;
}

TableData::State TableData::MatchState() const {
  MOZ_ASSERT(NS_IsMainThread());
  return mState;
}

bool TableData::IsEqual(URIData* aURIData, const nsACString& aTable) const {
  MOZ_ASSERT(NS_IsMainThread());
  return mURIData == aURIData && mTable == aTable;
}

bool TableData::DoLookup(nsUrlClassifierDBServiceWorker* aWorkerClassifier) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aWorkerClassifier);

  if (mState == TableData::eUnclassified) {
    UC_LOG_LEAK(
        ("AsyncChannelClassifier::TableData::DoLookup - starting lookup "
         "[this=%p]",
         this));

    const nsTArray<nsCString>& fragments = mURIData->Fragments();
    nsresult rv = aWorkerClassifier->DoSingleLocalLookupWithURIFragments(
        fragments, mTable, mResults);
    (void)NS_WARN_IF(NS_FAILED(rv));

    mState = mResults.IsEmpty() ? TableData::eNoMatch : TableData::eMatch;

    UC_LOG_LEAK(
        ("AsyncChannelClassifier::TableData::DoLookup - lookup completed. "
         "Matches: %d [this=%p]",
         (int)mResults.Length(), this));
  }

  return !mResults.IsEmpty();
}

// FeatureData
// ----------------------------------------------------------------------------

class FeatureTask;

// This is class contains all the Feature data.
class FeatureData {
  enum State {
    eUnclassified,
    eNoMatch,
    eMatchBlocklist,
    eMatchEntitylist,
  };

 public:
  FeatureData() = default;
  ~FeatureData();

  nsresult Initialize(FeatureTask* aTask, nsIChannel* aChannel,
                      nsIUrlClassifierFeature* aFeature);

  void DoLookup(nsUrlClassifierDBServiceWorker* aWorkerClassifier);

  // Returns true if the next feature should be processed.
  bool MaybeCompleteClassification(nsIChannel* aChannel);

 private:
  nsresult InitializeList(FeatureTask* aTask, nsIChannel* aChannel,
                          nsIUrlClassifierFeature::listType aListType,
                          nsTArray<RefPtr<TableData>>& aList);

  State mState{eUnclassified};
  nsCOMPtr<nsIUrlClassifierFeature> mFeature;

  nsTArray<RefPtr<TableData>> mBlocklistTables;
  nsTArray<RefPtr<TableData>> mEntitylistTables;

  // blocklist + entitylist.
  nsCString mHostInPrefTables[2];
};

FeatureData::~FeatureData() {
  NS_ReleaseOnMainThread("FeatureData:mFeature", mFeature.forget());
}

nsresult FeatureData::Initialize(FeatureTask* aTask, nsIChannel* aChannel,
                                 nsIUrlClassifierFeature* aFeature) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aTask);
  MOZ_ASSERT(aChannel);
  MOZ_ASSERT(aFeature);

  if (UC_LOG_ENABLED()) {
    nsAutoCString name;
    aFeature->GetName(name);
    UC_LOG_LEAK(
        ("AsyncChannelClassifier::FeatureData::Initialize - Feature %s "
         "[this=%p, channel=%p]",
         name.get(), this, aChannel));
  }

  mFeature = aFeature;

  nsresult rv = InitializeList(
      aTask, aChannel, nsIUrlClassifierFeature::blocklist, mBlocklistTables);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = InitializeList(aTask, aChannel, nsIUrlClassifierFeature::entitylist,
                      mEntitylistTables);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

void FeatureData::DoLookup(nsUrlClassifierDBServiceWorker* aWorkerClassifier) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aWorkerClassifier);
  MOZ_ASSERT(mState == eUnclassified);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::DoLookup - lookup starting "
       "[this=%p]",
       this));

  // This is wrong, but it's fast: we don't want to check if the host is in the
  // blocklist table if we know that it's going to be entitylisted by pref.
  // So, also if maybe it's not blocklisted, let's consider it 'entitylisted'.
  if (!mHostInPrefTables[nsIUrlClassifierFeature::entitylist].IsEmpty()) {
    UC_LOG_LEAK(
        ("AsyncChannelClassifier::FeatureData::DoLookup - entitylisted by pref "
         "[this=%p]",
         this));
    mState = eMatchEntitylist;
    return;
  }

  // Let's check if this feature blocklists the URI.

  bool isBlocklisted =
      !mHostInPrefTables[nsIUrlClassifierFeature::blocklist].IsEmpty();

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::DoLookup - blocklisted by pref: "
       "%d [this=%p]",
       isBlocklisted, this));

  if (!isBlocklisted) {
    // If one of the blocklist table matches the URI, we don't need to continue
    // with the others: the feature is blocklisted (but maybe also
    // entitylisted).
    for (TableData* tableData : mBlocklistTables) {
      if (tableData->DoLookup(aWorkerClassifier)) {
        isBlocklisted = true;
        break;
      }
    }
  }

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::DoLookup - blocklisted before "
       "entitylisting: %d [this=%p]",
       isBlocklisted, this));

  if (!isBlocklisted) {
    mState = eNoMatch;
    return;
  }

  // Now, let's check if we need to entitylist the same URI.

  for (TableData* tableData : mEntitylistTables) {
    // If one of the entitylist table matches the URI, we don't need to continue
    // with the others: the feature is entitylisted.
    if (tableData->DoLookup(aWorkerClassifier)) {
      UC_LOG_LEAK(
          ("AsyncChannelClassifier::FeatureData::DoLookup - entitylisted by "
           "table [this=%p]",
           this));
      mState = eMatchEntitylist;
      return;
    }
  }

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::DoLookup - blocklisted [this=%p]",
       this));
  mState = eMatchBlocklist;
}

bool FeatureData::MaybeCompleteClassification(nsIChannel* aChannel) {
  MOZ_ASSERT(NS_IsMainThread());

  nsAutoCString name;
  mFeature->GetName(name);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::MaybeCompleteClassification - "
       "completing "
       "classification [this=%p channel=%p]",
       this, aChannel));

  switch (mState) {
    case eNoMatch:
      UC_LOG(
          ("AsyncChannelClassifier::FeatureData::MaybeCompleteClassification - "
           "no match for feature %s. Let's "
           "move on [this=%p channel=%p]",
           name.get(), this, aChannel));
      return true;

    case eMatchEntitylist:
      UC_LOG(
          ("AsyncChannelClassifier::FeatureData::MayebeCompleteClassification "
           "- entitylisted by feature %s. Let's "
           "move on [this=%p channel=%p]",
           name.get(), this, aChannel));
      return true;

    case eMatchBlocklist:
      UC_LOG(
          ("AsyncChannelClassifier::FeatureData::MaybeCompleteClassification - "
           "blocklisted by feature %s [this=%p channel=%p]",
           name.get(), this, aChannel));
      break;

    case eUnclassified:
      MOZ_CRASH("We should not be here!");
      break;
  }

  MOZ_ASSERT(mState == eMatchBlocklist);

  // Maybe we have to ignore this host
  nsCOMPtr<nsIUrlClassifierExceptionList> exceptionList;
  nsresult rv = mFeature->GetExceptionList(getter_AddRefs(exceptionList));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    UC_LOG_WARN(
        ("AsyncChannelClassifier::FeatureData::MaybeCompleteClassification - "
         "error while getting exception list. Let's move on [exceptionList=%p "
         "this=%p channel=%p]",
         exceptionList.get(), this, aChannel));
    return true;
  }

  // Check if current load is allow-listed by the exception list.
  if (!mBlocklistTables.IsEmpty() && exceptionList) {
    // Get top level URI from channel.
    nsCOMPtr<nsIURI> topLevelURI;
    rv = UrlClassifierCommon::GetTopWindowURI(aChannel,
                                              getter_AddRefs(topLevelURI));
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to get top level URI");

    bool isPrivateBrowsing = NS_UsePrivateBrowsing(aChannel);
    bool isAllowListed = false;
    rv = exceptionList->Matches(mBlocklistTables[0]->URI(), topLevelURI,
                                isPrivateBrowsing, &isAllowListed);
    if (NS_SUCCEEDED(rv) && isAllowListed) {
      nsCString spec = mBlocklistTables[0]->URI()->GetSpecOrDefault();
      spec.Truncate(
          std::min(spec.Length(), UrlClassifierCommon::sMaxSpecLength));
      UC_LOG(
          ("AsyncChannelClassifier::FeatureData::MaybeCompleteClassification - "
           "uri %s found in "
           "exceptionlist of feature %s [this=%p channel=%p]",
           spec.get(), name.get(), this, aChannel));
      return true;
    }
  }

  nsTArray<nsCString> list;
  nsTArray<nsCString> hashes;
  if (!mHostInPrefTables[nsIUrlClassifierFeature::blocklist].IsEmpty()) {
    list.AppendElement(mHostInPrefTables[nsIUrlClassifierFeature::blocklist]);

    // Telemetry expects every tracking channel has hash, create it for test
    // entry
    Completion complete;
    complete.FromPlaintext(
        mHostInPrefTables[nsIUrlClassifierFeature::blocklist]);
    hashes.AppendElement(complete.ToString());
  }

  for (TableData* tableData : mBlocklistTables) {
    if (tableData->MatchState() == TableData::eMatch) {
      list.AppendElement(tableData->Table());

      for (const auto& r : tableData->Result()) {
        hashes.AppendElement(r->hash.complete.ToString());
      }
    }
  }

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::MaybeCompleteClassification - "
       "process channel [this=%p channel=%p]",
       this, aChannel));

  bool shouldContinue = false;
  rv = mFeature->ProcessChannel(aChannel, list, hashes, &shouldContinue);
  (void)NS_WARN_IF(NS_FAILED(rv));

  return shouldContinue;
}

// CallbackHolder
// ----------------------------------------------------------------------------

// This class keeps the callback alive and makes sure that we release it on the
// correct thread.
class CallbackHolder final {
 public:
  NS_INLINE_DECL_REFCOUNTING(CallbackHolder);

  explicit CallbackHolder(std::function<void()>&& aCallback)
      : mCallback(std::move(aCallback)) {}

  void Exec() const { mCallback(); }

 private:
  ~CallbackHolder() = default;

  std::function<void()> mCallback;
};

// FeatureTask
// ----------------------------------------------------------------------------

// A FeatureTask is a class that is able to classify a channel using a set of
// features. The features are grouped by:
// - URIs - to avoid extra URI parsing.
// - Tables - to avoid multiple lookup on the same table.
class FeatureTask {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FeatureTask);

  static nsresult Create(nsIChannel* aChannel,
                         std::function<void()>&& aCallback,
                         FeatureTask** aTask);
  static nsresult CreateWithFeatures(
      nsIChannel* aChannel,
      const nsTArray<nsCOMPtr<nsIUrlClassifierFeature>>& aFeatures,
      std::function<void()>&& aCallback, FeatureTask** aTask);

  // Called on the classifier thread.
  void DoLookup(nsUrlClassifierDBServiceWorker* aWorkerClassifier);

  // Called on the main-thread to process the channel.
  void CompleteClassification();

  nsresult GetOrCreateURIData(nsIURI* aURI, nsIURI* aInnermostURI,
                              nsIUrlClassifierFeature::URIType aURIType,
                              URIData** aData);

  nsresult GetOrCreateTableData(URIData* aURIData, const nsACString& aTable,
                                TableData** aData);

 private:
  FeatureTask(nsIChannel* aChannel, std::function<void()>&& aCallback);
  ~FeatureTask();

  nsCOMPtr<nsIChannel> mChannel;
  RefPtr<CallbackHolder> mCallbackHolder;

  nsTArray<FeatureData> mFeatures;
  nsTArray<RefPtr<URIData>> mURIs;
  nsTArray<RefPtr<TableData>> mTables;
};

// Features are able to classify particular URIs from a channel. For instance,
// tracking-annotation feature uses the top-level URI to entitylist the current
// channel's URI.  Because of
// this, this function aggregates feature per URI and tables.
/* static */
nsresult FeatureTask::Create(nsIChannel* aChannel,
                             std::function<void()>&& aCallback,
                             FeatureTask** aTask) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aChannel);
  MOZ_ASSERT(aTask);

  // We need to obtain the list of nsIUrlClassifierFeature objects able to
  // classify this channel. If the list is empty, we do an early return.
  nsTArray<nsCOMPtr<nsIUrlClassifierFeature>> features;
  UrlClassifierFeatureFactory::GetFeaturesFromChannel(aChannel, features);
  return CreateWithFeatures(aChannel, features, std::move(aCallback), aTask);
}

/* static */
nsresult FeatureTask::CreateWithFeatures(
    nsIChannel* aChannel,
    const nsTArray<nsCOMPtr<nsIUrlClassifierFeature>>& aFeatures,
    std::function<void()>&& aCallback, FeatureTask** aTask) {
  if (aFeatures.IsEmpty()) {
    UC_LOG(
        ("AsyncChannelClassifier::FeatureTask::CreateWithFeatures - no task is "
         "needed for "
         "channel %p",
         aChannel));
    return NS_OK;
  }

  RefPtr<FeatureTask> task = new FeatureTask(aChannel, std::move(aCallback));

  UC_LOG(
      ("AsyncChannelClassifier::FeatureTask::CreateWithFeatures - FeatureTask "
       "%p created "
       "for channel %p",
       task.get(), aChannel));

  for (nsIUrlClassifierFeature* feature : aFeatures) {
    FeatureData* featureData = task->mFeatures.AppendElement();
    nsresult rv = featureData->Initialize(task, aChannel, feature);
    if (NS_FAILED(rv)) {
      return rv;
    }
  }

  task.forget(aTask);
  return NS_OK;
}

FeatureTask::FeatureTask(nsIChannel* aChannel,
                         std::function<void()>&& aCallback)
    : mChannel(aChannel) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mChannel);

  std::function<void()> callback = std::move(aCallback);
  mCallbackHolder = new CallbackHolder(std::move(callback));
}

FeatureTask::~FeatureTask() {
  NS_ReleaseOnMainThread("FeatureTask::mChannel", mChannel.forget());
  NS_ReleaseOnMainThread("FeatureTask::mCallbackHolder",
                         mCallbackHolder.forget());
}

nsresult FeatureTask::GetOrCreateURIData(
    nsIURI* aURI, nsIURI* aInnermostURI,
    nsIUrlClassifierFeature::URIType aURIType, URIData** aData) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aURI);
  MOZ_ASSERT(aInnermostURI);
  MOZ_ASSERT(aData);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureTask::GetOrCreateURIData - checking if "
       "a URIData must be "
       "created [this=%p]",
       this));

  for (URIData* data : mURIs) {
    if (data->IsEqual(aURI)) {
      UC_LOG_LEAK(
          ("AsyncChannelClassifier::FeatureTask::GetOrCreateURIData - reuse "
           "existing URIData %p [this=%p]",
           data, this));

      RefPtr<URIData> uriData = data;
      uriData.forget(aData);
      return NS_OK;
    }
  }

  RefPtr<URIData> data;
  nsresult rv =
      URIData::Create(aURI, aInnermostURI, aURIType, getter_AddRefs(data));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  mURIs.AppendElement(data);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureTask::GetOrCreateURIData - create new "
       "URIData %p [this=%p]",
       data.get(), this));

  data.forget(aData);
  return NS_OK;
}

nsresult FeatureTask::GetOrCreateTableData(URIData* aURIData,
                                           const nsACString& aTable,
                                           TableData** aData) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aURIData);
  MOZ_ASSERT(aData);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureTask::GetOrCreateTableData - checking "
       "if TableData must be "
       "created [this=%p]",
       this));

  for (TableData* data : mTables) {
    if (data->IsEqual(aURIData, aTable)) {
      UC_LOG_LEAK(
          ("FeatureTask::GetOrCreateTableData - reuse existing TableData %p "
           "[this=%p]",
           data, this));

      RefPtr<TableData> tableData = data;
      tableData.forget(aData);
      return NS_OK;
    }
  }

  RefPtr<TableData> data = new TableData(aURIData, aTable);
  mTables.AppendElement(data);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureTask::GetOrCreateTableData - create new "
       "TableData %p [this=%p]",
       data.get(), this));

  data.forget(aData);
  return NS_OK;
}

void FeatureTask::DoLookup(nsUrlClassifierDBServiceWorker* aWorkerClassifier) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aWorkerClassifier);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureTask::DoLookup - starting lookup "
       "[this=%p]",
       this));

  for (FeatureData& feature : mFeatures) {
    feature.DoLookup(aWorkerClassifier);
  }

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureTask::DoLookup - lookup completed "
       "[this=%p]",
       this));
}

void FeatureTask::CompleteClassification() {
  MOZ_ASSERT(NS_IsMainThread());

  for (FeatureData& feature : mFeatures) {
    if (!feature.MaybeCompleteClassification(mChannel)) {
      break;
    }
  }

  UC_LOG(
      ("AsyncChannelClassifier::FeatureTask::CompleteClassification - complete "
       "classification for "
       "channel %p [this=%p]",
       mChannel.get(), this));

  mCallbackHolder->Exec();
}

nsresult FeatureData::InitializeList(
    FeatureTask* aTask, nsIChannel* aChannel,
    nsIUrlClassifierFeature::listType aListType,
    nsTArray<RefPtr<TableData>>& aList) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aTask);
  MOZ_ASSERT(aChannel);

  UC_LOG_LEAK(
      ("AsyncChannelClassifier::FeatureData::InitializeList - initialize list "
       "%d for channel %p [this=%p]",
       aListType, aChannel, this));

  nsCOMPtr<nsIURI> uri;
  nsIUrlClassifierFeature::URIType URIType;
  nsresult rv = mFeature->GetURIByListType(aChannel, aListType, &URIType,
                                           getter_AddRefs(uri));
  if (NS_FAILED(rv)) {
    if (UC_LOG_ENABLED()) {
      nsAutoCString errorName;
      GetErrorName(rv, errorName);
      UC_LOG_LEAK(
          ("AsyncChannelClassifier::FeatureData::InitializeList - Got an "
           "unexpected error (rv=%s) [this=%p]",
           errorName.get(), this));
    }
    return rv;
  }

  if (!uri) {
    // Return success when the URI is empty to conitnue to do the lookup.
    UC_LOG_LEAK(
        ("AsyncChannelClassifier::FeatureData::InitializeList - got an empty "
         "URL [this=%p]",
         this));
    return NS_OK;
  }

  nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(uri);
  if (NS_WARN_IF(!innermostURI)) {
    return NS_ERROR_FAILURE;
  }

  nsAutoCString host;
  rv = innermostURI->GetHost(host);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  bool found = false;
  nsAutoCString tableName;
  rv = mFeature->HasHostInPreferences(host, aListType, tableName, &found);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (found) {
    mHostInPrefTables[aListType] = tableName;
  }

  RefPtr<URIData> uriData;
  rv = aTask->GetOrCreateURIData(uri, innermostURI, URIType,
                                 getter_AddRefs(uriData));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  MOZ_ASSERT(uriData);

  nsTArray<nsCString> tables;
  rv = mFeature->GetTables(aListType, tables);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  for (const nsCString& table : tables) {
    RefPtr<TableData> data;
    rv = aTask->GetOrCreateTableData(uriData, table, getter_AddRefs(data));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    MOZ_ASSERT(data);
    aList.AppendElement(data);
  }

  return NS_OK;
}

}  // namespace

/* static */
void AsyncUrlChannelClassifier::WarmUp() {
  // Trigger the construction of the singleton instance.
  nsresult rv;
  RefPtr<nsUrlClassifierDBService> service =
      nsUrlClassifierDBService::GetInstance(&rv);
}

/* static */
nsresult AsyncUrlChannelClassifier::CheckChannel(
    nsIChannel* aChannel, std::function<void()>&& aCallback) {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(aChannel);
  return AntiTrackingChannelClassifierUtils::CheckChannelBeforeBeginConnect(
      aChannel, std::move(aCallback));
}

/* static */
nsresult AntiTrackingChannelClassifierUtils::CheckChannelHelper(
    nsIChannel* aChannel, std::function<void()>&& aCallback,
    bool aPerformAnnotations, bool aPerformBlocking) {
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(aChannel);

  if (!aCallback) {
    return NS_ERROR_INVALID_ARG;
  }

  if (UC_LOG_ENABLED()) {
    nsCOMPtr<nsIURI> chanURI;
    if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(chanURI)))) {
      nsCString chanSpec = chanURI->GetSpecOrDefault();
      chanSpec.Truncate(
          std::min(chanSpec.Length(), UrlClassifierCommon::sMaxSpecLength));

      nsCOMPtr<nsIURI> topWinURI;
      (void)UrlClassifierCommon::GetTopWindowURI(aChannel,
                                                 getter_AddRefs(topWinURI));
      nsCString topWinSpec =
          topWinURI ? topWinURI->GetSpecOrDefault() : "(null)"_ns;

      topWinSpec.Truncate(
          std::min(topWinSpec.Length(), UrlClassifierCommon::sMaxSpecLength));

      UC_LOG(
          ("AntiTrackingChannelClassifierUtils::CheckChannelHelper - starting "
           "the "
           "classification on channel %p - running classifiers for "
           "%sannotating and %sblocking",
           aChannel, aPerformAnnotations ? "" : "non-",
           aPerformBlocking ? "" : "non-"));
      UC_LOG(("    uri is %s [channel=%p]", chanSpec.get(), aChannel));
      UC_LOG(
          ("    top-level uri is %s [channel=%p]", topWinSpec.get(), aChannel));
    }
  }

  std::function<void()> callbackFromFeature = aCallback;

  // Acquire the ContentClassifier service. We check if it's initialized and
  // enabled before returning the instance. If it's returning a nullptr, the
  // service is either not initialized or disabled.
  RefPtr<ContentClassifierService> contentClassifier =
      ContentClassifierService::GetInstance();

  const bool skipURLClassifier =
      StaticPrefs::
          privacy_trackingprotection_urlclassifier_disable_for_channel_classifier() &&
      contentClassifier;

  RefPtr<FeatureTask> task = nullptr;

  if (!skipURLClassifier) {
    if (aPerformAnnotations && aPerformBlocking) {
      nsresult rv = FeatureTask::Create(aChannel, std::move(aCallback),
                                        getter_AddRefs(task));
      if (NS_FAILED(rv)) {
        return rv;
      }
    } else if (aPerformAnnotations) {
      nsTArray<nsCOMPtr<nsIUrlClassifierFeature>> features;
      UrlClassifierFeatureFactory::GetNonCancelingFeaturesFromChannel(aChannel,
                                                                      features);
      nsresult rv = FeatureTask::CreateWithFeatures(
          aChannel, features, std::move(aCallback), getter_AddRefs(task));
      if (NS_FAILED(rv)) {
        return rv;
      }
    } else if (aPerformBlocking) {
      nsTArray<nsCOMPtr<nsIUrlClassifierFeature>> features;
      UrlClassifierFeatureFactory::GetCancelingFeaturesFromChannel(aChannel,
                                                                   features);
      nsresult rv = FeatureTask::CreateWithFeatures(
          aChannel, features, std::move(aCallback), getter_AddRefs(task));
      if (NS_FAILED(rv)) {
        return rv;
      }
    } else {
      return NS_ERROR_FAILURE;
    }
  }

  Maybe<ContentClassifierRequest> contentClassifierRequest;
  if (contentClassifier) {
    contentClassifierRequest.emplace(aChannel);
  }

  if (!task && !contentClassifier) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<nsUrlClassifierDBServiceWorker> workerClassifier =
      nsUrlClassifierDBService::GetWorker();
  if (NS_WARN_IF(!workerClassifier)) {
    return NS_ERROR_FAILURE;
  }

  // raise the priority of URLClassifier's return dispatch to the MainThread if
  // the channel is considered important
  EventQueuePriority eventPriority = EventQueuePriority::Normal;
  if (aPerformBlocking) {
    if (nsCOMPtr<HttpBaseChannel> baseChannel = do_QueryInterface(aChannel)) {
      uint32_t classOfServiceFlags = 0;
      baseChannel->GetClassFlags(&classOfServiceFlags);
      if (classOfServiceFlags &
          (nsIClassOfService::Leader | nsIClassOfService::UrgentStart |
           nsIClassOfService::Unblocked)) {
        eventPriority = EventQueuePriority::MediumHigh;
      }
    }
    if (nsCOMPtr<nsISupportsPriority> supportsPriority =
            do_QueryInterface(aChannel)) {
      int32_t priority = nsISupportsPriority::PRIORITY_NORMAL;
      supportsPriority->GetPriority(&priority);
      // note that higher priorities have lower numeric values
      if (priority <= nsISupportsPriority::PRIORITY_HIGH) {
        eventPriority = EventQueuePriority::MediumHigh;
      }
    }
  }

  PROFILER_MARKER("AntiTrackingChannelClassifier::CheckChannelHelper", NETWORK,
                  MarkerTiming::IntervalStart(), FlowMarker,
                  Flow::FromPointer(aChannel));
  TimeStamp outerStartTime;
  if (aPerformBlocking) {
    outerStartTime = TimeStamp::Now();
  }

  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
      "AntiTrackingChannelClassifierUtils::CheckChannelHelper",
      [aPerformAnnotations, aPerformBlocking, task, workerClassifier,
       eventPriority, outerStartTime,
       contentClassifierRequest = std::move(contentClassifierRequest),
       contentClassifier, callbackFromFeature = std::move(callbackFromFeature),
       channel = nsCOMPtr<nsIChannel>(aChannel)]() mutable -> void {
        MOZ_ASSERT(!NS_IsMainThread());

        PROFILER_MARKER(
            "AntiTrackingChannelClassifier::CheckChannelHelper lookup", NETWORK,
            MarkerTiming::IntervalStart(), FlowMarker,
            Flow::FromPointer(channel.get()));
        TimeStamp workerStartTime;
        if (aPerformBlocking) {
          workerStartTime = TimeStamp::Now();
        }

        ContentClassifierResult cancelResult;
        ContentClassifierResult annotateResult;

        if (contentClassifier && contentClassifier->IsInitialized() &&
            contentClassifierRequest.isSome()) {
          if (aPerformBlocking) {
            cancelResult =
                contentClassifier->ClassifyForCancel(*contentClassifierRequest);
          }
          if (aPerformAnnotations) {
            annotateResult = contentClassifier->ClassifyForAnnotate(
                *contentClassifierRequest);
          }
        }

        if (task) {
          task->DoLookup(workerClassifier);
        }

        if (aPerformBlocking) {
          glean::urlclassifier::check_channel_helper_worker_time
              .AccumulateRawDuration(TimeStamp::Now() - workerStartTime);
        }
        PROFILER_MARKER(
            "AntiTrackingChannelClassifier::CheckChannelHelper lookup", NETWORK,
            MarkerTiming::IntervalEnd(), FlowMarker,
            Flow::FromPointer(channel.get()));

        NS_DispatchToMainThreadQueue(
            NS_NewRunnableFunction(
                "AntiTrackingChannelClassifierUtils::CheckChannelHelper - "
                "return",
                [aPerformBlocking, task, channel, outerStartTime,
                 cancelResult = std::move(cancelResult),
                 annotateResult = std::move(annotateResult),
                 callbackFromFeature = std::move(callbackFromFeature),
                 contentClassifier]() -> void {
                  if (contentClassifier) {
                    ChannelBlockDecision cancelAction =
                        contentClassifier->MaybeCancelChannel(channel,
                                                              cancelResult);
                    // Only provide annotations if we didn't block the channel.
                    if (cancelAction != ChannelBlockDecision::Blocked) {
                      contentClassifier->MaybeAnnotateChannel(channel,
                                                              annotateResult);
                    }
                  }
                  if (aPerformBlocking) {
                    TimeDuration checkChannelHelperDuration =
                        TimeStamp::Now() - outerStartTime;
                    glean::urlclassifier::check_channel_helper_time
                        .AccumulateRawDuration(checkChannelHelperDuration);
                    PerfStats::RecordMeasurement(
                        PerfStats::Metric::UrlClassifierCheckChannel,
                        checkChannelHelperDuration);
                  }
                  PROFILER_MARKER(
                      "AntiTrackingChannelClassifier::CheckChannelHelper",
                      NETWORK, MarkerTiming::IntervalEnd(), FlowMarker,
                      Flow::FromPointer(channel.get()));

                  // We must always call the callback. Either via the task or
                  // explicitly.
                  if (task) {
                    task->CompleteClassification();
                  } else {
                    callbackFromFeature();
                  }
                }),
            eventPriority);
      });

  // no need to prioritize the dispatch to the URLClassifier thread
  // since overriding prioritization is ignored if we aren't on the MainThread
  return nsUrlClassifierDBService::BackgroundThread()->Dispatch(
      r, NS_DISPATCH_NORMAL);
}

/* static */
nsresult AntiTrackingChannelClassifierUtils::CheckChannelBeforeBeginConnect(
    nsIChannel* aChannel, std::function<void()>&& aCallback) {
  if (StaticPrefs::privacy_trackingprotection_defer_annotation_enabled()) {
    return CheckChannelHelper(aChannel, std::move(aCallback), false, true);
  }
  return CheckChannelHelper(aChannel, std::move(aCallback), true, true);
}

/* static */
nsresult AntiTrackingChannelClassifierUtils::CheckChannelBeforeProcessResponse(
    nsIChannel* aChannel, std::function<void()>&& aCallback) {
  if (StaticPrefs::privacy_trackingprotection_defer_annotation_enabled()) {
    return CheckChannelHelper(aChannel, std::move(aCallback), true, false);
  }

  // The caller is expecting the callback to be called if we return success,
  // so we call it here, maintaining that invariant of CheckChannelHelper up.
  if (aCallback) {
    aCallback();
  }
  return NS_OK;
}

}  // namespace net
}  // namespace mozilla
