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

#import "components/safe_browsing/ios/browser/safe_browsing_url_allow_list.h"

#include "base/no_destructor.h"
#import "ios/web/public/web_state.h"

using safe_browsing::SBThreatType;

// static
GURL SafeBrowsingUrlAllowList::GetDecisionUrl(
    const security_interstitials::UnsafeResource& resource) {
  return resource.navigation_url.is_valid() ? resource.navigation_url
                                            : resource.url;
}

SafeBrowsingUrlAllowList::SafeBrowsingUrlAllowList(web::WebState* web_state)
    : web_state_(web_state) {}

SafeBrowsingUrlAllowList::~SafeBrowsingUrlAllowList() {
  for (auto& observer : observers_) {
    observer.SafeBrowsingAllowListDestroyed(this);
  }
}

void SafeBrowsingUrlAllowList::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void SafeBrowsingUrlAllowList::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

bool SafeBrowsingUrlAllowList::AreUnsafeNavigationsAllowed(
    const GURL& url,
    std::set<SBThreatType>* threat_types) const {
  return ContainsThreats(url, Policy::kAllowed, threat_types);
}

void SafeBrowsingUrlAllowList::AllowUnsafeNavigations(
    const GURL& url,
    SBThreatType threat_type) {
  SetThreatPolicy(url, threat_type, Policy::kAllowed);
}

void SafeBrowsingUrlAllowList::DisallowUnsafeNavigations(const GURL& url) {
  RevertPolicy(url, Policy::kAllowed);
}

bool SafeBrowsingUrlAllowList::IsUnsafeNavigationDecisionPending(
    const GURL& url,
    std::set<SBThreatType>* threat_types) const {
  return ContainsThreats(url, Policy::kPending, threat_types);
}

void SafeBrowsingUrlAllowList::AddPendingUnsafeNavigationDecision(
    const GURL& url,
    SBThreatType threat_type) {
  SetThreatPolicy(url, threat_type, Policy::kPending);
}

void SafeBrowsingUrlAllowList::RemovePendingUnsafeNavigationDecisions(
    const GURL& url) {
  RevertPolicy(url, Policy::kPending);
}

#pragma mark - Private

SafeBrowsingUrlAllowList::UnsafeNavigationDecisions&
SafeBrowsingUrlAllowList::GetUnsafeNavigationDecisions(const GURL& url) {
  return decisions_[url.GetWithEmptyPath()];
}

const SafeBrowsingUrlAllowList::UnsafeNavigationDecisions&
SafeBrowsingUrlAllowList::GetUnsafeNavigationDecisions(const GURL& url) const {
  static const base::NoDestructor<UnsafeNavigationDecisions> kEmptyDecisions;
  const auto& it = decisions_.find(url.GetWithEmptyPath());
  if (it == decisions_.end())
    return *kEmptyDecisions;
  return it->second;
}

void SafeBrowsingUrlAllowList::SetThreatPolicy(
    const GURL& url,
    safe_browsing::SBThreatType threat_type,
    Policy policy) {
  auto& decisions = GetUnsafeNavigationDecisions(url);
  if (policy == Policy::kDisallowed) {
    decisions.allowed_threats.erase(threat_type);
    decisions.pending_threats.erase(threat_type);
  }
  if (policy == Policy::kPending) {
    decisions.allowed_threats.erase(threat_type);
    decisions.pending_threats.insert(threat_type);
  }
  if (policy == Policy::kAllowed) {
    decisions.allowed_threats.insert(threat_type);
    decisions.pending_threats.erase(threat_type);
  }
  for (auto& observer : observers_) {
    observer.ThreatPolicyUpdated(this, url, threat_type, policy);
  }
  web_state_->DidChangeVisibleSecurityState();
}

bool SafeBrowsingUrlAllowList::ContainsThreats(
    const GURL& url,
    Policy policy,
    std::set<SBThreatType>* threat_types) const {
  const std::set<SBThreatType>* threats_with_policy = nullptr;
  switch (policy) {
    case Policy::kAllowed:
      threats_with_policy = &GetUnsafeNavigationDecisions(url).allowed_threats;
      break;
    case Policy::kPending:
      threats_with_policy = &GetUnsafeNavigationDecisions(url).pending_threats;
      break;
    case Policy::kDisallowed:
      break;
  }
  if (threats_with_policy && !threats_with_policy->empty()) {
    if (threat_types)
      *threat_types = *threats_with_policy;
    return true;
  }
  return false;
}

void SafeBrowsingUrlAllowList::RevertPolicy(const GURL& url, Policy policy) {
  std::set<SBThreatType>* decisions_to_revert = nullptr;
  switch (policy) {
    case Policy::kAllowed:
      decisions_to_revert = &GetUnsafeNavigationDecisions(url).allowed_threats;
      break;
    case Policy::kPending:
      decisions_to_revert = &GetUnsafeNavigationDecisions(url).pending_threats;
      break;
    case Policy::kDisallowed:
      break;
  }
  if (!decisions_to_revert)
    return;

  std::set<SBThreatType> disallowed_threats;
  disallowed_threats.swap(*decisions_to_revert);
  for (auto& observer : observers_) {
    observer.ThreatPolicyBatchUpdated(this, url, disallowed_threats,
                                      Policy::kDisallowed);
  }
  web_state_->DidChangeVisibleSecurityState();
}

#pragma mark - SafeBrowsingUrlAllowList::UnsafeNavigationDecisions

SafeBrowsingUrlAllowList::UnsafeNavigationDecisions::
    UnsafeNavigationDecisions() = default;

SafeBrowsingUrlAllowList::UnsafeNavigationDecisions::
    ~UnsafeNavigationDecisions() = default;
