/*
 * Copyright (C) 2023-2024 Intel Corporation
 *
 * SPDX-License-Identifier: MIT
 *
 */

#include "level_zero/sysman/source/api/events/linux/sysman_os_events_imp.h"

#include "shared/source/debug_settings/debug_settings_manager.h"
#include "shared/source/utilities/directory.h"

#include "level_zero/sysman/source/api/events/sysman_events_imp.h"
#include "level_zero/sysman/source/api/memory/linux/sysman_os_memory_imp.h"
#include "level_zero/sysman/source/driver/sysman_driver_handle_imp.h"
#include "level_zero/sysman/source/shared/linux/sysman_fs_access_interface.h"
#include "level_zero/sysman/source/shared/linux/zes_os_sysman_driver_imp.h"
#include "level_zero/sysman/source/shared/linux/zes_os_sysman_imp.h"

#include <sys/stat.h>

namespace L0 {
namespace Sysman {

const std::string LinuxEventsUtil::add("add");
const std::string LinuxEventsUtil::remove("remove");
const std::string LinuxEventsUtil::change("change");
const std::string LinuxEventsUtil::unbind("unbind");
const std::string LinuxEventsUtil::bind("bind");

bool LinuxEventsImp::eventListen(zes_event_type_flags_t &pEvent, uint64_t timeout) {
    // This is dummy implementation, Actual implementation is handled at driver level
    // for all devices.
    return false;
}

ze_result_t LinuxEventsImp::eventRegister(zes_event_type_flags_t events) {
    if (0x7fff < events) {
        return ZE_RESULT_ERROR_INVALID_ENUMERATION;
    }

    auto pLinuxSysmanDriverImp = static_cast<LinuxSysmanDriverImp *>(globalSysmanDriver->pOsSysmanDriver);
    if (pLinuxSysmanDriverImp == nullptr) {
        NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                              "%s", "Os Sysman driver not initialized\n");
        return ZE_RESULT_ERROR_UNINITIALIZED;
    }
    pLinuxSysmanDriverImp->eventRegister(events, pLinuxSysmanImp->getSysmanDeviceImp());
    return ZE_RESULT_SUCCESS;
}

LinuxEventsImp::LinuxEventsImp(OsSysman *pOsSysman) {
    pLinuxSysmanImp = static_cast<LinuxSysmanImp *>(pOsSysman);
}

OsEvents *OsEvents::create(OsSysman *pOsSysman) {
    LinuxEventsImp *pLinuxEventsImp = new LinuxEventsImp(pOsSysman);
    return static_cast<OsEvents *>(pLinuxEventsImp);
}

LinuxEventsUtil::LinuxEventsUtil(LinuxSysmanDriverImp *pOsSysmanDriverImp) : pLinuxSysmanDriverImp(pOsSysmanDriverImp) {
}

bool LinuxEventsUtil::checkRasEvent(zes_event_type_flags_t &pEvent, SysmanDeviceImp *pSysmanDeviceImp, zes_event_type_flags_t registeredEvents) {
    for (auto rasHandle : pSysmanDeviceImp->pRasHandleContext->handleList) {
        zes_ras_properties_t properties = {};
        rasHandle->rasGetProperties(&properties);
        if ((registeredEvents & ZES_EVENT_TYPE_FLAG_RAS_CORRECTABLE_ERRORS) && (properties.type == ZES_RAS_ERROR_TYPE_CORRECTABLE)) {
            if (LinuxEventsUtil::checkRasEventOccured(rasHandle) == true) {
                pEvent |= ZES_EVENT_TYPE_FLAG_RAS_CORRECTABLE_ERRORS;
                return true;
            }
        }
        if ((registeredEvents & ZES_EVENT_TYPE_FLAG_RAS_UNCORRECTABLE_ERRORS) && (properties.type == ZES_RAS_ERROR_TYPE_UNCORRECTABLE)) {
            if (LinuxEventsUtil::checkRasEventOccured(rasHandle) == true) {
                pEvent |= ZES_EVENT_TYPE_FLAG_RAS_UNCORRECTABLE_ERRORS;
                return true;
            }
        }
    }
    return false;
}

bool LinuxEventsUtil::checkRasEventOccured(Ras *rasHandle) {
    zes_ras_config_t config = {};
    zes_ras_state_t state = {};
    rasHandle->rasGetConfig(&config);
    if (ZE_RESULT_SUCCESS == rasHandle->rasGetState(&state, 0)) {
        uint64_t totalCategoryThreshold = 0;
        for (int i = 0; i < ZES_MAX_RAS_ERROR_CATEGORY_COUNT; i++) {
            totalCategoryThreshold += state.category[i];
            if ((config.detailedThresholds.category[i] > 0) && (state.category[i] > config.detailedThresholds.category[i])) {
                return true;
            }
        }
        if ((config.totalThreshold > 0) && (totalCategoryThreshold > config.totalThreshold)) {
            return true;
        }
    }
    return false;
}

void LinuxEventsUtil::eventRegister(zes_event_type_flags_t events, SysmanDeviceImp *pSysmanDevice) {
    std::call_once(initEventsOnce, [this]() {
        this->init();
    });

    zes_event_type_flags_t prevRegisteredEvents = 0;
    if (deviceEventsMap.find(pSysmanDevice) != deviceEventsMap.end()) {
        prevRegisteredEvents = deviceEventsMap[pSysmanDevice];
    }

    eventsMutex.lock();
    if (!events) {
        // If user is trying to register events with empty events argument, then clear all the registered events
        if (deviceEventsMap.find(pSysmanDevice) != deviceEventsMap.end()) {
            deviceEventsMap[pSysmanDevice] = events;
        } else {
            deviceEventsMap.emplace(pSysmanDevice, events);
        }
    } else {
        zes_event_type_flags_t registeredEvents = 0;
        // supportedEventMask --> this mask checks for events that supported currently
        zes_event_type_flags_t supportedEventMask = ZES_EVENT_TYPE_FLAG_FABRIC_PORT_HEALTH | ZES_EVENT_TYPE_FLAG_DEVICE_DETACH |
                                                    ZES_EVENT_TYPE_FLAG_DEVICE_ATTACH | ZES_EVENT_TYPE_FLAG_DEVICE_RESET_REQUIRED |
                                                    ZES_EVENT_TYPE_FLAG_MEM_HEALTH | ZES_EVENT_TYPE_FLAG_RAS_CORRECTABLE_ERRORS |
                                                    ZES_EVENT_TYPE_FLAG_RAS_UNCORRECTABLE_ERRORS;
        if (deviceEventsMap.find(pSysmanDevice) != deviceEventsMap.end()) {
            registeredEvents = deviceEventsMap[pSysmanDevice];
        }
        registeredEvents |= (events & supportedEventMask);
        deviceEventsMap[pSysmanDevice] = registeredEvents;
    }

    // Write to Pipe only if eventregister() is called during listen and previously registered events are modified.
    if ((pipeFd[1] != -1) && (prevRegisteredEvents != deviceEventsMap[pSysmanDevice])) {
        uint8_t value = 0x00;
        if (NEO::SysCalls::write(pipeFd[1], &value, 1) < 0) {
            NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                                  "%s", "Write to Pipe failed\n");
        }
    }
    eventsMutex.unlock();
}

