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

#include "nd_log-internals.h"

#if defined(OS_WINDOWS) && (defined(HAVE_ETW) || defined(HAVE_WEL))

// --------------------------------------------------------------------------------------------------------------------
// construct an event id

// load message resources generated header
#include "wevt_netdata.h"

// include the common definitions with the message resources and manifest generator
#include "nd_log-to-windows-common.h"

#if defined(HAVE_ETW)
// we need the manifest, only in ETW mode

// eliminate compiler warnings and load manifest generated header
#undef EXTERN_C
#define EXTERN_C
#undef __declspec
#define __declspec(x)
#include "wevt_netdata_manifest.h"

static REGHANDLE regHandle;
#endif

// Function to construct EventID
static DWORD complete_event_id(DWORD facility, DWORD severity, DWORD event_code) {
    DWORD event_id = 0;

    // Set Severity
    event_id |= ((DWORD)(severity) << EVENT_ID_SEV_SHIFT) & EVENT_ID_SEV_MASK;

    // Set Customer Code Flag (C)
    event_id |= (0x0 << EVENT_ID_C_SHIFT) & EVENT_ID_C_MASK;

    // Set Reserved Bit (R) - typically 0
    event_id |= (0x0 << EVENT_ID_R_SHIFT) & EVENT_ID_R_MASK;

    // Set Facility
    event_id |= ((DWORD)(facility) << EVENT_ID_FACILITY_SHIFT) & EVENT_ID_FACILITY_MASK;

    // Set Code
    event_id |= ((DWORD)(event_code) << EVENT_ID_CODE_SHIFT) & EVENT_ID_CODE_MASK;

    return event_id;
}

DWORD construct_event_id(ND_LOG_SOURCES source, ND_LOG_FIELD_PRIORITY priority, MESSAGE_ID messageID) {
    DWORD event_code = construct_event_code(source, priority, messageID);
    return complete_event_id(FACILITY_NETDATA, get_severity_from_priority(priority), event_code);
}

static bool check_event_id(ND_LOG_SOURCES source __maybe_unused, ND_LOG_FIELD_PRIORITY priority __maybe_unused, MESSAGE_ID messageID __maybe_unused, DWORD event_code __maybe_unused) {
#ifdef NETDATA_INTERNAL_CHECKS
    DWORD generated = construct_event_id(source, priority, messageID);
    if(generated != event_code) {

        // this is just used for a break point, to see the values in hex
        char current[UINT64_HEX_MAX_LENGTH];
        print_uint64_hex(current, generated);

        char wanted[UINT64_HEX_MAX_LENGTH];
        print_uint64_hex(wanted, event_code);

        const char *got = current;
        const char *good = wanted;
        internal_fatal(true, "EventIDs mismatch, expected %s, got %s", good, got);
    }
#endif

    return true;
}

// --------------------------------------------------------------------------------------------------------------------
// initialization

// Define provider names per source (only when not using ETW)
static const wchar_t *wel_provider_per_source[_NDLS_MAX] = {
        [NDLS_UNSET]        = NULL,                             // not used, linked to NDLS_DAEMON
        [NDLS_ACCESS]       = NETDATA_WEL_PROVIDER_ACCESS_W,    //
        [NDLS_ACLK]         = NETDATA_WEL_PROVIDER_ACLK_W,      //
        [NDLS_COLLECTORS]   = NETDATA_WEL_PROVIDER_COLLECTORS_W,//
        [NDLS_DAEMON]       = NETDATA_WEL_PROVIDER_DAEMON_W,    //
        [NDLS_HEALTH]       = NETDATA_WEL_PROVIDER_HEALTH_W,    //
        [NDLS_DEBUG]        = NULL,                             // used, linked to NDLS_DAEMON
};

