//
// Created by Dan Crosby on 10/5/20.
//

#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/ActionPointDistancesCache.hpp"

#include <algorithm>
#include <chrono>
#include <unordered_map>

#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"

#define CACHE_STATS_LOGGING_ false
#define CACHE_STATS_FREQUENCY_SECONDS_ 1

namespace shardok {

// Thread-local cache definition - stores raw pointers for zero overhead access
thread_local ActionPointDistancesCache::TLSCache ActionPointDistancesCache::tlsCache;

#if CACHE_STATS_LOGGING_
// Thread-local statistics for performance monitoring
thread_local struct {
    int persistentHits = 0;
    int persistentMisses = 0;
    int localHits = 0;
    int localMisses = 0;
    int sharedAccesses = 0;
    int evictionEvents = 0;
    int apdLoadedFromFile = 0;
    int apdGeneratedFresh = 0;
    std::chrono::steady_clock::time_point lastReportTime = std::chrono::steady_clock::now();
} cacheStats;

// Helper function to print stats periodically
static void MaybePrintCacheStats() {
    auto now = std::chrono::steady_clock::now();
    if (std::chrono::duration_cast<std::chrono::seconds>(now - cacheStats.lastReportTime).count() >=
        CACHE_STATS_FREQUENCY_SECONDS_) {
        printf("Thread cache stats: %d persistent hits, %d persistent misses, %d local hits, "
               "%d local misses, %d shared accesses, %d eviction events, "
               "%d APD loaded from file, %d APD generated fresh\n",
               cacheStats.persistentHits,
               cacheStats.persistentMisses,
               cacheStats.localHits,
               cacheStats.localMisses,
               cacheStats.sharedAccesses,
               cacheStats.evictionEvents,
               cacheStats.apdLoadedFromFile,
               cacheStats.apdGeneratedFresh);
        cacheStats.lastReportTime = now;
    }
}
#endif

class BadHashException final : public std::exception {
public:
    BadHashException() = default;

    [[nodiscard]] auto what() const noexcept -> const char* override { return "Bad map hash!"; };
};

// Helper function to check if any ice is present on the map
static auto HasIceOnMap(const HexMap* map) -> bool {
    return std::ranges::any_of(*map->terrain(), [](const auto* terrain) {
        return terrain->modifier().ice().present();
    });
}

// Helper function to create a copy of the map with all ice removed
// This ensures AI pathfinding treats ice as impassable water
// This should only be called if ice is present on the map
static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
    using namespace flatbuffers;
    using namespace net::eagle0::shardok::storage::fb;

    // First, create a full copy using the efficient memcpy approach
    auto mapCopy = fb::CopyHexMap(map);

    // Now modify the ice on the mutable copy
    auto* mutableMap = mapCopy.Get();
    const auto* terrainVec = mutableMap->mutable_terrain();

    for (size_t i = 0; i < terrainVec->size(); i++) {
        // Only process tiles with ice
        if (auto* terrain = terrainVec->GetMutableObject(i); terrain->modifier().ice().present()) {
            terrain->mutable_modifier().mutable_ice().mutate_present(false);
            terrain->mutable_modifier().mutable_ice().mutate_integrity(0.0f);
        }
    }

    // Recompute the modifier hash using the canonical function
    // This ensures consistency with the standard hash computation
    mutableMap->mutate_modifier_hash(GetModifierHash(mutableMap));

    return mapCopy;
}

auto ActionPointDistancesCache::MakeCacheKey(
        const MapId& mapId,
        const BattalionTypeSPtr& battalionType,
        const bool includeBravingWater,
        const int braveWaterActionPointCost) -> FullCacheKey {
    return FullCacheKey{
            mapId,
            static_cast<int>(battalionType->typeId),
            includeBravingWater,
            braveWaterActionPointCost >= 0 ? braveWaterActionPointCost : 0};
}

auto ActionPointDistancesCache::GetMapId(const HexMap* map) -> MapId {
    if (map->base_hash() == 0) { throw BadHashException(); }
    const auto modifierId = map->modifier_hash() ? map->modifier_hash() : GetModifierHash(map);

    return MapId{.terrainTypesId = map->base_hash(), .modifierId = modifierId};
}
void ActionPointDistancesCache::ConsolidateThreadLocalCache_Racy() {
    persistentCache.insert(std::begin(sharedDistances), std::end(sharedDistances));
    sharedDistances.clear();

    // Clear the current thread's cache since persistent cache now has everything
    tlsCache.clear();
}