void LinuxEventsUtil::init() {
    pUdevLib = pLinuxSysmanDriverImp->getUdevLibHandle();
}

ze_result_t LinuxEventsUtil::eventsListen(uint64_t timeout, uint32_t count, zes_device_handle_t *phDevices, uint32_t *pNumDeviceEvents, zes_event_type_flags_t *pEvents) {
    memset(pEvents, 0, count * sizeof(zes_event_type_flags_t));
    *pNumDeviceEvents = 0;
    std::vector<zes_event_type_flags_t> registeredEvents(count);
    for (uint32_t devIndex = 0; devIndex < count; devIndex++) {
        auto device = static_cast<SysmanDeviceImp *>(L0::Sysman::SysmanDevice::fromHandle(phDevices[devIndex]));
        if (device == nullptr) {
            return ZE_RESULT_ERROR_INVALID_ARGUMENT;
        }
        eventsMutex.lock();
        if (deviceEventsMap.find(device) != deviceEventsMap.end()) {
            registeredEvents[devIndex] = deviceEventsMap[device];
        }
        eventsMutex.unlock();
        if (registeredEvents[devIndex]) {
            if ((registeredEvents[devIndex] & ZES_EVENT_TYPE_FLAG_RAS_CORRECTABLE_ERRORS) || (registeredEvents[devIndex] & ZES_EVENT_TYPE_FLAG_RAS_UNCORRECTABLE_ERRORS)) {
                if (checkRasEvent(pEvents[devIndex], device, registeredEvents[devIndex])) {
                    *pNumDeviceEvents = 1;
                    return ZE_RESULT_SUCCESS;
                }
            }

            if (registeredEvents[devIndex] & ZES_EVENT_TYPE_FLAG_DEVICE_RESET_REQUIRED) {
                zes_device_state_t deviceState = {};
                device->pGlobalOperations->deviceGetState(&deviceState);
                if (deviceState.reset & ZES_RESET_REASON_FLAG_REPAIR) {
                    pEvents[devIndex] |= ZES_EVENT_TYPE_FLAG_DEVICE_RESET_REQUIRED;
                    *pNumDeviceEvents = 1;
                    return ZE_RESULT_SUCCESS;
                }
            }
        }
    }

    if (listenSystemEvents(pEvents, count, registeredEvents, phDevices, timeout)) {
        *pNumDeviceEvents = 1;
    }

    return ZE_RESULT_SUCCESS;
}

