// SPDX-License-Identifier: GPL-3.0-or-later

#include "windows-events.h"

static void wevt_event_done(WEVT_LOG *log);

static uint64_t wevt_log_file_size(const wchar_t *channel);

// --------------------------------------------------------------------------------------------------------------------

static const char *EvtGetExtendedStatus_utf8(void) {
    static __thread wchar_t wbuf[4096];
    static __thread char buf[4096];
    DWORD wbuf_used = 0;

    if(EvtGetExtendedStatus(sizeof(wbuf) / sizeof(wchar_t), wbuf, &wbuf_used) == ERROR_SUCCESS) {
        wbuf[sizeof(wbuf) / sizeof(wchar_t) - 1] = 0;
        unicode2utf8(buf, sizeof(buf), wbuf);
    }
    else
        buf[0] = '\0';

    // the EvtGetExtendedStatus() may be successful with an empty message
    if(!buf[0])
        strncpyz(buf, "no additional information", sizeof(buf) - 1);

    return buf;
}

// --------------------------------------------------------------------------------------------------------------------

bool EvtFormatMessage_utf16(
    TXT_UTF16 *dst, EVT_HANDLE hMetadata, EVT_HANDLE hEvent, DWORD dwMessageId, EVT_FORMAT_MESSAGE_FLAGS flags) {
    dst->used = 0;

    DWORD size = 0;
    if(!dst->data) {
        EvtFormatMessage(hMetadata, hEvent, dwMessageId, 0, NULL, flags, 0, NULL, &size);
        if(!size) {
            // nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtFormatMessage() to get message size failed.");
            goto cleanup;
        }
        txt_utf16_resize(dst, size, false);
    }

    // First, try to get the message using the existing buffer
    if (!EvtFormatMessage(hMetadata, hEvent, dwMessageId, 0, NULL, flags, dst->size, dst->data, &size) || !dst->data) {
        if (dst->data && GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
            // nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtFormatMessage() failed.");
            goto cleanup;
        }

        // Try again with the resized buffer
        if(!size)
            goto cleanup;

        txt_utf16_resize(dst, size, false);
        if (!EvtFormatMessage(hMetadata, hEvent, dwMessageId, 0, NULL, flags, dst->size, dst->data, &size)) {
            // nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtFormatMessage() failed after resizing buffer.");
            goto cleanup;
        }
    }

    if(!size || !dst->size)
        goto cleanup;

    // make sure it is null terminated
    if(size <= dst->size)
        dst->data[size - 1] = 0;
    else
        dst->data[dst->size - 1] = 0;

    // unfortunately we have to calculate the length every time
    // the size returned may not be the length of the dst string
    dst->used = wcslen(dst->data) + 1;

    return true;

cleanup:
    dst->used = 0;
    return false;
}

static bool EvtFormatMessage_utf8(
    TXT_UTF16 *tmp, PROVIDER_META_HANDLE *p, EVT_HANDLE hEvent,
        TXT_UTF8 *dst, EVT_FORMAT_MESSAGE_FLAGS flags) {

    dst->src = TXT_SOURCE_EVENT_LOG;

    if(EvtFormatMessage_utf16(tmp, provider_handle(p), hEvent, 0, flags))
        return txt_utf16_to_utf8(dst, tmp);

    txt_utf8_empty(dst);
    return false;
}

bool EvtFormatMessage_Event_utf8(TXT_UTF16 *tmp, PROVIDER_META_HANDLE *p, EVT_HANDLE hEvent, TXT_UTF8 *dst) {
    return EvtFormatMessage_utf8(tmp, p, hEvent, dst, EvtFormatMessageEvent);
}

bool EvtFormatMessage_Xml_utf8(TXT_UTF16 *tmp, PROVIDER_META_HANDLE *p, EVT_HANDLE hEvent, TXT_UTF8 *dst) {
    return EvtFormatMessage_utf8(tmp, p, hEvent, dst, EvtFormatMessageXml);
}

