#include "Readers/ItemListReader.h"

#include <algorithm>
#include <cstring>

#include "Offsets.h"
#include "RuntimeOffsets.h"
#include "Memory/MemoryValidation.h"
#include "Readers/ItemFilterCatalog.h"

// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------

ItemListReader::ItemListReader(ItemFilterCatalog& catalog,
                               int batchSize,
                               int readBudgetMs,
                               int staleAfterMs)
    : m_filterCatalog(catalog)
    , m_batchSize(batchSize)
    , m_readBudget(readBudgetMs)
    , m_staleAfter(staleAfterMs)
{}

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

bool ItemListReader::TryRead(VmmAccessor&                    mem,
                              const RuntimeSession&           session,
                              std::vector<DayZItemListEntry>& items)
{
    const uint32_t pid = session.processId;

    // 1. Item list pointer
    uint64_t itemListPtr = 0;
    if (!mem.TryReadPointer(pid,
                            session.worldAddress + RuntimeOffsets::World::ItemList,
                            itemListPtr))
    {
        return false;
    }

    // 2. Alloc count (total slots) and valid count (live items)
    int32_t rawAllocCount = 0;
    int32_t rawValidCount = 0;
    if (!mem.TryReadValue<int32_t>(pid,
                                   session.worldAddress + RuntimeOffsets::World::ItemTableAllocCount,
                                   rawAllocCount))
    {
        return false;
    }
    mem.TryReadValue<int32_t>(pid,
                              session.worldAddress + RuntimeOffsets::World::ItemTableValidCount,
                              rawValidCount);

    const int allocCount     = std::clamp(rawAllocCount, 0, kMaxItems);
    const int safeValidCount = std::clamp(rawValidCount, 0, allocCount);

    // 3. If cursor has reached the end, complete the current scan
    if (m_cursorOffset >= allocCount) {
        auto now = std::chrono::steady_clock::now();
        CompleteScan(now);
        PruneExpiredItems(now);
        items = BuildResult();
        return true;
    }

    // 4. Chunk size: max(batchSize, kMinTableChunkEntries), clamped to remaining
    const int remaining    = allocCount - m_cursorOffset;
    const int chunkEntries = std::min(remaining,
                                      std::max(m_batchSize, kMinTableChunkEntries));

    // 5. Bulk-read chunk — each slot is Offsets::SlowTable::EntrySize bytes
    const size_t entrySize  = static_cast<size_t>(Offsets::SlowTable::EntrySize);
    const size_t chunkBytes = static_cast<size_t>(chunkEntries) * entrySize;
    const uint64_t chunkAddr = itemListPtr +
                               static_cast<uint64_t>(m_cursorOffset) * entrySize;

    std::vector<uint8_t> tableBytes;
    tableBytes.reserve(chunkBytes);
    if (!mem.ReadBytes(pid, chunkAddr, chunkBytes, tableBytes)) {
        m_cursorOffset += chunkEntries;
        items = BuildResult();
        return true;
    }

    // 6. Process entries within the read budget
    const auto batchStart = std::chrono::steady_clock::now();
    bool budgetExceeded   = false;

    for (int i = 0; i < chunkEntries; ++i) {
        const uint8_t* slot = tableBytes.data() + static_cast<size_t>(i) * entrySize;

        // Flag at offset 0 — uint16_t, valid == 1
        uint16_t flag = 0;
        std::memcpy(&flag, slot, sizeof(uint16_t));
        if (flag != 1) continue;

        // Entity pointer at offset 0x8
        uint64_t itemAddr = 0;
        std::memcpy(&itemAddr,
                    slot + static_cast<size_t>(Offsets::SlowTable::EntryPointerOffset),
                    sizeof(uint64_t));

        if (!MemoryValidation::IsValidUserAddress(itemAddr)) continue;

        ++m_validEntriesSeenInScan;

        auto now = std::chrono::steady_clock::now();
        ProcessItem(mem, session, itemAddr, now);

        // Stop early when all valid entries have been seen
        if (safeValidCount > 0 && m_validEntriesSeenInScan >= safeValidCount) {
            m_cursorOffset += (i + 1);
            CompleteScan(now);
            PruneExpiredItems(now);
            items = BuildResult();
            return true;
        }

        // Check time budget
        auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
            std::chrono::steady_clock::now() - batchStart);
        if (elapsed >= m_readBudget) {
            m_cursorOffset += (i + 1);
            budgetExceeded  = true;
            break;
        }
    }

    if (!budgetExceeded) {
        m_cursorOffset += chunkEntries;
    }

    auto now = std::chrono::steady_clock::now();
    PruneExpiredItems(now);

    if (m_cursorOffset >= allocCount) {
        CompleteScan(now);
    }

    items = BuildResult();
    return true;
}

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