bool LinuxEventsUtil::isResetRequired(void *dev, zes_event_type_flags_t &pEvent) {
    if (action.compare(change) != 0) {
        return false;
    }

    std::vector<std::string> properties{"RESET_FAILED", "RESET_REQUIRED", "DEVICE_STATUS"};
    for (auto &property : properties) {
        const char *propVal = nullptr;
        propVal = pUdevLib->getEventPropertyValue(dev, property.c_str());
        if (propVal) {
            std::string expectedStr = "NEEDS_RESET";
            if (property != "DEVICE_STATUS") {
                if (atoi(propVal) == 1) {
                    pEvent |= ZES_EVENT_TYPE_FLAG_DEVICE_RESET_REQUIRED;
                    return true;
                }
            } else if (propVal == expectedStr) {
                pEvent |= ZES_EVENT_TYPE_FLAG_DEVICE_RESET_REQUIRED;
                return true;
            }
        }
    }

    return false;
}

bool LinuxEventsUtil::checkDeviceDetachEvent(zes_event_type_flags_t &pEvent) {
    if (action.compare(remove) == 0) {
        pEvent |= ZES_EVENT_TYPE_FLAG_DEVICE_DETACH;
        return true;
    }
    return false;
}

bool LinuxEventsUtil::checkDeviceAttachEvent(zes_event_type_flags_t &pEvent) {
    if (action.compare(add) == 0) {
        pEvent |= ZES_EVENT_TYPE_FLAG_DEVICE_ATTACH;
        return true;
    }
    return false;
}

bool LinuxEventsUtil::checkIfMemHealthChanged(void *dev, zes_event_type_flags_t &pEvent) {
    if (action.compare(change) != 0) {
        return false;
    }

    std::vector<std::string> properties{"MEM_HEALTH_ALARM", "RESET_REQUIRED", "EC_PENDING", "DEGRADED", "EC_FAILED", "SPARING_STATUS_UNKNOWN"};
    for (auto &property : properties) {
        const char *propVal = nullptr;
        propVal = pUdevLib->getEventPropertyValue(dev, property.c_str());
        if (propVal && atoi(propVal) == 1) {
            pEvent |= ZES_EVENT_TYPE_FLAG_MEM_HEALTH;
            return true;
        }
    }
    return false;
}

