<?php
/**
 * WebEngine CMS
 * https://webenginecms.org/
 * 
 * @version 1.2.6
 * @author Lautaro Angelico <http://lautaroangelico.com/>
 * @copyright (c) 2013-2025 Lautaro Angelico, All Rights Reserved
 * 
 * Licensed under the MIT license
 * http://opensource.org/licenses/MIT
 */

class weProfiles {
	
	private $_request;
	private $_type;
	
	private $_reqMaxLen;
	private $_guildsCachePath;
	private $_playersCachePath;
	private $_cacheUpdateTime;
	
	private $_fileData;
	private $_realm;
	private $_cacheAvailable = array('guilds' => true, 'players' => true);
	
	protected $common;
	protected $dB;
	protected $cfg;
	
	function __construct() {
		
		# database
		$this->common = new common();
		$this->_realm = class_exists('NexoraRealm') ? NexoraRealm::active() : null;
		$this->dB = Connection::Database('MuOnline', $this->_realm);
		
		# settings
		$this->_guildsCachePath = class_exists('NexoraRealm') ? NexoraRealm::profileCacheDirectory('guilds') : __PATH_CACHE__ . 'profiles/guilds/';
		$this->_playersCachePath = class_exists('NexoraRealm') ? NexoraRealm::profileCacheDirectory('players') : __PATH_CACHE__ . 'profiles/players/';
		$this->_cacheUpdateTime = 300;
		
		# Cache is an optimization, never a requirement for profile functionality.
		$this->_cacheAvailable['guilds'] = $this->checkCacheDir($this->_guildsCachePath);
		$this->_cacheAvailable['players'] = $this->checkCacheDir($this->_playersCachePath);
		
		# configs
		$profileConfig = loadConfigurations('profiles');
		if(!is_array($profileConfig)) throw new Exception(lang('error_25',true));
		$this->cfg = $profileConfig;
		
	}
	
	public function setType($input) {
		switch($input) {
			case "guild":
				$this->_type = "guild";
				$this->_reqMaxLen = 8;
				break;
			default:
				$this->_type = "player";
				$this->_reqMaxLen = 10;
		}
	}
	
	public function setRequest($input) {
		if(array_key_exists('encode', $this->cfg) && $this->cfg['encode'] == 1) {
			if(!Validator::Chars($input, array('a-z', 'A-Z', '0-9', '_', '-'))) throw new Exception(lang('error_25',true));
			$decodedReq = base64url_decode($input);
			if($decodedReq == false) throw new Exception(lang('error_25',true));
			$this->_request = $decodedReq;
			return true;
		}
		
		if(!Validator::AlphaNumeric($input)) throw new Exception(lang('error_25',true));
		if(strlen($input) > $this->_reqMaxLen) throw new Exception(lang('error_25',true));
		if(strlen($input) < 4) throw new Exception(lang('error_25',true));
		
		$this->_request = $input;
	}
	
	private function checkCacheDir($path) {
		if(!check_value($path)) return false;
		if(!is_dir($path)) @mkdir($path, 0755, true);
		return is_dir($path) && is_writable($path);
	}

	private function writeCache($path, $data) {
		if(!is_dir(dirname($path)) || !is_writable(dirname($path))) return false;
		return @file_put_contents($path, $data, LOCK_EX) !== false;
	}
	
	private function checkCache() {
		$isGuild = $this->_type === 'guild';
		$cachePath = $isGuild ? $this->_guildsCachePath : $this->_playersCachePath;
		$cacheKey = $isGuild ? 'guilds' : 'players';
		$reqFile = $cachePath . strtolower($this->_request) . '.cache';

		if($this->_cacheAvailable[$cacheKey] && is_file($reqFile) && is_readable($reqFile)) {
			$fileData = @file_get_contents($reqFile);
			$parts = is_string($fileData) ? explode('|', $fileData) : array();
			$needsRefresh = !isset($parts[0]) || !is_numeric($parts[0]) || time() > ((int)$parts[0] + $this->_cacheUpdateTime);
			// Player cache v2 includes Zen at index 15. Refresh legacy cache files automatically.
			if(!$isGuild && count($parts) < 16) $needsRefresh = true;
			if(!$needsRefresh) {
				$this->_fileData = $fileData;
				return;
			}
		}

		$this->_fileData = $isGuild ? $this->cacheGuildData() : $this->cachePlayerData();
	}

