// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/blacklist/opt_out_blacklist/opt_out_blacklist.h"

#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram.h"
#include "base/optional.h"
#include "base/strings/stringprintf.h"
#include "base/time/clock.h"
#include "components/blacklist/opt_out_blacklist/opt_out_blacklist_delegate.h"
#include "components/blacklist/opt_out_blacklist/opt_out_blacklist_item.h"
#include "components/blacklist/opt_out_blacklist/opt_out_store.h"

namespace blacklist {

OptOutBlacklist::OptOutBlacklist(std::unique_ptr<OptOutStore> opt_out_store,
                                 base::Clock* clock,
                                 OptOutBlacklistDelegate* blacklist_delegate)
    : loaded_(false),
      opt_out_store_(std::move(opt_out_store)),
      clock_(clock),
      blacklist_delegate_(blacklist_delegate),
      weak_factory_(this) {
  DCHECK(clock_);
  DCHECK(blacklist_delegate_);
}

OptOutBlacklist::~OptOutBlacklist() = default;

void OptOutBlacklist::Init() {
  DCHECK(!loaded_);
  DCHECK(!blacklist_data_);
  base::TimeDelta duration;
  size_t history = 0;
  int threshold = 0;

  std::unique_ptr<BlacklistData::Policy> session_policy;
  if (ShouldUseSessionPolicy(&duration, &history, &threshold)) {
    session_policy =
        std::make_unique<BlacklistData::Policy>(duration, history, threshold);
  }

  std::unique_ptr<BlacklistData::Policy> persistent_policy;
  if (ShouldUsePersistentPolicy(&duration, &history, &threshold)) {
    persistent_policy =
        std::make_unique<BlacklistData::Policy>(duration, history, threshold);
  }

  size_t max_hosts = 0;
  std::unique_ptr<BlacklistData::Policy> host_policy;
  if (ShouldUseHostPolicy(&duration, &history, &threshold, &max_hosts)) {
    host_policy =
        std::make_unique<BlacklistData::Policy>(duration, history, threshold);
  }

  std::unique_ptr<BlacklistData::Policy> type_policy;
  if (ShouldUseTypePolicy(&duration, &history, &threshold)) {
    type_policy =
        std::make_unique<BlacklistData::Policy>(duration, history, threshold);
  }

  auto blacklist_data = std::make_unique<BlacklistData>(
      std::move(session_policy), std::move(persistent_policy),
      std::move(host_policy), std::move(type_policy), max_hosts,
      GetAllowedTypes());

  if (opt_out_store_) {
    opt_out_store_->LoadBlackList(
        std::move(blacklist_data),
        base::BindOnce(&OptOutBlacklist::LoadBlackListDone,
                       weak_factory_.GetWeakPtr()));
  } else {
    LoadBlackListDone(std::move(blacklist_data));
  }
}

base::Time OptOutBlacklist::AddEntry(const std::string& host_name,
                                     bool opt_out,
                                     int type) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  base::Time now = clock_->Now();

  // If the |blacklist_data| has been loaded from |opt_out_store_|,  synchronous
  // operations will be accurate. Otherwise, queue the task to run
  // asynchronously.
  if (loaded_) {
    AddEntrySync(host_name, opt_out, type, now);
  } else {
    QueuePendingTask(base::BindOnce(&OptOutBlacklist::AddEntrySync,
                                    base::Unretained(this), host_name, opt_out,
                                    type, now));
  }

  return now;
}