bool wel_replace_program_with_wevt_netdata_dll(wchar_t *str, size_t size) {
    const wchar_t *replacement = L"\\wevt_netdata.dll";

    // Find the last occurrence of '\\' to isolate the filename
    wchar_t *lastBackslash = wcsrchr(str, L'\\');

    if (lastBackslash != NULL) {
        // Calculate new length after replacement
        size_t newLen = (lastBackslash - str) + wcslen(replacement);

        // Ensure new length does not exceed buffer size
        if (newLen >= size)
            return false; // Not enough space in the buffer

        // Terminate the string at the last backslash
        *lastBackslash = L'\0';

        // Append the replacement filename
        wcsncat(str, replacement, size - wcslen(str) - 1);

        // Check if the new file exists
        if (GetFileAttributesW(str) != INVALID_FILE_ATTRIBUTES)
            return true; // The file exists
        else
            return false; // The file does not exist
    }

    return false; // No backslash found (likely invalid input)
}

static bool wel_add_to_registry(const wchar_t *channel, const wchar_t *provider, DWORD defaultMaxSize) {
    // Build the registry path: SYSTEM\CurrentControlSet\Services\EventLog\<LogName>\<SourceName>
    wchar_t key[MAX_PATH];
    if(!provider)
        swprintf(key, MAX_PATH, L"SYSTEM\\CurrentControlSet\\Services\\EventLog\\%ls", channel);
    else
        swprintf(key, MAX_PATH, L"SYSTEM\\CurrentControlSet\\Services\\EventLog\\%ls\\%ls", channel, provider);

    HKEY hRegKey;
    DWORD disposition;
    LONG result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, key,
                                  0, NULL, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, NULL, &hRegKey, &disposition);

    if (result != ERROR_SUCCESS)
        return false; // Could not create the registry key

    // Check if MaxSize is already set
    DWORD maxSize = 0;
    DWORD size = sizeof(maxSize);
    if (RegQueryValueExW(hRegKey, L"MaxSize", NULL, NULL, (LPBYTE)&maxSize, &size) != ERROR_SUCCESS) {
        // MaxSize is not set, set it to the default value
        RegSetValueExW(hRegKey, L"MaxSize", 0, REG_DWORD, (const BYTE*)&defaultMaxSize, sizeof(defaultMaxSize));
    }

    wchar_t modulePath[MAX_PATH];
    if (GetModuleFileNameW(NULL, modulePath, MAX_PATH) == 0) {
        RegCloseKey(hRegKey);
        return false;
    }

    if(wel_replace_program_with_wevt_netdata_dll(modulePath, _countof(modulePath))) {
        RegSetValueExW(hRegKey, L"EventMessageFile", 0, REG_EXPAND_SZ,
                       (LPBYTE)modulePath, (wcslen(modulePath) + 1) * sizeof(wchar_t));

        DWORD types_supported = EVENTLOG_SUCCESS | EVENTLOG_ERROR_TYPE | EVENTLOG_WARNING_TYPE | EVENTLOG_INFORMATION_TYPE;
        RegSetValueExW(hRegKey, L"TypesSupported", 0, REG_DWORD, (LPBYTE)&types_supported, sizeof(DWORD));
    }

    RegCloseKey(hRegKey);
    return true;
}

#if defined(HAVE_ETW)
static void etw_set_source_meta(struct nd_log_source *source, USHORT channelID, const EVENT_DESCRIPTOR *ed) {
    // It turns out that the keyword varies per only per channel!
    // so, to log with the right keyword, Task, Opcode we copy the ids from the header
    // the messages compiler (mc.exe) generated from the manifest.

    source->channelID = channelID;
    source->Opcode = ed->Opcode;
    source->Task = ed->Task;
    source->Keyword = ed->Keyword;
}

// Callback for provider enable/disable notifications
static void NTAPI ProviderEnableCallback(
        LPCGUID SourceId __maybe_unused,
        ULONG IsEnabled,
        UCHAR Level __maybe_unused,
        ULONGLONG MatchAnyKeyword __maybe_unused,
        ULONGLONG MatchAllKeyword __maybe_unused,
        PEVENT_FILTER_DESCRIPTOR FilterData __maybe_unused,
        PVOID CallbackContext __maybe_unused
) {
    spinlock_lock(&nd_log.eventlog.provider_lock);
    nd_log.eventlog.provider_enabled = IsEnabled ? true : false;
    spinlock_unlock(&nd_log.eventlog.provider_lock);
}

