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

#include "windows_plugin.h"
#include "windows-internals.h"

#include "driver/netdata_driver.h"

static const char *srv_name = "NetdataDriver";
static const char *drv_path = "%SystemRoot%\\system32\\drivers\\netdata_driver.sys";

struct cpu_data {
    RRDDIM *rd_cpu_temp;

    collected_number cpu_temp;
    collected_number last_valid_temp;
    int read_errors;
};

struct cpu_data *cpus = NULL;
size_t ncpus = 0;
static ND_THREAD *hardware_info_thread = NULL;
static collected_number (*temperature_fcnt)(MSR_REQUEST *) = NULL;
static CRITICAL_SECTION cpus_lock;
bool cpus_lock_initialized = false;
static HANDLE msr_device = INVALID_HANDLE_VALUE;
static CRITICAL_SECTION device_lock;
bool device_lock_initialized = false;
// Set by the worker immediately before exit so cleanup can distinguish
// "join failed but thread is done" from "join failed and thread may still run".
static volatile LONG hardware_info_thread_finished = 0;
static int consecutive_errors = 0;
static const int MAX_CONSECUTIVE_ERRORS = 5;
static const int IOCTL_RETRIES = 3;
static const int IOCTL_RETRY_DELAY_MS = 10;
static const int THREAD_JOIN_FALLBACK_WAIT_MS = 2000;
#define INVALID_TEMP ((collected_number)(-1))

static bool netdata_expand_driver_path(char *expanded_path, size_t expanded_path_size)
{
    DWORD ret = ExpandEnvironmentStringsA(drv_path, expanded_path, (DWORD)expanded_path_size);
    if (ret == 0) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot expand environment strings. Error= %lu \n", GetLastError());
        return false;
    }

    if (ret > expanded_path_size) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Expanded driver path exceeds buffer size (%lu bytes needed)\n", ret);
        return false;
    }

    return true;
}

static void netdata_stop_driver()
{
    SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (scm == NULL) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open Service Manager. Error= %lu \n", GetLastError());
        return;
    }

    SC_HANDLE service = OpenService(scm, srv_name, SERVICE_STOP | SERVICE_QUERY_STATUS | DELETE);
    if (service == NULL) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open the service. Error= %lu \n", GetLastError());
        CloseServiceHandle(scm);
        return;
    }

    SERVICE_STATUS ss_status = {};
    if (ControlService(service, SERVICE_CONTROL_STOP, &ss_status) == 0) {
        DWORD err = GetLastError();
        if (err != ERROR_SERVICE_NOT_ACTIVE) {
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot stop the service. Error= %lu \n", err);
        }
    }

    if (!DeleteService(service)) {
        DWORD err = GetLastError();
        if (err != ERROR_SERVICE_MARKED_FOR_DELETE) {
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot delete service. Error= %lu \n", err);
        }
    }

    CloseServiceHandle(service);
    CloseServiceHandle(scm);
}

int netdata_install_driver()
{
    SC_HANDLE scm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);

    if (unlikely(!scm)) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open Service Manager. Error= %lu \n", GetLastError());
        return -1;
    }

    char expanded_path[MAX_PATH];
    if (!netdata_expand_driver_path(expanded_path, sizeof(expanded_path))) {
        CloseServiceHandle(scm);
        return -1;
    }

    // Create the service entry for the driver
    SC_HANDLE service = CreateServiceA(
        scm,
        srv_name,
        srv_name,
        SERVICE_START | SERVICE_STOP | DELETE,
        SERVICE_KERNEL_DRIVER,
        SERVICE_DEMAND_START,
        SERVICE_ERROR_NORMAL,
        expanded_path,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);

    if (unlikely(!service)) {
        if (GetLastError() == ERROR_SERVICE_EXISTS) {
            SC_HANDLE existing = OpenServiceA(scm, srv_name, SERVICE_CHANGE_CONFIG);
            if (!existing) {
                nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open existing service. Error= %lu \n", GetLastError());
                CloseServiceHandle(scm);
                return -1;
            }

            if (!ChangeServiceConfigA(
                    existing,
                    SERVICE_KERNEL_DRIVER,
                    SERVICE_DEMAND_START,
                    SERVICE_ERROR_NORMAL,
                    expanded_path,
                    NULL,
                    NULL,
                    NULL,
                    NULL,
                    NULL,
                    NULL)) {
                nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot reconfigure existing service. Error= %lu \n", GetLastError());
                CloseServiceHandle(existing);
                CloseServiceHandle(scm);
                return -1;
            }

            CloseServiceHandle(existing);
            CloseServiceHandle(scm);
            return 0;
        }

        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot create Service. Error= %lu \n", GetLastError());
        CloseServiceHandle(scm);
        return -1;
    }

    CloseServiceHandle(service);
    CloseServiceHandle(scm);

    return 0;
}