	private function cacheGuildData() {
		// General Data
		$guildData = $this->dB->query_fetch_single("SELECT *, CONVERT(varchar(max), "._CLMN_GUILD_LOGO_.", 2) as "._CLMN_GUILD_LOGO_." FROM "._TBL_GUILD_." WHERE "._CLMN_GUILD_NAME_." = ?", array($this->_request));
		if(!$guildData) throw new Exception(lang('error_25',true));
		
		// Members
		$guildMembers = $this->dB->query_fetch("SELECT * FROM "._TBL_GUILDMEMB_." WHERE "._CLMN_GUILDMEMB_NAME_." = ?", array($this->_request));
		if(!$guildMembers) throw new Exception(lang('error_25',true));
		$members = array();
		foreach($guildMembers as $gmember) {
			$members[] = $gmember[_CLMN_GUILDMEMB_CHAR_];
		}
		$gmembers_str = implode(",", $members);
		
		// Cache
		$data = array(
			time(),
			$guildData[_CLMN_GUILD_NAME_],
			$guildData[_CLMN_GUILD_LOGO_],
			$guildData[_CLMN_GUILD_SCORE_],
			$guildData[_CLMN_GUILD_MASTER_],
			$gmembers_str
		);
		
		// Cache Ready Data
		$cacheData = implode("|", $data);
		
		// Update cache when available, but never block the profile if cache cannot be written.
		$reqFile = $this->_guildsCachePath . strtolower($this->_request) . '.cache';
		if($this->_cacheAvailable['guilds']) $this->writeCache($reqFile, $cacheData);
		return $cacheData;
	}
	
	private function cachePlayerData() {
		// Public profiles need only scalar data. Avoid SELECT * so Inventory, MagicList
		// and other binary columns are not transferred on every profile view.
		$columns = array(
			_CLMN_CHR_NAME_, _CLMN_CHR_CLASS_, _CLMN_CHR_LVL_, _CLMN_CHR_RSTS_,
			_CLMN_CHR_STAT_STR_, _CLMN_CHR_STAT_AGI_, _CLMN_CHR_STAT_VIT_,
			_CLMN_CHR_STAT_ENE_, _CLMN_CHR_STAT_CMD_, _CLMN_CHR_PK_KILLS_,
			_CLMN_CHR_GRSTS_, _CLMN_CHR_ZEN_
		);
		if(_TBL_MASTERLVL_ == _TBL_CHR_) $columns[] = _CLMN_ML_LVL_;
		$columns = array_unique($columns);
		$playerData = $this->dB->query_fetch_single(
			"SELECT " . implode(', ', $columns) . " FROM "._TBL_CHR_." WHERE "._CLMN_CHR_NAME_." = ?",
			array($this->_request)
		);
		if(!is_array($playerData)) {
			$realmLabel = class_exists('NexoraRealm') ? NexoraRealm::label($this->_realm) : '';
			throw new Exception($realmLabel !== ''
				? 'El personaje no fue encontrado en '.$realmLabel.'. El ranking puede estar pendiente de actualizarse.'
				: lang('error_25', true));
		}

		$playerMasterLevel = 0;
		if(_TBL_MASTERLVL_ == _TBL_CHR_) {
			$playerMasterLevel = isset($playerData[_CLMN_ML_LVL_]) ? $playerData[_CLMN_ML_LVL_] : 0;
		} else {
			$masterLevelInfo = $this->dB->query_fetch_single(
				"SELECT "._CLMN_ML_LVL_." FROM "._TBL_MASTERLVL_." WHERE "._CLMN_ML_NAME_." = ?",
				array($this->_request)
			);
			if(is_array($masterLevelInfo) && isset($masterLevelInfo[_CLMN_ML_LVL_])) {
				$playerMasterLevel = $masterLevelInfo[_CLMN_ML_LVL_];
			}
		}

		$guild = '';
		$guildData = $this->dB->query_fetch_single(
			"SELECT "._CLMN_GUILDMEMB_NAME_." FROM "._TBL_GUILDMEMB_." WHERE "._CLMN_GUILDMEMB_CHAR_." = ?",
			array($this->_request)
		);
		if(is_array($guildData) && isset($guildData[_CLMN_GUILDMEMB_NAME_])) $guild = $guildData[_CLMN_GUILDMEMB_NAME_];

		$data = array(
			time(),
			$playerData[_CLMN_CHR_NAME_],
			$playerData[_CLMN_CHR_CLASS_],
			$playerData[_CLMN_CHR_LVL_],
			$playerData[_CLMN_CHR_RSTS_],
			$playerData[_CLMN_CHR_STAT_STR_],
			$playerData[_CLMN_CHR_STAT_AGI_],
			$playerData[_CLMN_CHR_STAT_VIT_],
			$playerData[_CLMN_CHR_STAT_ENE_],
			$playerData[_CLMN_CHR_STAT_CMD_],
			$playerData[_CLMN_CHR_PK_KILLS_],
			(check_value($playerData[_CLMN_CHR_GRSTS_]) ? $playerData[_CLMN_CHR_GRSTS_] : 0),
			$guild,
			0,
			(check_value($playerMasterLevel) ? $playerMasterLevel : 0),
			(isset($playerData[_CLMN_CHR_ZEN_]) ? $playerData[_CLMN_CHR_ZEN_] : 0),
		);

		$cacheData = implode('|', $data);
		$reqFile = $this->_playersCachePath . strtolower($this->_request) . '.cache';
		if($this->_cacheAvailable['players']) $this->writeCache($reqFile, $cacheData);
		return $cacheData;
	}
	
	public function data() {
		if(!check_value($this->_type)) throw new Exception(lang('error_21',true));
		if(!check_value($this->_request)) throw new Exception(lang('error_21',true));
		$this->checkCache();
		return(explode("|", $this->_fileData));
	}
	
}
