/*
 *  This file is part of nzbget. See <https://nzbget.com>.
 *
 *  Copyright (C) 2014-2019 Andrey Prygunkov <hugbug@users.sourceforge.net>
 *  Copyright (C) 2024 Denis <denis@nzbget.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */


#include "nzbget.h"
#include "StatMeter.h"
#include "Options.h"
#include "WorkState.h"
#include "ServerPool.h"
#include "DiskState.h"
#include "Util.h"

static const int DAYS_UP_TO_2013_JAN_1 = 15706;
static const int DAYS_IN_TWENTY_YEARS = 366*20;

void ServerVolume::CalcSlots(time_t locCurTime)
{
	m_secSlot = (int)locCurTime % 60;
	m_minSlot = ((int)locCurTime / 60) % 60;
	m_hourSlot = ((int)locCurTime % 86400) / 3600;
	int daysSince1970 = (int)locCurTime / 86400;
	m_daySlot = daysSince1970 - DAYS_UP_TO_2013_JAN_1 + 1;
	if (0 <= m_daySlot && m_daySlot < DAYS_IN_TWENTY_YEARS)
	{
		int curDay = daysSince1970;
		if (m_firstDay == 0 || m_firstDay > curDay)
		{
			m_firstDay = curDay;
		}
		m_daySlot = curDay - m_firstDay;
		if (m_daySlot + 1 > (int)m_bytesPerDays.size())
		{
			m_bytesPerDays.resize(m_daySlot + 1);
		}
	}
	else
	{
		m_daySlot = -1;
	}
}

void ServerVolume::AddData(int bytes)
{
	time_t curTime = Util::CurrentTime();
	time_t locCurTime = curTime + g_WorkState->GetLocalTimeOffset();
	time_t locDataTime = m_dataTime + g_WorkState->GetLocalTimeOffset();

	int lastMinSlot = m_minSlot;
	int lastHourSlot = m_hourSlot;

	CalcSlots(locCurTime);

	if (locCurTime != locDataTime)
	{
		// clear seconds/minutes/hours slots if necessary
		// also handle the backwards changes of system clock

		int totalDelta = (int)(locCurTime - locDataTime);
		int deltaSign = totalDelta >= 0 ? 1 : -1;
		totalDelta = abs(totalDelta);

		int secDelta = totalDelta;
		if (deltaSign < 0) secDelta++;
		if (secDelta >= 60) secDelta = 60;
		for (int i = 0; i < secDelta; i++)
		{
			int nulSlot = m_secSlot - i * deltaSign;
			if (nulSlot < 0) nulSlot += 60;
			if (nulSlot >= 60) nulSlot -= 60;
			m_bytesPerSeconds[nulSlot] = 0;
		}

		int minDelta = totalDelta / 60;
		if (deltaSign < 0) minDelta++;
		if (abs(minDelta) >= 60) minDelta = 60;
		if (minDelta == 0 && m_minSlot != lastMinSlot) minDelta = 1;
		for (int i = 0; i < minDelta; i++)
		{
			int nulSlot = m_minSlot - i * deltaSign;
			if (nulSlot < 0) nulSlot += 60;
			if (nulSlot >= 60) nulSlot -= 60;
			m_bytesPerMinutes[nulSlot] = 0;
		}

		int hourDelta = totalDelta / (60 * 60);
		if (deltaSign < 0) hourDelta++;
		if (hourDelta >= 24) hourDelta = 24;
		if (hourDelta == 0 && m_hourSlot != lastHourSlot) hourDelta = 1;
		for (int i = 0; i < hourDelta; i++)
		{
			int nulSlot = m_hourSlot - i * deltaSign;
			if (nulSlot < 0) nulSlot += 24;
			if (nulSlot >= 24) nulSlot -= 24;
			m_bytesPerHours[nulSlot] = 0;
		}
	}

	// add bytes to every slot
	m_bytesPerSeconds[m_secSlot] += bytes;
	m_bytesPerMinutes[m_minSlot] += bytes;
	m_bytesPerHours[m_hourSlot] += bytes;
	if (m_daySlot >= 0)
	{
		m_bytesPerDays[m_daySlot] += bytes;
	}
	m_totalBytes += bytes;
	m_customBytes += bytes;

	m_dataTime = curTime;
}

void ServerVolume::ResetCustom()
{
	m_customBytes = 0;
	m_customTime = Util::CurrentTime();
}