bool LinuxEventsUtil::checkIfFabricPortStatusChanged(void *dev, zes_event_type_flags_t &pEvent) {
    if (action.compare(change) != 0) {
        return false;
    }

    const char *str = pUdevLib->getEventPropertyValue(dev, "TYPE");
    if (str == nullptr) {
        return false;
    }

    std::string expectedStr = "PORT_CHANGE";
    if (expectedStr == str) {
        pEvent |= ZES_EVENT_TYPE_FLAG_FABRIC_PORT_HEALTH;
        return true;
    }
    return false;
}

void LinuxEventsUtil::getDevIndexToDevPathMap(std::vector<zes_event_type_flags_t> &registeredEvents, uint32_t count, zes_device_handle_t *phDevices, std::map<uint32_t, std::string> &mapOfDevIndexToDevPath) {
    for (uint32_t devIndex = 0; devIndex < count; devIndex++) {
        auto device = static_cast<SysmanDeviceImp *>(L0::Sysman::SysmanDevice::fromHandle(phDevices[devIndex]));
        if (deviceEventsMap.find(device) != deviceEventsMap.end()) {
            registeredEvents[devIndex] = deviceEventsMap[device];
        }
        if (!registeredEvents[devIndex]) {
            continue;
        }

        auto *osInterface = static_cast<L0::Sysman::LinuxSysmanImp *>(device->deviceGetOsInterface());
        if (!osInterface) {
            NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                                  "%s", "Failed to get OS Interface\n");
            UNRECOVERABLE_IF(true);
        }
        std::string bdf;
        auto pSysfsAccess = &osInterface->getSysfsAccess();
        if (pSysfsAccess->getRealPath("device", bdf) == ZE_RESULT_SUCCESS) {
            // /sys needs to be removed from real path inorder to equate with
            // DEVPATH property of uevent.
            // Example of real path: /sys/devices/pci0000:97/0000:97:02.0/0000:98:00.0/0000:99:01.0/0000:9a:00.0
            // Example of DEVPATH: /devices/pci0000:97/0000:97:02.0/0000:98:00.0/0000:99:01.0/0000:9a:00.0/i915.iaf.0
            const auto loc = bdf.find("/devices");
            if (loc == std::string::npos) {
                NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                                      "%s", "Invalid device path\n");
                continue;
            }

            bdf = bdf.substr(loc);
            mapOfDevIndexToDevPath.insert({devIndex, bdf});
        } else {
            NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                                  "%s", "Failed to get real path of device\n");
        }
    }
}

bool LinuxEventsUtil::checkDeviceEvents(std::vector<zes_event_type_flags_t> &registeredEvents, std::map<uint32_t, std::string> mapOfDevIndexToDevPath, zes_event_type_flags_t *pEvents, void *dev) {
    const char *devicePath = pUdevLib->getEventPropertyValue(dev, "DEVPATH");
    bool retVal = false;
    if (devicePath != nullptr) {
        std::string devPath(devicePath);
        for (auto it = mapOfDevIndexToDevPath.begin(); it != mapOfDevIndexToDevPath.end(); it++) {
            if (devPath.find(it->second.c_str()) != std::string::npos) {
                if (registeredEvents[it->first] & ZES_EVENT_TYPE_FLAG_DEVICE_DETACH) {
                    if (checkDeviceDetachEvent(pEvents[it->first])) {
                        retVal = true;
                    }
                }
                if (registeredEvents[it->first] & ZES_EVENT_TYPE_FLAG_DEVICE_ATTACH) {
                    if (checkDeviceAttachEvent(pEvents[it->first])) {
                        retVal = true;
                    }
                }
                if (registeredEvents[it->first] & ZES_EVENT_TYPE_FLAG_DEVICE_RESET_REQUIRED) {
                    if (isResetRequired(dev, pEvents[it->first])) {
                        retVal = true;
                    }
                }
                if (registeredEvents[it->first] & ZES_EVENT_TYPE_FLAG_MEM_HEALTH) {
                    if (checkIfMemHealthChanged(dev, pEvents[it->first])) {
                        retVal = true;
                    }
                }
                if (registeredEvents[it->first] & ZES_EVENT_TYPE_FLAG_FABRIC_PORT_HEALTH) {
                    if (checkIfFabricPortStatusChanged(dev, pEvents[it->first])) {
                        retVal = true;
                    }
                }
                break;
            }
        }
    }
    return retVal;
}

