#include "GarbageClean.h"

#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdio.h>
#include <dirent.h>

#include <iterator>

#include "Logger.h"
#include "ProcessConfig.h"


#define SQLITE_DB_LOCK_TIMEOUT       1000                       // SQLITE_BUSY 상황 발생 문제 해결을 위해 DB Lock 관련 timeout 설정값. (1 sec)
#define SQLITE_DB_LOCK_MAX_RETRY       60                       // SQLITE_BUSY 발생시 재시도 최대 횟수


// meta 에 존재하지 않아.. unlink 여부 판단시... 삭제 방지 day 값
// - unlink 대상인 경우.. 최종 수정일, 최종 access 시간이 본 설정값 이하이면 삭제되지 않도록 처리.
// - 2019-08-29 일감 #32774 에 의해 7일 -> 2일로 변경 처리함.
#define UNLINK_PREVENT_MAX_DAY			2                     


// unlink slow 모드인 경우.. unlink 후 sleep 시간 (sec)
// - 2 sec sleep : 일 4만건 unlink
// - 1 sec sleep : 일 8만건 unlink
#define UNLINK_SLOW_MODE_SLEEP          1


// 생성자
// - pLocalDb : sqlite memory db 객체에 대한 포인트
CGarbageClean::CGarbageClean( sqlite3 * pLocalDb )
	: m_pLocalStmt( NULL )
{
	m_pLocalDb = pLocalDb;
}


// 소멸자
CGarbageClean::~CGarbageClean()
{
	m_vecTargetDirectory.clear();
	
	// Local DB stamt clear
	if( m_pLocalStmt != NULL )
	{
		sqlite3_finalize( m_pLocalStmt );
		m_pLocalStmt = NULL;
	}
}


// 객체 초기화 함수
bool CGarbageClean::Init()
{
	// 작업대상 폴더 정보를 저장할 변수 초기화.
	m_vecTargetDirectory.clear();
	
	// Local sqlite db 관련 변수	
	int nResult = 0;
	int nRetryCount = 0;


	// strQuery = "CREATE TABLE meta( resource_id integer, filename_hash text, get_content_length integer, deleted_yn text )";

	// 조회 처리를 수행할 stmt 객체 생성.	
	// - 결과 추출 오류 발생시 sqlite3_column_int() 함수가 0 을 반환하므로.. 오류 와.. 실제 미존재 상태를 구분하기 위해... +1 처리함.		
	std::string strQuery;
	strQuery = "SELECT count( resource_id ) +1 FROM meta WHERE filename_hash = ? ;";
	
	nResult = sqlite3_prepare( m_pLocalDb, strQuery.c_str(), strQuery.size(), &m_pLocalStmt, NULL );

	// sqlite3_prepare() 함수 실행시 SQLITE_BUSY 오류가 발생할 수 있으므로.. 재시도 로직을 추가한다.
	while( nResult == SQLITE_BUSY  && nRetryCount < SQLITE_DB_LOCK_MAX_RETRY )
	{
		sqlite3_busy_timeout( m_pLocalDb, SQLITE_DB_LOCK_TIMEOUT );
		++nRetryCount;

		_LOG( LWAR, "Local db prepare sql SQLITE_BUSY. Retry[%d]", nRetryCount );

		nResult = sqlite3_prepare( m_pLocalDb, strQuery.c_str(), strQuery.size(), &m_pLocalStmt, NULL );
	}

	// 최종 실패시...
	if( nResult != SQLITE_OK )
	{
		LOG( LERR, "sqlite3 prepare sql stmt create failed. [%d][%s]", nResult, sqlite3_errmsg( m_pLocalDb ) );
		
		return false;
	}
	
	return true;
}


