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

#include "components/autofill/core/browser/data_model/payments/iban.h"

#include <stddef.h>
#include <stdint.h>

#include <ostream>
#include <string>
#include <string_view>
#include <utility>
#include <variant>

#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/map_util.h"
#include "base/i18n/case_conversion.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/browser/data_model/payments/payments_metadata.h"
#include "components/autofill/core/browser/metrics/payments/iban_metrics.h"
#include "components/autofill/core/browser/suggestions/payments/payments_suggestion_generator_util.h"
#include "components/autofill/core/common/autofill_clock.h"
#include "components/autofill/core/common/autofill_regexes.h"

namespace autofill {

namespace {

// This prefix and suffix length are for local-based IBANs only. Server-based
// IBANs should generally use the same value but the client will respect
// whatever it receives from the server.
static constexpr int kPrefixLength = 2;
static constexpr int kSuffixLength = 4;

// This method does the following steps:
// 1. Move the four initial characters to the end of the string.
// 2. Replace each letter in with two digits, thereby expanding the string,
//    where 'A' = 10, 'B' = 11, ..., 'Z' = 35.
// 3. Treat the converted string as decimal and get the remainder of it on
//    division by 97.
//
// The algorithm is from:
// https://en.wikipedia.org/wiki/International_Bank_Account_Number#Modulo_operation_on_IBAN
int GetRemainderOfIbanValue(const std::u16string& stripped_value) {
  // Move the four initial characters to the end of the string.
  // E.g., GB82WEST12345698765432 -> WEST12345698765432GB82
  std::string rearranged_value =
      base::UTF16ToUTF8(stripped_value.substr(4) + stripped_value.substr(0, 4));

  // Replace each letter in with two digits where 'A' = 10, 'B' = 11, ...,
  // 'Z' = 35.
  std::string iban_decimal_string;
  for (char iban_character : rearranged_value) {
    if (iban_character - 'A' >= 0 && iban_character - 'A' < 26) {
      iban_decimal_string.append(
          base::NumberToString(iban_character - 'A' + 10));
    } else {
      iban_decimal_string.push_back(iban_character);
    }
  }

  // Returns the remainder of `iban_decimal_string` on division by 97.
  // This function returns remainder of `iban_decimal_string` because of the
  // followings:
  // 1) 10^9 <= 2^32. Max int value is 2147483647 which has 10 digits, so 10^9
  //    <= 2^32.
  // 2) a % 97 < 10^2. The remainder of a given number divided by 97 must be
  // less than 10^2, otherwise, it can be divided further.
  // 3) If a, b and c are integers, then (a + b) % c = ((a % c) + b) % c.
  auto mod97 = [](std::string_view s) {
    DCHECK_LE(s.length(), 9u);
    uint32_t i = 0;
    bool success = base::StringToUint(s, &i);
    DCHECK(success);
    return i % 97;
  };
  int remainder = mod97(iban_decimal_string.substr(0, 9));
  std::string fragment = iban_decimal_string.substr(9);
  for (size_t i = 0; i < fragment.length(); i += 7) {
    remainder = mod97(base::NumberToString(remainder) + fragment.substr(i, 7));
  }
  return remainder;
}

std::u16string RemoveIbanSeparators(std::u16string_view value) {
  std::u16string stripped_value;
  base::RemoveChars(value, base::kWhitespaceUTF16, &stripped_value);
  return stripped_value;
}

}  // namespace

constexpr char16_t kCapitalizedIbanGeneralPattern[] =
    u"^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$";

// \u2006 - SIX-PER-EM SPACE (small space).
constexpr char16_t kEllipsisOneSpace = u'\u2006';

Iban::Iban() : record_type_(RecordType::kUnknown) {}

Iban::Iban(const Guid& guid)
    : identifier_(guid), record_type_(RecordType::kLocalIban) {}

Iban::Iban(const InstrumentId& instrument_id)
    : identifier_(instrument_id), record_type_(RecordType::kServerIban) {}

Iban::Iban(const Iban& iban) : Iban() {
  operator=(iban);
}

Iban::~Iban() = default;

Iban& Iban::operator=(const Iban& iban) = default;

PaymentsMetadata Iban::GetMetadata() const {
  CHECK_NE(record_type_, Iban::kUnknown);
  PaymentsMetadata metadata(usage_history_information_);
  metadata.id = record_type_ == Iban::kLocalIban
                    ? guid()
                    : base::NumberToString(instrument_id());
  return metadata;
}

// static
bool Iban::IsValid(std::u16string_view value) {
  std::u16string iban_value = RemoveIbanSeparators(value);
  iban_value = base::i18n::ToUpper(iban_value);
  // IBANs must be at least 15 digits and at most 33 digits long.
  if (iban_value.length() < 15 || iban_value.length() > 33) {
    return false;
  }

  // IBAN must match the regex pattern. Note that we made the IBAN uppercased,
  // so we only need to check against an uppercased pattern.
  if (!MatchesRegex<kCapitalizedIbanGeneralPattern>(iban_value)) {
    return false;
  }

  // IBAN length must match the length of IBANs in the country the IBAN is from.
  const std::string country_code = base::UTF16ToUTF8(iban_value.substr(0, 2));
  if (!IsIbanApplicableInCountry(country_code) ||
      GetLengthOfIbanCountry(GetIbanSupportedCountry(country_code)) !=
          iban_value.length()) {
    return false;
  }

  // IBAN decimal value must have a remainder of 1 when divided by 97.
  return GetRemainderOfIbanValue(iban_value) == 1;
}

// static
std::string Iban::GetCountryCode(const std::u16string& iban_value) {
  CHECK(iban_value.length() >= 2);
  return base::UTF16ToUTF8(base::i18n::ToUpper(iban_value.substr(0, 2)));
}

// static
bool Iban::IsIbanApplicableInCountry(const std::string& country_code) {
  return GetIbanSupportedCountry(country_code) !=
         IbanSupportedCountry::kUnsupported;
}

// static
Iban::IbanSupportedCountry Iban::GetIbanSupportedCountry(
    std::string_view country_code) {
  static constexpr auto kSupportedCountryMap =
      base::MakeFixedFlatMap<std::string_view, IbanSupportedCountry>({
          {"AD", IbanSupportedCountry::kAD}, {"AE", IbanSupportedCountry::kAE},
          {"AL", IbanSupportedCountry::kAL}, {"AT", IbanSupportedCountry::kAT},
          {"AZ", IbanSupportedCountry::kAZ}, {"BA", IbanSupportedCountry::kBA},
          {"BE", IbanSupportedCountry::kBE}, {"BG", IbanSupportedCountry::kBG},
          {"BH", IbanSupportedCountry::kBH}, {"BR", IbanSupportedCountry::kBR},
          {"BY", IbanSupportedCountry::kBY}, {"CH", IbanSupportedCountry::kCH},
          {"CR", IbanSupportedCountry::kCR}, {"CY", IbanSupportedCountry::kCY},
          {"CZ", IbanSupportedCountry::kCZ}, {"DE", IbanSupportedCountry::kDE},
          {"DK", IbanSupportedCountry::kDK}, {"DO", IbanSupportedCountry::kDO},
          {"EE", IbanSupportedCountry::kEE}, {"EG", IbanSupportedCountry::kEG},
          {"ES", IbanSupportedCountry::kES}, {"FI", IbanSupportedCountry::kFI},
          {"FO", IbanSupportedCountry::kFO}, {"FR", IbanSupportedCountry::kFR},
          {"GB", IbanSupportedCountry::kGB}, {"GE", IbanSupportedCountry::kGE},
          {"GI", IbanSupportedCountry::kGI}, {"GL", IbanSupportedCountry::kGL},
          {"GR", IbanSupportedCountry::kGR}, {"GT", IbanSupportedCountry::kGT},
          {"HR", IbanSupportedCountry::kHR}, {"HU", IbanSupportedCountry::kHU},
          {"IE", IbanSupportedCountry::kIE}, {"IL", IbanSupportedCountry::kIL},
          {"IQ", IbanSupportedCountry::kIQ}, {"IS", IbanSupportedCountry::kIS},
          {"IT", IbanSupportedCountry::kIT}, {"JO", IbanSupportedCountry::kJO},
          {"KW", IbanSupportedCountry::kKW}, {"KZ", IbanSupportedCountry::kKZ},
          {"LB", IbanSupportedCountry::kLB}, {"LC", IbanSupportedCountry::kLC},
          {"LI", IbanSupportedCountry::kLI}, {"LT", IbanSupportedCountry::kLT},
          {"LU", IbanSupportedCountry::kLU}, {"LV", IbanSupportedCountry::kLV},
          {"LY", IbanSupportedCountry::kLY}, {"MC", IbanSupportedCountry::kMC},
          {"MD", IbanSupportedCountry::kMD}, {"ME", IbanSupportedCountry::kME},
          {"MK", IbanSupportedCountry::kMK}, {"MR", IbanSupportedCountry::kMR},
          {"MT", IbanSupportedCountry::kMT}, {"MU", IbanSupportedCountry::kMU},
          {"NL", IbanSupportedCountry::kNL}, {"NO", IbanSupportedCountry::kNO},
          {"PK", IbanSupportedCountry::kPK}, {"PL", IbanSupportedCountry::kPL},
          {"PS", IbanSupportedCountry::kPS}, {"PT", IbanSupportedCountry::kPT},
          {"QA", IbanSupportedCountry::kQA}, {"RO", IbanSupportedCountry::kRO},
          {"RS", IbanSupportedCountry::kRS}, {"RU", IbanSupportedCountry::kRU},
          {"SA", IbanSupportedCountry::kSA}, {"SC", IbanSupportedCountry::kSC},
          {"SD", IbanSupportedCountry::kSD}, {"SE", IbanSupportedCountry::kSE},
          {"SI", IbanSupportedCountry::kSI}, {"SK", IbanSupportedCountry::kSK},
          {"SM", IbanSupportedCountry::kSM}, {"ST", IbanSupportedCountry::kST},
          {"SV", IbanSupportedCountry::kSV}, {"TL", IbanSupportedCountry::kTL},
          {"TN", IbanSupportedCountry::kTN}, {"TR", IbanSupportedCountry::kTR},
          {"UA", IbanSupportedCountry::kUA}, {"VA", IbanSupportedCountry::kVA},
          {"VG", IbanSupportedCountry::kVG}, {"XK", IbanSupportedCountry::kXK},
      });
  // Ensure every country enum (excluding kUnsupported) is registered in the
  // map.
  static_assert(kSupportedCountryMap.size() ==
                    static_cast<size_t>(IbanSupportedCountry::kMaxValue),
                "Missing IbanSupportedCountry mapping in kSupportedCountryMap");
  const IbanSupportedCountry* country =
      base::FindOrNull(kSupportedCountryMap, country_code);
  return country ? *country : IbanSupportedCountry::kUnsupported;
}

// static
// IBAN lengths taken from:
// https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country.
size_t Iban::GetLengthOfIbanCountry(IbanSupportedCountry supported_country) {
  switch (supported_country) {
    case IbanSupportedCountry::kAD:
      return 24;
    case IbanSupportedCountry::kAE:
      return 23;
    case IbanSupportedCountry::kAL:
      return 28;
    case IbanSupportedCountry::kAT:
      return 20;
    case IbanSupportedCountry::kAZ:
      return 28;
    case IbanSupportedCountry::kBA:
      return 20;
    case IbanSupportedCountry::kBE:
      return 16;
    case IbanSupportedCountry::kBG:
      return 22;
    case IbanSupportedCountry::kBH:
      return 22;
    case IbanSupportedCountry::kBR:
      return 29;
    case IbanSupportedCountry::kBY:
      return 28;
    case IbanSupportedCountry::kCH:
      return 21;
    case IbanSupportedCountry::kCR:
      return 22;
    case IbanSupportedCountry::kCY:
      return 28;
    case IbanSupportedCountry::kCZ:
      return 24;
    case IbanSupportedCountry::kDE:
      return 22;
    case IbanSupportedCountry::kDK:
      return 18;
    case IbanSupportedCountry::kDO:
      return 28;
    case IbanSupportedCountry::kEE:
      return 20;
    case IbanSupportedCountry::kEG:
      return 29;
    case IbanSupportedCountry::kES:
      return 24;
    case IbanSupportedCountry::kFI:
      return 18;
    case IbanSupportedCountry::kFO:
      return 18;
    case IbanSupportedCountry::kFR:
      return 27;
    case IbanSupportedCountry::kGB:
      return 22;
    case IbanSupportedCountry::kGE:
      return 22;
    case IbanSupportedCountry::kGI:
      return 23;
    case IbanSupportedCountry::kGL:
      return 18;
    case IbanSupportedCountry::kGR:
      return 27;
    case IbanSupportedCountry::kGT:
      return 28;
    case IbanSupportedCountry::kHR:
      return 21;
    case IbanSupportedCountry::kHU:
      return 28;
    case IbanSupportedCountry::kIE:
      return 22;
    case IbanSupportedCountry::kIL:
      return 23;
    case IbanSupportedCountry::kIQ:
      return 23;
    case IbanSupportedCountry::kIS:
      return 26;
    case IbanSupportedCountry::kIT:
      return 27;
    case IbanSupportedCountry::kJO:
      return 30;
    case IbanSupportedCountry::kKW:
      return 30;
    case IbanSupportedCountry::kKZ:
      return 20;
    case IbanSupportedCountry::kLB:
      return 28;
    case IbanSupportedCountry::kLC:
      return 32;
    case IbanSupportedCountry::kLI:
      return 21;
    case IbanSupportedCountry::kLT:
      return 20;
    case IbanSupportedCountry::kLU:
      return 20;
    case IbanSupportedCountry::kLV:
      return 21;
    case IbanSupportedCountry::kLY:
      return 25;
    case IbanSupportedCountry::kMC:
      return 27;
    case IbanSupportedCountry::kMD:
      return 24;
    case IbanSupportedCountry::kME:
      return 22;
    case IbanSupportedCountry::kMK:
      return 19;
    case IbanSupportedCountry::kMR:
      return 27;
    case IbanSupportedCountry::kMT:
      return 31;
    case IbanSupportedCountry::kMU:
      return 30;
    case IbanSupportedCountry::kNL:
      return 18;
    case IbanSupportedCountry::kNO:
      return 15;
    case IbanSupportedCountry::kPK:
      return 24;
    case IbanSupportedCountry::kPL:
      return 28;
    case IbanSupportedCountry::kPS:
      return 29;
    case IbanSupportedCountry::kPT:
      return 25;
    case IbanSupportedCountry::kQA:
      return 29;
    case IbanSupportedCountry::kRO:
      return 24;
    case IbanSupportedCountry::kRU:
      return 33;
    case IbanSupportedCountry::kRS:
      return 22;
    case IbanSupportedCountry::kSA:
      return 24;
    case IbanSupportedCountry::kSC:
      return 31;
    case IbanSupportedCountry::kSD:
      return 18;
    case IbanSupportedCountry::kSE:
      return 24;
    case IbanSupportedCountry::kSI:
      return 19;
    case IbanSupportedCountry::kSK:
      return 24;
    case IbanSupportedCountry::kSM:
      return 27;
    case IbanSupportedCountry::kST:
      return 25;
    case IbanSupportedCountry::kSV:
      return 28;
    case IbanSupportedCountry::kTL:
      return 23;
    case IbanSupportedCountry::kTN:
      return 24;
    case IbanSupportedCountry::kTR:
      return 26;
    case IbanSupportedCountry::kUA:
      return 29;
    case IbanSupportedCountry::kVA:
      return 22;
    case IbanSupportedCountry::kVG:
      return 24;
    case IbanSupportedCountry::kXK:
      return 20;
    case IbanSupportedCountry::kUnsupported:
      NOTREACHED();
  }
}

bool Iban::SetMetadata(const PaymentsMetadata& metadata) {
  // Make sure the ids match.
  if (metadata.id != (record_type_ == RecordType::kLocalIban
                          ? guid()
                          : base::NumberToString(instrument_id()))) {
    return false;
  }
  usage_history_information_.set_use_count(metadata.use_count);
  usage_history_information_.set_use_date(metadata.use_date);
  return true;
}

int Iban::Compare(const Iban& iban) const {
  if (identifier_ < iban.identifier_) {
    return -1;
  }

  if (identifier_ > iban.identifier_) {
    return 1;
  }

  int comparison = nickname_.compare(iban.nickname_);
  if (comparison != 0) {
    return comparison;
  }

  comparison = value_.compare(iban.value_);
  if (comparison != 0) {
    return comparison;
  }

  comparison = prefix_.compare(iban.prefix_);
  if (comparison != 0) {
    return comparison;
  }

  comparison = suffix_.compare(iban.suffix_);
  if (comparison != 0) {
    return comparison;
  }

  if (record_type_ != iban.record_type_) {
    return 1;
  }
  return 0;
}

bool Iban::operator==(const Iban& iban) const {
  return Compare(iban) == 0;
}

void Iban::set_identifier(const std::variant<Guid, InstrumentId>& identifier) {
  if (std::holds_alternative<Guid>(identifier_)) {
    CHECK_NE(record_type_, kServerIban);
  } else {
    CHECK_EQ(record_type_, kServerIban);
  }
  identifier_ = identifier;
}

const std::string& Iban::guid() const {
  CHECK(std::holds_alternative<Guid>(identifier_));
  return std::get<Guid>(identifier_).value();
}

int64_t Iban::instrument_id() const {
  CHECK(std::holds_alternative<InstrumentId>(identifier_));
  return std::get<InstrumentId>(identifier_).value();
}

void Iban::set_value(const std::u16string& value) {
  if (!IsValid(value)) {
    return;
  }
  CHECK_NE(record_type_, Iban::kServerIban);
  // Get rid of all separators in the value and capitalize them before storing.
  value_ = RemoveIbanSeparators(value);
  value_ = base::ToUpperASCII(value_);
  // The `IsValid()` call above ensures we have a valid IBAN length. We should
  // never set the `kPrefixLength` and `kSuffixLength` in a way where they can
  // be longer than the total length of the IBAN.
  CHECK(value_.length() >= kPrefixLength + kSuffixLength);
  prefix_ = value_.substr(0, kPrefixLength);
  suffix_ = value_.substr(value_.length() - kSuffixLength);
}

void Iban::set_nickname(const std::u16string& nickname) {
  // First replace all tabs and newlines with whitespaces and store it as
  // |nickname_|.
  base::ReplaceChars(nickname, u"\t\r\n", u" ", &nickname_);
  // An additional step to collapse whitespaces, this step does:
  // 1. Trim leading and trailing whitespaces.
  // 2. All other whitespace sequences are converted to a single space.
  nickname_ =
      base::CollapseWhitespace(nickname_,
                               /*trim_sequences_with_line_breaks=*/true);
}

void Iban::set_prefix(std::u16string prefix) {
  CHECK_NE(record_type_, Iban::kLocalIban);
  std::u16string capitalized_prefix = base::ToUpperASCII(prefix);
  prefix_ = std::move(capitalized_prefix);
}

void Iban::set_suffix(std::u16string suffix) {
  CHECK_NE(record_type_, Iban::kLocalIban);
  std::u16string capitalized_suffix = base::ToUpperASCII(suffix);
  suffix_ = std::move(capitalized_suffix);
}

bool Iban::IsValid() {
  CHECK_NE(record_type_, RecordType::kUnknown);
  return record_type_ == kServerIban || IsValid(value_);
}

std::string Iban::GetCountryCode() const {
  CHECK(prefix_.length() >= 2);
  return GetCountryCode(prefix_);
}

void Iban::RecordAndLogUse() {
  autofill_metrics::LogDaysSinceLastIbanUse(*this);
  usage_history_information_.RecordUseDate(AutofillClock::Now());
  usage_history_information_.set_use_count(
      usage_history_information_.use_count() + 1);
}

std::u16string Iban::GetIdentifierStringForAutofillDisplay(
    bool is_value_masked) const {
  // `value_` is expected to be empty for server-based IBANs. For local IBANs,
  // it might be empty in rare situations (e.g., keychain is locked).
  if (value_.empty() && record_type_ == kLocalIban) {
    return value_;
  }

  if (is_value_masked) {
    return GetObfuscatedIban(prefix(), suffix());
  }

  // Displaying the full IBAN value is not possible for server-based IBANs.
  CHECK(record_type_ != Iban::kServerIban);

  // Add space separators into the IBAN identifier to display it in groups of
  // four characters. For example, an IBAN with value of DE91100000000123456789
  // will be displayed as: DE91 1000 0000 0123 4567 89.
  std::u16string output;
  output.reserve(value_.length() + (value_.length() - 1) / 4);
  for (size_t i = 0; i < value_.length(); ++i) {
    if (i % 4 == 0 && i > 0) {
      output.push_back(kEllipsisOneSpace);
    }
    output.push_back(value_[i]);
  }

  return output;
}

bool Iban::MatchesPrefixAndSuffix(const Iban& iban) const {
  // Unlike the `Compare()` function, which seeks an exact match between
  // `prefix_` and `suffix_`, the comparison performed here involves matching
  // the prefixes between each other and similarly comparing the suffixes
  // between each other This approach is adopted because the `prefix` and
  // `suffix` received from the server are considered the source of truth.
  // Therefore, even if the values of `kPrefixLength` or `kSuffixLength` change
  // later, leading to differences in length between the client and server, it
  // remains essential to match substrings and identify the matched IBAN.
  bool prefix_matched = prefix().starts_with(iban.prefix()) ||
                        iban.prefix().starts_with(prefix());
  if (!prefix_matched) {
    return false;
  }

  bool suffix_matched = base::EndsWith(suffix(), iban.suffix()) ||
                        base::EndsWith(iban.suffix(), suffix());
  if (!suffix_matched) {
    return false;
  }

  return true;
}

UsageHistoryInformation& Iban::usage_history() {
  return usage_history_information_;
}
const UsageHistoryInformation& Iban::usage_history() const {
  return usage_history_information_;
}

std::ostream& operator<<(std::ostream& os, const Iban& iban) {
  return os << "[id: "
            << (iban.record_type() == Iban::RecordType::kLocalIban
                    ? iban.guid()
                    : base::NumberToString(iban.instrument_id()))
            << ", record_type: "
            << (iban.record_type() == Iban::RecordType::kLocalIban
                    ? "Local IBAN"
                    : "Server IBAN")
            << ", value: " << base::UTF16ToUTF8(iban.value())
            << ", prefix: " << base::UTF16ToUTF8(iban.prefix())
            << ", suffix: " << base::UTF16ToUTF8(iban.suffix())
            << ", nickname: " << base::UTF16ToUTF8(iban.nickname()) << "]";
}

}  // namespace autofill
