/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#pragma once

#include <chrono>
#include <cstdint>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <utility>

#include "fmt/format.h"
#include "paimon/result.h"
#include "paimon/traits.h"

namespace paimon {
/// A generic LRU cache with support for weight-based eviction, time-based expiration,
/// and removal callbacks.
///
/// Uses std::list + unordered_map for O(1) get/put/evict:
/// - list stores entries in LRU order (most recently used at front)
/// - map stores key -> list::iterator for O(1) lookup
///
/// @tparam K Key type
/// @tparam V Value type
/// @tparam Hash Hash function for K (default: std::hash<K>)
/// @tparam KeyEqual Equality function for K (default: std::equal_to<K>)
///
/// @note Thread-safe: all public methods are protected by a read-write lock.
template <typename K, typename V, typename Hash = std::hash<K>,
          typename KeyEqual = std::equal_to<K>>
class GenericLruCache {
 public:
    /// Cause of a cache entry removal, passed to the removal callback.
    enum class RemovalCause {
        EXPLICIT,  // Removed by Invalidate() or InvalidateAll()
        SIZE,      // Evicted because total weight exceeded max_weight
        EXPIRED,   // Evicted because the entry expired (expireAfterAccess)
        REPLACED   // Replaced by a new value for the same key via Put()
    };

    using WeighFunc = std::function<int64_t(const K&, const V&)>;
    using RemovalCallback = std::function<void(const K&, const V&, RemovalCause)>;

    /// Configuration options for the cache.
    struct Options {
        /// Maximum total weight of all entries. Entries are evicted (LRU) when exceeded.
        int64_t max_weight = INT64_MAX;

        /// Time in milliseconds after last access before an entry expires.
        /// A value < 0 disables expiration.
        int64_t expire_after_access_ms = -1;

        /// Function to compute the weight of an entry. If nullptr, each entry has weight 1.
        WeighFunc weigh_func = nullptr;

        /// Callback invoked when an entry is removed (evicted, invalidated, or replaced).
        /// If nullptr, no callback is invoked.
        RemovalCallback removal_callback = nullptr;
    };

    explicit GenericLruCache(Options options) : options_(std::move(options)) {}

    /// Look up a key in the cache. On hit, promotes the entry to the front (most recently
    /// used) and updates its access time. Returns std::nullopt on miss or if the entry
    /// has expired.
    std::optional<V> GetIfPresent(const K& key) {
        std::unique_lock<std::shared_mutex> lock(mutex_);
        return FindPromoteOrExpire(key);
    }

    /// Look up a key. On miss, load via the supplier, insert into cache, and return.
    /// If the supplier returns an error, the error is propagated and nothing is cached.
    Result<V> Get(const K& key, std::function<Result<V>(const K&)> supplier) {
        {
            std::unique_lock<std::shared_mutex> lock(mutex_);
            auto cached = FindPromoteOrExpire(key);
            if (cached.has_value()) {
                return std::move(cached.value());
            }
        }

        // Cache miss: load via supplier outside the lock
        PAIMON_ASSIGN_OR_RAISE(V value, supplier(key));
        int64_t weight = ComputeWeight(key, value);
        if (weight > options_.max_weight) {
            return value;
        }

        std::unique_lock<std::shared_mutex> lock(mutex_);
        // Double-check: another thread may have inserted while we were loading
        auto cached = FindPromoteOrExpire(key);
        if (cached.has_value()) {
            return std::move(cached.value());
        }

        InsertEntry(key, value, weight);
        EvictIfNeeded();
        return value;
    }

    /// Insert or update an entry. If the key already exists, the old value is replaced
    /// and the REPLACED callback is invoked. Triggers eviction if needed.
    /// @return Status::Invalid if the entry's weight exceeds max_weight, Status::OK otherwise.
    Status Put(const K& key, V value) {
        int64_t weight = ComputeWeight(key, value);
        if (weight > options_.max_weight) {
            return Status::Invalid(
                fmt::format("Entry weight {} exceeds cache max weight {}, entry will not be cached",
                            weight, options_.max_weight));
        }

        std::unique_lock<std::shared_mutex> lock(mutex_);
        auto it = lru_map_.find(key);
        if (it != lru_map_.end()) {
            if (ValuesEqual(it->second->value, value)) {
                Promote(it->second);
                return Status::OK();
            }
            ReplaceEntry(it->second, std::move(value), weight);
        } else {
            InsertEntry(key, std::move(value), weight);
        }

        EvictIfNeeded();
        return Status::OK();
    }

    /// Remove a specific entry. Invokes the EXPLICIT removal callback if the key exists.
    void Invalidate(const K& key) {
        std::unique_lock<std::shared_mutex> lock(mutex_);
        auto it = lru_map_.find(key);
        if (it != lru_map_.end()) {
            RemoveEntry(it->second, RemovalCause::EXPLICIT);
        }
    }

    /// Remove all entries. Each entry's EXPLICIT removal callback is invoked.
    void InvalidateAll() {
        std::unique_lock<std::shared_mutex> lock(mutex_);
        while (!lru_list_.empty()) {
            RemoveEntry(std::prev(lru_list_.end()), RemovalCause::EXPLICIT);
        }
        current_weight_ = 0;
    }

    /// @return The number of entries currently in the cache.
    size_t Size() const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        return lru_map_.size();
    }