// --------------------------------------------------------------------------------------------------------------------

static void wevt_get_field_from_cache(
        WEVT_LOG *log, uint64_t value, PROVIDER_META_HANDLE *h,
        TXT_UTF8 *dst, const ND_UUID *provider,
        WEVT_FIELD_TYPE cache_type, EVT_FORMAT_MESSAGE_FLAGS flags) {

    if (field_cache_get(cache_type, provider, value, dst))
        return;

    EvtFormatMessage_utf8(&log->ops.unicode, h, log->hEvent, dst, flags);
    field_cache_set(cache_type, provider, value, dst);
}

// --------------------------------------------------------------------------------------------------------------------
// Level

#define SET_LEN_AND_RETURN(constant) *len = sizeof(constant) - 1; return constant

static inline const char *wevt_level_hardcoded(uint64_t level, size_t *len) {
    switch(level) {
        case WEVT_LEVEL_NONE:        SET_LEN_AND_RETURN(WEVT_LEVEL_NAME_NONE);
        case WEVT_LEVEL_CRITICAL:    SET_LEN_AND_RETURN(WEVT_LEVEL_NAME_CRITICAL);
        case WEVT_LEVEL_ERROR:       SET_LEN_AND_RETURN(WEVT_LEVEL_NAME_ERROR);
        case WEVT_LEVEL_WARNING:     SET_LEN_AND_RETURN(WEVT_LEVEL_NAME_WARNING);
        case WEVT_LEVEL_INFORMATION: SET_LEN_AND_RETURN(WEVT_LEVEL_NAME_INFORMATION);
        case WEVT_LEVEL_VERBOSE:     SET_LEN_AND_RETURN(WEVT_LEVEL_NAME_VERBOSE);
        default: *len = 0; return NULL;
    }
}

static void wevt_get_level(WEVT_LOG *log, WEVT_EVENT *ev, PROVIDER_META_HANDLE *h) {
    TXT_UTF8 *dst = &log->ops.level;
    uint64_t value = ev->level;

    txt_utf8_empty(dst);

    EVT_FORMAT_MESSAGE_FLAGS flags = EvtFormatMessageLevel;
    WEVT_FIELD_TYPE cache_type = WEVT_FIELD_TYPE_LEVEL;
    bool is_provider = is_valid_provider_level(value, true);

    if(!is_provider) {
        size_t len;
        const char *hardcoded = wevt_level_hardcoded(value, &len);
        if(hardcoded) {
            txt_utf8_set(dst, hardcoded,  len);
            dst->src = TXT_SOURCE_HARDCODED;
        }
        else {
            // since this is not a provider value
            // we expect to get the system description of it
            wevt_get_field_from_cache(log, value, h, dst, &ev->provider, cache_type, flags);
        }
    }
    else if (!provider_get_level(dst, h, value)) {
        // not found in the manifest, get it from the cache
        wevt_get_field_from_cache(log, value, h, dst, &ev->provider, cache_type, flags);
    }

    txt_utf8_set_numeric_if_empty(
            dst, WEVT_PREFIX_LEVEL, sizeof(WEVT_PREFIX_LEVEL) - 1, ev->level);
}

// --------------------------------------------------------------------------------------------------------------------
// Opcode

static inline const char *wevt_opcode_hardcoded(uint64_t opcode, size_t *len) {
    switch(opcode) {
        case WEVT_OPCODE_INFO:      SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_INFO);
        case WEVT_OPCODE_START:     SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_START);
        case WEVT_OPCODE_STOP:      SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_STOP);
        case WEVT_OPCODE_DC_START:  SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_DC_START);
        case WEVT_OPCODE_DC_STOP:   SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_DC_STOP);
        case WEVT_OPCODE_EXTENSION: SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_EXTENSION);
        case WEVT_OPCODE_REPLY:     SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_REPLY);
        case WEVT_OPCODE_RESUME:    SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_RESUME);
        case WEVT_OPCODE_SUSPEND:   SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_SUSPEND);
        case WEVT_OPCODE_SEND:      SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_SEND);
        case WEVT_OPCODE_RECEIVE:   SET_LEN_AND_RETURN(WEVT_OPCODE_NAME_RECEIVE);
        default: *len = 0; return NULL;
    }
}