void ServerVolume::LogDebugInfo()
{
	info("   ---------- ServerVolume");

	StringBuilder msg;

	for (int i = 0; i < 60; i++)
	{
		msg.AppendFmt("[%i]=%" PRIi64 " ", i, m_bytesPerSeconds[i]);
	}
	info("Secs: %s", *msg);

	msg.Clear();
	for (int i = 0; i < 60; i++)
	{
		msg.AppendFmt("[%i]=%" PRIi64 " ", i, m_bytesPerMinutes[i]);
	}
	info("Mins: %s", *msg);

	msg.Clear();
	for (int i = 0; i < 24; i++)
	{
		msg.AppendFmt("[%i]=%" PRIi64 " ", i, m_bytesPerHours[i]);
	}
	info("Hours: %s", *msg);

	msg.Clear();
	for (int i = 0; i < (int)m_bytesPerDays.size(); i++)
	{
		msg.AppendFmt("[%i]=%" PRIi64 " ", m_firstDay + i, m_bytesPerDays[i]);
	}
	info("Days: %s", *msg);
}

StatMeter::StatMeter()
{
	debug("Creating StatMeter");

	ResetSpeedStat();
}

void StatMeter::Init()
{
	m_startServer = Util::CurrentTime();
	m_lastCheck = m_startServer.load();
	AdjustTimeOffset();

	m_serverVolumes.resize(1 + g_ServerPool->GetServers()->size());
}

void StatMeter::AdjustTimeOffset()
{
	time_t utcTime = Util::CurrentTime();
	tm tmSplittedTime;
	gmtime_r(&utcTime, &tmSplittedTime);
	tmSplittedTime.tm_isdst = -1;
	time_t locTime = mktime(&tmSplittedTime);
	time_t localTimeDelta = utcTime - locTime;
	g_WorkState->SetLocalTimeOffset((int)localTimeDelta + g_Options->GetTimeCorrection());
	m_lastTimeOffset = utcTime;

	debug("UTC delta: %i (%i+%i)", g_WorkState->GetLocalTimeOffset(), (int)localTimeDelta, g_Options->GetTimeCorrection());
}

/*
 * Called once per second.
 *  - detect large step changes of system time and adjust statistics;
 *  - save volume stats (if changed).
 */
void StatMeter::IntervalCheck()
{
	time_t m_curTime = Util::CurrentTime();
	time_t diff = m_curTime - m_lastCheck;
	if (diff > 60 || diff < 0)
	{
		m_startServer += diff + 1; // "1" because the method is called once per second
		if (m_startDownload != 0 && !m_standBy)
		{
			m_startDownload += diff + 1;
		}
		AdjustTimeOffset();
	}
	else if (m_lastTimeOffset > m_curTime ||
		m_curTime - m_lastTimeOffset > 60 * 60 * 3 ||
		(m_curTime - m_lastTimeOffset > 60 && !m_standBy))
	{
		// checking time zone settings may prevent the device from entering sleep/hibernate mode
		// check every minute if not in standby
		// check at least every 3 hours even in standby
		AdjustTimeOffset();
	}

	m_lastCheck = m_curTime;

	CheckQuota();

	if (m_statChanged)
	{
		Save();
	}
}

void StatMeter::EnterLeaveStandBy(bool enter)
{
	m_standBy = enter;
	if (enter)
	{
		m_pausedFrom = Util::CurrentTime();
	}
	else
	{
		time_t startDownload = 0;
		if (!m_startDownload.compare_exchange_strong(startDownload, Util::CurrentTime()))
		{
			m_startDownload += Util::CurrentTime() - m_pausedFrom;
		}

		m_pausedFrom = 0;
		ResetSpeedStat();
	}
}

void StatMeter::CalcTotalStat(int* upTimeSec, int* dnTimeSec, int64* allBytes, bool* standBy)
{
	time_t startServer = m_startServer.load();
	if (startServer > 0)
	{
		*upTimeSec = static_cast<int>(Util::CurrentTime() - startServer);
	}
	else
	{
		*upTimeSec = 0;
	}
	*standBy = m_standBy;
	if (*standBy)
	{
		*dnTimeSec = static_cast<int>(m_pausedFrom - m_startDownload);
	}
	else
	{
		*dnTimeSec = static_cast<int>(Util::CurrentTime() - m_startDownload);
	}
	*allBytes = m_allBytes;
}

// Average speed in last 30 seconds
int64 StatMeter::CalcCurrentDownloadSpeed()
{
	if (m_standBy)
	{
		return 0;
	}

	int64 timeDiff = static_cast<int64>(Util::CurrentTime()) - m_speedStartTime * SPEEDMETER_SLOTSIZE;
	if (timeDiff == 0)
	{
		return 0;
	}

	return m_speedTotalBytes / timeDiff;
}

