#include "scanner.h"
#include <QtConcurrent>
#include <QMetaObject>
#include <QElapsedTimer>
#include <QDebug>
#include <cstring>
#include <cmath>
#include <algorithm>

namespace rcx {

// ── Pattern parsing ──

static int hexVal(QChar c) {
    ushort u = c.unicode();
    if (u >= '0' && u <= '9') return u - '0';
    if (u >= 'a' && u <= 'f') return u - 'a' + 10;
    if (u >= 'A' && u <= 'F') return u - 'A' + 10;
    return -1;
}

bool parseSignature(const QString& input, QByteArray& pattern, QByteArray& mask,
                    QString* errorMsg)
{
    pattern.clear();
    mask.clear();

    QString trimmed = input.trimmed();
    if (trimmed.isEmpty()) {
        if (errorMsg) *errorMsg = QStringLiteral("Empty pattern");
        return false;
    }

    // Check for C-style: \xAB\xCD
    if (trimmed.startsWith(QStringLiteral("\\x"))) {
        QStringList parts = trimmed.split(QStringLiteral("\\x"), Qt::SkipEmptyParts);
        for (const QString& part : parts) {
            if (part.size() != 2) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid C-style byte: \\x%1").arg(part);
                return false;
            }
            int hi = hexVal(part[0]);
            int lo = hexVal(part[1]);
            if (hi < 0 || lo < 0) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid hex char in: \\x%1").arg(part);
                return false;
            }
            pattern.append(char((hi << 4) | lo));
            mask.append(char(0xFF));
        }
        return !pattern.isEmpty();
    }

    // Space-separated or packed hex
    bool hasSpaces = trimmed.contains(' ');

    if (hasSpaces) {
        QStringList tokens = trimmed.split(' ', Qt::SkipEmptyParts);
        for (const QString& tok : tokens) {
            if (tok == QStringLiteral("??") || tok == QStringLiteral("?")) {
                pattern.append(char(0));
                mask.append(char(0));
            } else if (tok.size() == 2) {
                int hi = hexVal(tok[0]);
                int lo = hexVal(tok[1]);
                if (hi < 0 || lo < 0) {
                    if (errorMsg) *errorMsg = QStringLiteral("Invalid hex byte: %1").arg(tok);
                    return false;
                }
                pattern.append(char((hi << 4) | lo));
                mask.append(char(0xFF));
            } else {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid token: %1 (expected 2 hex chars or wildcards)").arg(tok);
                return false;
            }
        }
    } else {
        // Packed: "488B??05"
        if (trimmed.size() % 2 != 0) {
            if (errorMsg) *errorMsg = QStringLiteral("Odd number of characters in packed pattern");
            return false;
        }
        for (int i = 0; i < trimmed.size(); i += 2) {
            QChar c0 = trimmed[i], c1 = trimmed[i + 1];
            if ((c0 == '?' && c1 == '?')) {
                pattern.append(char(0));
                mask.append(char(0));
            } else {
                int hi = hexVal(c0);
                int lo = hexVal(c1);
                if (hi < 0 || lo < 0) {
                    if (errorMsg) *errorMsg = QStringLiteral("Invalid hex chars at position %1: %2%3")
                        .arg(i).arg(c0).arg(c1);
                    return false;
                }
                pattern.append(char((hi << 4) | lo));
                mask.append(char(0xFF));
            }
        }
    }

    if (pattern.isEmpty()) {
        if (errorMsg) *errorMsg = QStringLiteral("Empty pattern after parsing");
        return false;
    }

    return true;
}

// ── Value serialization ──

template<typename T>
static void appendLE(QByteArray& out, T val) {
    out.append(reinterpret_cast<const char*>(&val), sizeof(T));
}