static void wevt_get_opcode(WEVT_LOG *log, WEVT_EVENT *ev, PROVIDER_META_HANDLE *h) {
    TXT_UTF8 *dst = &log->ops.opcode;
    uint64_t value = ev->opcode;

    txt_utf8_empty(dst);

    EVT_FORMAT_MESSAGE_FLAGS flags = EvtFormatMessageOpcode;
    WEVT_FIELD_TYPE cache_type = WEVT_FIELD_TYPE_OPCODE;
    bool is_provider = is_valid_provider_opcode(value, true);

    if(!is_provider) {
        size_t len;
        const char *hardcoded = wevt_opcode_hardcoded(value, &len);
        if(hardcoded) {
            txt_utf8_set(dst, hardcoded,  len);
            dst->src = TXT_SOURCE_HARDCODED;
        }
        else {
            // since this is not a provider value
            // we expect to get the system description of it
            wevt_get_field_from_cache(log, value, h, dst, &ev->provider, cache_type, flags);
        }
    }
    else if (!provider_get_opcode(dst, h, value)) {
        // not found in the manifest, get it from the cache
        wevt_get_field_from_cache(log, value, h, dst, &ev->provider, cache_type, flags);
    }

    txt_utf8_set_numeric_if_empty(
            dst, WEVT_PREFIX_OPCODE, sizeof(WEVT_PREFIX_OPCODE) - 1, ev->opcode);
}

// --------------------------------------------------------------------------------------------------------------------
// Task

static const char *wevt_task_hardcoded(uint64_t task, size_t *len) {
    switch(task) {
        case WEVT_TASK_NONE: SET_LEN_AND_RETURN(WEVT_TASK_NAME_NONE);
        default: *len = 0; return NULL;
    }
}

static void wevt_get_task(WEVT_LOG *log, WEVT_EVENT *ev, PROVIDER_META_HANDLE *h) {
    TXT_UTF8 *dst = &log->ops.task;
    uint64_t value = ev->task;

    txt_utf8_empty(dst);

    EVT_FORMAT_MESSAGE_FLAGS flags = EvtFormatMessageTask;
    WEVT_FIELD_TYPE cache_type = WEVT_FIELD_TYPE_TASK;
    bool is_provider = is_valid_provider_task(value, true);

    if(!is_provider) {
        size_t len;
        const char *hardcoded = wevt_task_hardcoded(value, &len);
        if(hardcoded) {
            txt_utf8_set(dst, hardcoded,  len);
            dst->src = TXT_SOURCE_HARDCODED;
        }
        else {
            // since this is not a provider value
            // we expect to get the system description of it
            wevt_get_field_from_cache(log, value, h, dst, &ev->provider, cache_type, flags);
        }
    }
    else if (!provider_get_task(dst, h, value)) {
        // not found in the manifest, get it from the cache
        wevt_get_field_from_cache(log, value, h, dst, &ev->provider, cache_type, flags);
    }

    txt_utf8_set_numeric_if_empty(
            dst, WEVT_PREFIX_TASK, sizeof(WEVT_PREFIX_TASK) - 1, ev->task);
}

// --------------------------------------------------------------------------------------------------------------------
// Keyword

#define SET_BITS(msk, txt) { .mask = msk, .name = txt, .len = sizeof(txt) - 1, }