// Amount of data downloaded in current second
int64 StatMeter::CalcMomentaryDownloadSpeed()
{
	time_t curTime = Util::CurrentTime();
	int64 speed = curTime == m_curSecTime ? m_curSecBytes.load() : 0;
	return speed;
}

void StatMeter::AddSpeedReading(int bytes)
{
	std::lock_guard<std::mutex> guard{m_speedTotalBytesMtx};

	time_t curTime = Util::CurrentTime();
	int nowSlot = static_cast<int>(curTime) / SPEEDMETER_SLOTSIZE;

	if (curTime != m_curSecTime)
	{
		m_curSecTime =	curTime;
		m_curSecBytes = 0;
	}
	m_curSecBytes += bytes;

	while (nowSlot > m_speedTime[m_speedBytesIndex])
	{
		//record bytes in next slot
		++m_speedBytesIndex;
		if (m_speedBytesIndex >= SPEEDMETER_SLOTS)
		{
			m_speedBytesIndex = 0;
		}

		//Adjust counters with outgoing information.
		m_speedTotalBytes = m_speedTotalBytes - static_cast<int64>(m_speedBytes[m_speedBytesIndex]);

		//Note we should really use the start time of the next slot
		//but its easier to just use the outgoing slot time. This
		//will result in a small error.
		m_speedStartTime = m_speedTime[m_speedBytesIndex].load();

		//Now reset.
		m_speedBytes[m_speedBytesIndex] = 0;
		m_speedTime[m_speedBytesIndex] = nowSlot;
	}

	// Once per second recalculate summary field "m_iSpeedTotalBytes" to recover from possible synchronisation errors
	if (curTime > m_speedCorrection)
	{
		int64 speedTotalBytes = 0;
		for (int i = 0; i < SPEEDMETER_SLOTS; i++)
		{
			speedTotalBytes += m_speedBytes[i];
		}
		m_speedTotalBytes = speedTotalBytes;
		m_speedCorrection = curTime;
	}

	if (m_speedTotalBytes == 0)
	{
		m_speedStartTime = nowSlot;
	}
	m_speedBytes[m_speedBytesIndex] += bytes;
	m_speedTotalBytes += bytes;
	m_allBytes += bytes;
}

void StatMeter::ResetSpeedStat()
{
	time_t curTime = Util::CurrentTime();
	m_speedStartTime = (int)curTime / SPEEDMETER_SLOTSIZE;
	for (int i = 0; i < SPEEDMETER_SLOTS; i++)
	{
		m_speedBytes[i] = 0;
		m_speedTime[i] = m_speedStartTime.load();
	}
	m_speedBytesIndex = 0;
	m_speedTotalBytes = 0;
	m_speedCorrection = curTime;
	m_curSecTime =	0;
	m_curSecBytes = 0;
}

void StatMeter::LogDebugInfo()
{
	info("   ---------- SpeedMeter");
	int speed = CalcCurrentDownloadSpeed() / 1024;
	int timeDiff = (int)Util::CurrentTime() - m_speedStartTime * SPEEDMETER_SLOTSIZE;
	info("      Speed: %i", speed);
	info("      SpeedStartTime: %i", m_speedStartTime.load());
	info("      SpeedTotalBytes: %" PRIi64, m_speedTotalBytes.load());
	info("      SpeedBytesIndex: %i", m_speedBytesIndex.load());
	info("      AllBytes: %" PRIi64, m_allBytes.load());
	info("      Time: %i", (int)Util::CurrentTime());
	info("      TimeDiff: %i", timeDiff);
	for (int i=0; i < SPEEDMETER_SLOTS; i++)
	{
		info("      Bytes[%i]: %i, Time[%i]: %i", i, m_speedBytes[i].load(), i, m_speedTime[i].load());
	}

	Guard guard(m_volumeMutex);
	int index = 0;
	for (ServerVolume& serverVolume : m_serverVolumes)
	{
		info("      ServerVolume %i", index);
		serverVolume.LogDebugInfo();
		index++;
	}
}

void StatMeter::AddServerData(int bytes, int serverId)
{
	if (bytes == 0)
	{
		return;
	}

	Guard guard(m_volumeMutex);
	m_serverVolumes[0].AddData(bytes);
	m_serverVolumes[serverId].AddData(bytes);
	m_statChanged = true;
}