bool serializeValue(ValueType type, const QString& input,
                    QByteArray& pattern, QByteArray& mask,
                    QString* errorMsg)
{
    pattern.clear();
    mask.clear();

    QString trimmed = input.trimmed();
    if (trimmed.isEmpty()) {
        if (errorMsg) *errorMsg = QStringLiteral("Empty value");
        return false;
    }

    bool ok = false;

    switch (type) {
    case ValueType::Int8: {
        int v = trimmed.toInt(&ok);
        if (!ok || v < -128 || v > 127) {
            if (errorMsg) *errorMsg = QStringLiteral("Invalid int8 value");
            return false;
        }
        appendLE<int8_t>(pattern, (int8_t)v);
        break;
    }
    case ValueType::Int16: {
        int v = trimmed.toInt(&ok);
        if (!ok || v < -32768 || v > 32767) {
            if (errorMsg) *errorMsg = QStringLiteral("Invalid int16 value");
            return false;
        }
        appendLE<int16_t>(pattern, (int16_t)v);
        break;
    }
    case ValueType::Int32: {
        int v = trimmed.toInt(&ok);
        if (!ok) {
            if (errorMsg) *errorMsg = QStringLiteral("Invalid int32 value");
            return false;
        }
        appendLE<int32_t>(pattern, (int32_t)v);
        break;
    }
    case ValueType::Int64: {
        qlonglong v = trimmed.toLongLong(&ok);
        if (!ok) {
            if (errorMsg) *errorMsg = QStringLiteral("Invalid int64 value");
            return false;
        }
        appendLE<int64_t>(pattern, (int64_t)v);
        break;
    }
    case ValueType::UInt8: {
        uint v = trimmed.toUInt(&ok);
        if (!ok || v > 255) {
            // Try hex
            if (trimmed.startsWith("0x", Qt::CaseInsensitive))
                v = trimmed.toUInt(&ok, 16);
            if (!ok || v > 255) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid uint8 value");
                return false;
            }
        }
        appendLE<uint8_t>(pattern, (uint8_t)v);
        break;
    }
    case ValueType::UInt16: {
        uint v = trimmed.toUInt(&ok);
        if (!ok || v > 65535) {
            if (trimmed.startsWith("0x", Qt::CaseInsensitive))
                v = trimmed.toUInt(&ok, 16);
            if (!ok || v > 65535) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid uint16 value");
                return false;
            }
        }
        appendLE<uint16_t>(pattern, (uint16_t)v);
        break;
    }
    case ValueType::UInt32: {
        quint32 v = trimmed.toULong(&ok);
        if (!ok) {
            if (trimmed.startsWith("0x", Qt::CaseInsensitive))
                v = trimmed.toULong(&ok, 16);
            if (!ok) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid uint32 value");
                return false;
            }
        }
        appendLE<uint32_t>(pattern, v);
        break;
    }
    case ValueType::UInt64: {
        quint64 v = trimmed.toULongLong(&ok);
        if (!ok) {
            if (trimmed.startsWith("0x", Qt::CaseInsensitive))
                v = trimmed.toULongLong(&ok, 16);
            if (!ok) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid uint64 value");
                return false;
            }
        }
        appendLE<uint64_t>(pattern, v);
        break;
    }
    case ValueType::Float: {
        float v = trimmed.toFloat(&ok);
        if (!ok) {
            if (errorMsg) *errorMsg = QStringLiteral("Invalid float value");
            return false;
        }
        appendLE<float>(pattern, v);
        break;
    }
    case ValueType::Double: {
        double v = trimmed.toDouble(&ok);
        if (!ok) {
            if (errorMsg) *errorMsg = QStringLiteral("Invalid double value");
            return false;
        }
        appendLE<double>(pattern, v);
        break;
    }
    case ValueType::Vec2: {
        QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
        if (parts.size() != 2) {
            if (errorMsg) *errorMsg = QStringLiteral("Vec2 requires 2 space-separated floats");
            return false;
        }
        for (const QString& p : parts) {
            float v = p.toFloat(&ok);
            if (!ok) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec2: %1").arg(p);
                return false;
            }
            appendLE<float>(pattern, v);
        }
        break;
    }
    case ValueType::Vec3: {
        QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
        if (parts.size() != 3) {
            if (errorMsg) *errorMsg = QStringLiteral("Vec3 requires 3 space-separated floats");
            return false;
        }
        for (const QString& p : parts) {
            float v = p.toFloat(&ok);
            if (!ok) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec3: %1").arg(p);
                return false;
            }
            appendLE<float>(pattern, v);
        }
        break;
    }
    case ValueType::Vec4: {
        QStringList parts = trimmed.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
        if (parts.size() != 4) {
            if (errorMsg) *errorMsg = QStringLiteral("Vec4 requires 4 space-separated floats");
            return false;
        }
        for (const QString& p : parts) {
            float v = p.toFloat(&ok);
            if (!ok) {
                if (errorMsg) *errorMsg = QStringLiteral("Invalid float in vec4: %1").arg(p);
                return false;
            }
            appendLE<float>(pattern, v);
        }
        break;
    }
    case ValueType::UTF8: {
        QByteArray encoded = trimmed.toUtf8();
        if (encoded.isEmpty()) {
            if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-8 string");
            return false;
        }
        pattern = encoded;
        break;
    }
    case ValueType::UTF16: {
        // UTF-16LE encoding
        for (int i = 0; i < trimmed.size(); i++) {
            ushort u = trimmed[i].unicode();
            appendLE<uint16_t>(pattern, u);
        }
        if (pattern.isEmpty()) {
            if (errorMsg) *errorMsg = QStringLiteral("Empty UTF-16 string");
            return false;
        }
        break;
    }
    case ValueType::HexBytes: {
        // Parse hex bytes (like signature but no wildcards)
        QByteArray dummyMask;
        if (!parseSignature(trimmed, pattern, dummyMask, errorMsg))
            return false;
        // HexBytes = exact match, no wildcards
        break;
    }
    }

    // Set mask to all 0xFF (exact match) for value scans
    mask.fill(char(0xFF), pattern.size());
    return true;
}