static uint64_t wevt_keyword_handle_reserved(uint64_t value, TXT_UTF8 *dst) {
    struct {
        uint64_t mask;
        const char *name;
        size_t len;
    } bits[] = {
        SET_BITS(WEVT_KEYWORD_EVENTLOG_CLASSIC, WEVT_KEYWORD_NAME_EVENTLOG_CLASSIC),
        SET_BITS(WEVT_KEYWORD_CORRELATION_HINT, WEVT_KEYWORD_NAME_CORRELATION_HINT),
        SET_BITS(WEVT_KEYWORD_AUDIT_SUCCESS, WEVT_KEYWORD_NAME_AUDIT_SUCCESS),
        SET_BITS(WEVT_KEYWORD_AUDIT_FAILURE, WEVT_KEYWORD_NAME_AUDIT_FAILURE),
        SET_BITS(WEVT_KEYWORD_SQM, WEVT_KEYWORD_NAME_SQM),
        SET_BITS(WEVT_KEYWORD_WDI_DIAG, WEVT_KEYWORD_NAME_WDI_DIAG),
        SET_BITS(WEVT_KEYWORD_WDI_CONTEXT, WEVT_KEYWORD_NAME_WDI_CONTEXT),
        SET_BITS(WEVT_KEYWORD_RESPONSE_TIME, WEVT_KEYWORD_NAME_RESPONSE_TIME),
    };

    txt_utf8_empty(dst);

    for(size_t i = 0; i < sizeof(bits) / sizeof(bits[0]) ;i++) {
        if((value & bits[i].mask) == bits[i].mask) {
            txt_utf8_add_keywords_separator_if_needed(dst);
            txt_utf8_append(dst, bits[i].name, bits[i].len);
            value &= ~(bits[i].mask);
            dst->src = TXT_SOURCE_HARDCODED;
        }
    }

    // return it without any remaining reserved bits
    return value & 0x0000FFFFFFFFFFFF;
}

static void wevt_get_keyword(WEVT_LOG *log, WEVT_EVENT *ev, PROVIDER_META_HANDLE *h) {
    TXT_UTF8 *dst = &log->ops.keywords;

    if(ev->keywords == WEVT_KEYWORD_NONE) {
        txt_utf8_set(dst, WEVT_KEYWORD_NAME_NONE, sizeof(WEVT_KEYWORD_NAME_NONE) - 1);
        dst->src = TXT_SOURCE_HARDCODED;
    }

    uint64_t value = wevt_keyword_handle_reserved(ev->keywords, dst);

    EVT_FORMAT_MESSAGE_FLAGS flags = EvtFormatMessageKeyword;
    WEVT_FIELD_TYPE cache_type = WEVT_FIELD_TYPE_KEYWORD;

    if(!value && dst->used <= 1) {
        // no hardcoded info in the buffer, make it None
        txt_utf8_set(dst, WEVT_KEYWORD_NAME_NONE, sizeof(WEVT_KEYWORD_NAME_NONE) - 1);
        dst->src = TXT_SOURCE_HARDCODED;
    }
    else if (value && !provider_get_keywords(dst, h, value) && dst->used <= 1) {
        // the provider did not provide any info and the description is still empty.
        // the system returns 1 keyword, the highest bit, not a list
        // so, when we call the system, we pass the original value (ev->keywords)
        wevt_get_field_from_cache(log, ev->keywords, h, dst, &ev->provider, cache_type, flags);
    }

    txt_utf8_set_hex_if_empty(
            dst, WEVT_PREFIX_KEYWORDS, sizeof(WEVT_PREFIX_KEYWORDS) - 1, ev->keywords);
}

// --------------------------------------------------------------------------------------------------------------------
// Fetching Events

