/*
 * Copyright (c) 2022-2023 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "thermal_dfx.h"
#include <cerrno>
#include <cstdio>
#include <deque>
#include <dirent.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <unistd.h>
#include <hdf_log.h>
#include <hdf_base.h>

#include "directory_ex.h"
#include <datetime_ex.h>
#ifdef DATA_SIZE_HISYSEVENT_ENABLE
#include "hisysevent.h"
#endif
#include "param_wrapper.h"
#include "parameter.h"
#include "securec.h"
#include "string_ex.h"
#include "sysparam_errno.h"
#include "thermal_hdf_utils.h"
#ifdef THERMAL_HITRACE_ENABLE
#include "hitrace_meter.h"
#endif
#include "thermal_log.h"
#include "thermal_zone_manager.h"
#include "zlib.h"

namespace OHOS {
namespace HDI {
namespace Thermal {
namespace V1_1 {
namespace {
constexpr int32_t MAX_FILE_NUM = 10;
constexpr long int MAX_FILE_SIZE = 10 * 1024 * 1024;
constexpr int32_t MAX_TIME_LEN = 20;
constexpr int32_t TIME_FORMAT_1 = 1;
constexpr int32_t TIME_FORMAT_2 = 2;
constexpr int32_t COMPRESS_READ_BUF_SIZE = 4096;
constexpr int32_t DEFAULT_WIDTH = 20;
constexpr int32_t DEFAULT_INTERVAL = 5000;
constexpr int32_t MIN_INTERVAL = 100;
constexpr int32_t LOG_COMPARE_START_INDEX = 13;
constexpr int32_t LOG_COMPARE_SUB_LEN = 15;
constexpr int64_t TWENTY_FOUR_HOURS = 60 * 60 * 24 * 1000;
const std::string TIMESTAMP_TITLE = "timestamp";
const std::string THERMAL_LOG_ENABLE = "persist.thermal.log.enable";
const std::string THERMAL_LOG_WIDTH = "persist.thermal.log.width";
const std::string THERMAL_LOG_INTERVAL = "persist.thermal.log.interval";
const std::string DATA_FILE_PATH = "/data";
bool g_firstReport = true;
bool g_firstCreate = true;
std::deque<std::string> g_saveLogFile;
std::string g_outPath = "";
std::string g_logTime = "";
}

std::shared_ptr<ThermalDfx> ThermalDfx::instance_ = nullptr;
std::mutex ThermalDfx::mutexInstance_;

ThermalDfx& ThermalDfx::GetInstance()
{
    std::lock_guard<std::mutex> lock(mutexInstance_);
    if (instance_ == nullptr) {
        instance_ = std::make_shared<ThermalDfx>();
    }
    return *(instance_.get());
}

void ThermalDfx::DestroyInstance()
{
    std::lock_guard<std::mutex> lock(mutexInstance_);
    instance_ = nullptr;
}

static std::string GetCurrentTime(const int32_t format)
{
    struct tm* pTime;
    char strTime[MAX_TIME_LEN] = {0};
    time_t t;
    if (time(&t) == -1) {
        THERMAL_HILOGW(COMP_HDI, "call time failed");
        return "";
    }

    pTime = localtime(&t);
    if (pTime == nullptr) {
        THERMAL_HILOGW(COMP_HDI, "pTime Get localtime failed");
        return "";
    }
    if (format == TIME_FORMAT_1) {
        if (strftime(strTime, sizeof(strTime), "%Y%m%d-%H%M%S", pTime) == 0U) {
            THERMAL_HILOGW(COMP_HDI, "call strfime failed");
            return "";
        }
    } else if (format == TIME_FORMAT_2) {
        if (strftime(strTime, sizeof(strTime), "%Y-%m-%d %H:%M:%S", pTime) == 0U) {
            THERMAL_HILOGW(COMP_HDI, "call strfime failed");
            return "";
        }
    } else {
        THERMAL_HILOGW(COMP_HDI, "invalid format value");
        return "";
    }
    return strTime;
}

ThermalDfx::ThermalDfx() :
    width_(static_cast<uint8_t>(DEFAULT_WIDTH)), interval_(static_cast<uint32_t>(DEFAULT_INTERVAL)), enable_(true)
{
}

ThermalDfx::~ThermalDfx()
{
    enable_ = false;
}

std::string ThermalDfx::CanonicalizeSpecPath(const char* src)
{
    if (src == nullptr || strlen(src) >= PATH_MAX) {
        fprintf(stderr, "Error: CanonicalizeSpecPath failed, invalid path");
        return "";
    }
    char resolvedPath[PATH_MAX] = { 0 };
    if (access(src, F_OK) == 0) {
        if (realpath(src, resolvedPath) == nullptr) {
            fprintf(stderr, "Error: realpath %s failed", src);
            return "";
        }
    } else {
        std::string fileName(src);
        if (fileName.find("..") == std::string::npos) {
            if (snprintf_s(resolvedPath, PATH_MAX, sizeof(resolvedPath) - 1, src) == -1) {
                fprintf(stderr, "Error: sprintf_s %s failed", src);
                return "";
            }
        } else {
            fprintf(stderr, "Error: find .. %s failed", src);
            return "";
        }
    }

    std::string res(resolvedPath);
    return res;
}

bool ThermalDfx::Compress(const std::string& dataFile, const std::string& destFile)
{
    std::string resolvedPath = CanonicalizeSpecPath(dataFile.c_str());
    FILE* fp = fopen(resolvedPath.c_str(), "rb");
    if (fp == nullptr) {
        THERMAL_HILOGE(COMP_HDI, "Fail to open data file %{public}s", dataFile.c_str());
        perror("Fail to fopen(rb)");
        return false;
    }

    std::unique_ptr<gzFile_s, decltype(&gzclose)> fgz(gzopen(destFile.c_str(), "wb"), gzclose);
    if (fgz == nullptr) {
        THERMAL_HILOGE(COMP_HDI, "Fail to call gzopen(%{public}s)", destFile.c_str());
        fclose(fp);
        return false;
    }

    std::vector<char> buf(COMPRESS_READ_BUF_SIZE);
    size_t len = 0;
    while ((len = fread(buf.data(), sizeof(uint8_t), buf.size(), fp))) {
        if (gzwrite(fgz.get(), buf.data(), len) == 0) {
            THERMAL_HILOGE(COMP_HDI, "Fail to call gzwrite for %{public}zu bytes", len);
            fclose(fp);
            return false;
        }
    }
    if (!feof(fp)) {
        if (ferror(fp) != 0) {
            THERMAL_HILOGE(COMP_HDI, "ferror return err");
            fclose(fp);
            return false;
        }
    }
    if (fclose(fp) < 0) {
        return false;
    }
    return true;
}

void ThermalDfx::CompressFile()
{
#ifdef THERMAL_HITRACE_ENABLE
    HitraceScopedEx trace(HITRACE_LEVEL_COMMERCIAL, HITRACE_TAG_POWER, "ThermalDfx_CompressFile");
#endif
    THERMAL_HILOGD(COMP_HDI, "CompressFile start");
    std::string unCompressFile = g_outPath + "/thermal.000." + g_logTime;

    FILE* fp = fopen(unCompressFile.c_str(), "rb");
    if (fp == nullptr) {
        THERMAL_HILOGE(COMP_HDI, "open uncompressfile failed");
        return;
    }

    if (fseek(fp, SEEK_SET, SEEK_END) != 0) {
        THERMAL_HILOGE(COMP_HDI, "fseek() failed");
        fclose(fp);
        return;
    }

    long int size = ftell(fp);
    if (size < MAX_FILE_SIZE) {
        if (fclose(fp) < 0) {
            THERMAL_HILOGW(COMP_HDI, "fclose() failed");
        }
        THERMAL_HILOGD(COMP_HDI, "file is not enough for compress");
        return;
    }
    if (fclose(fp) < 0) {
        THERMAL_HILOGW(COMP_HDI, "fclose() failed");
    }

    std::string compressFile = g_outPath + "/thermal.000." + g_logTime + ".gz";
    if (!Compress(unCompressFile, compressFile)) {
        THERMAL_HILOGE(COMP_HDI, "CompressFile fail");
        return;
    }

    if (remove(unCompressFile.c_str()) != 0) {
        THERMAL_HILOGW(COMP_HDI, "failed to remove file %{public}s", unCompressFile.c_str());
    }

    if (g_saveLogFile.size() >= MAX_FILE_NUM) {
        if (remove(g_saveLogFile.front().c_str()) != 0) {
            THERMAL_HILOGW(COMP_HDI, "failed to remove file %{public}s", compressFile.c_str());
        }
        g_saveLogFile.pop_front();
    }
    g_saveLogFile.push_back(compressFile);
    g_logTime = GetCurrentTime(TIME_FORMAT_1);
    THERMAL_HILOGD(COMP_HDI, "CompressFile done");
}

void ThermalDfx::RemoveLogFile()
{
    DIR* dir = opendir(g_outPath.c_str());
    if (dir == nullptr) {
        THERMAL_HILOGE(COMP_HDI, "Failed to open directory: %{public}s, errno = %{public}d",
            g_outPath.c_str(), errno);
        return;
    }

    struct dirent* entry;
    g_saveLogFile.clear();
    while ((entry = readdir(dir)) != nullptr) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        g_saveLogFile.push_back(g_outPath + "/" + entry->d_name);
    }
    std::sort(g_saveLogFile.begin(), g_saveLogFile.end(), [](const std::string &a, const std::string &b) {
        return a.substr(g_outPath.length() + LOG_COMPARE_START_INDEX, LOG_COMPARE_SUB_LEN) <
            b.substr(g_outPath.length() + LOG_COMPARE_START_INDEX, LOG_COMPARE_SUB_LEN);
    });
    while (static_cast<int32_t>(g_saveLogFile.size()) > MAX_FILE_NUM) {
        if (remove(g_saveLogFile.front().c_str()) != 0) {
            THERMAL_HILOGW(COMP_HDI, "failed to remove file %{public}s", g_saveLogFile.front().c_str());
        }
        g_saveLogFile.pop_front();
    }

    closedir(dir);
}

void ThermalDfx::CompressAllFile()
{
    std::string compressExtension = ".gz";
    std::deque<std::string> compressFileList(g_saveLogFile);
    g_saveLogFile.clear();
    for (auto& originFile : compressFileList) {
        if (originFile.rfind(compressExtension) != std::string::npos) {
            g_saveLogFile.emplace_back(originFile);
            continue;
        }
        std::string compressFile = originFile + compressExtension;
        if (!Compress(originFile, compressFile)) {
            THERMAL_HILOGE(COMP_HDI, "CompressFile fail");
            continue;
        }
        if (remove(originFile.c_str()) != 0) {
            THERMAL_HILOGW(COMP_HDI, "failed to remove file %{public}s", originFile.c_str());
        }
        g_saveLogFile.emplace_back(compressFile);
    }
}

bool ThermalDfx::PrepareWriteDfxLog()
{
    if (g_outPath == "") {
        THERMAL_HILOGW(COMP_HDI, "parse thermal_hdi_config.xml outpath fail");
        return false;
    }
    if (!enable_) {
        THERMAL_HILOGD(COMP_HDI, "param does not start recording");
        return false;
    }

    return true;
}

void ThermalDfx::CreateLogFile()
{
#ifdef THERMAL_HITRACE_ENABLE
    HitraceScopedEx trace(HITRACE_LEVEL_COMMERCIAL, HITRACE_TAG_POWER, "ThermalDfx_CreateLogFile");
#endif
    THERMAL_HILOGD(COMP_HDI, "CreateLogFile start");
    if (!PrepareWriteDfxLog()) {
        THERMAL_HILOGD(COMP_HDI, "prepare write dfx log failed");
        return;
    }
    if (g_firstCreate) {
        g_logTime = GetCurrentTime(TIME_FORMAT_1);
        RemoveLogFile();
        CompressAllFile();
        g_firstCreate = false;
    }
    std::string logFile = g_outPath + "/thermal.000." + g_logTime;
    if (access(g_outPath.c_str(), 0) == -1) {
        auto ret = ForceCreateDirectory(g_outPath.c_str());
        if (!ret) {
            THERMAL_HILOGE(COMP_HDI, "create output dir failed");
            return;
        }
    }

    bool isEmpty = false;
    std::ifstream fin(logFile);
    std::fstream file;
    file.open(logFile, std::ios::in);
    if (file.eof() || !fin) {
        isEmpty = true;
    }
    file.close();

    ProcessLogInfo(logFile, isEmpty);
    THERMAL_HILOGD(COMP_HDI, "CreateLogFile done");
}

void ThermalDfx::ProcessLogInfo(std::string& logFile, bool isEmpty)
{
    std::string currentTime = GetCurrentTime(TIME_FORMAT_2);
    std::ofstream wStream(logFile, std::ios::app);
    if (wStream.is_open()) {
        if (isEmpty) {
            WriteToEmptyFile(wStream, currentTime);
            return;
        }

        WriteToFile(wStream, currentTime);
        wStream.close();
    }
}

void ThermalDfx::WriteToEmptyFile(std::ofstream& wStream, std::string& currentTime)
{
    wStream << TIMESTAMP_TITLE;
    for (uint8_t i = 0; i < width_; ++i) {
        wStream << " ";
    }
    std::vector<DfxTraceInfo> logInfo = ThermalHdfConfig::GetInstance().GetTracingInfo();
    for (const auto& info : logInfo) {
        wStream << info.title;
        if (info.valuePath == logInfo.back().valuePath && info.title == logInfo.back().title) {
            break;
        }
        for (uint8_t i = 0; i < width_ - info.title.length(); ++i) {
            wStream << " ";
        }
    }
    wStream << "\n";

    WriteToFile(wStream, currentTime);
    wStream.close();
}

void ThermalDfx::WriteToFile(std::ofstream& wStream, std::string& currentTime)
{
    wStream << currentTime;
    for (uint8_t i = 0; i < width_ + TIMESTAMP_TITLE.length() - currentTime.length(); ++i) {
        wStream << " ";
    }
    std::vector<DfxTraceInfo>& logInfo = ThermalHdfConfig::GetInstance().GetTracingInfo();
    std::string value;
    for (const auto& info : logInfo) {
        if (!ThermalHdfUtils::ReadNode(info.valuePath, value)) {
            THERMAL_HILOGW(COMP_HDI, "Read node failed, title = %{public}s", info.title.c_str());
        }
        wStream << value;
        if (info.valuePath == logInfo.back().valuePath && info.title == logInfo.back().title) {
            break;
        }
        for (uint8_t i = 0; i < width_ - value.length(); ++i) {
            wStream << " ";
        }
    }
    wStream << "\n";
}

void ThermalDfx::InfoChangedCallback(const char* key, const char* value, void* context)
{
    if (key == nullptr || value == nullptr) {
        return;
    }
    std::string keyStr(key);
    std::string valueStr(value);
    THERMAL_HILOGI(COMP_HDI, "thermal log param change, key = %{public}s, value = %{public}s", keyStr.c_str(),
        valueStr.c_str());
    auto& thermalDfx = ThermalDfx::GetInstance();
    if (keyStr == THERMAL_LOG_ENABLE) {
        thermalDfx.EnableWatchCallback(valueStr);
    }
    if (keyStr == THERMAL_LOG_WIDTH) {
        thermalDfx.WidthWatchCallback(valueStr);
    }
    if (keyStr == THERMAL_LOG_INTERVAL) {
        thermalDfx.IntervalWatchCallback(valueStr);
    }
}

void ThermalDfx::WidthWatchCallback(const std::string& value)
{
    int32_t width = OHOS::StrToInt(value, width) ? width : DEFAULT_WIDTH;
    width_ = static_cast<uint8_t>((width < DEFAULT_WIDTH) ? DEFAULT_WIDTH : width);
}

void ThermalDfx::IntervalWatchCallback(const std::string& value)
{
    int32_t interval = OHOS::StrToInt(value, interval) ? interval : DEFAULT_INTERVAL;
    interval_ = static_cast<uint32_t>((interval < MIN_INTERVAL) ? MIN_INTERVAL : interval);
}

void ThermalDfx::EnableWatchCallback(const std::string& value)
{
    enable_ = (value == "true");
}

int32_t ThermalDfx::GetIntParameter(const std::string& key, const int32_t def, const int32_t minValue)
{
    int32_t value = OHOS::system::GetIntParameter(key, def);
    return (value < minValue) ? def : value;
}

bool ThermalDfx::GetBoolParameter(const std::string& key, const bool def)
{
    std::string value;
    if (OHOS::system::GetStringParameter(THERMAL_LOG_ENABLE, value) != 0) {
        return def;
    }
    return (value == "true");
}

uint32_t ThermalDfx::GetInterval()
{
    return interval_;
}

double ThermalDfx::GetDeviceValidSize(const std::string& path)
{
    struct statfs stat;
    if (statfs(path.c_str(), &stat) != 0) {
        return 0;
    }
    constexpr double units = 1024.0;
    return (static_cast<double>(stat.f_bfree) / units) * (static_cast<double>(stat.f_bsize) / units);
}

uint64_t ThermalDfx::GetDirectorySize(const std::string& directoryPath)
{
    uint64_t totalSize = 0;
    DIR* dir = opendir(directoryPath.c_str());
    if (dir == nullptr) {
        THERMAL_HILOGE(COMP_HDI, "Failed to open directory: %{public}s, errno = %{public}d",
            directoryPath.c_str(), errno);
        return totalSize;
    }

    struct dirent* entry;
    while ((entry = readdir(dir)) != nullptr) {
        if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
            continue;
        }

        std::string filePath = directoryPath + "/" + entry->d_name;
        struct stat fileStat;
        if (stat(filePath.c_str(), &fileStat) == 0) {
            totalSize += static_cast<uint64_t>(fileStat.st_size);
        }
    }

    closedir(dir);
    return totalSize;
}

void ThermalDfx::WriteDataHisysevent()
{
    THERMAL_HILOGD(COMP_HDI, "Report data hisysevent, g_outPath: %{public}s", g_outPath.c_str());
#ifdef DATA_SIZE_HISYSEVENT_ENABLE
    uint64_t remainSize = static_cast<uint64_t>(GetDeviceValidSize(DATA_FILE_PATH));
    uint64_t fileSize = GetDirectorySize(g_outPath);
    HiSysEventWrite(HiviewDFX::HiSysEvent::Domain::FILEMANAGEMENT, "USER_DATA_SIZE",
        HiviewDFX::HiSysEvent::EventType::STATISTIC,
        "COMPONENT_NAME", "drivers_peripheral_thermal",
        "PARTITION_NAME", DATA_FILE_PATH,
        "REMAIN_PARTITION_SIZE", remainSize,
        "FILE_OR_FOLDER_PATH", g_outPath,
        "FILE_OR_FOLDER_SIZE", fileSize);
#endif
}

void ThermalDfx::ReportDataHisysevent()
{
    if (g_firstReport) {
        WriteDataHisysevent();
        beginTimeMs_ = GetTickCount();
        g_firstReport = false;
        return;
    }
    int64_t endTimeMs = GetTickCount();
    if (endTimeMs - beginTimeMs_ >= TWENTY_FOUR_HOURS) {
        WriteDataHisysevent();
        beginTimeMs_ = endTimeMs;
    }
}

void ThermalDfx::DoWork()
{
    if (enable_) {
        CreateLogFile();
        CompressFile();
#ifdef DATA_SIZE_HISYSEVENT_ENABLE
        ReportDataHisysevent();
#endif
    }
}

void ThermalDfx::Init()
{
    beginTimeMs_ = GetTickCount();
    interval_ = static_cast<uint32_t>(GetIntParameter(THERMAL_LOG_INTERVAL, DEFAULT_INTERVAL, MIN_INTERVAL));
    width_ = static_cast<uint8_t>(GetIntParameter(THERMAL_LOG_WIDTH, DEFAULT_WIDTH, DEFAULT_WIDTH));
    enable_ = GetBoolParameter(THERMAL_LOG_ENABLE, true);
    THERMAL_HILOGI(COMP_HDI,
        "The thermal log param is init, interval_ = %{public}d, width = %{public}d, enable = %{public}d",
        interval_.load(), width_.load(), enable_.load());

    WatchParameter(THERMAL_LOG_ENABLE.c_str(), InfoChangedCallback, nullptr);
    WatchParameter(THERMAL_LOG_WIDTH.c_str(), InfoChangedCallback, nullptr);
    int32_t code = WatchParameter(THERMAL_LOG_INTERVAL.c_str(), InfoChangedCallback, nullptr);
    if (code != OHOSStartUpSysParamErrorCode::EC_SUCCESS) {
        THERMAL_HILOGW(COMP_HDI, "thermal log watch parameters failed. error = %{public}d", code);
    }

    XmlTraceConfig& config = ThermalHdfConfig::GetInstance().GetXmlTraceConfig();
    g_outPath = config.outPath;
}
} // V1_1
} // Thermal
} // HDI
} // OHOS