auto ActionPointDistancesCache::GetRaw(
        const HexMap* map,
        const MapId& mapId,
        const BattalionTypeSPtr& battalionType,
        const bool includeBravingWater,
        const int braveWaterActionPointCost) -> const ActionPointDistances* {
    // Create cache key first - check cache before expensive ice-clearing operation
    auto cacheKey =
            MakeCacheKey(mapId, battalionType, includeBravingWater, braveWaterActionPointCost);

    // Check the persistent map first
    if (auto persistentIt = persistentCache.find(cacheKey); persistentIt != persistentCache.end()) {
#if CACHE_STATS_LOGGING_
        cacheStats.persistentHits++;
        MaybePrintCacheStats();
#endif
        // Return directly from persistent cache without TLS insertion
        // This avoids the overhead of thread-local storage operations on hot path
        return persistentIt->second.rawPtr;
    }

#if CACHE_STATS_LOGGING_
    cacheStats.persistentMisses++;
#endif

    // Check thread-local cache first (no locks needed!)
    if (auto localIt = tlsCache.find(cacheKey); localIt != tlsCache.end()) {
#if CACHE_STATS_LOGGING_
        cacheStats.localHits++;
        MaybePrintCacheStats();
#endif
        return localIt->second.rawPtr;  // Raw pointer - zero overhead access!
    }

#if CACHE_STATS_LOGGING_
    cacheStats.localMisses++;
#endif

    // Check shared cache before expensive ice-clearing operation
    shared_ptr<ActionPointDistances> sharedResult;
    if (sharedDistances.if_contains(cacheKey, [&sharedResult](const auto& kv) {
            sharedResult = kv.second;
        })) {
#if CACHE_STATS_LOGGING_
        cacheStats.sharedAccesses++;
#endif
        // Cache hit in shared cache - store in thread-local cache and return
        tlsCache.emplace(cacheKey, CacheEntry(sharedResult));
#if CACHE_STATS_LOGGING_
        MaybePrintCacheStats();
#endif
        return sharedResult.get();
    }

    // Cache miss in both caches - need to create ice-cleared map for pathfinding computation
    const bool hasIce = HasIceOnMap(map);

    // Declaring here to keep the copied map in scope
    const HexMap* mapToUse = map;

    // ReSharper disable once CppTooWideScope
    // ReSharper disable once CppJoinDeclarationAndAssignment
    fb::HexMapW iceClearedMap;
    if (hasIce) {
        // Create ice-cleared map for pathfinding
        // This prevents AI from considering ice as a valid path toward enemies
        iceClearedMap = CreateIceClearedMap(map);
        mapToUse = iceClearedMap.Get();
    }

    // Create new pathfinding result using factory method
    auto creationResult = FixedActionPointDistances::Create(
            mapToUse,
            mapId.terrainTypesId,
            mapId.modifierId,
            battalionType,
            includeBravingWater,
            braveWaterActionPointCost);

#if CACHE_STATS_LOGGING_
    // Track whether this was loaded from file or generated fresh
    if (creationResult.loadedFromFile) {
        cacheStats.apdLoadedFromFile++;
    } else {
        cacheStats.apdGeneratedFresh++;
    }
#endif

    auto result = creationResult.apd;

    // Store in shared cache
    sharedDistances.lazy_emplace_l(
            cacheKey,
            [](const auto& kv) { /* already checked above */ },
            [=](const auto& ctor) { ctor(cacheKey, result); });

    // Cache result locally for future lookups by this thread
    // Store both shared_ptr and raw pointer for hybrid access
    tlsCache.emplace(cacheKey, CacheEntry(result));

    // Prevent unbounded cache growth - limit to reasonable size
    if (tlsCache.size() > 100) {
        // Simple eviction: clear half the cache when it gets too large

#if CACHE_STATS_LOGGING_
        cacheStats.evictionEvents++;
#endif
        auto it = tlsCache.begin();
        std::advance(it, tlsCache.size() / 2);
        tlsCache.erase(tlsCache.begin(), it);
    }

    return result.get();
}

void ActionPointDistancesCache::ClearThreadLocalCache() { tlsCache.clear(); }

size_t ActionPointDistancesCache::GetThreadLocalCacheSize() { return tlsCache.size(); }

}  // namespace shardok
