#include "TrafficControlThread.h"
#include "Logger.h"
#include "ProcessConfig.h"
#include "RcdbInfo.h"
#include "Database.h"
#include "ReportLog.h"

#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sqlite3.h>


// 네트워크 통계 모듈의 sqlite SQLITE_BUSY 잦은 발생으로 INSERT 지연 발생 (#21440)
// 이로 인해 Traffic 통계 합산이 정상적으로 처리되지 않을 수 있으므로 다음과 같이 조정 처리
// - Timestamp delay 15 sec -> 16 sec 로 변경 처리 ( 기존 3.4 rc_statd 는 15 sec 였으나.. 신규 fimngd 적용에 따라 조정 처리)
// - sqlite lock timeout 값을 3 sec -> 1 sec 로 변경 처리 

#define TIMESTAMP_DELAY_SEC           16    // 5분단위 timestamp 변경시... 변경 후.. 어느만큼 delay 를 허용할지 시간값.
#define SQLITE_DB_LOCK_TIMEOUT      1000    // 세션 중에 SQLITE_BUSY 상황 발생 문제 해결을 위해 DB Lock 관련 timeout 설정값. (1 sec)


#define TRAFFIC_CONTROL_LOW_LIMIT     25    // 트래픽 제한 관련 전송률의 최소값(%), 해당값 이하로는 떨어지지 않도록 처리.
#define REPORT_TRAFFIC_CONTROL_LIMIT  50    // 트래픽 제한 관련 전송률이 해당 값(%) 이하로 떨어질 경우.. report 로그에 기록 처리한다.


// 생성자..
CTrafficControlThread::CTrafficControlThread()
{
	// Thread Handle 초기화
	m_threadHandle = 0;
}

// 소멸자..
CTrafficControlThread::~CTrafficControlThread()
{
	// Thread 가 동작 중인 경우 Thread 동작 정지 처리.
	if( m_threadHandle != 0 )
		pthread_cancel(m_threadHandle); 

}


// Thread 를 생성하여 작업을 시작한다...
bool CTrafficControlThread::Start()
{
	int nResult = pthread_create( &m_threadHandle, NULL, CTrafficControlThread::threadFunc, this );
	if( nResult != 0 )
	{
		// Thread 생성 실패시...
		int errorNum = errno;
		LOG( LERR, "TrafficControlThread: Thread create failed.[%d][%s]", errorNum, strerror(errorNum) );

		return false;
	}


	return true;
}


// Thread 함수...
void * CTrafficControlThread::threadFunc( void * arg )
{
	CTrafficControlThread * pObject = reinterpret_cast<CTrafficControlThread *>(arg);
	pthread_detach( pthread_self() );

	// 초기 기동시...통계 정보 및 timestamp 처리 관련 문제로 인해....
	// 지정된 시간 sleep 했다가.. 작업을 시작한다.
	sleep( TIMESTAMP_DELAY_SEC );

	// 본 Thread 에서는 
	// RCDB t_sms_sp_svc_product_band 테이블에 저장된 서비스별 트래픽 제어 정보를 조회하여..
	// Local DB 에 저장된 서비스별 Network 통계 정보를 바탕으로... 현재 5분간 트래픽을 어느 수준으로 제어할 것인지 판단하여...
	// RCDB t_sms_sp_svc_product_band 테이블에 UPDATE 처리를 주기적으로 수행한다.
	// 실제 본 thread function 에서 loop 를 동작하는 것이 아니라....
	// 객체 execute 함수 내에서 loop 로 동작하도록 한다.
	
	pthread_testcancel();
	pObject->Execute();
	pthread_testcancel();

	// Thread 종료시 Handle 초기화
	pObject->m_threadHandle = 0;

	return NULL;
}

