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

#ifndef COMPONENTS_PERSISTENT_CACHE_PERSISTENT_CACHE_COLLECTION_H_
#define COMPONENTS_PERSISTENT_CACHE_PERSISTENT_CACHE_COLLECTION_H_

#include <stdint.h>

#include <memory>
#include <optional>
#include <string>
#include <string_view>

#include "base/component_export.h"
#include "base/containers/hashing_lru_cache.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/gtest_prod_util.h"
#include "base/sequence_checker.h"
#include "base/thread_annotations.h"
#include "base/types/expected.h"
#include "components/persistent_cache/backend.h"
#include "components/persistent_cache/backend_storage.h"
#include "components/persistent_cache/buffer_provider.h"
#include "components/persistent_cache/entry_metadata.h"

namespace persistent_cache {

enum class Client;
struct PendingBackend;
class PersistentCache;

// Use PersistentCacheCollection to seamlessly access multiple PersistentCache
// instances. For example when used instead of double-keying with backends that
// use disk storage this can result in smaller separated files. Unlike
// PersistentCache itself PersistentCacheCollection is not thread-safe in any
// way.
//
// Example:
//    PersistentCacheCollection collection(temp_dir.GetPath(), 4096);
//    collection.Insert("first_cache_id", "key", value_span);
//    collection.Insert("second_cache_id","key", value_span);
//    ASSIGN_OR_RETURN(auto entry, collection->Find("first_cache_id", "key"),
//      [](persistent_cache::TransactionError error) {
//        // Translate error to return type here.
//      });
//
// Use PersistentCacheCollection to store and retrieve key-value pairs from
// multiple `PersistentCache`s which are created just-in-time.
//
// PersistentCaches stored in the collection can be shared through exported
// parameters but cannot keep being used after they are evicted from the
// collection. PersistentCacheCollection ensures this doesn't happen by
// automatically abandoning caches when evicted.
class COMPONENT_EXPORT(PERSISTENT_CACHE) PersistentCacheCollection {
 public:
  static constexpr size_t kDefaultLruCacheCapacity = 100;

  // Constructs an instance that will use the default storage backend for file
  // management within `top_directory`.
  PersistentCacheCollection(base::FilePath top_directory,
                            int64_t target_footprint,
                            Client client,
                            size_t lru_capacity = kDefaultLruCacheCapacity);

  // Constructs an instance that will use `storage_delegate` for file management
  // within `top_directory`.
  PersistentCacheCollection(
      base::FilePath top_directory,
      int64_t target_footprint,
      std::unique_ptr<BackendStorage::Delegate> storage_delegate,
      Client client,
      size_t lru_capacity = kDefaultLruCacheCapacity);

  PersistentCacheCollection(const PersistentCacheCollection&) = delete;
  PersistentCacheCollection& operator=(const PersistentCacheCollection&) =
      delete;
  ~PersistentCacheCollection();

  // Pass-through to PersistentCache functions that first select the correct
  // cache. `cache_id` must be a US-ASCII string consisting more-or-less of
  // lower-case letters, numbers, and select punctuation; see
  // `BaseNameFromCacheId()` below for gory details.
  base::expected<std::optional<EntryMetadata>, TransactionError> Find(
      const std::string& cache_id,
      base::span<const uint8_t> key,
      BufferProvider buffer_provider);

  base::expected<void, TransactionError> Insert(
      const std::string& cache_id,
      base::span<const uint8_t> key,
      base::span<const uint8_t> content,
      EntryMetadata metadata = EntryMetadata{});

  // Deletes all files used by the collection, including any present on-disk
  // that are not actively in-use.
  void DeleteAllFiles();

  // Returns a pending backend for an independent read-only connection to the
  // persistent cache at `cache_id`, or nothing if the cache's backend is not
  // operating or the params cannot be exported.
  std::optional<PendingBackend> ShareReadOnlyConnection(
      const std::string& cache_id);

  // Returns a pending backend for an independent read-write connection to the
  // cache at `cache_id`, or nothing if the cache's backend is not operating or
  // the params cannot be exported.
  std::optional<PendingBackend> ShareReadWriteConnection(
      const std::string& cache_id);

 private:
  using PersistentCacheLRUMap =
      base::HashingLRUCache<std::string, std::unique_ptr<PersistentCache>>;

  friend class PersistentCacheCollectionTest;
  FRIEND_TEST_ALL_PREFIXES(PersistentCacheCollectionTest, BaseNameFromCacheId);
  FRIEND_TEST_ALL_PREFIXES(PersistentCacheCollectionTest,
                           FullAllowedCharacterSetHandled);
  FRIEND_TEST_ALL_PREFIXES(PersistentCacheCollectionTest, RetrievalAfterClear);
  FRIEND_TEST_ALL_PREFIXES(PersistentCacheCollectionTest,
                           InstancesAbandonnedOnClear);
  FRIEND_TEST_ALL_PREFIXES(PersistentCacheCollectionTest,
                           EvictWhileLockedDeletesFiles);

  // Abandon `cache` associated with `cache_id` in the LRU cache.
  void AbandonCache(const std::string& cache_id,
                    PersistentCache* persistent_cache)
      VALID_CONTEXT_REQUIRED(sequence_checker_);

  // To be called on receiving a transaction error from the cache at `cache_id`.
  TransactionError HandleTransactionError(const std::string& cache_id,
                                          TransactionError error);

  // Deletes files in the instance's directory from oldest to newest until the
  // instance is using no more than 90% of its target footprint.
  void ReduceFootPrint();

  // Returns the PersistentCache for `cache_id`, creating it if needed. Returns
  // nullptr if creation fails.
  PersistentCache* GetOrCreateCache(const std::string& cache_id);

  // Clears out the LRU map for testing.
  void Clear();

  // Returns the basename of the file(s) used by a backend given a cache id. An
  // extension MUST be added to a returned basename before use. Returns an empty
  // path if `cache_id` contains any character that does not match the following
  // regular expression (where '\' escapes the character it precedes):
  // "[\n !\"#$&'()*+,\-./0-9:;<=>?@[\\\]_a-z|~]". In other words, `cache_id`
  // must be a subset of US-ASCII consisting of newline, space, numbers,
  // lower-case letters, and select punctuation.
  static base::FilePath BaseNameFromCacheId(const std::string& cache_id);

  // Returns a string holding all valid characters for a cache id.
  static std::string GetAllAllowedCharactersInCacheIds();

  // Must outlive `persistent_caches_`.
  BackendStorage backend_storage_ GUARDED_BY_CONTEXT(sequence_checker_);

  // Desired maximum disk footprint for the cache collection in bytes.
  const int64_t target_footprint_;
  const size_t lru_capacity_;

  PersistentCacheLRUMap persistent_caches_
      GUARDED_BY_CONTEXT(sequence_checker_);

  // Running tally of how many bytes can be inserted before a footprint
  // reduction is triggered.
  int64_t bytes_until_footprint_reduction_
      GUARDED_BY_CONTEXT(sequence_checker_);

  SEQUENCE_CHECKER(sequence_checker_);
};

}  // namespace persistent_cache

#endif  // COMPONENTS_PERSISTENT_CACHE_PERSISTENT_CACHE_COLLECTION_H_