static inline bool wEvtRender(WEVT_LOG *log, EVT_HANDLE context, WEVT_VARIANT *raw) {
    DWORD bytes_used = 0, property_count = 0;
    if (!EvtRender(context, log->hEvent, EvtRenderEventValues, raw->size, raw->data, &bytes_used, &property_count)) {
        // information exceeds the allocated space
        if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
            nd_log(NDLS_COLLECTORS, NDLP_ERR,
                   "EvtRender() failed, hRenderSystemContext: 0x%lx, hEvent: 0x%lx, content: 0x%lx, size: %u, extended info: %s",
                   (uintptr_t)context, (uintptr_t)log->hEvent, (uintptr_t)raw->data, raw->size,
                   EvtGetExtendedStatus_utf8());
            return false;
        }

        if (!bytes_used)
            return false;

        wevt_variant_resize(raw, bytes_used);
        if (!EvtRender(context, log->hEvent, EvtRenderEventValues, raw->size, raw->data, &bytes_used, &property_count)) {
            nd_log(NDLS_COLLECTORS, NDLP_ERR,
                   "EvtRender() failed, after bytes_used increase, extended info: %s",
                   EvtGetExtendedStatus_utf8());
            return false;
        }
    }

    if (property_count) {
#if SIZE_MAX <= UINT32_MAX
        if ((size_t)property_count > SIZE_MAX / sizeof(*raw->data))
            return false;
#endif

        if (!raw->data || bytes_used < property_count * sizeof(*raw->data))
            return false;
    }

    raw->used = bytes_used;
    raw->count = property_count;

    return true;
}

static bool wevt_get_next_event_one(WEVT_LOG *log, WEVT_EVENT *ev) {
    bool ret = false;

    if(!wEvtRender(log, log->hRenderSystemContext, &log->ops.raw.system))
        goto cleanup;

    EVT_VARIANT *content = log->ops.raw.system.data;
    if(!content || log->ops.raw.system.count < EvtSystemPropertyIdEND) {
        nd_log_limit_static_thread_var(system_fields_log_limit, 60, 0);
        nd_log_limit(&system_fields_log_limit, NDLS_COLLECTORS, NDLP_ERR,
                     "EvtRender() returned %u system fields, expected at least %u",
                     log->ops.raw.system.count, EvtSystemPropertyIdEND);
        goto cleanup;
    }

    ev->id          = wevt_field_get_uint64(&content[EvtSystemEventRecordId]);
    ev->event_id    = wevt_field_get_uint16(&content[EvtSystemEventID]);
    ev->level       = wevt_field_get_uint8(&content[EvtSystemLevel]);
    ev->opcode      = wevt_field_get_uint8(&content[EvtSystemOpcode]);
    ev->keywords    = wevt_field_get_uint64_hex(&content[EvtSystemKeywords]);
    ev->version     = wevt_field_get_uint8(&content[EvtSystemVersion]);
    ev->task        = wevt_field_get_uint16(&content[EvtSystemTask]);
    ev->qualifiers  = wevt_field_get_uint16(&content[EvtSystemQualifiers]);
    ev->process_id  = wevt_field_get_uint32(&content[EvtSystemProcessID]);
    ev->thread_id   = wevt_field_get_uint32(&content[EvtSystemThreadID]);
    ev->created_ns  = wevt_field_get_filetime_to_ns(&content[EvtSystemTimeCreated]);

    if(log->type & WEVT_QUERY_EXTENDED) {
        wevt_field_get_string_utf8(&content[EvtSystemChannel], &log->ops.channel);
        wevt_field_get_string_utf8(&content[EvtSystemComputer], &log->ops.computer);
        wevt_field_get_string_utf8(&content[EvtSystemProviderName], &log->ops.provider);
        wevt_get_uuid_by_type(&content[EvtSystemProviderGuid], &ev->provider);
        wevt_get_uuid_by_type(&content[EvtSystemActivityID], &ev->activity_id);
        wevt_get_uuid_by_type(&content[EvtSystemRelatedActivityID], &ev->related_activity_id);
        wevt_field_get_sid(&content[EvtSystemUserID], &log->ops.account, &log->ops.domain, &log->ops.sid);

        PROVIDER_META_HANDLE *p = log->provider =
                provider_get(ev->provider, wevt_field_get_string(&content[EvtSystemProviderName]));

        ev->platform = provider_get_platform(p);

        wevt_get_level(log, ev, p);
        wevt_get_task(log, ev, p);
        wevt_get_opcode(log, ev, p);
        wevt_get_keyword(log, ev, p);

        if(log->type & WEVT_QUERY_EVENT_DATA && wEvtRender(log, log->hRenderUserContext, &log->ops.raw.user)) {
#if (ON_FTS_PRELOAD_MESSAGE == 1)
            EvtFormatMessage_Event_utf8(&log->ops.unicode, log->provider, log->hEvent, &log->ops.event);
#endif
#if (ON_FTS_PRELOAD_XML == 1)
            EvtFormatMessage_Xml_utf8(&log->ops.unicode, log->provider, log->hEvent, &log->ops.xml);
#endif
#if (ON_FTS_PRELOAD_EVENT_DATA == 1)
            for(size_t i = 0; i < log->ops.raw.user.count ;i++)
                evt_variant_to_buffer(log->ops.event_data, &log->ops.raw.user.data[i], " ||| ");
#endif
        }
    }

    ret = true;

cleanup:
    return ret;
}