void ItemListReader::Reset()
{
    m_items.clear();
    m_typeCache.Clear();
    m_cachedResult.clear();
    m_sortedAddresses.clear();
    m_cursorOffset           = 0;
    m_validEntriesSeenInScan = 0;
    m_scanGeneration         = 1;
    m_hasCompletedScan       = false;
    m_sortDirty              = true;
    m_resultDirty            = true;
}

// -------------------------------------------------------------------------
// ProcessItem
// -------------------------------------------------------------------------

void ItemListReader::ProcessItem(VmmAccessor&                          mem,
                                  const RuntimeSession&                 session,
                                  uint64_t                              itemAddr,
                                  std::chrono::steady_clock::time_point now)
{
    const uint32_t pid = session.processId;

    // 1. Entity type pointer
    uint64_t typeAddr = 0;
    if (!mem.TryReadPointer(pid, itemAddr + Offsets::Common::Type, typeAddr)) {
        auto it = m_items.find(itemAddr);
        if (it != m_items.end()) {
            ++it->second.failedTypeReads;
            it->second.lastSeenGeneration = m_scanGeneration;
        }
        return;
    }

    // 2. Type metadata (cached)
    EntityTypeMetadata typeMeta;
    if (!m_typeCache.GetOrRead(typeAddr, typeMeta)) {
        typeMeta = ReadEntityTypeMetadata(mem, pid, typeAddr);
        if (typeMeta.typeName.empty() && typeMeta.configName.empty() &&
            typeMeta.cleanName.empty() && typeMeta.modelName.empty())
        {
            auto it = m_items.find(itemAddr);
            if (it != m_items.end()) {
                ++it->second.failedTypeReads;
                it->second.lastSeenGeneration = m_scanGeneration;
            }
            return;
        }
        m_typeCache.Set(typeAddr, typeMeta);
    }

    // 3. Position: VisualState ptr -> Vector3 at +Position
    std::optional<Vector3> position;
    uint64_t visualStateAddr = 0;
    if (mem.TryReadPointer(pid, itemAddr + Offsets::Common::VisualState, visualStateAddr)) {
        Vector3 pos{};
        if (mem.TryReadValue<Vector3>(pid,
                                      visualStateAddr + Offsets::Common::Position,
                                      pos) &&
            MemoryValidation::IsValidVector(pos))
        {
            position = pos;
        }
    }

    // Use cached position if current read failed
    auto& cached = m_items[itemAddr];
    if (!position.has_value() && cached.entry.position.has_value()) {
        position = cached.entry.position;
        ++cached.failedPositionReads;
    } else if (!position.has_value()) {
        ++cached.failedPositionReads;
    }

    // 4. Build display name
    const std::string entityName = GetBestItemName(typeMeta.cleanName,
                                                    typeMeta.configName,
                                                    typeMeta.modelName,
                                                    typeMeta.typeName,
                                                    itemAddr);

    // 5. Classify via filter catalog
    const std::string filterKey = m_filterCatalog.Classify(entityName,
                                                            typeMeta.typeName,
                                                            typeMeta.configName,
                                                            typeMeta.modelName);

    // 6. Update cached entry
    cached.entry.address    = itemAddr;
    cached.entry.position   = position;
    cached.entry.entityName = entityName;
    cached.entry.cleanName  = typeMeta.cleanName;
    cached.entry.typeName   = typeMeta.typeName;
    cached.entry.configName = typeMeta.configName;
    cached.entry.modelName  = typeMeta.modelName;
    cached.entry.filterKey  = filterKey;

    cached.lastSeenGeneration = m_scanGeneration;
    cached.lastSeenTime       = now;
    cached.failedTypeReads    = 0;

    if (position.has_value()) {
        cached.lastSuccessfulRead  = now;
        cached.failedPositionReads = 0;
    }

    m_resultDirty = true;
}