int naturalAlignment(ValueType type) {
    switch (type) {
    case ValueType::Int8:
    case ValueType::UInt8:
    case ValueType::UTF8:
    case ValueType::HexBytes:
        return 1;
    case ValueType::Int16:
    case ValueType::UInt16:
    case ValueType::UTF16:
        return 2;
    case ValueType::Int32:
    case ValueType::UInt32:
    case ValueType::Float:
    case ValueType::Vec2:
    case ValueType::Vec3:
    case ValueType::Vec4:
        return 4;
    case ValueType::Int64:
    case ValueType::UInt64:
    case ValueType::Double:
        return 8;
    }
    return 1;
}

int valueSizeForType(ValueType type) {
    switch (type) {
    case ValueType::Int8:  case ValueType::UInt8:  return 1;
    case ValueType::Int16: case ValueType::UInt16: return 2;
    case ValueType::Int32: case ValueType::UInt32: case ValueType::Float: return 4;
    case ValueType::Int64: case ValueType::UInt64: case ValueType::Double: return 8;
    case ValueType::Vec2: return 8;
    case ValueType::Vec3: return 12;
    case ValueType::Vec4: return 16;
    default: return 4;
    }
}

// ── Typed comparison for rescan conditions ──

static int compareTyped(const QByteArray& a, const QByteArray& b, ValueType vt) {
    const char* da = a.constData();
    const char* db = b.constData();
    int sz = qMin(a.size(), b.size());

    switch (vt) {
    case ValueType::Int8:
        if (sz >= 1) { int8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); }
        break;
    case ValueType::UInt8:
        if (sz >= 1) { uint8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); }
        break;
    case ValueType::Int16:
        if (sz >= 2) { int16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); }
        break;
    case ValueType::UInt16:
        if (sz >= 2) { uint16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); }
        break;
    case ValueType::Int32:
        if (sz >= 4) { int32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
        break;
    case ValueType::UInt32:
        if (sz >= 4) { uint32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
        break;
    case ValueType::Int64:
        if (sz >= 8) { int64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
        break;
    case ValueType::UInt64:
        if (sz >= 8) { uint64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
        break;
    case ValueType::Float:
        if (sz >= 4) { float va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); }
        break;
    case ValueType::Double:
        if (sz >= 8) { double va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); }
        break;
    default:
        break;
    }
    // Fallback: byte comparison
    return memcmp(da, db, sz);
}

// ── Scan engine ──

ScanEngine::ScanEngine(QObject* parent)
    : QObject(parent)
{
    qRegisterMetaType<QVector<ScanResult>>("QVector<rcx::ScanResult>");
}

bool ScanEngine::isRunning() const {
    return m_watcher && m_watcher->isRunning();
}

void ScanEngine::abort() {
    m_abort.store(true);
}

void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
    if (isRunning()) return;

    if (req.condition != ScanCondition::UnknownValue) {
        if (req.pattern.isEmpty()) {
            emit error(QStringLiteral("Empty pattern"));
            return;
        }
        if (req.pattern.size() != req.mask.size()) {
            emit error(QStringLiteral("Pattern and mask size mismatch"));
            return;
        }
    }

    m_abort.store(false);

    auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
    m_watcher = watcher;

    connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
        auto results = watcher->result();
        watcher->deleteLater();
        if (m_watcher == watcher)
            m_watcher = nullptr;
        emit finished(results);
    });

    watcher->setFuture(QtConcurrent::run([this, provider, req]() {
        return runScan(provider, req);
    }));
}

QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
                                         const ScanRequest& req)
{
    QElapsedTimer timer;
    timer.start();

    QVector<ScanResult> results;
    const bool isUnknown = (req.condition == ScanCondition::UnknownValue);

    if (!prov || (!isUnknown && req.pattern.isEmpty()))
        return results;

    auto regions = prov->enumerateRegions();
    qDebug() << "[scan] regions:" << regions.size()
             << " pattern:" << req.pattern.size() << "bytes"
             << " align:" << req.alignment
             << " condition:" << (int)req.condition
             << " filterExec:" << req.filterExecutable
             << " filterWrite:" << req.filterWritable;

    // Fallback for providers that don't enumerate regions (file/buffer/syscall without modules)
    if (regions.isEmpty()) {
        MemoryRegion fallback;
        fallback.base = 0;
        fallback.size = (uint64_t)prov->size();
        fallback.readable = true;
        fallback.writable = true;
        fallback.executable = true;  // unknown; include so filters don't exclude the only region
        regions.append(fallback);
    }

    const int patternLen = isUnknown ? req.valueSize : req.pattern.size();
    const char* pat = isUnknown ? nullptr : req.pattern.constData();
    const char* msk = isUnknown ? nullptr : req.mask.constData();
    const int alignment = qMax(1, req.alignment);
    const int valSize = isUnknown ? req.valueSize : patternLen;
    const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
                           req.endAddress > req.startAddress;

    // If constrainRegions specified, intersect with provider regions
    if (!req.constrainRegions.isEmpty()) {
        // Sort and merge overlapping/adjacent constraints to avoid duplicate sub-regions
        auto constraints = req.constrainRegions;
        std::sort(constraints.begin(), constraints.end(),
                  [](const AddressRange& a, const AddressRange& b) { return a.start < b.start; });
        QVector<AddressRange> merged;
        for (const auto& c : constraints) {
            if (c.end <= c.start) continue;  // skip degenerate ranges
            if (!merged.isEmpty() && c.start <= merged.last().end)
                merged.last().end = qMax(merged.last().end, c.end);
            else
                merged.append(c);
        }

        QVector<MemoryRegion> clipped;
        for (const auto& region : regions) {
            uint64_t rEnd = region.base + region.size;
            for (const auto& c : merged) {
                if (c.end <= region.base || c.start >= rEnd) continue;
                uint64_t iStart = qMax(region.base, c.start);
                uint64_t iEnd   = qMin(rEnd, c.end);
                if (iEnd <= iStart) continue;
                MemoryRegion sub = region;
                sub.base = iStart;
                sub.size = iEnd - iStart;
                clipped.append(sub);
            }
        }
        regions = std::move(clipped);
        qDebug() << "[scan] constrained to" << regions.size() << "sub-regions from"
                 << req.constrainRegions.size() << "address ranges ("
                 << merged.size() << "after merge)";
    }

    // Pre-compute total bytes for progress
    uint64_t totalBytes = 0;
    for (const auto& r : regions) {
        if (req.filterExecutable && !r.executable) continue;
        if (req.filterWritable && !r.writable) continue;
        uint64_t rStart = r.base, rEnd = r.base + r.size;
        if (hasRange) {
            if (rEnd <= req.startAddress || rStart >= req.endAddress) continue;
            rStart = qMax(rStart, req.startAddress);
            rEnd   = qMin(rEnd, req.endAddress);
        }
        totalBytes += rEnd - rStart;
    }

    qDebug() << "[scan] total scannable:" << (totalBytes / 1024) << "KB across filtered regions";

    if (totalBytes == 0) return results;

    uint64_t scannedBytes = 0;
    int lastPct = -1;

    constexpr int kChunk = 256 * 1024;

    for (int regionIndex = 0; regionIndex < regions.size(); ++regionIndex) {
        const auto& region = regions[regionIndex];
        if (m_abort.load()) break;

        if (req.filterExecutable && !region.executable) continue;
        if (req.filterWritable && !region.writable) continue;

        // Clip region to requested address range
        uint64_t regStart = region.base;
        uint64_t regEnd   = region.base + region.size;
        if (hasRange) {
            if (regEnd <= req.startAddress || regStart >= req.endAddress) {
                // Entirely outside range — skip
                continue;
            }
            regStart = qMax(regStart, req.startAddress);
            regEnd   = qMin(regEnd, req.endAddress);
        }
        uint64_t regSize = regEnd - regStart;
        if (regSize == 0) continue;

        if ((uint64_t)patternLen > regSize) {
            scannedBytes += regSize;
            continue;
        }

        const int overlap = patternLen - 1;
        QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized);
        uint64_t regOffset = regStart - region.base; // offset within provider region

        for (uint64_t off = 0; off < regSize; ) {
            if (m_abort.load()) break;

            uint64_t remaining = regSize - off;
            int readLen = (int)qMin((uint64_t)chunk.size(), remaining);

            if (!prov->read(regStart + off, chunk.data(), readLen)) {
                // Skip unreadable chunk
                qDebug() << "[scan] read failed region" << regionIndex << "addr" << Qt::showbase << Qt::hex
                         << (region.base + off) << "base" << region.base << "off" << off << "len" << readLen << Qt::dec;
                off += readLen;
                scannedBytes += readLen;
                continue;
            }

            int scanEnd = readLen - patternLen;
            const char* data = chunk.constData();

            if (isUnknown) {
                // Unknown value: capture every aligned address
                for (int i = 0; i <= scanEnd; i += alignment) {
                    ScanResult r;
                    r.address = regStart + off + (uint64_t)i;
                    r.scanValue = QByteArray(data + i, valSize);
                    results.append(r);

                    if (results.size() >= req.maxResults)
                        goto done;
                }
            } else {
                // Exact pattern match
                for (int i = 0; i <= scanEnd; i += alignment) {
                    bool match = true;
                    for (int j = 0; j < patternLen; j++) {
                        if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
                            match = false;
                            break;
                        }
                    }
                    if (match) {
                        ScanResult r;
                        r.address = regStart + off + (uint64_t)i;
                        r.regionModule = region.moduleName;
                        r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
                        results.append(r);

                        if (results.size() >= req.maxResults)
                            goto done;
                    }
                }
            }

            // Advance with overlap to catch patterns that straddle chunks.
            // Skip overlap on the final chunk -- nothing follows to overlap into.
            uint64_t advance;
            if ((uint64_t)readLen >= remaining)
                advance = remaining;  // last chunk, no overlap needed
            else if (readLen > overlap)
                advance = (uint64_t)(readLen - overlap);
            else
                advance = 1; // prevent infinite loop on tiny regions
            scannedBytes += advance;
            off += advance;

            // Throttled progress
            int pct = (int)(scannedBytes * 100 / totalBytes);
            if (pct > 100) pct = 100;
            if (pct != lastPct) {
                lastPct = pct;
                QMetaObject::invokeMethod(this, "progress",
                    Qt::QueuedConnection, Q_ARG(int, pct));
            }
        }
    }