bool wevt_get_next_event(WEVT_LOG *log, WEVT_EVENT *ev) {
    DWORD size = (log->type & WEVT_QUERY_EXTENDED) ? BATCH_NEXT_EVENT : 1;
    DWORD max_failures = 10;

    fatal_assert(log && log->hQuery && log->hRenderSystemContext);

    while(max_failures > 0) {
        if (log->batch.used >= log->batch.size) {
            log->batch.size = 0;
            log->batch.used = 0;
            DWORD err;
            if(!EvtNext(log->hQuery, size, log->batch.hEvents, INFINITE, 0, &log->batch.size)) {
                err = GetLastError();
                if(err == ERROR_NO_MORE_ITEMS)
                    return false; // no data available, return failure
            }

            if(!log->batch.size) {
                if(size == 1) {
                    nd_log(NDLS_COLLECTORS, NDLP_ERR,
                           "EvtNext() failed, hQuery: 0x%lx, size: %zu, extended info: %s",
                           (uintptr_t)log->hQuery, (size_t)size, EvtGetExtendedStatus_utf8());
                    return false;
                }

                // EvtNext() returns true when it can full the array
                // so, let's retry with a smaller array.
                size /= 2;
                if(size < 1) size = 1;
                continue;
            }
        }

        log->query_stats.event_count++;
        log->log_stats.event_count++;

        // cleanup any previous event data
        wevt_event_done(log);

        log->hEvent = log->batch.hEvents[log->batch.used];
        log->batch.hEvents[log->batch.used] = NULL;
        log->batch.used++;

        if(wevt_get_next_event_one(log, ev))
            return true;
        else {
            log->query_stats.failed_count++;
            log->log_stats.failed_count++;
            max_failures--;
        }
    }

    return false;
}

static void wevt_event_done(WEVT_LOG *log) {
    if (log->provider) {
        provider_release(log->provider);
        log->provider = NULL;
    }

    if (log->hEvent) {
        EvtClose(log->hEvent);
        log->hEvent = NULL;
    }

    log->ops.channel.src = TXT_SOURCE_UNKNOWN;
    log->ops.provider.src = TXT_SOURCE_UNKNOWN;
    log->ops.computer.src = TXT_SOURCE_UNKNOWN;
    log->ops.account.src = TXT_SOURCE_UNKNOWN;
    log->ops.domain.src = TXT_SOURCE_UNKNOWN;
    log->ops.sid.src = TXT_SOURCE_UNKNOWN;

    log->ops.event.src = TXT_SOURCE_UNKNOWN;
    log->ops.level.src = TXT_SOURCE_UNKNOWN;
    log->ops.keywords.src = TXT_SOURCE_UNKNOWN;
    log->ops.opcode.src = TXT_SOURCE_UNKNOWN;
    log->ops.task.src = TXT_SOURCE_UNKNOWN;
    log->ops.xml.src = TXT_SOURCE_UNKNOWN;

    log->ops.channel.used = 0;
    log->ops.provider.used = 0;
    log->ops.computer.used = 0;
    log->ops.account.used = 0;
    log->ops.domain.used = 0;
    log->ops.sid.used = 0;

    log->ops.event.used = 0;
    log->ops.level.used = 0;
    log->ops.keywords.used = 0;
    log->ops.opcode.used = 0;
    log->ops.task.used = 0;
    log->ops.xml.used = 0;

    if(log->ops.event_data)
        log->ops.event_data->len = 0;
}

