#include "Readers/ClientScoreboardReader.h"

#include <algorithm>
#include <cstring>
#include <unordered_set>

#include "Offsets.h"
#include "Memory/MemoryValidation.h"

ClientScoreboardReader::ClientScoreboardReader(int refreshIntervalMs)
    : m_networkIdRefreshInterval(refreshIntervalMs)
{}

// -------------------------------------------------------------------------
// RefreshNetworkId
// -------------------------------------------------------------------------

void ClientScoreboardReader::RefreshNetworkId(VmmAccessor&      mem,
                                              uint32_t          pid,
                                              uint64_t          identityAddr,
                                              IdentityMetadata& meta)
{
    uint32_t id = 0;
    if (mem.TryReadValue<uint32_t>(pid,
                                   identityAddr + Offsets::Network::PlayerIdentityNetworkId,
                                   id))
    {
        meta.networkId = id;
    }
    meta.lastNetworkIdReadTime = std::chrono::steady_clock::now();
}

// -------------------------------------------------------------------------
// TryRead
// -------------------------------------------------------------------------

bool ClientScoreboardReader::TryRead(VmmAccessor&                       mem,
                                     const RuntimeSession&              session,
                                     std::vector<DayZScoreboardPlayer>& players)
{
    const uint32_t pid = session.processId;

    // -----------------------------------------------------------------
    // 1. NetworkClient pointer
    // -----------------------------------------------------------------
    uint64_t networkClientAddr = 0;
    if (!mem.TryReadPointer(pid,
                            session.networkManagerAddress + Offsets::Network::ManagerNetworkClient,
                            networkClientAddr))
    {
        return false;
    }

    // -----------------------------------------------------------------
    // 2. Scoreboard base pointer
    // -----------------------------------------------------------------
    uint64_t scoreboardPtr = 0;
    if (!mem.TryReadPointer(pid,
                            networkClientAddr + Offsets::Network::ClientScoreboard,
                            scoreboardPtr))
    {
        return false;
    }

    // -----------------------------------------------------------------
    // 3. Player count
    // -----------------------------------------------------------------
    int32_t rawPlayerCount = 0;
    if (!mem.TryReadValue<int32_t>(pid,
                                   networkClientAddr + Offsets::Network::ClientPlayerCount,
                                   rawPlayerCount))
    {
        return false;
    }

    const int playerCount = std::clamp(rawPlayerCount, 0, kMaxPlayers);
    if (playerCount == 0) {
        players.clear();
        return true;
    }

    // -----------------------------------------------------------------
    // 4. Bulk-read identity pointer array (kScoreboardCapacity * 8 bytes)
    // -----------------------------------------------------------------
    const int slotsToRead = std::min(playerCount, kScoreboardCapacity);
    std::vector<uint8_t> ptrTable;
    ptrTable.reserve(static_cast<size_t>(slotsToRead) * 8);
    if (!mem.ReadBytes(pid, scoreboardPtr,
                       static_cast<size_t>(slotsToRead) * 8, ptrTable))
    {
        return false;
    }

    // -----------------------------------------------------------------
    // 5. Build list of valid identity addresses from the table
    // -----------------------------------------------------------------
    std::vector<uint64_t> validAddresses;
    validAddresses.reserve(static_cast<size_t>(slotsToRead));

    for (int i = 0; i < slotsToRead; ++i) {
        uint64_t identityAddr = 0;
        std::memcpy(&identityAddr,
                    ptrTable.data() + static_cast<size_t>(i) * 8,
                    sizeof(uint64_t));

        if (!MemoryValidation::IsValidUserAddress(identityAddr)) {
            continue;
        }
        validAddresses.push_back(identityAddr);
    }

    // -----------------------------------------------------------------
    // 6. Ensure every address has a cached metadata entry
    //    Read identity strings once; they are stable for the session.
    // -----------------------------------------------------------------
    for (uint64_t identityAddr : validAddresses) {
        if (m_metadataByAddress.count(identityAddr)) {
            continue;  // already cached
        }

        IdentityMetadata meta;

        // SteamId: read ptr at offset, then ArmaString
        {
            uint64_t steamIdPtr = 0;
            if (mem.TryReadPointer(pid,
                                   identityAddr + Offsets::Network::PlayerIdentitySteamId,
                                   steamIdPtr))
            {
                meta.steamId = mem.ReadArmaString(pid, steamIdPtr);
            }
        }

        // PlayerName: read ptr at offset, then ArmaString
        {
            uint64_t namePtr = 0;
            if (mem.TryReadPointer(pid,
                                   identityAddr + Offsets::Network::PlayerIdentityPlayerName,
                                   namePtr))
            {
                meta.playerName = mem.ReadArmaString(pid, namePtr);
            }
        }

        // NetworkId (initial read — will be refreshed on schedule)
        RefreshNetworkId(mem, pid, identityAddr, meta);

        m_metadataByAddress[identityAddr] = std::move(meta);
    }

    // -----------------------------------------------------------------
    // 7. Round-robin refresh of stale NetworkIds
    // -----------------------------------------------------------------
    if (!validAddresses.empty()) {
        const auto now = std::chrono::steady_clock::now();
        int refreshed  = 0;

        const int total = static_cast<int>(validAddresses.size());
        // Clamp cursor to valid range after potential list shrinkage
        if (m_networkIdRefreshCursor >= total) {
            m_networkIdRefreshCursor = 0;
        }

        for (int attempt = 0;
             attempt < total && refreshed < kNetworkIdRefreshBatchSize;
             ++attempt)
        {
            const int idx = (m_networkIdRefreshCursor + attempt) % total;
            uint64_t  addr = validAddresses[static_cast<size_t>(idx)];

            auto it = m_metadataByAddress.find(addr);
            if (it == m_metadataByAddress.end()) continue;

            IdentityMetadata& meta = it->second;
            if (now - meta.lastNetworkIdReadTime >= m_networkIdRefreshInterval) {
                RefreshNetworkId(mem, pid, addr, meta);
                ++refreshed;
            }
        }

        m_networkIdRefreshCursor =
            (m_networkIdRefreshCursor + refreshed) % total;
    }

    // -----------------------------------------------------------------
    // 8. Build result, deduplicate by NetworkId (or playerName when id==0)
    // -----------------------------------------------------------------
    std::vector<DayZScoreboardPlayer> result;
    result.reserve(validAddresses.size());

    std::unordered_set<uint32_t> seenNetworkIds;
    std::unordered_set<std::string> seenNames;

    for (uint64_t identityAddr : validAddresses) {
        auto it = m_metadataByAddress.find(identityAddr);
        if (it == m_metadataByAddress.end()) continue;

        const IdentityMetadata& meta = it->second;

        if (meta.networkId != 0) {
            if (!seenNetworkIds.insert(meta.networkId).second) {
                continue;  // duplicate
            }
        } else {
            // No network ID yet — deduplicate by name
            if (meta.playerName.empty()) continue;
            if (!seenNames.insert(meta.playerName).second) {
                continue;
            }
        }

        DayZScoreboardPlayer player;
        player.networkId  = meta.networkId;
        player.steamId    = meta.steamId;
        player.playerName = meta.playerName;
        result.push_back(std::move(player));
    }

    // Sort by player name (case-sensitive, matches C# behaviour)
    std::sort(result.begin(), result.end(),
              [](const DayZScoreboardPlayer& a, const DayZScoreboardPlayer& b) {
                  return a.playerName < b.playerName;
              });

    players = std::move(result);
    return true;
}

// -------------------------------------------------------------------------
// Reset
// -------------------------------------------------------------------------

void ClientScoreboardReader::Reset()
{
    m_metadataByAddress.clear();
    m_cachedResult.clear();
    m_networkIdRefreshCursor = 0;
}