static inline void log_invalid_image_hash_error(void)
{
    nd_log(
        NDLS_COLLECTORS,
        NDLP_ERR,
        "Driver failed to start: ERROR_INVALID_IMAGE_HASH (577). "
        "This usually indicates a driver signature verification failure. "
        "The driver binary may be corrupted, unsigned, or signed with an untrusted certificate.\n");
}

int netdata_start_driver()
{
    SC_HANDLE scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT);
    if (unlikely(!scm)) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open Service Manager. Error= %lu \n", GetLastError());
        return -1;
    }

    SC_HANDLE service = OpenServiceA(scm, srv_name, SERVICE_START | SERVICE_QUERY_STATUS);
    if (unlikely(!service)) {
        DWORD open_err = GetLastError();
        CloseServiceHandle(scm);
        scm = NULL;

        // Service missing: attempt self-healing install then retry
        if (open_err == ERROR_SERVICE_DOES_NOT_EXIST) {
            nd_log(NDLS_COLLECTORS, NDLP_INFO, "Service not found, attempting to install driver and retry start\n");

            if (netdata_install_driver() != 0) {
                nd_log(NDLS_COLLECTORS, NDLP_ERR, "Failed to install driver during self-healing\n");
                return -1;
            }

            scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT);
            if (unlikely(!scm)) {
                nd_log(
                    NDLS_COLLECTORS,
                    NDLP_ERR,
                    "Cannot open Service Manager after install. Error= %lu \n",
                    GetLastError());
                return -1;
            }

            service = OpenServiceA(scm, srv_name, SERVICE_START | SERVICE_QUERY_STATUS);
            if (unlikely(!service)) {
                nd_log(
                    NDLS_COLLECTORS,
                    NDLP_ERR,
                    "Cannot open Service after install. Error= %lu \n",
                    GetLastError());
                CloseServiceHandle(scm);
                return -1;
            }
            // fall through to StartServiceA with the newly opened handle
        } else {
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open Service. Error= %lu \n", open_err);
            return -1;
        }
    }

    int ret = 0;
    if (!StartServiceA(service, 0, NULL)) {
        DWORD err = GetLastError();

        if (err == ERROR_SERVICE_ALREADY_RUNNING) {
            ret = 0;
        } else if (err == ERROR_INVALID_IMAGE_HASH) {
            log_invalid_image_hash_error();
            ret = -1;
        } else {
            nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot start Service. Error= %lu \n", err);
            ret = -1;
        }
    }

    if (service)
        CloseServiceHandle(service);
    if (scm)
        CloseServiceHandle(scm);
    return ret;
}