done:
    qDebug() << "[scan] done:" << results.size() << "results in" << timer.elapsed() << "ms"
             << " scanned:" << (scannedBytes / 1024) << "KB";
    return results;
}

void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
                              QVector<ScanResult> results, int readSize,
                              ScanCondition condition, ValueType valueType,
                              const QByteArray& filterPattern,
                              const QByteArray& filterMask) {
    if (isRunning()) return;

    m_abort.store(false);

    auto* watcher = new QFutureWatcher<QVector<ScanResult>>(this);
    m_watcher = watcher;

    connect(watcher, &QFutureWatcher<QVector<ScanResult>>::finished, this, [this, watcher]() {
        auto results = watcher->result();
        watcher->deleteLater();
        if (m_watcher == watcher)
            m_watcher = nullptr;
        emit rescanFinished(results);
    });

    watcher->setFuture(QtConcurrent::run(
        [this, provider, results = std::move(results), readSize,
         condition, valueType, filterPattern, filterMask]() mutable {
            return runRescan(provider, std::move(results), readSize,
                             condition, valueType, filterPattern, filterMask);
        }));
}

QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
                                           QVector<ScanResult> results, int readSize,
                                           ScanCondition condition, ValueType valueType,
                                           const QByteArray& filterPattern,
                                           const QByteArray& filterMask) {
    QElapsedTimer timer;
    timer.start();

    int total = results.size();
    if (total == 0 || !prov) return results;

    bool hasExactFilter = !filterPattern.isEmpty() && condition == ScanCondition::ExactValue;
    bool hasComparison = (condition == ScanCondition::Changed ||
                          condition == ScanCondition::Unchanged ||
                          condition == ScanCondition::Increased ||
                          condition == ScanCondition::Decreased);
    bool needsFilter = hasExactFilter || hasComparison;

    qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize
             << "condition:" << (int)condition
             << "exactFilter:" << (hasExactFilter ? "yes" : "no")
             << "comparison:" << (hasComparison ? "yes" : "no");

    // Save previous values
    for (auto& r : results)
        r.previousValue = r.scanValue;

    // Sort indices by address for sequential chunked reads
    QVector<int> order(total);
    for (int i = 0; i < total; i++) order[i] = i;
    std::sort(order.begin(), order.end(), [&results](int a, int b) {
        return results[a].address < results[b].address;
    });

    constexpr int kChunk = 256 * 1024;
    int updated = 0;
    int lastPct = -1;
    int chunks = 0;
    uint64_t totalBytesRead = 0;
    int i = 0;

    // Track which results matched (by original index)
    QVector<bool> matched(total, !needsFilter); // if no filter, all match

    while (i < total && !m_abort.load()) {
        uint64_t spanBase = results[order[i]].address;
        int spanEnd = i;

        // Extend span while next result fits in the same chunk
        while (spanEnd + 1 < total) {
            uint64_t endAddr = results[order[spanEnd + 1]].address + readSize;
            if (endAddr - spanBase > (uint64_t)kChunk) break;
            spanEnd++;
        }

        uint64_t spanLast = results[order[spanEnd]].address;
        int chunkLen = (int)(spanLast + readSize - spanBase);
        QByteArray chunk(chunkLen, '\0');
        prov->read(spanBase, chunk.data(), chunkLen);

        for (int j = i; j <= spanEnd; j++) {
            int idx = order[j];
            auto& r = results[idx];
            int off = (int)(r.address - spanBase);
            r.scanValue = chunk.mid(off, readSize);

            // Apply exact-value filter
            if (hasExactFilter) {
                int patLen = filterPattern.size();
                if (r.scanValue.size() >= patLen) {
                    bool ok = true;
                    const char* data = r.scanValue.constData();
                    const char* pat  = filterPattern.constData();
                    const char* msk  = filterMask.constData();
                    for (int k = 0; k < patLen; k++) {
                        if ((data[k] & msk[k]) != (pat[k] & msk[k])) {
                            ok = false;
                            break;
                        }
                    }
                    matched[idx] = ok;
                }
            }

            // Apply comparison-based filter
            if (hasComparison && !r.previousValue.isEmpty()) {
                int cmp = compareTyped(r.scanValue, r.previousValue, valueType);
                switch (condition) {
                case ScanCondition::Changed:   matched[idx] = (cmp != 0); break;
                case ScanCondition::Unchanged: matched[idx] = (cmp == 0); break;
                case ScanCondition::Increased: matched[idx] = (cmp > 0);  break;
                case ScanCondition::Decreased: matched[idx] = (cmp < 0);  break;
                default: break;
                }
            }
        }

        chunks++;
        totalBytesRead += chunkLen;
        updated += (spanEnd - i + 1);
        i = spanEnd + 1;

        int pct = updated * 100 / total;
        if (pct != lastPct) {
            lastPct = pct;
            QMetaObject::invokeMethod(this, "progress",
                Qt::QueuedConnection, Q_ARG(int, pct));
        }
    }

    // Filter out non-matching results
    if (needsFilter) {
        QVector<ScanResult> filtered;
        filtered.reserve(total);
        for (int k = 0; k < total; k++) {
            if (matched[k])
                filtered.append(std::move(results[k]));
        }
        qDebug() << "[rescan] done:" << filtered.size() << "/" << total
                 << "matched in" << timer.elapsed() << "ms |" << chunks
                 << "chunks," << (totalBytesRead / 1024) << "KB read";
        return filtered;
    }

    qDebug() << "[rescan] done:" << updated << "/" << total << "results in"
             << timer.elapsed() << "ms |" << chunks << "chunks,"
             << (totalBytesRead / 1024) << "KB read";
    return results;
}

} // namespace rcx