// RCDB t_sms_sp_svc_product_band 테이블에 저장된 서비스별 트래픽 제어 정보를 조회하여.. 
// Local DB 에 저장된 Network 통계 정보와 계산을 통해.. 향후 5분간 서비스 트래픽을 어느수준으로 제어할 것인지 계산...
// 해당 결과를 RCDB t_sms_sp_svc_product_band 테이블에 업데이트 처리한다.
void CTrafficControlThread::Execute()
{
	// 변수 선언 및 초기화..
	time_t currentTime;        // 현재 시간 정보를 저장하기 위한 변수..

	unsigned long nowTimestamp   = 0;  // 현재 시간 기준으로 계산된 timestamp;
	unsigned long delayTimestamp = 0;  // 지정된 시간동안 delay 를 통해 보정 처리된 timestamp;


	// RCDB 접속 정보 저장 관련 객체 생성 및 초기화...
	CRcdbInfo rcdbInfo;
	rcdbInfo.Load();

	// Local DB 관련 변수 초기화.
	sqlite3      * db   = NULL;
	sqlite3_stmt * stmt = NULL;
	int db_result;
	int nRetryCount;


	const char * select_sql = "SELECT sum(down_traffic)*8/300 FROM network_stat WHERE timestamp = ? AND user_seq = ? AND svc_seq = ?";


	// RCDB 조회 결과를 저장하기 위한 임시 변수.
	struct traffic_control_info stTraffic;
	int nResult;
	

	// Logging
	_LOG( LINF, "TrafficControlThread: start...");


	// 주기적으로 Network 통계 정보를 바탕으로 RCDB 트래픽 제어 정보를 업데이트 한다.
	while( 1 )
	{

		// FHS 는 장비의 시간을 기준으로.. timestamp 값이 변경 후 20 sec 후에... 트래픽 제한 정보를 refresh 처리한다.
		// 기존 rc_statd(3.4 이전) 는 대략 timestamp 변경 후 12 sec 후에 RCDB 에 트래픽 제한 정보를 업데이트 처리한다.
		// 따라서.. 본 loop 에서는 timestamp 값 변경 후 약 TIMESTAMP_DELAY_SEC sec 후에 RCDB 에 트래픽 제한 정보를 업데이트토록 처리한다.

		// 1. 현재 시간 정보 추출..
		currentTime = time(0);

		// 2. 현재 시간 정보를 기준으로 timestamp 값을 계산한다...
		//    계산시.. FHS 장비들이 시간 동기화 오차로... 통계 정보가 늦게 올라와 저장될 수 있으므로...
		//    위에서 언급한대로 TIMESTAMP_DELAY_SEC sec 보정 처리하여 5분 단위 timestamp 값을 추출한다.
		nowTimestamp   = (currentTime + 299)/300;
		delayTimestamp = (currentTime - TIMESTAMP_DELAY_SEC + 299)/300;

		
		// 3. 현재 timestamp 와 보정 처리된 delayTimestamp 값이 동일한 경우....
		if( nowTimestamp == delayTimestamp )
		{
			// 1 sec 대기 후 timestamp 변경이 되었는지 다시 계산하도록  처리...
			sleep(1);
			continue;
		}
		else
		{
			// nowTimestamp 와 delayTimestamp 는 5분단위 정각에 값이 변경되어 서로 값이 달라지므로....
			// 지정된 시간 동안 sleep 처리 후... 실제 작업을 수행한다.
			sleep( TIMESTAMP_DELAY_SEC );
		}
		

		_LOG( LINF, "TrafficControlThread: Traffic Control update job start. timestamp[%lu]", nowTimestamp );

		// 이제 실제 작업을 수행한다.

		// 1. RCDB 와 연결을 수행한다.
		//   - RCDB 연결은 5분마다 발생하므로.. 세션을 유지하지 않고.. 필요할 경우에만 연결/연결해제 하도록 처리한다.
		DataBase rcdb;
		m_vecTraffic.clear();


		// RCDB 와 연결을 시도한다.
		if( rcdb.PgOpenDB( rcdbInfo.m_strRcdbIp, rcdbInfo.m_nRcdbPort, rcdbInfo.m_strRcdbName, rcdbInfo.m_strRcdbAcct, rcdbInfo.m_strRcdbAcctPw) == NULL )
		{
			// 연결 실패시... 5분 후 재시도하도록 한다.
			LOG( LERR, "TrafficControlThread: RCDB connection failed.");
		}
		else
		{
			// RCDB 와 연결 성공시....
			// t_sms_sp_svc_product_band 테이블을 조회하여 트래픽 제어가 필요한 서비스 관련 정보를 추출, 멤버 변수에 저장한다.
			char szQuery[1024];

			snprintf( szQuery, 1023,
					  "SELECT A.sp_user_seq, A.sp_svc_tran_id, B.volume_bandwidth_limit, B.volume_bandwidth_current, B.volume_bandwidth_control_current "
					  "FROM t_sms_sp_svc_product A, t_sms_sp_svc_product_band B "
					  "WHERE A.sp_user_seq = B.sp_user_seq "
					  "  AND A.sp_svc_tran_id = B.sp_svc_tran_id "
					  "  AND A.bandwidth_ctrl_yn = 'Y'");

			// 조회 Query 수행.
			rcdb.PgDoExec(szQuery);

			// 수행 결과 확인
			if( rcdb.PgResult( DataBase::NOT_CLEAR ) < 0 )
			{
				// Query 수행 결과 오류 발생시...
				LOG( LERR, "TrafficControlThread: RCDB select query failed.[%s][%s]", rcdb.GetErrorMessage().c_str(), szQuery );
			}
			else
			{
				// query 수행이 정상인 경우.
				nResult = rcdb.GetNoTuples();

				// 조회 결과를 멤버변수 vector 에 저장한다.
				for( int i = 0; i < nResult; i++ )
				{					
					stTraffic.user_seq                 = atoi ( rcdb.GetValue( i, 0 ) );
					stTraffic.svc_seq                  = atoi ( rcdb.GetValue( i, 1 ) );
					stTraffic.limit                    = atoll( rcdb.GetValue( i, 2 ) );
					stTraffic.previous_traffic         = atoll( rcdb.GetValue( i, 3 ) );
					stTraffic.previous_control_precent = atoi ( rcdb.GetValue( i, 4 ) );

					// 나머지 변수도 초기화 처리
					stTraffic.current_traffic         = 0;       // Local db 조회시 정보가 없을 수 있으므로 0 으로 설정.
					stTraffic.current_control_precent = 10000;   // Local db 조회시 정보가 없을 수 있으므로... 100 % 의미의 값을 default 로 설정.

					// vector 에 push 처리
					m_vecTraffic.push_back( stTraffic );
				}

			}
			
			// RCDB 조회 결과 set 를 clear 처리한다.
			rcdb.PgClear();

			// 트래픽 제어 설정된 서비스가 존재하는 경우..
			if( m_vecTraffic.empty() == false )
			{
				// Local DB 에서 해당 서비스 및 timestamp 에 맞는 트래픽 정보를 추출한다.
				std::vector<struct traffic_control_info>::iterator it;
				
				// Local DB 와 연결을 수행한다.
				db_result = sqlite3_open( CProcessConfig::GetInstance()->GetLocalDbFileName(), &db);
				if( db_result != SQLITE_OK )
				{
					LOG( LERR, "TrafficControlThread: sqlite3 open failed.[%s][%d][%s]", CProcessConfig::GetInstance()->GetLocalDbFileName(), db_result, sqlite3_errmsg(db));
					sqlite3_close(db);
					db = NULL;
				}
				else
				{   
					// DB File 이 open 된 경우...
					nRetryCount = 0;
					
					// bind 관련 Query 를 수행할 stmt 객체 생성.
					// - 만약 sqlite DB File 이 사전에 생성되어 있지 않은 경우...  위의  open 함수에서 0 파일을 생성되어 정상적으로 open 처리되지만.. 
					//   sqlite3_prepare 함수에서 table 이 생성되어 있지 않으므로 오류가 발생함.
					//   따라서 loop 중에 재시도하도록 한다.
					db_result = sqlite3_prepare(db, select_sql, strlen(select_sql), &stmt, NULL );

					// sqlite3_prepare() 함수 실행시 SQLITE_BUSY 오류가 발생할 수 있으므로.. 재시도 로직을 추가한다.
					while( db_result == SQLITE_BUSY  && nRetryCount < 30 )
					{
						// sqlite 에서 SQLITE_BUSY 반환 관련 lock 대기 timeout 을 설정.
						sqlite3_busy_timeout( db, SQLITE_DB_LOCK_TIMEOUT );
						++nRetryCount;

						LOG( LWAR, "TrafficControlThread: sqlite3 prepare SQLITE_BUSY, Retry[%d]", nRetryCount );

						db_result = sqlite3_prepare(db, select_sql, strlen(select_sql), &stmt, NULL );	
					}
					
					if( db_result != SQLITE_OK )
					{
						LOG( LERR, "TrafficControlThread: sqlite3 prepare failed.[%d][%s]", db_result, sqlite3_errmsg(db));
						sqlite3_close(db);
						db = NULL;
						stmt = NULL;
					}
				}


				// sqlite 접속이 정상적으로 된 경우...
				if( stmt != NULL )
				{       
					// vector 에 저장된 서비스 정보에 맞는 트래픽 정보를 추출한다.
					for( it = m_vecTraffic.begin(); it != m_vecTraffic.end(); ++it )
					{   
						/*
						   _LOG( LDBG, "TrafficControlThread: RCDB result. svc[%d][%d] traffic[%llu/%llu] control[%u]"
						   , it->user_seq, it->svc_seq, it->previous_traffic, it->limit, it->previous_control_precent );
						   */

						sqlite3_bind_int64( stmt,  1, nowTimestamp-1 );
						sqlite3_bind_int  ( stmt,  2, it->user_seq );
						sqlite3_bind_int  ( stmt,  3, it->svc_seq );

						// Query 수행
						db_result = sqlite3_step(stmt);
						nRetryCount = 0;

						// 만약 BUSY 상태로 인해 오류가 발생하면..20번 재시도
						while( db_result == SQLITE_BUSY  && nRetryCount < 15 )
						{
							sqlite3_busy_timeout( db, SQLITE_DB_LOCK_TIMEOUT );
							++nRetryCount;

							LOG( LWAR, "TrafficControlThread: sqlite3 step SQLITE_BUSY, Retry[%d]",nRetryCount );

							// query 재시도
							db_result = sqlite3_step(stmt);	
						}


						// select 조회 결과가 존재하는 경우... SQLITE_ROW (100 ) 을 반환...
						// select 조회 결과가 없는 경우.. SQLITE_DONE (101) 을 반환.

						// select query 수행시 오류가 발생한 경우...
						if( db_result != SQLITE_ROW && db_result != SQLITE_DONE )
						{
							LOG( LERR, "TrafficControlThread: sqlite3 step error.[%d][%s]", db_result, sqlite3_errmsg(db));

							// 조회 loop 를 벗어나.. 마무리 작업을 한 후 다음 5분 후에 다시 시도하도록 한다.
							break;
						}
						else
						{
							// select 조회 결과가 존재하는 경우... 1개 Row 만 존재하므로...
							if( db_result == SQLITE_ROW )
							{
								it->current_traffic = sqlite3_column_int64(stmt, 0);
							}
						}

						// Query 수행된 경우.. prepared 문을 재사용하기 위해 reset 처리한다.
						sqlite3_reset(stmt);
					}


					// Local DB 와의 연결을 종료 처리한다.
					sqlite3_finalize(stmt);
					sqlite3_close(db);
					db = NULL;
					stmt = NULL;
				}


				// vector 에 수집된 정보를 바탕으로 
				// 계산을 통해 현재 5분간 트래픽을 어느 수준을 제어할지 결정한다.
				for( it = m_vecTraffic.begin(); it != m_vecTraffic.end(); ++it )
				{
					// 기본 조건
					// 1. 제한값이 0 인 경우.. 전송률은 0.01% (1) 로 설정한다.
					// 2. 트래픽 전송율은 서비스 품질을 위해 최대 25% 수준까지만 제한한다.
					//    - 일반적으로 50% 이하로 제한되면...트래픽 증설을 권고할 수 있도록... report 로그에 기록 처리.
					// 3. 트래픽 전송율은 감소시킬 경우... Slow 하게... 원복시킬 경우에는 Fast 하게 처리될 수 있도록 한다.


					// 제한값이 0인 경우.. -> 운영자가 트래픽을 0으로 제한해 버린 경우...
					if( it->limit == 0 )
					{
						it->current_control_precent = 1; // 0.01 % 로 설정 처리.
					}
					else
					{
						// limit 값이 0 을 초과하는 경우...

						// 현재 트래픽이 0 인 경우.. 전송율은 default 100% 로 설정처리.
						// - 해당 사항은 실제 traffic 전송한 것이 없거나....
						// - local db 조회 결과가 존재하지 않는 경우에 발생한다.
						if( it->current_traffic == 0 )
						{
							it->current_control_precent = 10000;
						}
						else
						{
							// 현재 트래픽이 존재하는 경우....

							if( it->current_traffic > it->limit )
							{
								// 현재 트래픽이... 제한 트래픽을 초과한 경우...
								// - 초과된 비율을 계산한 후..
								// - 기존 비율에서 초과된 비율만큼 감소시키되.. 최대 20% 로 제한.
								unsigned int nOverPercent = (it->current_traffic - it->limit ) * 10000 / it->limit ;

								if( nOverPercent > 2000 )
									it->current_control_precent = it->previous_control_precent - 2000;
								else
									it->current_control_precent = it->previous_control_precent - nOverPercent;

							}
							else if( it->current_traffic < it->limit  )
							{
								// 현재 트래픽이 제한 트래픽을 초과하지 않은 경우...
								// - 현재 트래픽이 제한 트래픽의 90% 초과 수준인 경우에는 현재 제한율 유지...
								// - 현재 트래픽이 제한 트래픽의 90% 이하인 경우...10 % 씩 증설 처리 ( 최대 100 % )

								unsigned int nUsedPercent = it->current_traffic * 10000 / it->limit;

								if( nUsedPercent > 9000 )
									it->current_control_precent = it->previous_control_precent;
								else
									it->current_control_precent = it->previous_control_precent + 1000;
							}
							else
							{
								// 제한 트래픽과 현재 트래픽이 동일한 경우...
								// 기존 전송율을 유지.
								it->current_control_precent = it->previous_control_precent;
							}
						}


						// 마지막으로 전송율 최소값 25%, 최대값 100% 이상을 초과하는 경우.. 보정 처리한다.
						if( it->current_control_precent < (TRAFFIC_CONTROL_LOW_LIMIT * 100) )
						{
							it->current_control_precent = (TRAFFIC_CONTROL_LOW_LIMIT * 100);
						}
						else if( it->current_control_precent > 10000 )
						{
							it->current_control_precent = 10000;
						}
					}


					// 전송율이 50% 미만으로 떨어질 경우.... report log 에 기록하여.. 운영자가 확인할 수 있도록 한다.
					if( it->limit > 0 && it->current_control_precent < (REPORT_TRAFFIC_CONTROL_LIMIT * 100) )
					{
						_LOG( LWAR, "TrafficControlThread: service traffic control value set below %d%%. svc[%d][%d] limit[%llu] traffic[%llu->%llu] control[%5.2f->%5.2f%%]"
								, REPORT_TRAFFIC_CONTROL_LIMIT, it->user_seq, it->svc_seq, it->limit
								, it->previous_traffic, it->current_traffic, it->previous_control_precent/100.0, it->current_control_precent/100.0 );

						// report 로그에 해당 내역 기록 처리...
						CReportLog reportLog;
						std::string strErrorMessage;
						strErrorMessage.clear();

						if( reportLog.Init( CProcessConfig::GetInstance()->GetLogPath() ) == false )
						{
							reportLog.GetLastError( strErrorMessage );
							LOG( LERR, "TrafficControlThread: report log init failed.[%s]", strErrorMessage.c_str() );
						}
						else
						{
							reportLog.Write( "WAR", "[%s][service traffic control value set below %d%%. svc[%d][%d] limit[%llu] traffic[%llu->%llu] control[%5.2f->%5.2f%%]]"
								, PROG_NAME, REPORT_TRAFFIC_CONTROL_LIMIT
								, it->user_seq, it->svc_seq, it->limit
								, it->previous_traffic, it->current_traffic, it->previous_control_precent/100.0, it->current_control_precent/100.0 );
						}
					}
				}


				// vector 에 저장된 최종 트래픽 제어 정보를 RCDB 에 UPDATE 처리한다.
				for( it = m_vecTraffic.begin(); it != m_vecTraffic.end(); ++it )
				{
					snprintf( szQuery, 1023,
							  "UPDATE t_sms_sp_svc_product_band "
							  "SET "
							  "  volume_bandwidth_current = '%llu' "
							  "  , volume_bandwidth_before = '%llu' "
							  "  , volume_bandwidth_control_current = '%u' "
							  "  , volume_bandwidth_control_before = '%u' "
							  "  , volume_bandwidth_update_datetime = now() "
							  "WHERE sp_user_seq = %d AND sp_svc_tran_id = %d "
							  , it->current_traffic, it->previous_traffic, it->current_control_precent, it->previous_control_precent
							  , it->user_seq, it->svc_seq );


					// update query 수행.
					rcdb.PgDoExec(szQuery);

					// update 실패 발생시...
					if( rcdb.GetCmdTuples() <= 0 )
					{						
						LOG( LERR, "TrafficControlThread: RCDB update query failed.[%s][%s]", rcdb.GetErrorMessage().c_str(), szQuery);
					}
					else
					{

						_LOG( LINF, "TrafficControlThread: RCDB update OK..svc[%d][%d] limit[%llu] traffic[%llu->%llu] control[%5.2f->%5.2f%%]"
								, it->user_seq, it->svc_seq, it->limit
								, it->previous_traffic, it->current_traffic, it->previous_control_precent/100.0, it->current_control_precent/100.0 );
					}

					// Update 처리 정상 수행시... RCDB 결과 Set 을 Clear 처리한다.
					rcdb.PgClear();
				}

			   
			}// RCDB 조회 결과가 존재하는 경우...


			// RCDB 관련 종료 처리 작업 수행.
			rcdb.PgCloseDB();
		}		


		// 작업 완료 후.. 불필요하게 loop 를 동작시킬 필요가 없고.. timestamp 체크 관련 루프 계산 문제로 인해서...
		// 5분 단위로 동작해야 하므로... 약 3분 정도 sleep 한다. 
		sleep(180);
	}


	// 종료 처리 코드
	return;
}