static inline HANDLE netdata_open_device()
{
    HANDLE msr_h =
        CreateFileA(MSR_USER_PATH, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (msr_h == INVALID_HANDLE_VALUE) {
        nd_log(NDLS_COLLECTORS, NDLP_ERR, "Cannot open device. GetLastError= %lu \n", GetLastError());
    }
    return msr_h;
}

static bool netdata_reopen_device_if_needed()
{
    EnterCriticalSection(&device_lock);
    if (msr_device != INVALID_HANDLE_VALUE) {
        LeaveCriticalSection(&device_lock);
        return true;
    }

    msr_device = netdata_open_device();
    bool ok = (msr_device != INVALID_HANDLE_VALUE);
    LeaveCriticalSection(&device_lock);
    return ok;
}

static bool netdata_read_msr(MSR_REQUEST *req)
{
    if (!req)
        return false;

    EnterCriticalSection(&device_lock);
    if (msr_device == INVALID_HANDLE_VALUE) {
        LeaveCriticalSection(&device_lock);
        return false;
    }

    bool success = false;
    for (int retry = 0; retry < IOCTL_RETRIES; retry++) {
        DWORD bytes = 0;
        if (DeviceIoControl(msr_device, IOCTL_MSR_READ, req, sizeof(*req), req, sizeof(*req), &bytes, NULL)) {
            success = true;
            break;
        }

        if (retry < IOCTL_RETRIES - 1) {
            Sleep(IOCTL_RETRY_DELAY_MS);
        }
    }

    LeaveCriticalSection(&device_lock);
    return success;
}

static collected_number netdata_intel_cpu_temp(MSR_REQUEST *req)
{
    if (!req)
        return INVALID_TEMP;

    const ULONG TJMAX = 100;
    ULONG digital_readout = (req->low >> 16) & 0x7F; // bits [22:16]

    collected_number temp = (collected_number)(TJMAX - digital_readout);

    if (temp < 0 || temp > 150)
        return INVALID_TEMP;

    return temp;
}

static collected_number netdata_amd_cpu_temp(MSR_REQUEST *req)
{
    if (!req)
        return INVALID_TEMP;

    ULONG amd_temp = (req->low >> 21) & 0x7FF;
    collected_number temp = (collected_number)amd_temp / 8;

    if (temp < 0 || temp > 150)
        return INVALID_TEMP;

    return temp;
}

void netdata_collect_cpu_chart()
{
    if (!netdata_reopen_device_if_needed()) {
        consecutive_errors++;
        if (consecutive_errors >= MAX_CONSECUTIVE_ERRORS) {
            nd_log(
                NDLS_COLLECTORS, NDLP_ERR, "MSR device unavailable for %d consecutive attempts\n", consecutive_errors);
        }
        return;
    }

    consecutive_errors = 0;
    const uint32_t MSR_THERM_STATUS = 0x19C;

    EnterCriticalSection(&cpus_lock);
    for (size_t cpu = 0; cpu < ncpus; cpu++) {
        MSR_REQUEST req = {MSR_THERM_STATUS, (ULONG)cpu, 0, 0};

        if (netdata_read_msr(&req)) {
            collected_number temp = 0;
            if (temperature_fcnt) {
                temp = temperature_fcnt(&req);
            }

            if (temp != INVALID_TEMP) {
                cpus[cpu].last_valid_temp = temp;
                cpus[cpu].cpu_temp = temp;
                cpus[cpu].read_errors = 0;
            } else {
                cpus[cpu].cpu_temp = cpus[cpu].last_valid_temp;
                cpus[cpu].read_errors++;
            }
        } else {
            cpus[cpu].cpu_temp = cpus[cpu].last_valid_temp;
            cpus[cpu].read_errors++;
        }
    }
    LeaveCriticalSection(&cpus_lock);
}

static void get_hardware_info_thread(void *ptr __maybe_unused)
{
    heartbeat_t hb;
    heartbeat_init(&hb, USEC_PER_SEC);

    while (service_running(SERVICE_COLLECTORS)) {
        (void)heartbeat_next(&hb);

        netdata_collect_cpu_chart();
    }

    InterlockedExchange(&hardware_info_thread_finished, 1);
}

static void netdata_detect_cpu()
{
    SYSTEM_INFO sysInfo;
    GetSystemInfo(&sysInfo);

    WORD test = sysInfo.wProcessorArchitecture;
    if (test != PROCESSOR_ARCHITECTURE_AMD64 && test != PROCESSOR_ARCHITECTURE_IA64) {
        return;
    }

    int cpuInfo[4];
    __cpuid(cpuInfo, 0);

    char vendorID[13];
    memcpy(vendorID, &cpuInfo[1], 4);
    memcpy(&vendorID[4], &cpuInfo[3], 4);
    memcpy(&vendorID[8], &cpuInfo[2], 4);
    vendorID[12] = '\0';

    if (!strcmp(vendorID, "GenuineIntel"))
        temperature_fcnt = netdata_intel_cpu_temp;
    else if (!strcmp(vendorID, "AuthenticAMD"))
        temperature_fcnt = netdata_amd_cpu_temp;
}

static int initialize()
{
    char expanded_path[MAX_PATH];
    if (!netdata_expand_driver_path(expanded_path, sizeof(expanded_path))) {
        return -1;
    }

    if (GetFileAttributesA(expanded_path) == INVALID_FILE_ATTRIBUTES) {
        nd_log(
            NDLS_COLLECTORS,
            NDLP_ERR,
            "Driver not found at '%s'. Please ensure the driver is properly installed.\n",
            expanded_path);
        return -1;
    }

    InitializeCriticalSection(&cpus_lock);
    cpus_lock_initialized = true;

    InitializeCriticalSection(&device_lock);
    device_lock_initialized = true;

    netdata_detect_cpu();
    if (!temperature_fcnt) {
        return -1;
    }

    if (netdata_install_driver()) {
        return -1;
    }

    if (netdata_start_driver()) {
        return -1;
    }

    ncpus = os_get_system_cpus();
    cpus = callocz(ncpus, sizeof(struct cpu_data));

    for (size_t i = 0; i < ncpus; i++) {
        cpus[i].cpu_temp = INVALID_TEMP;
        cpus[i].last_valid_temp = INVALID_TEMP;
        cpus[i].read_errors = 0;
    }

    InterlockedExchange(&hardware_info_thread_finished, 0);
    hardware_info_thread =
        nd_thread_create("hw_info_thread", NETDATA_THREAD_OPTION_DEFAULT, get_hardware_info_thread, NULL);

    return 0;
}

static RRDSET *netdata_publish_cpu_chart(int update_every)
{
    static RRDSET *st_cpu_temp = NULL;
    if (!st_cpu_temp) {
        st_cpu_temp = rrdset_create_localhost(
            "cpu",
            "temperature",
            NULL,
            "temperature",
            "cpu.temperature",
            "Core temperature",
            "Celsius",
            PLUGIN_WINDOWS_NAME,
            "GetHardwareInfo",
            NETDATA_CHART_PRIO_CPU_TEMPERATURE,
            update_every,
            RRDSET_TYPE_LINE);
    }

    return st_cpu_temp;
}

static void netdata_loop_cpu_chart(int update_every)
{
    if (unlikely(!cpus_lock_initialized || !cpus))
        return;

    RRDSET *chart = netdata_publish_cpu_chart(update_every);

    EnterCriticalSection(&cpus_lock);
    for (int i = 0; i < (int)ncpus; i++) {
        struct cpu_data *lcpu = &cpus[i];
        if (!lcpu->rd_cpu_temp) {
            char id[RRD_ID_LENGTH_MAX + 1];
            snprintfz(id, RRD_ID_LENGTH_MAX, "cpu%d.temp", i);
            lcpu->rd_cpu_temp = rrddim_add(chart, id, NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
        }

        if (lcpu->cpu_temp != INVALID_TEMP) {
            rrddim_set_by_pointer(chart, lcpu->rd_cpu_temp, lcpu->cpu_temp);
        } else {
            rrddim_set_by_pointer(chart, lcpu->rd_cpu_temp, 0);
        }
    }
    LeaveCriticalSection(&cpus_lock);

    rrdset_done(chart);
}

int do_GetHardwareInfo(int update_every, usec_t dt __maybe_unused)
{
    static bool initialized = false;
    static bool init_failed = false;

    if (unlikely(init_failed))
        return -1;

    if (unlikely(!initialized)) {
        if (initialize()) {
            init_failed = true;
            return -1;
        }
        initialized = true;
    }

    netdata_loop_cpu_chart(update_every);

    return 0;
}

void do_GetHardwareInfo_cleanup()
{
    if (hardware_info_thread) {
        if (nd_thread_join(hardware_info_thread)) {
            // nd_thread_join() frees the ND_THREAD object even on failure,
            // so we cannot retry. The Windows/MSYS2 UV_EINVAL fast-exit case
            // is already handled inside nd_thread_join(). For any other error,
            // wait for up to one heartbeat interval plus slack for the worker
            // to report completion before tearing down local resources it may
            // still be touching. If it never does, abort cleanup: leaking here
            // is safer than racing a live worker or hanging plugin shutdown
            // indefinitely.
            nd_log_daemon(NDLP_ERR, "Failed to join Get Hardware Info thread");

            size_t retries = 0;
            while (!InterlockedCompareExchange(&hardware_info_thread_finished, 1, 1) &&
                   retries < (size_t)THREAD_JOIN_FALLBACK_WAIT_MS) {
                Sleep(1);
                retries++;
            }

            if (!InterlockedCompareExchange(&hardware_info_thread_finished, 1, 1)) {
                hardware_info_thread = NULL;
                return;
            }
        }
        hardware_info_thread = NULL;
    }

    if (device_lock_initialized) {
        EnterCriticalSection(&device_lock);
        if (msr_device != INVALID_HANDLE_VALUE) {
            CloseHandle(msr_device);
            msr_device = INVALID_HANDLE_VALUE;
        }
        LeaveCriticalSection(&device_lock);
    } else if (msr_device != INVALID_HANDLE_VALUE) {
        CloseHandle(msr_device);
        msr_device = INVALID_HANDLE_VALUE;
    }

    netdata_stop_driver();

    if (cpus_lock_initialized)
        DeleteCriticalSection(&cpus_lock);

    if (device_lock_initialized)
        DeleteCriticalSection(&device_lock);

    if (cpus) {
        freez(cpus);
        cpus = NULL;
        ncpus = 0;
    }
}