static bool etw_register_provider(void) {
    spinlock_init(&nd_log.eventlog.provider_lock);
    nd_log.eventlog.provider_enabled = false;

    // Register the ETW provider
    if (EventRegister(&NETDATA_ETW_PROVIDER_GUID, ProviderEnableCallback, NULL, &regHandle) != ERROR_SUCCESS)
        return false;

    etw_set_source_meta(&nd_log.sources[NDLS_DAEMON], CHANNEL_DAEMON, &ED_DAEMON_INFO_MESSAGE_ONLY);
    etw_set_source_meta(&nd_log.sources[NDLS_COLLECTORS], CHANNEL_COLLECTORS, &ED_COLLECTORS_INFO_MESSAGE_ONLY);
    etw_set_source_meta(&nd_log.sources[NDLS_ACCESS], CHANNEL_ACCESS, &ED_ACCESS_INFO_MESSAGE_ONLY);
    etw_set_source_meta(&nd_log.sources[NDLS_HEALTH], CHANNEL_HEALTH, &ED_HEALTH_INFO_MESSAGE_ONLY);
    etw_set_source_meta(&nd_log.sources[NDLS_ACLK], CHANNEL_ACLK, &ED_ACLK_INFO_MESSAGE_ONLY);
    etw_set_source_meta(&nd_log.sources[NDLS_UNSET], CHANNEL_DAEMON, &ED_DAEMON_INFO_MESSAGE_ONLY);
    etw_set_source_meta(&nd_log.sources[NDLS_DEBUG], CHANNEL_DAEMON, &ED_DAEMON_INFO_MESSAGE_ONLY);

    DWORD wait_start = GetTickCount();
    while(true) {
        spinlock_lock(&nd_log.eventlog.provider_lock);
        bool enabled = nd_log.eventlog.provider_enabled;
        spinlock_unlock(&nd_log.eventlog.provider_lock);

        if(enabled)
            return true;

        // Timeout after 5 seconds
        if(GetTickCount() - wait_start > 5000) {
            EventUnregister(regHandle);
            return false;
        }

        Sleep(10); // Short sleep between checks
    }
}
#endif

bool nd_log_init_windows(void) {
    if(nd_log.eventlog.initialized)
        return true;

    // validate we have the right keys
    if(
            !check_event_id(NDLS_COLLECTORS, NDLP_INFO, MSGID_MESSAGE_ONLY, MC_COLLECTORS_INFO_MESSAGE_ONLY) ||
            !check_event_id(NDLS_DAEMON, NDLP_ERR, MSGID_MESSAGE_ONLY, MC_DAEMON_ERR_MESSAGE_ONLY) ||
            !check_event_id(NDLS_ACCESS, NDLP_WARNING, MSGID_ACCESS_USER, MC_ACCESS_WARN_ACCESS_USER) ||
            !check_event_id(NDLS_HEALTH, NDLP_CRIT, MSGID_ALERT_TRANSITION, MC_HEALTH_CRIT_ALERT_TRANSITION) ||
            !check_event_id(NDLS_DEBUG, NDLP_ALERT, MSGID_ACCESS_FORWARDER_USER, MC_DEBUG_ALERT_ACCESS_FORWARDER_USER))
       return false;

#if defined(HAVE_ETW)
    if(nd_log.eventlog.etw && !etw_register_provider())
        return false;
#endif

//    if(!nd_log.eventlog.etw && !wel_add_to_registry(NETDATA_WEL_CHANNEL_NAME_W, NULL, 50 * 1024 * 1024))
//        return false;

    // Loop through each source and add it to the registry
    for(size_t i = 0; i < _NDLS_MAX; i++) {
        nd_log.sources[i].source = i;

        const wchar_t *sub_channel = wel_provider_per_source[i];

        if(!sub_channel)
            // we will map these to NDLS_DAEMON
            continue;

        DWORD defaultMaxSize = 0;
        switch (i) {
            case NDLS_ACLK:
                defaultMaxSize = 5 * 1024 * 1024;
                break;

            case NDLS_HEALTH:
                defaultMaxSize = 35 * 1024 * 1024;
                break;

            default:
            case NDLS_ACCESS:
            case NDLS_COLLECTORS:
            case NDLS_DAEMON:
                defaultMaxSize = 20 * 1024 * 1024;
                break;
        }

        if(!nd_log.eventlog.etw) {
            if(!wel_add_to_registry(NETDATA_WEL_CHANNEL_NAME_W, sub_channel, defaultMaxSize))
                return false;

            // when not using a manifest, each source is a provider
            nd_log.sources[i].hEventLog = RegisterEventSourceW(NULL, sub_channel);
            if (!nd_log.sources[i].hEventLog)
                return false;
        }
    }

    if(!nd_log.eventlog.etw) {
        // Map the unset ones to NDLS_DAEMON
        for (size_t i = 0; i < _NDLS_MAX; i++) {
            if (!nd_log.sources[i].hEventLog)
                nd_log.sources[i].hEventLog = nd_log.sources[NDLS_DAEMON].hEventLog;
        }
    }

    nd_log.eventlog.initialized = true;
    return true;
}