// --------------------------------------------------------------------------------------------------------------------
// Query management

bool wevt_query(WEVT_LOG *log, LPCWSTR channel, LPCWSTR query, EVT_QUERY_FLAGS direction) {
    wevt_query_done(log);
    log->log_stats.queries_count++;

    EVT_HANDLE hQuery = EvtQuery(NULL, channel, query, EvtQueryChannelPath | (direction & (EvtQueryReverseDirection | EvtQueryForwardDirection)) | EvtQueryTolerateQueryErrors);
    if (!hQuery) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtQuery() failed, query: %s | extended info: %s",
               query2utf8(query), EvtGetExtendedStatus_utf8());

        log->log_stats.queries_failed++;
        return false;
    }

    log->hQuery = hQuery;
    return true;
}

void wevt_query_done(WEVT_LOG *log) {
    // close the last working hEvent
    wevt_event_done(log);

    // close all batched hEvents
    for(DWORD i = log->batch.used; i < log->batch.size ;i++) {
        if(log->batch.hEvents[i])
            EvtClose(log->batch.hEvents[i]);

        log->batch.hEvents[i] = NULL;
    }
    log->batch.used = 0;
    log->batch.size = 0;

    if (log->hQuery) {
        EvtClose(log->hQuery);
        log->hQuery = NULL;
    }

    log->query_stats.event_count = 0;
    log->query_stats.failed_count = 0;
}

// --------------------------------------------------------------------------------------------------------------------
// Log management

WEVT_LOG *wevt_openlog6(WEVT_QUERY_TYPE type) {
    WEVT_LOG *log = callocz(1, sizeof(*log));
    log->type = type;

    // create the system render
    log->hRenderSystemContext = EvtCreateRenderContext(0, NULL, EvtRenderContextSystem);
    if (!log->hRenderSystemContext) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR,
               "EvtCreateRenderContext() on system context failed, extended info: %s",
               EvtGetExtendedStatus_utf8());
        goto cleanup;
    }

    if(type & WEVT_QUERY_EVENT_DATA) {
        log->hRenderUserContext = EvtCreateRenderContext(0, NULL, EvtRenderContextUser);
        if (!log->hRenderUserContext) {
            nd_log(NDLS_COLLECTORS, NDLP_ERR,
                   "EvtCreateRenderContext failed, on user context failed, extended info: %s",
                   EvtGetExtendedStatus_utf8());
            goto cleanup;
        }

        log->ops.event_data = buffer_create(4096, NULL);
    }

    return log;

cleanup:
    wevt_closelog6(log);
    return NULL;
}

void wevt_closelog6(WEVT_LOG *log) {
    wevt_query_done(log);

    if (log->hRenderSystemContext)
        EvtClose(log->hRenderSystemContext);

    if (log->hRenderUserContext)
        EvtClose(log->hRenderUserContext);

    wevt_variant_cleanup(&log->ops.raw.system);
    wevt_variant_cleanup(&log->ops.raw.user);
    txt_utf16_cleanup(&log->ops.unicode);
    txt_utf8_cleanup(&log->ops.channel);
    txt_utf8_cleanup(&log->ops.provider);
    txt_utf8_cleanup(&log->ops.computer);
    txt_utf8_cleanup(&log->ops.account);
    txt_utf8_cleanup(&log->ops.domain);
    txt_utf8_cleanup(&log->ops.sid);

    txt_utf8_cleanup(&log->ops.event);
    txt_utf8_cleanup(&log->ops.level);
    txt_utf8_cleanup(&log->ops.keywords);
    txt_utf8_cleanup(&log->ops.opcode);
    txt_utf8_cleanup(&log->ops.task);
    txt_utf8_cleanup(&log->ops.xml);

    buffer_free(log->ops.event_data);

    freez(log);
}