bool LinuxEventsUtil::listenSystemEvents(zes_event_type_flags_t *pEvents, uint32_t count, std::vector<zes_event_type_flags_t> &registeredEvents, zes_device_handle_t *phDevices, uint64_t timeout) {
    std::call_once(initEventsOnce, [this]() {
        this->init();
    });

    bool retval = false;
    struct pollfd pfd[2];
    std::vector<std::string> subsystemList;
    std::map<uint32_t, std::string> mapOfDevIndexToDevPath = {};

    if (pUdevLib == nullptr) {
        NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                              "%s", "libudev library instantiation failed\n");
        return retval;
    }

    subsystemList.push_back("drm");
    subsystemList.push_back("auxiliary");
    pfd[0].fd = pUdevLib->registerEventsFromSubsystemAndGetFd(subsystemList);
    pfd[0].events = POLLIN;
    pfd[0].revents = 0;

    eventsMutex.lock();
    if (NEO::SysCalls::pipe(pipeFd) < 0) {
        NEO::printDebugString(NEO::debugManager.flags.PrintDebugMessages.get(), stderr,
                              "%s", "Creation of pipe failed\n");
    }

    pfd[1].fd = pipeFd[0];
    pfd[1].events = POLLIN;
    pfd[1].revents = 0;

    auto start = L0::Sysman::SteadyClock::now();
    std::chrono::duration<double, std::milli> timeElapsed;
    getDevIndexToDevPathMap(registeredEvents, count, phDevices, mapOfDevIndexToDevPath);
    eventsMutex.unlock();
    while (NEO::SysCalls::poll(pfd, 2, static_cast<int>(timeout)) > 0) {
        bool eventReceived = false;
        for (auto i = 0; i < 2; i++) {
            if (pfd[i].revents != 0) {
                if (pfd[i].fd == pipeFd[0]) {
                    eventsMutex.lock();
                    uint8_t dummy;
                    NEO::SysCalls::read(pipeFd[0], &dummy, 1);
                    mapOfDevIndexToDevPath.clear();
                    getDevIndexToDevPathMap(registeredEvents, count, phDevices, mapOfDevIndexToDevPath);
                    eventsMutex.unlock();
                } else {
                    eventReceived = true;
                }
            }
        }

        if (mapOfDevIndexToDevPath.empty()) {
            break;
        }

        if (!eventReceived) {
            timeElapsed = L0::Sysman::SteadyClock::now() - start;
            if (timeout > timeElapsed.count()) {
                timeout = timeout - timeElapsed.count();
                continue;
            } else {
                break;
            }
        }

        void *dev = nullptr;
        dev = pUdevLib->allocateDeviceToReceiveData();
        if (dev == nullptr) {
            timeElapsed = L0::Sysman::SteadyClock::now() - start;
            if (timeout > timeElapsed.count()) {
                timeout = timeout - timeElapsed.count();
                continue;
            } else {
                break;
            }
        }

        auto eventTypePtr = pUdevLib->getEventType(dev);
        if (eventTypePtr != nullptr) {
            action = std::string(eventTypePtr);
        } else {
            break;
        }

        retval = checkDeviceEvents(registeredEvents, mapOfDevIndexToDevPath, pEvents, dev);
        pUdevLib->dropDeviceReference(dev);
        if (retval) {
            break;
        }
        timeElapsed = L0::Sysman::SteadyClock::now() - start;
        if (timeout > timeElapsed.count()) {
            timeout = timeout - timeElapsed.count();
            continue;
        } else {
            break;
        }
    }

    eventsMutex.lock();
    for (uint8_t i = 0; i < 2; i++) {
        if (pipeFd[i] != -1) {
            NEO::SysCalls::close(pipeFd[i]);
            pipeFd[i] = -1;
        }
    }
    eventsMutex.unlock();
    return retval;
}

} // namespace Sysman
} // namespace L0