void OptOutBlacklist::AddEntrySync(const std::string& host_name,
                                   bool opt_out,
                                   int type,
                                   base::Time time) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(loaded_);

  bool host_was_blacklisted =
      blacklist_data_->IsHostBlacklisted(host_name, time);
  bool user_was_blacklisted = blacklist_data_->IsUserOptedOutInGeneral(time);
  blacklist_data_->AddEntry(host_name, opt_out, type, time, false);

  if (!host_was_blacklisted &&
      blacklist_data_->IsHostBlacklisted(host_name, time)) {
    // Notify |blacklist_delegate_| about a new blacklisted host.
    blacklist_delegate_->OnNewBlacklistedHost(host_name, time);
  }

  if (user_was_blacklisted != blacklist_data_->IsUserOptedOutInGeneral(time)) {
    // Notify |blacklist_delegate_| about a new blacklisted host.
    blacklist_delegate_->OnUserBlacklistedStatusChange(
        blacklist_data_->IsUserOptedOutInGeneral(time));
  }

  if (!opt_out_store_)
    return;
  opt_out_store_->AddEntry(opt_out, host_name, type, time);
}

BlacklistReason OptOutBlacklist::IsLoadedAndAllowed(
    const std::string& host_name,
    int type,
    bool ignore_long_term_black_list_rules,
    std::vector<BlacklistReason>* passed_reasons) const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (!loaded_)
    return BlacklistReason::kBlacklistNotLoaded;
  passed_reasons->push_back(BlacklistReason::kBlacklistNotLoaded);

  return blacklist_data_->IsAllowed(host_name, type,
                                    ignore_long_term_black_list_rules,
                                    clock_->Now(), passed_reasons);
}

void OptOutBlacklist::ClearBlackList(base::Time begin_time,
                                     base::Time end_time) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_LE(begin_time, end_time);
  // If the |blacklist_data| has been loaded from |opt_out_store_|,
  // synchronous operations will be accurate. Otherwise, queue the task to run
  // asynchronously.
  if (loaded_) {
    ClearBlackListSync(begin_time, end_time);
  } else {
    QueuePendingTask(base::BindOnce(&OptOutBlacklist::ClearBlackListSync,
                                    base::Unretained(this), begin_time,
                                    end_time));
  }
}

void OptOutBlacklist::ClearBlackListSync(base::Time begin_time,
                                         base::Time end_time) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(loaded_);
  DCHECK_LE(begin_time, end_time);

  // Clear the in-memory rules entirely.
  blacklist_data_->ClearData();
  loaded_ = false;

  // Notify |blacklist_delegate_| that the blacklist is cleared.
  blacklist_delegate_->OnBlacklistCleared(clock_->Now());

  // Delete relevant entries and reload the blacklist into memory.
  if (opt_out_store_) {
    opt_out_store_->ClearBlackList(begin_time, end_time);
    opt_out_store_->LoadBlackList(
        std::move(blacklist_data_),
        base::BindOnce(&OptOutBlacklist::LoadBlackListDone,
                       weak_factory_.GetWeakPtr()));
  } else {
    LoadBlackListDone(std::move(blacklist_data_));
  }
}

void OptOutBlacklist::QueuePendingTask(base::OnceClosure callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!loaded_);
  DCHECK(!callback.is_null());
  pending_callbacks_.push(std::move(callback));
}

void OptOutBlacklist::LoadBlackListDone(
    std::unique_ptr<BlacklistData> blacklist_data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(blacklist_data);
  DCHECK(!loaded_);
  DCHECK(!blacklist_data_);
  loaded_ = true;
  blacklist_data_ = std::move(blacklist_data);

  // Notify |blacklist_delegate_| on current user blacklisted status.
  blacklist_delegate_->OnUserBlacklistedStatusChange(
      blacklist_data_->IsUserOptedOutInGeneral(clock_->Now()));

  // Notify the |blacklist_delegate_| on historical blacklisted hosts.
  for (const auto& entry : blacklist_data_->black_list_item_host_map()) {
    if (blacklist_data_->IsHostBlacklisted(entry.first, clock_->Now())) {
      blacklist_delegate_->OnNewBlacklistedHost(
          entry.first, entry.second.most_recent_opt_out_time().value());
    }
  }

  // Run all pending tasks. |loaded_| may change if ClearBlackList is queued.
  while (pending_callbacks_.size() > 0 && loaded_) {
    std::move(pending_callbacks_.front()).Run();
    pending_callbacks_.pop();
  }
}

}  // namespace blacklist