// garbage clean 작업 수행.
// - bGarbageCleanSlow : unlink 수행 후 sleep 를 호출하는 slow 모드로 동작시킬지 여부.
// - bGarbageForceDelete: unlink 처리시 access time 정보를 check 할지 여부
bool CGarbageClean::Start( bool bGarbageCleanSlow, bool bGarbageForceDelete )
{
	// 작업 소요 시간 정보 계산을 위한 변수.
	// - 시간 동기화로 인한 문제를 해결하기 위해 clock_gettime() 함수 사용.
	struct timespec timeStart;
	struct timespec timeEnd;
	double	dTimeInterval = 0.0;

	memset( &timeStart, 0x00, sizeof( struct timespec ) );
	memset( &timeEnd, 0x00, sizeof( struct timespec ) );
	
	// Meta 에 존재하지 않는 파일 삭제 처리 관련... 
	// 현재 시간 정보 추출하여.. 정의된 보호 시간을 뺀다.
	time_t unlinkPreventMaxTime;
	unlinkPreventMaxTime = time( 0 ) - ( UNLINK_PREVENT_MAX_DAY * 24 * 60 * 60 );
	
	// 작업 시작 로깅 및 시작 시간 정보 추출.
	_LOG( LINF, "Garbage clean start." );
	_LOG( LINF, "- unlink speed: %s", ( bGarbageCleanSlow == true ? "slow" : "normal" ) );
	_LOG( LINF, "- unlink modifiy time max prevent : %d day, %ld time.", UNLINK_PREVENT_MAX_DAY, unlinkPreventMaxTime);
	if( bGarbageForceDelete == false )
	{
		_LOG( LINF, "- unlink access time max prevent  : %d day, %ld time.", UNLINK_PREVENT_MAX_DAY, unlinkPreventMaxTime );
	}
	else
	{
		_LOG( LINF, "- unlink access time not check. force unlink." );
	}


	clock_gettime( CLOCK_MONOTONIC, &timeStart );
		
	// 통계 정보 저장을 위한 변수
	// 각 폴더별 삭제 count
	unsigned long long ullDirTotalCount = 0;      // 각 폴더별 처리한 count 정보 저장.
	unsigned long long ullDirUnlinkCount = 0;     // 각 폴더별 삭제한 count 정보 저장.

	unsigned long long ullTotalCount = 0;         // 누적 처리 count 정보 저장.
	unsigned long long ullTotalUnlinkCount = 0;   // 누적 삭제 count 정보를 저장
	unsigned long long ullTotalUnlinkSize  = 0;   // 누적 삭제 Size 정보를 저장


	// 1. 설정된 대상 경로를 바탕으로.. 실제 최하위 서비스 폴더 목록 list 를 추출한다.
	if( GetTargetDir() == false )	
	{
		// 오류 발생시.. 해당 모듈에서 로깅 처리 했으므로.. 실패로 처리.
		return false;
	}
	else
	{
		_LOG( LINF, "Garbage clean [%s] sub service directory count [%lu]", CProcessConfig::GetInstance()->GetJobRoot(), m_vecTargetDirectory.size() );
	}

	// 2. 추출된 각 경로에 대해.. 파일 정보를 추출하여.. 검증 수행.
	DIR * pDir = NULL;
	struct dirent * pDirEntry = NULL;
	std::vector< std::string >::iterator it;

	// 파일에 대한 정보를 저장할 변수.
	std::string strFullFileName;
	int nFullFileNameExistCount;
	struct stat stFileStat;
		
	// Local sqlite db 관련 변수
	int nResult = 0;
	int nRetryCount = 0;

	
	for( it = m_vecTargetDirectory.begin(); it != m_vecTargetDirectory.end(); ++it )
	{
		pDir = opendir( it->c_str() );
		if( pDir == NULL )
		{
			// 폴더 open 실패시...
			// - 오류 내역 로깅하고 계속 진행.

			LOG( LERR, "directory[%s] open faild. [%d][%s]", it->c_str(), errno, strerror( errno ) );
			continue;
		}

		// 개별 폴더 통계 초기화.
		ullDirTotalCount  = 0;
		ullDirUnlinkCount = 0;

		_LOG( LINF, "Garbage clean [%s] start", it->c_str() );
				
		while( ( pDirEntry = readdir( pDir ) ) != NULL )
		{
			// 만약 하위에 폴더, 링크, FIFO 등이 존재할 수 있으므로... 
			// 이를 제외하고 only 파일만 비교 처리.
			
			// man 5 dir
			// d_type 정의 내역.
			// #define DT_DIR           4
			// #define DT_REG           8

			if( pDirEntry->d_type == DT_REG )
			{
				// '.'으로 시작하는 숨김 파일 등은 제외 처리. ( '.', '..', '..sujournal' 등 )
				if( pDirEntry->d_name[0] == '.' )
					continue;
				
				// 진행 사항 로깅.
				if( ullDirTotalCount % 10000 == 0 && ullDirTotalCount != 0 )
				{
					_LOG( LINF, "Garbage checking. [%s] count[%10llu] unlink[%10llu]"
						  , it->c_str(), ullDirTotalCount, ullDirUnlinkCount );
				}
				
				// 통계 정보 증감 처리
				++ullDirTotalCount;
												
				// 파일명 전체 경로 정보 조합
				strFullFileName = it->c_str();
				strFullFileName.append( "/" );
				strFullFileName.append( pDirEntry->d_name );
								
				// 이제 실제로 파일이 존재하는지 local DB 에서 확인한다.
				// select query 에 변수 bind 처리. ( bind 값을 1부터 시작 )
				// - bind 처리시.. SQLITE_TRANSIENT 도 사용할 수 있으나.. 내부 copy 로 속도 느림
				// - query 처리 중간에 변수값이 변경될 가능성 없으므로.. SQLITE_STATIC 사용.
				nResult = sqlite3_bind_text( m_pLocalStmt, 1, strFullFileName.c_str(), strFullFileName.size(), SQLITE_STATIC );
				if( nResult != SQLITE_OK )
				{
					// bind 처리 실패시....
					LOG( LERR, "sqlite3 bind error.[%s] [%d][%s]", strFullFileName.c_str(), nResult, sqlite3_errmsg( m_pLocalDb ) );

					// reset 처리 후.. 다음 항목을 진행 처리
					sqlite3_reset( m_pLocalStmt );
					continue;
				}
								
				// Query 실행.
				nResult = sqlite3_step( m_pLocalStmt );
				nRetryCount = 0;

				while( nResult == SQLITE_BUSY  && nRetryCount < SQLITE_DB_LOCK_MAX_RETRY )
				{
					sqlite3_busy_timeout( m_pLocalDb, SQLITE_DB_LOCK_TIMEOUT );
					++nRetryCount;

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

					// query 재시도
					nResult = sqlite3_step( m_pLocalStmt );
				}
				
				// SELECT query 수행시....
				// - 결과 row 가 2 개 이상인 경우....SQLITE_ROW(100) 을 반환하고.. 더 이상 조회 결과가 없는 경우.....SQLITE_DONE (101) 반환.
				// - 결과가 없거나...결과 row 가 1개 인 경우.. SQLITE_DONE (101) 을 반환한다.
				// - 따라서 SQLITE_DONE 을 반환한다고 해서.. 결과가 없는 것으로 판단하면 안됨.

				// select query 수행시 오류가 발생한 경우...
				if( nResult != SQLITE_ROW && nResult != SQLITE_DONE )
				{
					LOG( LERR, "sqlite3 step error.[%s] [%d][%s]", strFullFileName.c_str(), nResult, sqlite3_errmsg( m_pLocalDb ) );
					
					// 만약 오류 코드가 SQLITE_ERROR, SQLITE_INTERNAL, SQLITE_EMPTY, SQLITE_SCHEMA 등이라면.... DB에 문제 있는 것으로 보고.. 작업 중지.
					if( nResult == SQLITE_ERROR || nResult == SQLITE_INTERNAL || nResult == SQLITE_EMPTY || nResult == SQLITE_SCHEMA )
					{
						LOG( LERR, "sqlite3 db not valid. garbage clean stop.");
						
						closedir( pDir );
						pDir = NULL;

						sqlite3_finalize( m_pLocalStmt );						
						m_pLocalStmt = NULL;
						
						return false;
					}


					// 그 외 오류에 대해서는 무시하고 계속 진행.					
				}
				else				
				{
					// SELECT query 수행시....
					// - 결과 row 가 2 개 이상인 경우....SQLITE_ROW(100) 을 반환하고.. 더 이상 조회 결과가 없는 경우.....SQLITE_DONE (101) 반환.
					// - 결과가 없거나...결과 row 가 1개 인 경우.. SQLITE_DONE (101) 을 반환한다.
					// - 따라서 SQLITE_DONE 을 반환한다고 해서.. 결과가 없는 것으로 판단하면 안됨.
										
					// strQuery = "SELECT count( resource_id ) +1 FROM meta WHERE filename_hash = ? ;";
					// - sqlite3_column_int() 호출시 오류가 발생할 경우에 대한 처리를 위해 +1 처리함.
					
					// 조회 결과를 변수에 저장 처리.
					nFullFileNameExistCount = sqlite3_column_int( m_pLocalStmt, 0 );
					
					// nFullFileNameExistCount == 0 : sqlite3_column_int() 함수 오류로 default 값이 반환된 경우...
					// nFullFileNameExistCount == 1 : Meta 에 해당 파일 정보 미존재시.
					// 그 이외에는 Meta 에 해당 파일 정보 존재로 처리.

					//_LOG( LINF, "[GARBAGE] Meta SELECT. [%s] count [%d]", strFullFileName.c_str(), nFullFileNameExistCount -1 );
					
					if( nFullFileNameExistCount == 1 )
					{
						// Meta 에 해당 파일 미존재시.
												
						// 해당 파일 속성 정보 추출.
						if( stat( strFullFileName.c_str(), &stFileStat ) == 0 )
						{
							// 만약 해당 파일이 Meta에 존재하지 않는 경우...
							// 최종 수정일 정보를 추출하여... UNLINK_PREVENT_MAX_DAY 일이 지났는지 확인 후.
							// UNLINK_PREVENT_MAX_DAY 일 이상된 content 인 경우.. 삭제 처리한다.

							// 파일의 최종 수정 시간이 UNLINK_PREVENT_MAX_DAY 일 이상이면 삭제 처리
							// - difftime() 함수를 사용해도 되지만.. CPU 사용률이 거의 100% 수준이라... CPU 부하 낮추기 위해 날코딩 처리.
							if( stFileStat.st_mtime < unlinkPreventMaxTime )
							{
								// access time 을 check 하지 않고.. unlink 처리 수행한다.
								if( bGarbageForceDelete == true )
								{
									// 해당 파일 unlink 처리.
									unlink( strFullFileName.c_str() );

									++ullDirUnlinkCount;
									ullTotalUnlinkSize += stFileStat.st_size;

									_LOG( LINF, "[GARBAGE] unlink with force[%s] mtime[%ld] size[%jd]"
										, strFullFileName.c_str(), stFileStat.st_mtime, stFileStat.st_size );

									// unlink slow 모드인 경우.. unlink 처리 후.. 정의된 시간만큼 sleep 처리한다.
									if( bGarbageCleanSlow == true )
										sleep( UNLINK_SLOW_MODE_SLEEP );
								}
								else if( stFileStat.st_atime < unlinkPreventMaxTime )
								{
									// -f 옵션을 사용하지 않은 상태이고 access time 이 2일 이상인 경우 
									// 삭제 처리를 수행한다.
									
									// 해당 파일 unlink 처리.
									unlink( strFullFileName.c_str() );

									++ullDirUnlinkCount;
									ullTotalUnlinkSize += stFileStat.st_size;

									_LOG( LINF, "[GARBAGE] unlink[%s] atime[%ld] mtime[%ld] size[%jd]"
										, strFullFileName.c_str(), stFileStat.st_atime, stFileStat.st_mtime, stFileStat.st_size );

									// unlink slow 모드인 경우.. unlink 처리 후.. 정의된 시간만큼 sleep 처리한다.
									if( bGarbageCleanSlow == true )
										sleep( UNLINK_SLOW_MODE_SLEEP );
							
								}
								else
								{
									// 파일의 최종 수정 시간은 UNLINK_PREVENT_MAX_DAY 값을 초과했지만.
									// force mode 가 아니고, access 시간이 UNLINK_PREVENT_MAX_DAY 일 이내인 경우..
									// - 로깅만 처리.
									_LOG( LINF, "[%s] meta not exist, but access occurred within [%d] day. a[%ld] m[%ld] limit[%ld] size[%jd]"
										, strFullFileName.c_str(), UNLINK_PREVENT_MAX_DAY, stFileStat.st_atime, stFileStat.st_mtime, unlinkPreventMaxTime, stFileStat.st_size );
								}
							}
							else
							{
								// 파일의 최종 수정 시간이 UNLINK_PREVENT_MAX_DAY 일 이내인 경우..
								// - 로깅만 처리.
								_LOG( LINF, "[%s] meta not exist, but modify occurred within [%d] day. a[%ld] m[%ld] limit[%ld] size[%jd]"
									, strFullFileName.c_str(), UNLINK_PREVENT_MAX_DAY, stFileStat.st_atime, stFileStat.st_mtime, unlinkPreventMaxTime, stFileStat.st_size );
							}
						}
						else
						{
							// stat 호출 오류인 경우...
							// - multi thread 가 아직 1 process 구조라서.. errno 를 그대로 사용....
							// - 만약 multi thread 구조로 변경할 경우.. errno 그대로 사용하면 안됨.

							// garbage clean 를 very slow 모드로 동작시킬 경우...
							// - 작업 시간이 오래 걸려...
							// - 해당 물리 파일이 rc_gcmd 등의 다른 프로세스에 의해 삭제될 가능성이 존재함.
							// - 이를 경우... 다음과 같은 오류 발생
							// [17:25:42] [ERR ] [GARBAGE] meta not exist. but file stat call error. name[/stg/node0/250/22a753b521dfc43ae468854c2089e8ef] [2][No such file or directory]
							// - 따라서.. 다른 프로세스에 의해 삭제된 경우.. 불필요하게 오류 찍을 필요 없다.

							if( errno != ENOENT )
							{
								_LOG( LERR, "[GARBAGE] meta not exist. but file stat call error. name[%s] [%d][%s]", strFullFileName.c_str(), errno, strerror( errno ) );
							}

						}
					}
					else if( nFullFileNameExistCount == 0 )
					{
						//sqlite3_column_int() 함수 오류로 default 값이 반환된 경우...
						LOG( LERR, "sqlite3 column get error.[%s] [%d][%s]", strFullFileName.c_str(), nFullFileNameExistCount, sqlite3_errmsg( m_pLocalDb ) );
					}
					else
					{
						// Meta 에 해당 파일 정보 존재로 처리.
						//_LOG( LINF, "[GARBAGE] Meta OK. [%s] count [%d]", strFullFileName.c_str(), nFullFileNameExistCount - 1 );
					}										
				}
				
				// Query 수행된 경우.. prepared 문을 재사용하기 위해 reset 처리한다.
				sqlite3_reset( m_pLocalStmt );
			}		

		} // while( ( pDirEntry = readdir( pDir ) ) != NULL )

		
		// open 처리된 dir 객체를 close 처리한다.
		closedir( pDir );
		pDir = NULL;

		
		// 통계 저장 처리
		ullTotalCount       += ullDirTotalCount;
		ullTotalUnlinkCount += ullDirUnlinkCount;
		
		
		_LOG( LINF, "Garbage clean [%s] end. unlink count[ %llu / %llu ] ", it->c_str(), ullDirUnlinkCount, ullDirTotalCount );


	} // for( it = m_vecTargetDirectory.begin(); it != m_vecTargetDirectory.end(); ++it )

	
	// 작업 완료 후 stmt 객체 clear
	sqlite3_finalize( m_pLocalStmt );
	m_pLocalStmt = NULL;
	
	// 작업 완료시 시간 정보 추출 후 소요시간 계산.
	clock_gettime( CLOCK_MONOTONIC, &timeEnd );

	dTimeInterval = timeEnd.tv_sec - timeStart.tv_sec;
	dTimeInterval += ( timeEnd.tv_nsec - timeStart.tv_nsec ) * 1e-9;

	_LOG( LINF, "Garbage clean complete. path[%s] unlink mode[%s] elapsed time: %.1lf sec."
		  , CProcessConfig::GetInstance()->GetJobRoot()
		  , ( bGarbageCleanSlow == true ? "slow" : "normal" )
		  , dTimeInterval );
	
	_LOG( LINF, "Garbage clean report: total count[%llu] unlink count[%llu] unlink size[%llu byte]"		  
		  , ullTotalCount, ullTotalUnlinkCount, ullTotalUnlinkSize );
	
	return true;
}