// --------------------------------------------------------------------------------------------------------------------
// Retention

bool wevt_channel_retention(WEVT_LOG *log, const wchar_t *channel, const wchar_t *query, EVT_RETENTION *retention) {
    bool ret = false;

    // get the number of the oldest record in the log
    // "EvtGetLogInfo()" does not work properly with "EvtLogOldestRecordNumber"
    // we have to get it from the first EventRecordID

    // query the eventlog
    log->hQuery = EvtQuery(NULL, channel, query, EvtQueryChannelPath | EvtQueryForwardDirection | EvtQueryTolerateQueryErrors);
    if (!log->hQuery) {
        if (GetLastError() == ERROR_EVT_CHANNEL_NOT_FOUND)
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtQuery() for retention failed, channel '%s' not found, cannot get retention, extended info: %s",
                   channel2utf8(channel), EvtGetExtendedStatus_utf8());
        else
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtQuery() for retention on channel '%s' failed, cannot get retention, extended info: %s",
                   channel2utf8(channel), EvtGetExtendedStatus_utf8());

        goto cleanup;
    }

    if (!wevt_get_next_event(log, &retention->first_event))
        goto cleanup;

    if (!retention->first_event.id) {
        // no data in the event log
        retention->first_event = retention->last_event = WEVT_EVENT_EMPTY;
        ret = true;
        goto cleanup;
    }
    EvtClose(log->hQuery);

    log->hQuery = EvtQuery(NULL, channel, query, EvtQueryChannelPath | EvtQueryReverseDirection | EvtQueryTolerateQueryErrors);
    if (!log->hQuery) {
        if (GetLastError() == ERROR_EVT_CHANNEL_NOT_FOUND)
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtQuery() for retention failed, channel '%s' not found, extended info: %s",
                   channel2utf8(channel), EvtGetExtendedStatus_utf8());
        else
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtQuery() for retention on channel '%s' failed, extended info: %s",
                   channel2utf8(channel), EvtGetExtendedStatus_utf8());

        goto cleanup;
    }

    if (!wevt_get_next_event(log, &retention->last_event) || retention->last_event.id == 0) {
        // no data in eventlog
        retention->last_event = retention->first_event;
    }
    retention->last_event.id += 1;	// we should read the last record
    ret = true;

cleanup:
    wevt_query_done(log);

    if(ret) {
        retention->entries = (channel && !query) ? retention->last_event.id - retention->first_event.id : 0;

        if(retention->last_event.created_ns >= retention->first_event.created_ns)
            retention->duration_ns = retention->last_event.created_ns - retention->first_event.created_ns;
        else
            retention->duration_ns = retention->first_event.created_ns - retention->last_event.created_ns;

        retention->size_bytes = wevt_log_file_size(channel);
    }
    else
        memset(retention, 0, sizeof(*retention));

    return ret;
}

static uint64_t wevt_log_file_size(const wchar_t *channel) {
    EVT_HANDLE hLog = NULL;
    EVT_VARIANT evtVariant;
    DWORD bufferUsed = 0;
    uint64_t file_size = 0;

    // Open the event log channel
    hLog = EvtOpenLog(NULL, channel, EvtOpenChannelPath);
    if (!hLog) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtOpenLog() on channel '%s' failed, extended info: %s",
               channel2utf8(channel), EvtGetExtendedStatus_utf8());
        goto cleanup;
    }

    // Get the file size of the log
    if (!EvtGetLogInfo(hLog, EvtLogFileSize, sizeof(evtVariant), &evtVariant, &bufferUsed)) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "EvtGetLogInfo() on channel '%s' failed, extended info: %s",
               channel2utf8(channel), EvtGetExtendedStatus_utf8());
        goto cleanup;
    }

    // Extract the file size from the EVT_VARIANT structure
    file_size = evtVariant.UInt64Val;

cleanup:
    if (hLog)
        EvtClose(hLog);

    return file_size;
}
