<?php
/**
 * Event Rankings — Nexora Edition
 * Adapted for MU Nexora multi-realm architecture.
 */

namespace Plugin\EventRankings;

class EventRankings {

    private $_configXml = 'config.xml';
    private $_modulesPath = 'modules';
    private $_cronFile = 'event_rankings.php';
    private $_userColumn = 'Name';
    private $_scoreColumn = 'Score';

    private $_resultsLimit = 50;
    private $_showLevel = true;
    private $_showClass = true;
    private $_liveRankingData = false;
    private $_excludedPlayers = array();
    private $_ranking;
    private $_realm;

    private $_rankingList = array(
        'bloodcastle' => array(
            'table' => 'RankingBloodCastle',
            'cache' => 'rankings_bloodcastle.cache'
        ),
        'devilsquare' => array(
            'table' => 'RankingDevilSquare',
            'cache' => 'rankings_devilsquare.cache'
        ),
        'chaoscastle' => array(
            'table' => 'RankingChaosCastle',
            'cache' => 'rankings_chaoscastle.cache'
        )
    );

    private $mu;
    private $configFilePath;
    private $_configs;

    public function __construct($realm = null) {
        if(class_exists('\\NexoraRealm') && \NexoraRealm::enabled()) {
            $this->_realm = \NexoraRealm::validate($realm === null ? \NexoraRealm::active() : $realm);
            $this->mu = \Connection::Database('MuOnline', $this->_realm);
        } else {
            $this->_realm = null;
            $this->mu = \Connection::Database('MuOnline');
        }

        $this->configFilePath = __PATH_EVENTRANKINGS_ROOT__ . $this->_configXml;
        if(!is_file($this->configFilePath)) throw new \Exception(lang('eventrankings_error_5', true));

        $xml = simplexml_load_file($this->configFilePath);
        if(!$xml) throw new \Exception(lang('eventrankings_error_5', true));

        $this->_configs = convertXML($xml->children());
        if(!is_array($this->_configs)) throw new \Exception(lang('eventrankings_error_5', true));

        $limit = isset($this->_configs['results_limit']) ? (int)$this->_configs['results_limit'] : 50;
        $this->_resultsLimit = $limit > 0 ? $limit : 50;
        $this->_showLevel = !empty($this->_configs['show_level']);
        $this->_showClass = !empty($this->_configs['show_class']);
        $this->_liveRankingData = !empty($this->_configs['live_ranking']);

        if(!empty($this->_configs['excluded_players'])) {
            $raw = explode(',', (string)$this->_configs['excluded_players']);
            foreach($raw as $name) {
                $name = trim($name);
                if($name === '') continue;
                if(strlen($name) > 10) continue;
                $this->_excludedPlayers[] = $name;
            }
            $this->_excludedPlayers = array_values(array_unique($this->_excludedPlayers));
        }
    }

    public function loadModule($module) {
        if(!\Validator::Alpha($module)) throw new \Exception(lang('eventrankings_error_4', true));
        if(!$this->_moduleExists($module)) throw new \Exception(lang('eventrankings_error_4', true));
        if(!@include(__PATH_EVENTRANKINGS_ROOT__ . $this->_modulesPath . '/' . $module . '.php')) {
            throw new \Exception(lang('eventrankings_error_4', true));
        }
    }

    public function setRankingType($ranking) {
        if(!array_key_exists($ranking, $this->_rankingList)) throw new \Exception(lang('eventrankings_error_6', true));
        $this->_ranking = $ranking;
    }

    public function realm() {
        return $this->_realm;
    }

    public function realmLabel() {
        if($this->_realm !== null && class_exists('\\NexoraRealm')) return \NexoraRealm::label($this->_realm);
        return '';
    }