bool nd_log_init_etw(void) {
    nd_log.eventlog.etw = true;
    return nd_log_init_windows();
}

bool nd_log_init_wel(void) {
    nd_log.eventlog.etw = false;
    return nd_log_init_windows();
}

// --------------------------------------------------------------------------------------------------------------------
// we pass all our fields to the windows events logs
// numbered the same way we have them in memory.
//
// to avoid runtime memory allocations, we use a static allocations with ready to use buffers
// which are immediately available for logging.

#define SMALL_WIDE_BUFFERS_SIZE 256
#define MEDIUM_WIDE_BUFFERS_SIZE 2048
#define BIG_WIDE_BUFFERS_SIZE 16384
static wchar_t small_wide_buffers[_NDF_MAX][SMALL_WIDE_BUFFERS_SIZE];
static wchar_t medium_wide_buffers[2][MEDIUM_WIDE_BUFFERS_SIZE];
static wchar_t big_wide_buffers[2][BIG_WIDE_BUFFERS_SIZE];

static struct {
    size_t size;
    wchar_t *buf;
} fields_buffers[_NDF_MAX] = { 0 };

#if defined(HAVE_ETW)
static EVENT_DATA_DESCRIPTOR etw_eventData[_NDF_MAX - 1];
#endif

static LPCWSTR wel_messages[_NDF_MAX - 1];

__attribute__((constructor)) void wevents_initialize_buffers(void) {
    for(size_t i = 0; i < _NDF_MAX ;i++) {
        fields_buffers[i].buf = small_wide_buffers[i];
        fields_buffers[i].size = SMALL_WIDE_BUFFERS_SIZE;
    }

    fields_buffers[NDF_NIDL_INSTANCE].buf = medium_wide_buffers[0];
    fields_buffers[NDF_NIDL_INSTANCE].size = MEDIUM_WIDE_BUFFERS_SIZE;

    fields_buffers[NDF_REQUEST].buf = big_wide_buffers[0];
    fields_buffers[NDF_REQUEST].size = BIG_WIDE_BUFFERS_SIZE;
    fields_buffers[NDF_MESSAGE].buf = big_wide_buffers[1];
    fields_buffers[NDF_MESSAGE].size = BIG_WIDE_BUFFERS_SIZE;

    for(size_t i = 1; i < _NDF_MAX ;i++)
        wel_messages[i - 1] = fields_buffers[i].buf;
}

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

#define is_field_set(fields, fields_max, field) ((field) < (fields_max) && (fields)[field].entry.set)