    /// @return The current total weight of all entries.
    int64_t GetCurrentWeight() const {
        std::shared_lock<std::shared_mutex> lock(mutex_);
        return current_weight_;
    }

    /// @return The maximum weight configured for this cache.
    int64_t GetMaxWeight() const {
        return options_.max_weight;
    }

 private:
    struct CacheEntry {
        K key;
        V value;
        int64_t weight;
        std::chrono::steady_clock::time_point last_access_time;
    };

    using EntryList = std::list<CacheEntry>;
    using EntryMap = std::unordered_map<K, typename EntryList::iterator, Hash, KeyEqual>;

    /// Look up a key, promote if found and not expired, or remove if expired.
    /// Must be called with mutex_ held.
    /// @return The value if found and valid, std::nullopt otherwise.
    std::optional<V> FindPromoteOrExpire(const K& key) {
        auto it = lru_map_.find(key);
        if (it == lru_map_.end()) {
            return std::nullopt;
        }
        auto list_it = it->second;
        if (IsExpired(list_it->last_access_time)) {
            RemoveEntry(list_it, RemovalCause::EXPIRED);
            return std::nullopt;
        }
        Promote(list_it);
        return list_it->value;
    }

    /// Move an entry to the front of the LRU list and update its access time.
    void Promote(typename EntryList::iterator list_it) {
        list_it->last_access_time = std::chrono::steady_clock::now();
        lru_list_.splice(lru_list_.begin(), lru_list_, list_it);
    }

    /// Insert a new entry at the front of the LRU list.
    void InsertEntry(const K& key, V value, int64_t weight) {
        lru_list_.emplace_front(
            CacheEntry{key, std::move(value), weight, std::chrono::steady_clock::now()});
        lru_map_[key] = lru_list_.begin();
        current_weight_ += weight;
    }

    /// Compare two values for equality. For pointers, compares the underlying
    /// pointer first, then dereferences and compares the pointed-to objects.
    /// For other types, uses operator==.
    static bool ValuesEqual(const V& lhs, const V& rhs) {
        if constexpr (is_pointer<V>::value) {
            if (lhs == rhs) {
                return true;
            }
            if (!lhs || !rhs) {
                return false;
            }
            return *lhs == *rhs;
        } else {
            return lhs == rhs;
        }
    }

    /// Replace the value of an existing entry, invoke the REPLACED callback for the old value,
    /// and promote the entry to the front.
    void ReplaceEntry(typename EntryList::iterator list_it, V new_value, int64_t new_weight) {
        current_weight_ -= list_it->weight;

        K key = list_it->key;
        V old_value = std::move(list_it->value);
        list_it->value = std::move(new_value);
        list_it->weight = new_weight;
        list_it->last_access_time = std::chrono::steady_clock::now();
        current_weight_ += new_weight;
        lru_list_.splice(lru_list_.begin(), lru_list_, list_it);

        InvokeCallback(key, old_value, RemovalCause::REPLACED);
    }

    /// Remove an entry from the cache and invoke the removal callback.
    void RemoveEntry(typename EntryList::iterator list_it, RemovalCause cause) {
        lru_map_.erase(list_it->key);
        K key = std::move(list_it->key);
        V value = std::move(list_it->value);
        current_weight_ -= list_it->weight;
        lru_list_.erase(list_it);
        InvokeCallback(key, value, cause);
    }

    /// Evict expired entries from the tail, then evict by weight if still over capacity.
    void EvictIfNeeded() {
        EvictExpired();
        while (current_weight_ > options_.max_weight && !lru_list_.empty()) {
            RemoveEntry(std::prev(lru_list_.end()), RemovalCause::SIZE);
        }
    }

    /// Evict expired entries from the tail of the LRU list.
    /// Since the tail has the oldest access time, we can stop as soon as we find
    /// a non-expired entry.
    void EvictExpired() {
        if (options_.expire_after_access_ms < 0) {
            return;
        }
        auto now = std::chrono::steady_clock::now();
        while (!lru_list_.empty()) {
            auto it = std::prev(lru_list_.end());
            if (!IsExpired(it->last_access_time, now)) {
                break;
            }
            RemoveEntry(it, RemovalCause::EXPIRED);
        }
    }

    /// Compute the weight of an entry using the configured weigh function.
    int64_t ComputeWeight(const K& key, const V& value) const {
        if (options_.weigh_func) {
            return options_.weigh_func(key, value);
        }
        return 1;
    }

    /// Check if an entry has expired based on its last access time.
    bool IsExpired(
        const std::chrono::steady_clock::time_point& last_access_time,
        const std::chrono::steady_clock::time_point& now = std::chrono::steady_clock::now()) const {
        if (options_.expire_after_access_ms < 0) {
            return false;
        }
        auto elapsed =
            std::chrono::duration_cast<std::chrono::milliseconds>(now - last_access_time);
        return elapsed.count() >= options_.expire_after_access_ms;
    }

    /// Invoke the removal callback if one is configured.
    void InvokeCallback(const K& key, const V& value, RemovalCause cause) {
        if (options_.removal_callback) {
            options_.removal_callback(key, value, cause);
        }
    }

    Options options_;
    int64_t current_weight_ = 0;
    EntryList lru_list_;
    EntryMap lru_map_;
    mutable std::shared_mutex mutex_;
};

}  // namespace paimon