// 설정된 기본 경로를 바탕으로.. 작업대상 폴더명 정보를 추출하여.. 멤버변수에 저장 처리.
bool CGarbageClean::GetTargetDir()
{
	// 사용자가 입력했거나.. 기본으로 설정된 경로에서...
	// 실제 정리 작업을 수행할 서비스 폴더를 추출한다.
	// - 서비스 폴더가 아닌 경우.. 운영 목적으로 설정한 파일들이 존재하여..
	// - 삭제될 수 있으므로...
	// - 서비스 폴더에 대해서만 정리 작업이 수행되도록 한다.

	// 추출 방식
	// - 직접 코드로 opendir, readdir 함수를 이용하여 추출 가능하지만....
	// - 불필요하게 시간이 오래 걸리고.. 버그의 가능성 존재.
	// - 따라서 외부 명령어를 이용하여.. 
	// - 하위 폴더 정보를 추출하고.... 서비스 폴더인지 검증 처리.

	// 외부 tool
	// find /stg/node0 -type d -print
	// find /stg/node0 -type d -print | grep '/[1-9]*$'

	// 참고사항
	// main 모듈에서 root 경로에 대한 
	// 맨 끝의 '/' 제거 및
	// 존재 여부 확인 했으므로... 그냥 사용하면 됨.
	
	char tempBuffer[2048];
	std::string strTempPath;
	FILE * fd = NULL;

	// trim 처리 관련 변수
	std::string::size_type pos;
		

	// 명령어 조립
	snprintf( tempBuffer, sizeof( tempBuffer ), "find %s -type d -print | grep '/[0-9]*$'", CProcessConfig::GetInstance()->GetJobRoot() );
	fd = popen( tempBuffer, "r" );
	if( fd == NULL )
	{
		int errorNum = errno;
		LOG( LERR, "service directory search error. popen error. [%d][%s]", errorNum, strerror( errorNum ) );
		
		return false;
	}
	else
	{
		// 작업 대상 경로를 저장할 멤버 변수 초기화.
		m_vecTargetDirectory.clear();

		memset( tempBuffer, 0x00, sizeof( tempBuffer ) );
		while( fgets( tempBuffer, sizeof( tempBuffer ) - 1, fd ) != NULL )
		{
			// 결과 맨 끝에 줄바꿈 문자가 있어.. trim 처리 필요
			strTempPath = tempBuffer;
			if( strTempPath.length() == 0 )
				continue;

			pos = strTempPath.find_last_not_of( " \a\b\f\n\r\t\v" );
			if( pos != std::string::npos )
				strTempPath.erase( pos + 1 );			

			// 원래는 추출된 전체 경로.. (/stg/node0/290 ) 에서
			// 마지막 290 정보 추출하여 숫자인지 비교해야 하지만...
			// 귀찬아서.. 외부 명령어의 grep 으로 비교 처리 넣어버림...

			// 따라서.. 추출된 폴더 정보를 바로 저장 처리하면 됨.

			m_vecTargetDirectory.push_back( strTempPath );
		}

		pclose( fd );
	}
	
	return true;
}