static const char *get_field_value_unsafe(struct log_field *fields, ND_LOG_FIELD_ID i, size_t fields_max, BUFFER **tmp) {
    if(!is_field_set(fields, fields_max, i) || !fields[i].eventlog)
        return "";

    static char number_str[MAX(MAX(UINT64_MAX_LENGTH, DOUBLE_MAX_LENGTH), UUID_STR_LEN)];

    const char *s = NULL;
    if (fields[i].logfmt_annotator)
        s = fields[i].logfmt_annotator(&fields[i]);

    else
        switch (fields[i].entry.type) {
            case NDFT_TXT:
                s = fields[i].entry.txt;
                break;
            case NDFT_STR:
                s = string2str(fields[i].entry.str);
                break;
            case NDFT_BFR:
                s = buffer_tostring(fields[i].entry.bfr);
                break;
            case NDFT_U64:
                print_uint64(number_str, fields[i].entry.u64);
                s = number_str;
                break;
            case NDFT_I64:
                print_int64(number_str, fields[i].entry.i64);
                s = number_str;
                break;
            case NDFT_DBL:
                print_netdata_double(number_str, fields[i].entry.dbl);
                s = number_str;
                break;
            case NDFT_UUID:
                if (!uuid_is_null(*fields[i].entry.uuid)) {
                    uuid_unparse_lower_compact(*fields[i].entry.uuid, number_str);
                    s = number_str;
                }
                break;
            case NDFT_CALLBACK:
                if (!*tmp)
                    *tmp = buffer_create(1024, NULL);
                else
                    buffer_flush(*tmp);

                if (fields[i].entry.cb.formatter(*tmp, fields[i].entry.cb.formatter_data))
                    s = buffer_tostring(*tmp);
                else
                    s = NULL;
                break;

            default:
                s = "UNHANDLED";
                break;
        }

    if(!s || !*s) return "";
    return s;
}
static void etw_replace_percent_with_unicode(wchar_t *s, size_t size) {
    size_t original_len = wcslen(s);

    // Traverse the string, replacing '%' with the Unicode fullwidth percent sign
    for (size_t i = 0; i < original_len && i < size - 1; i++) {
        if (s[i] == L'%' && iswdigit(s[i + 1])) {
            // s[i] = 0xFF05;  // Replace '%' with fullwidth percent sign '％'
            // s[i] = 0x29BC; // ⦼
            s[i] = 0x2105; // ℅
        }
    }

    // Ensure null termination if needed
    s[size - 1] = L'\0';
}

static void wevt_generate_all_fields_unsafe(struct log_field *fields, size_t fields_max, BUFFER **tmp) {
    for (size_t i = 0; i < fields_max; i++) {
        fields_buffers[i].buf[0] = L'\0';

        if (!fields[i].entry.set || !fields[i].eventlog)
            continue;

        const char *s = get_field_value_unsafe(fields, i, fields_max, tmp);
        if (s && *s) {
            utf8_to_utf16(fields_buffers[i].buf, (int) fields_buffers[i].size, s, -1);

            if(nd_log.eventlog.etw)
                // UNBELIEVABLE! they do recursive parameter expansion in ETW...
                etw_replace_percent_with_unicode(fields_buffers[i].buf, fields_buffers[i].size);
        }
    }
}

static bool has_user_role_permissions(struct log_field *fields, size_t fields_max, BUFFER **tmp) {
    const char *t;

    t = get_field_value_unsafe(fields, NDF_USER_NAME, fields_max, tmp);
    if (*t) return true;

    t = get_field_value_unsafe(fields, NDF_USER_ROLE, fields_max, tmp);
    if (*t && strcmp(t, "none") != 0) return true;

    t = get_field_value_unsafe(fields, NDF_USER_ACCESS, fields_max, tmp);
    if (*t && strcmp(t, "0x0") != 0) return true;

    return false;
}