// -------------------------------------------------------------------------
// CompleteScan
// -------------------------------------------------------------------------

void ItemListReader::CompleteScan(std::chrono::steady_clock::time_point /*now*/)
{
    for (auto it = m_items.begin(); it != m_items.end(); ) {
        if (it->second.lastSeenGeneration != m_scanGeneration) {
            it = m_items.erase(it);
            m_sortDirty   = true;
            m_resultDirty = true;
        } else {
            ++it;
        }
    }

    m_cursorOffset           = 0;
    m_validEntriesSeenInScan = 0;
    m_hasCompletedScan       = true;
    ++m_scanGeneration;
}

// -------------------------------------------------------------------------
// PruneExpiredItems
// -------------------------------------------------------------------------

void ItemListReader::PruneExpiredItems(std::chrono::steady_clock::time_point now)
{
    for (auto it = m_items.begin(); it != m_items.end(); ) {
        const auto age = std::chrono::duration_cast<std::chrono::milliseconds>(
            now - it->second.lastSuccessfulRead);
        if (age > m_staleAfter) {
            it = m_items.erase(it);
            m_sortDirty   = true;
            m_resultDirty = true;
        } else {
            ++it;
        }
    }
}

// -------------------------------------------------------------------------
// BuildResult
// -------------------------------------------------------------------------

std::vector<DayZItemListEntry>& ItemListReader::BuildResult()
{
    if (!m_resultDirty) {
        return m_cachedResult;
    }

    if (m_sortDirty) {
        m_sortedAddresses.clear();
        m_sortedAddresses.reserve(m_items.size());
        for (const auto& kv : m_items) {
            m_sortedAddresses.push_back(kv.first);
        }
        std::sort(m_sortedAddresses.begin(), m_sortedAddresses.end(),
                  [this](uint64_t a, uint64_t b) {
                      const auto& ea = m_items.at(a).entry;
                      const auto& eb = m_items.at(b).entry;
                      if (ea.entityName != eb.entityName)
                          return ea.entityName < eb.entityName;
                      return a < b;
                  });
        m_sortDirty = false;
    }

    m_cachedResult.clear();
    m_cachedResult.reserve(m_sortedAddresses.size());
    for (uint64_t addr : m_sortedAddresses) {
        auto it = m_items.find(addr);
        if (it != m_items.end()) {
            m_cachedResult.push_back(it->second.entry);
        }
    }

    m_resultDirty = false;
    return m_cachedResult;
}

// -------------------------------------------------------------------------
// GetBestItemName
// Priority: cleanName > configName > filename-from-modelName > typeName
// -------------------------------------------------------------------------

std::string ItemListReader::GetBestItemName(const std::string& cleanName,
                                             const std::string& configName,
                                             const std::string& modelName,
                                             const std::string& typeName,
                                             uint64_t           itemAddress)
{
    if (!cleanName.empty()) return cleanName;
    if (!configName.empty()) return configName;

    // Extract filename from model path (strip directory and extension)
    if (!modelName.empty()) {
        // Find last slash (forward or back)
        const auto slashPos = modelName.find_last_of("/\\");
        std::string filename = (slashPos != std::string::npos)
                                   ? modelName.substr(slashPos + 1)
                                   : modelName;
        // Strip extension
        const auto dotPos = filename.rfind('.');
        if (dotPos != std::string::npos) {
            filename = filename.substr(0, dotPos);
        }
        if (!filename.empty()) return filename;
    }

    if (!typeName.empty()) return typeName;

    // Last resort: hex address
    char buf[32];
    std::snprintf(buf, sizeof(buf), "0x%016llX",
                  static_cast<unsigned long long>(itemAddress));
    return buf;
}