    public function updateCache() {
        if(!$this->_ranking) return false;

        $rankingData = $this->_getRankingData();
        if(!is_array($rankingData)) $rankingData = array();

        $encoded = json_encode($rankingData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        if($encoded === false) return false;

        return $this->_updateCacheFile($encoded);
    }

    public function getRankingData() {
        if($this->_liveRankingData) return $this->_getRankingData();

        $cacheData = $this->_loadCacheData();
        if(is_array($cacheData)) return $cacheData;

        // First-request fallback: one optimized SQL query, then create the realm cache.
        $rankingData = $this->_getRankingData();
        if(is_array($rankingData)) {
            $encoded = json_encode($rankingData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if($encoded !== false) $this->_updateCacheFile($encoded);
        }
        return $rankingData;
    }

    public function getCacheUpdatedAt() {
        if(!$this->_ranking) return null;
        $file = $this->_cacheFilePath();
        if(!is_file($file)) return null;
        $mtime = @filemtime($file);
        return $mtime ? (int)$mtime : null;
    }

    public function showLevel() {
        return $this->_showLevel;
    }

    public function showClass() {
        return $this->_showClass;
    }

    public function liveRankingEnabled() {
        return $this->_liveRankingData;
    }

    /**
     * Registers the shared cron entry. Call from AdminCP, not on public ranking requests.
     */
    public function ensureCronRegistration() {
        $cronPath = __PATH_CRON__ . $this->_cronFile;
        if(!is_file($cronPath)) throw new \Exception(lang('eventrankings_error_3', true));

        $cronMd5 = md5_file($cronPath);
        if(!$cronMd5) throw new \Exception(lang('eventrankings_error_3', true));

        $targets = array($this->_realm);
        if(class_exists('\\NexoraRealm') && \NexoraRealm::enabled() && !\NexoraRealm::platformEnabled()) {
            // Compatibility mode keeps WEBENGINE_CRON inside each realm DB.
            $targets = \NexoraRealm::ids();
        }

        foreach($targets as $realm) {
            $db = class_exists('\\NexoraRealm') && \NexoraRealm::enabled()
                ? \Connection::Database('Platform', $realm)
                : \Connection::Database('Me_MuOnline');

            $result = $db->query_fetch_single(
                'SELECT cron_id FROM ' . WEBENGINE_CRON . ' WHERE cron_file_run = ?',
                array($this->_cronFile)
            );
            if(is_array($result)) continue;

            $insert = $db->query(
                'INSERT INTO ' . WEBENGINE_CRON . ' (cron_name, cron_file_run, cron_run_time, cron_status, cron_protected, cron_file_md5) VALUES (?, ?, ?, ?, ?, ?)',
                array('Event Rankings', $this->_cronFile, 300, 1, 0, $cronMd5)
            );
            if(!$insert) throw new \Exception(lang('eventrankings_error_3', true));
        }

        return true;
    }

    private function _moduleExists($module) {
        return is_file(__PATH_EVENTRANKINGS_ROOT__ . $this->_modulesPath . '/' . $module . '.php');
    }

    private function _cacheFilePath() {
        if(!$this->_ranking) return null;
        $fileName = $this->_rankingList[$this->_ranking]['cache'];
        if(class_exists('\\NexoraRealm') && \NexoraRealm::enabled()) {
            return \NexoraRealm::cachePath($fileName, $this->_realm);
        }
        return __PATH_CACHE__ . $fileName;
    }

    private function _updateCacheFile($data) {
        $file = $this->_cacheFilePath();
        if(!$file) return false;

        $dir = dirname($file);
        if(!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) return false;

        $tmp = $file . '.tmp.' . getmypid();
        $written = @file_put_contents($tmp, $data, LOCK_EX);
        if($written === false) {
            @unlink($tmp);
            return false;
        }

        if(!@rename($tmp, $file)) {
            @unlink($tmp);
            return false;
        }

        return true;
    }

    private function _loadCacheData() {
        $file = $this->_cacheFilePath();
        if(!$file || !is_file($file) || !is_readable($file)) return null;

        $raw = @file_get_contents($file);
        if($raw === false || trim($raw) === '') return null;

        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : null;
    }

    /**
     * Single-query ranking loader. Avoids the original N+1 CharacterData() pattern.
     */
    private function _getRankingData() {
        if(!$this->_ranking) return null;

        $definition = $this->_rankingList[$this->_ranking];
        $top = max(1, (int)$this->_resultsLimit);
        $params = array();
        $where = array('r.' . $this->_scoreColumn . ' > 0');

        if(count($this->_excludedPlayers) > 0) {
            $placeholders = implode(',', array_fill(0, count($this->_excludedPlayers), '?'));
            $where[] = 'r.' . $this->_userColumn . ' NOT IN (' . $placeholders . ')';
            foreach($this->_excludedPlayers as $name) $params[] = $name;
        }

        $sql = 'SELECT TOP ' . $top . ' '
            . 'r.' . $this->_userColumn . ' AS event_name, '
            . 'c.' . _CLMN_CHR_LVL_ . ' AS character_level, '
            . 'c.' . _CLMN_CHR_CLASS_ . ' AS character_class, '
            . 'r.' . $this->_scoreColumn . ' AS event_score '
            . 'FROM ' . $definition['table'] . ' AS r '
            . 'INNER JOIN ' . _TBL_CHR_ . ' AS c ON c.' . _CLMN_CHR_NAME_ . ' = r.' . $this->_userColumn . ' '
            . 'WHERE ' . implode(' AND ', $where) . ' '
            . 'ORDER BY r.' . $this->_scoreColumn . ' DESC, r.' . $this->_userColumn . ' ASC';

        $result = $this->mu->query_fetch($sql, $params);
        if(!is_array($result)) return array();

        $players = array();
        foreach($result as $row) {
            if(!isset($row['event_name'])) continue;
            $players[] = array(
                'name' => (string)$row['event_name'],
                'level' => isset($row['character_level']) ? (int)$row['character_level'] : 0,
                'class' => isset($row['character_class']) ? (int)$row['character_class'] : 0,
                'score' => isset($row['event_score']) ? (int)$row['event_score'] : 0,
            );
        }

        return $players;
    }
}