static bool nd_logger_windows(struct nd_log_source *source, struct log_field *fields, size_t fields_max) {
    if (!nd_log.eventlog.initialized)
        return false;

    ND_LOG_FIELD_PRIORITY priority = NDLP_INFO;
    if (fields[NDF_PRIORITY].entry.set)
        priority = (ND_LOG_FIELD_PRIORITY) fields[NDF_PRIORITY].entry.u64;

    DWORD wType = get_event_type_from_priority(priority);
    (void) wType;

    CLEAN_BUFFER *tmp = NULL;

    static SPINLOCK spinlock = SPINLOCK_INITIALIZER;
    spinlock_lock(&spinlock);
    wevt_generate_all_fields_unsafe(fields, fields_max, &tmp);

    MESSAGE_ID messageID;
    switch (source->source) {
        default:
        case NDLS_DEBUG:
        case NDLS_DAEMON:
        case NDLS_COLLECTORS:
            messageID = MSGID_MESSAGE_ONLY;
            break;

        case NDLS_HEALTH:
            messageID = MSGID_ALERT_TRANSITION;
            break;

        case NDLS_ACCESS:
            if (is_field_set(fields, fields_max, NDF_MESSAGE)) {
                messageID = MSGID_ACCESS_MESSAGE;

                if (has_user_role_permissions(fields, fields_max, &tmp))
                    messageID = MSGID_ACCESS_MESSAGE_USER;
                else if (*get_field_value_unsafe(fields, NDF_REQUEST, fields_max, &tmp))
                    messageID = MSGID_ACCESS_MESSAGE_REQUEST;
            } else if (is_field_set(fields, fields_max, NDF_RESPONSE_CODE)) {
                messageID = MSGID_ACCESS;

                if (*get_field_value_unsafe(fields, NDF_SRC_FORWARDED_FOR, fields_max, &tmp))
                    messageID = MSGID_ACCESS_FORWARDER;

                if (has_user_role_permissions(fields, fields_max, &tmp)) {
                    if (messageID == MSGID_ACCESS)
                        messageID = MSGID_ACCESS_USER;
                    else
                        messageID = MSGID_ACCESS_FORWARDER_USER;
                }
            } else
                messageID = MSGID_REQUEST_ONLY;
            break;

        case NDLS_ACLK:
            messageID = MSGID_MESSAGE_ONLY;
            break;
    }

    if (messageID == MSGID_MESSAGE_ONLY && (
            *get_field_value_unsafe(fields, NDF_ERRNO, fields_max, &tmp) ||
            *get_field_value_unsafe(fields, NDF_WINERROR, fields_max, &tmp))) {
        messageID = MSGID_MESSAGE_ERRNO;
    }

    DWORD eventID = construct_event_id(source->source, priority, messageID);

    // wType
    //
    // without a manifest => this determines the Level of the event
    // with a manifest    => Level from the manifest is used (wType ignored)
    //                       [however it is good to have, in case the manifest is not accessible somehow]
    //

    // wCategory
    //
    // without a manifest => numeric Task values appear
    // with a manifest    => Task from the manifest is used (wCategory ignored)

    BOOL rc;
#if defined(HAVE_ETW)
    if (nd_log.eventlog.etw) {
        // metadata based logging - ETW

        for (size_t i = 1; i < _NDF_MAX; i++)
            EventDataDescCreate(&etw_eventData[i - 1], fields_buffers[i].buf,
                                (wcslen(fields_buffers[i].buf) + 1) * sizeof(WCHAR));

        EVENT_DESCRIPTOR EventDesc = {
                .Id = eventID & EVENT_ID_CODE_MASK, // ETW needs the raw event id
                .Version = 0,
                .Channel = source->channelID,
                .Level = get_level_from_priority(priority),
                .Opcode = source->Opcode,
                .Task = source->Task,
                .Keyword = source->Keyword,
        };

        rc = ERROR_SUCCESS == EventWrite(regHandle, &EventDesc, _NDF_MAX - 1, etw_eventData);

    }
    else
#endif
    {
        // eventID based logging - WEL
        rc = ReportEventW(source->hEventLog, wType, 0, eventID, NULL, _NDF_MAX - 1, 0, wel_messages, NULL);
    }

    spinlock_unlock(&spinlock);

    return rc == TRUE;
}

#if defined(HAVE_ETW)
bool nd_logger_etw(struct nd_log_source *source, struct log_field *fields, size_t fields_max) {
    return nd_logger_windows(source, fields, fields_max);
}
#endif

#if defined(HAVE_WEL)
bool nd_logger_wel(struct nd_log_source *source, struct log_field *fields, size_t fields_max) {
    return nd_logger_windows(source, fields, fields_max);
}
#endif

#endif