GuardedServerVolumes StatMeter::GuardServerVolumes()
{
	GuardedServerVolumes serverVolumes(&m_serverVolumes, &m_volumeMutex);

	// update slots
	for (ServerVolume& serverVolume : m_serverVolumes)
	{
		serverVolume.AddData(0);
	}

	return serverVolumes;
}

void StatMeter::Save()
{
	if (!g_Options->GetServerMode())
	{
		return;
	}

	Guard guard(m_volumeMutex);
	g_DiskState->SaveStats(g_ServerPool->GetServers(), &m_serverVolumes);
	m_statChanged = false;
}

bool StatMeter::Load(bool* perfectServerMatch)
{
	Guard guard(m_volumeMutex);

	bool ok = g_DiskState->LoadStats(g_ServerPool->GetServers(), &m_serverVolumes, perfectServerMatch);

	for (ServerVolume& serverVolume : m_serverVolumes)
	{
		serverVolume.CalcSlots(serverVolume.GetDataTime() + g_WorkState->GetLocalTimeOffset());
	}

	return ok;
}

void StatMeter::CheckQuota()
{
	if ((g_Options->GetDailyQuota() == 0 && g_Options->GetMonthlyQuota() == 0))
	{
		return;
	}

	int64 monthBytes, dayBytes;
	CalcQuotaUsage(monthBytes, dayBytes);

	bool monthlyQuotaReached = g_Options->GetMonthlyQuota() > 0 && monthBytes >= (int64)g_Options->GetMonthlyQuota() * 1024 * 1024;
	bool dailyQuotaReached = g_Options->GetDailyQuota() > 0 && dayBytes >= (int64)g_Options->GetDailyQuota() * 1024 * 1024;

	if (monthlyQuotaReached && !g_WorkState->GetQuotaReached())
	{
		warn("Monthly quota reached at %s", *Util::FormatSize(monthBytes));
	}
	else if (dailyQuotaReached && !g_WorkState->GetQuotaReached())
	{
		warn("Daily quota reached at %s", *Util::FormatSize(dayBytes));
	}
	else if (!monthlyQuotaReached && !dailyQuotaReached && g_WorkState->GetQuotaReached())
	{
		info("Quota lifted");
	}

	g_WorkState->SetQuotaReached(monthlyQuotaReached || dailyQuotaReached);
}

void StatMeter::CalcQuotaUsage(int64& monthBytes, int64& dayBytes)
{
	Guard guard(m_volumeMutex);

	ServerVolume totalVolume = m_serverVolumes[0];

	time_t locTime = Util::CurrentTime() + g_WorkState->GetLocalTimeOffset();
	int daySlot = (int)(locTime / 86400) - totalVolume.GetFirstDay();

	dayBytes = 0;
	if (daySlot < (int)totalVolume.BytesPerDays()->size())
	{
		dayBytes = totalVolume.BytesPerDays()->at(daySlot);
	}

	int elapsedSlots = CalcMonthSlots(totalVolume);
	monthBytes = 0;
	int endSlot = std::max(daySlot - elapsedSlots, -1);
	for (int slot = daySlot; slot >= 0 && slot > endSlot; slot--)
	{
		if (slot < (int)totalVolume.BytesPerDays()->size())
		{
			monthBytes += totalVolume.BytesPerDays()->at(slot);
			debug("adding slot %i: %i", slot, (int)(totalVolume.BytesPerDays()->at(slot) / 1024 / 1024));
		}
	}

	debug("month volume: %i MB", (int)(monthBytes / 1024 / 1024));
}

int StatMeter::CalcMonthSlots(ServerVolume& volume)
{
	int elapsedDays;

	time_t locCurTime = Util::CurrentTime() + g_WorkState->GetLocalTimeOffset();
	tm dayparts;
	gmtime_r(&locCurTime, &dayparts);

	if (g_Options->GetQuotaStartDay() > dayparts.tm_mday)
	{
		dayparts.tm_mon--;
		dayparts.tm_mday = g_Options->GetQuotaStartDay();
		time_t prevMonth = Util::Timegm(&dayparts);
		tm prevparts;
		gmtime_r(&prevMonth, &prevparts);
		if (prevparts.tm_mday != g_Options->GetQuotaStartDay())
		{
			dayparts.tm_mday = 1;
			dayparts.tm_mon++;
			prevMonth = Util::Timegm(&dayparts);
		}
		elapsedDays = (int)(locCurTime - prevMonth) / 60 / 60 / 24 + 1;
	}
	else
	{
		elapsedDays = dayparts.tm_mday - g_Options->GetQuotaStartDay() + 1;
	}

	return elapsedDays;
}
