<?php
/**
 * Nexora V6 Custom — funciones de plantilla
 */

function nexora_lang($key, $fallback) {
	if (!function_exists('lang')) {
		return $fallback;
	}
	$v = lang($key, true);
	if ($v === 'ERROR' || $v === '' || $v === null) {
		return $fallback;
	}
	return $v;
}


/**
 * Iconografía unificada del UserCP Nexora.
 * Se resuelve por ruta funcional para no depender del icono antiguo guardado
 * en usercp.json. Los módulos desconocidos conservan su icono configurado.
 */
function nexoraUsercpIcon($element) {
    $link = '';
    if (is_array($element) && isset($element['link'])) {
        $link = strtolower(trim((string) $element['link'], "/ \t\n\r\0\x0B"));
    }

    $map = array(
        'usercp/myaccount' => 'account.webp',
        'usercp/reset' => 'reset.webp',
        'usercp/unstick' => 'unstick.webp',
        'usercp/clearpk' => 'clearpk.webp',
        'usercp/resetstats' => 'resetstats.webp',
        'usercp/addstats' => 'addstats.webp',
        'usercp/clearskilltree' => 'clearskilltree.webp',
        'usercp/vote' => 'vote.webp',
        'donation' => 'donation.webp',
        'usercp/buyzen' => 'buyzen.webp',
        'vip/info' => 'vip.webp',
        'usercp/purchases' => 'purchases.webp',
        'roulette' => 'roulette.webp',
        'usercp/roulette' => 'roulette.webp',
        'exchangeresets/characters' => 'exchangeresets.webp',
        'redeem/code' => 'redeemcode.webp',
        'usercp/renamecharacter' => 'renamecharacter.webp',
        'usercp/transfercharacter' => 'transfercharacter.webp',
        'referral/list' => 'referralsystem.webp',
    );

    if (isset($map[$link])) {
        return __PATH_TEMPLATE_IMG__ . 'icons/nexora/' . $map[$link];
    }

    if (is_array($element) && !empty($element['icon'])) {
        return __PATH_TEMPLATE_IMG__ . 'icons/' . $element['icon'];
    }

    return __PATH_TEMPLATE_IMG__ . 'icons/usercp_default.png';
}

/** Selector visual usado por login, registro y recuperación. */
function nexoraRealmAuthField($fieldName='nexora_realm') {
	if(!class_exists('NexoraRealm') || !NexoraRealm::enabled()) return;

	$selected = null;
	if(isset($_POST[$fieldName]) && NexoraRealm::exists($_POST[$fieldName])) {
		$selected = strtolower($_POST[$fieldName]);
	} elseif(NexoraRealm::hasExplicitPublicRealm()) {
		$selected = NexoraRealm::publicRealm();
	}

	$realms = NexoraRealm::publicList();
	echo '<fieldset class="nex-realm-fieldset">';
	echo '<legend class="nex-auth-label">Selecciona dónde pertenece esta cuenta</legend>';
	echo '<div class="nex-realm-auth-grid">';
	foreach($realms as $id => $realm) {
		$checked = $selected === $id ? ' checked' : '';
		$accent = preg_replace('/[^a-z0-9_-]/', '', strtolower($realm['accent']));
		echo '<label class="nex-realm-card nex-realm-card--'.htmlspecialchars($accent, ENT_QUOTES, 'UTF-8').'">';
		echo '<input type="radio" name="'.htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8').'" value="'.htmlspecialchars($id, ENT_QUOTES, 'UTF-8').'" required'.$checked.'>';
		echo '<span class="nex-realm-card-copy">';
		echo '<strong>'.htmlspecialchars($realm['label'], ENT_QUOTES, 'UTF-8').'</strong>';
		echo '<small>'.htmlspecialchars($realm['short_label'], ENT_QUOTES, 'UTF-8').'</small>';
		echo '</span></label>';
	}
	echo '</div>';
	echo '<p class="nex-auth-help">El reino define la base usada por esta operación. La cuenta no se buscará en el otro servidor.</p>';
	echo '</fieldset>';
}

/** Registro compartido: conserva el reino visual para el acceso posterior. */
function nexoraDualRegistrationAuthField($fieldName='nexora_realm') {
	if(!class_exists('NexoraRealm') || !NexoraRealm::enabled()) return;
	$preferredRealm = NexoraRealm::active();
	if(isset($_POST[$fieldName]) && NexoraRealm::exists($_POST[$fieldName])) {
		$preferredRealm = strtolower($_POST[$fieldName]);
	}

	echo '<input type="hidden" name="'.htmlspecialchars($fieldName, ENT_QUOTES, 'UTF-8').'" value="'.htmlspecialchars($preferredRealm, ENT_QUOTES, 'UTF-8').'">';
	echo '<div class="nex-dual-registration" role="status">';
	echo '<span class="nex-dual-registration__marks" aria-hidden="true"><b>L</b><i>+</i><b>S</b></span>';
	echo '<span class="nex-dual-registration__copy"><strong>Una cuenta para Legado + Seasonal</strong>';
	echo '<small>Crearemos el mismo usuario y contraseña en ambos servidores. En el login eliges dónde entrar.</small></span>';
	echo '</div>';
}

/** Selector público; una sesión autenticada queda fijada a su reino. */
function nexoraRealmSwitcher() {
	if(!class_exists('NexoraRealm') || !NexoraRealm::enabled()) return;

	$active = NexoraRealm::active();
	$realms = NexoraRealm::publicList();
	$locked = isLoggedIn() && NexoraRealm::hasAccountRealm();

	echo '<div class="nex-realm-switcher'.($locked ? ' is-locked' : '').'" aria-label="Reino activo">';
	foreach($realms as $id => $realm) {
		$activeClass = $id === $active ? ' is-active' : '';
		$accent = preg_replace('/[^a-z0-9_-]/', '', strtolower($realm['accent']));
		if($locked) {
			if($id !== $active) continue;
			echo '<span class="nex-realm-pill nex-realm-pill--'.$accent.$activeClass.'">'.htmlspecialchars($realm['label'], ENT_QUOTES, 'UTF-8').' · sesión fijada</span>';
			continue;
		}
		$url = NexoraRealm::currentUrlFor($id);
		echo '<a class="nex-realm-pill nex-realm-pill--'.$accent.$activeClass.'" href="'.htmlspecialchars($url, ENT_QUOTES, 'UTF-8').'"'.($id === $active ? ' aria-current="true"' : '').'>'.htmlspecialchars($realm['label'], ENT_QUOTES, 'UTF-8').'</a>';
	}
	echo '</div>';
}

function nexoraRealmContextBanner() {
	if(!class_exists('NexoraRealm') || !NexoraRealm::enabled()) return;
	$realm = NexoraRealm::info();
	$id = NexoraRealm::active();
	echo '<div class="nex-realm-context nex-realm-context--'.htmlspecialchars($id, ENT_QUOTES, 'UTF-8').'">';
	echo '<span class="nex-realm-context-kicker">Reino activo</span>';
	echo '<strong>'.htmlspecialchars($realm['label'], ENT_QUOTES, 'UTF-8').'</strong>';
	echo '<span>'.htmlspecialchars($realm['description'], ENT_QUOTES, 'UTF-8').'</span>';
	echo '</div>';
}

/** Tiempo online desde segundos (misma escala que rankings_online). */
function nexora_format_rank_online_seconds($seconds) {
	if ($seconds === null || $seconds === '') {
		return '—';
	}
	$s = (float) $seconds;
	if ($s <= 0) {
		return '—';
	}
	$h = (int) round($s / 3600);
	$unit = function_exists('lang') ? lang('rankings_txt_16', true) : 'h';
	if ($unit === 'ERROR' || $unit === '') {
		$unit = 'h';
	}
	return number_format($h) . ' ' . $unit;
}

/** Filas por página en rankings estándar (fila 0 del cache WebEngine = metadata). */
function nexora_rankings_pagination_state(array $ranking_data, $perPage = 50) {
	$perPage = max(1, (int) $perPage);
	$totalRecords = max(0, count($ranking_data) - 1);
	$totalPages = $totalRecords > 0 ? (int) ceil($totalRecords / $perPage) : 1;
	$currentPage = isset($_GET['page_num']) ? (int) $_GET['page_num'] : 1;
	if ($currentPage < 1) {
		$currentPage = 1;
	}
	if ($totalPages > 0 && $currentPage > $totalPages) {
		$currentPage = $totalPages;
	}
	$offset = ($currentPage - 1) * $perPage;
	return array(
		'per_page' => $perPage,
		'total_records' => $totalRecords,
		'total_pages' => $totalPages,
		'current_page' => $currentPage,
		'offset' => $offset,
	);
}

/**
 * URL paginación rankings (?page_num=N), conserva otros query params.
 */
function nexora_rankings_pagination_url($pageNum)
{
	$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
	$parts = parse_url($uri);
	$path = isset($parts['path']) ? $parts['path'] : '/';
	parse_str(isset($parts['query']) ? $parts['query'] : '', $q);
	if (!is_array($q)) {
		$q = array();
	}
	unset($q['page_num']);
	$p = (int) $pageNum;
	if ($p > 1) {
		$q['page_num'] = $p;
	}
	$qs = http_build_query($q);
	return htmlspecialchars($path . ($qs !== '' ? '?' . $qs : ''), ENT_QUOTES, 'UTF-8');
}

/**
 * Controles Anterior / Siguiente + página actual (solo si hay más de una página).
 */
function nexora_rankings_pagination_nav($currentPage, $totalPages)
{
	if ($totalPages <= 1) {
		return;
	}
	$prev = max(1, (int) $currentPage - 1);
	$next = min((int) $totalPages, (int) $currentPage + 1);
	$lblPrev = htmlspecialchars(nexora_lang('nexora_rank_prev', 'Anterior'), ENT_QUOTES, 'UTF-8');
	$lblNext = htmlspecialchars(nexora_lang('nexora_rank_next', 'Siguiente'), ENT_QUOTES, 'UTF-8');
	$aria = htmlspecialchars(nexora_lang('nexora_rank_pagination_aria', 'Paginación del ranking'), ENT_QUOTES, 'UTF-8');
	$pgOf = htmlspecialchars(nexora_lang('nexora_rank_page_of', 'Página'), ENT_QUOTES, 'UTF-8');

	echo '<nav class="nex-rank-pagination" aria-label="' . $aria . '">';
	echo '<div class="nex-rank-pagination-inner">';
	if ($currentPage > 1) {
		echo '<a class="nex-rank-pagination-btn" href="' . nexora_rankings_pagination_url($prev) . '">' . $lblPrev . '</a>';
	} else {
		echo '<span class="nex-rank-pagination-btn nex-rank-pagination-btn--disabled" aria-disabled="true">' . $lblPrev . '</span>';
	}
	echo '<span class="nex-rank-pagination-info">' . $pgOf . ' <strong>' . (int) $currentPage . '</strong> / ' . (int) $totalPages . '</span>';
	if ($currentPage < $totalPages) {
		echo '<a class="nex-rank-pagination-btn" href="' . nexora_rankings_pagination_url($next) . '">' . $lblNext . '</a>';
	} else {
		echo '<span class="nex-rank-pagination-btn nex-rank-pagination-btn--disabled" aria-disabled="true">' . $lblNext . '</span>';
	}
	echo '</div></nav>';
}

/**
 * Event schedule (home) — estructura nexoraves6; horarios desde api/events.php (events.js).
 */
function nexora_home_events_card() {
	$eventIds = array(
		'bloodcastle',
		'devilsquare',
		'chaoscastle',
		'dragoninvasion',
		'goldeninvasion',
		'castlesiege',
	);
	$eventLabel = function_exists('lang') ? lang('event_schedule', true) : 'Event';
	if ($eventLabel === 'ERROR' || $eventLabel === '') {
		$eventLabel = 'Event';
	}
	$startsLabel = function_exists('lang') ? lang('event_schedule_start', true) : 'Starts in';
	if ($startsLabel === 'ERROR' || $startsLabel === '') {
		$startsLabel = 'Starts in';
	}

	echo '<div class="nex-rank-card nex-rank-card--events">';
	echo '<div class="nex-rank-card-head">';
	echo '<span>' . htmlspecialchars($eventLabel, ENT_QUOTES, 'UTF-8') . '</span>';
	echo '</div>';
	echo '<div class="nex-rank-table-wrap nex-events-schedule-wrap">';
	echo '<table class="nex-rank-table nex-events-schedule-table">';
	echo '<thead><tr>';
	echo '<th>' . htmlspecialchars($eventLabel, ENT_QUOTES, 'UTF-8') . '</th>';
	echo '<th class="nex-events-schedule-time">' . htmlspecialchars($startsLabel, ENT_QUOTES, 'UTF-8') . '</th>';
	echo '</tr></thead><tbody>';
	foreach ($eventIds as $eventId) {
		$id = preg_replace('/[^a-z0-9]/', '', strtolower($eventId));
		if ($id === '') {
			continue;
		}
		echo '<tr>';
		echo '<td><span id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '_name"></span>';
		echo '<br><span class="nex-events-schedule-label">' . htmlspecialchars($startsLabel, ENT_QUOTES, 'UTF-8') . '</span></td>';
		echo '<td class="nex-events-schedule-time">';
		echo '<span id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '_next"></span>';
		echo '<br><span class="nex-events-schedule-label" id="' . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . '"></span>';
		echo '</td>';
		echo '</tr>';
	}
	echo '</tbody></table></div></div>';
}

function nexoraNavbarItems($includeFeatured = true, array $excludeLinks = array()) {
	$cfg = loadConfig('navbar');
	if (!is_array($cfg)) {
		return;
	}
	$exclude = array();
	foreach ($excludeLinks as $excluded) {
		$exclude[] = strtolower(trim((string) $excluded, '/'));
	}
	foreach ($cfg as $element) {
		if (!is_array($element) || empty($element['active'])) {
			continue;
		}
		$normalizedLink = strtolower(trim(isset($element['link']) ? (string) $element['link'] : '', '/'));
		if (in_array($normalizedLink, $exclude, true)) {
			continue;
		}
		$link = ($element['type'] == 'internal' ? __BASE_URL__ . $element['link'] : $element['link']);
		$titleRaw = lang($element['phrase'], true);
		$title = (check_value($titleRaw) && $titleRaw !== 'ERROR') ? $titleRaw : ($element['phrase'] === 'menu_txt_benefits' ? 'Beneficios' : '—');
		if ($element['visibility'] == 'guest' && isLoggedIn()) {
			continue;
		}
		if ($element['visibility'] == 'user' && !isLoggedIn()) {
			continue;
		}
		$tgt = !empty($element['newtab']) ? ' target="_blank" rel="noopener"' : '';
		$isFeatured = !empty($element['featured']);
		if ($isFeatured && !$includeFeatured) {
			continue;
		}
		$featured = $isFeatured ? ' nex-nav-link--featured' : '';
		echo '<li role="none"><a class="nex-nav-link' . $featured . '" role="menuitem" href="' . htmlspecialchars($link, ENT_QUOTES, 'UTF-8') . '"' . $tgt . '>' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</a></li>';
	}
}

/**
 * Resuelve una entrada comercial desde navbar.json sin duplicar su fuente de verdad.
 */
function nexoraCommerceNavbarEntry($route) {
	$route = strtolower(trim((string) $route, '/'));
	$cfg = loadConfig('navbar');
	if (!is_array($cfg)) {
		return null;
	}
	foreach ($cfg as $element) {
		if (!is_array($element) || empty($element['active'])) {
			continue;
		}
		$link = strtolower(trim(isset($element['link']) ? (string) $element['link'] : '', '/'));
		if ($link !== $route) {
			continue;
		}
		if ($element['visibility'] === 'guest' && isLoggedIn()) {
			return null;
		}
		if ($element['visibility'] === 'user' && !isLoggedIn()) {
			return null;
		}
		$url = $element['type'] === 'internal' ? __BASE_URL__ . trim((string) $element['link'], '/') . '/' : (string) $element['link'];
		return array('url' => $url, 'newtab' => !empty($element['newtab']));
	}
	return null;
}

/**
 * Navegación comercial Nexora: Arsenal como acceso directo y economía secundaria agrupada.
 * No consulta saldo ni base de datos; solo usa configuración y contexto de reino.
 */
function nexoraCommerceMenu($mobile = false) {
	$shop = nexoraCommerceNavbarEntry('shop');
	if (!is_array($shop)) {
		return;
	}
	$benefits = nexoraCommerceNavbarEntry('benefits');
	$credits = nexoraCommerceNavbarEntry('donation');
	$realmLabel = class_exists('NexoraRealm') ? NexoraRealm::label() : 'Nexora';
	$page = isset($_REQUEST['page']) ? strtolower((string) $_REQUEST['page']) : '';
	$subpage = isset($_REQUEST['subpage']) ? strtolower((string) $_REQUEST['subpage']) : '';
	$isShop = $page === 'shop';
	$isBenefits = $page === 'benefits';
	$isDonation = $page === 'donation';
	$isVip = $page === 'vip';
	$shopLabel = nexora_lang('nexora_commerce_arsenal', 'Arsenal');
	$shopMeta = nexora_lang('nexora_commerce_arsenal_meta', 'Equipamiento del reino');
	$economyLabel = nexora_lang('nexora_commerce_economy', 'Economía Nexora');
	$realmNote = nexora_lang('nexora_commerce_realm_note', 'Saldo, stock y entrega pertenecen al reino activo.');

	if ($mobile) {
		echo '<li class="nex-commerce-mobile" role="none">';
		echo '<div class="nex-commerce-mobile__head"><span>' . htmlspecialchars($economyLabel, ENT_QUOTES, 'UTF-8') . '</span><strong>' . htmlspecialchars($realmLabel, ENT_QUOTES, 'UTF-8') . '</strong></div>';
		echo '<a class="nex-commerce-mobile__primary' . ($isShop ? ' is-active' : '') . '" role="menuitem" href="' . htmlspecialchars($shop['url'], ENT_QUOTES, 'UTF-8') . '"><span class="nex-commerce-mark" aria-hidden="true">◇</span><span><strong>' . htmlspecialchars($shopLabel, ENT_QUOTES, 'UTF-8') . '</strong><small>' . htmlspecialchars($shopMeta, ENT_QUOTES, 'UTF-8') . '</small></span><b aria-hidden="true">→</b></a>';
		echo '<div class="nex-commerce-mobile__links">';
		if (is_array($benefits)) {
			echo '<a class="' . ($isBenefits ? 'is-active' : '') . '" role="menuitem" href="' . htmlspecialchars($benefits['url'], ENT_QUOTES, 'UTF-8') . '"><strong>' . htmlspecialchars(nexora_lang('menu_txt_benefits', 'Beneficios'), ENT_QUOTES, 'UTF-8') . '</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_benefits_meta', 'Ventajas activas'), ENT_QUOTES, 'UTF-8') . '</small></a>';
		}
		if (is_array($credits)) {
			echo '<a class="' . ($isDonation ? 'is-active' : '') . '" role="menuitem" href="' . htmlspecialchars($credits['url'], ENT_QUOTES, 'UTF-8') . '"><strong>' . htmlspecialchars(nexora_lang('nexora_commerce_credits', 'WCoinC'), ENT_QUOTES, 'UTF-8') . '</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_credits_meta', 'Saldo del reino'), ENT_QUOTES, 'UTF-8') . '</small></a>';
		}
		if (isLoggedIn()) {
			echo '<a class="' . ($isVip ? 'is-active' : '') . '" role="menuitem" href="' . htmlspecialchars(__BASE_URL__ . 'vip/info/', ENT_QUOTES, 'UTF-8') . '"><strong>VIP</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_vip_meta', 'Membresía de cuenta'), ENT_QUOTES, 'UTF-8') . '</small></a>';
		}
		echo '</div><p>' . htmlspecialchars($realmNote, ENT_QUOTES, 'UTF-8') . '</p></li>';
		return;
	}

	echo '<div class="nex-commerce-switch' . ($isShop || $isBenefits || $isDonation || $isVip ? ' has-active' : '') . '">';
	echo '<a class="nex-commerce-primary' . ($isShop ? ' is-active' : '') . '" href="' . htmlspecialchars($shop['url'], ENT_QUOTES, 'UTF-8') . '">';
	echo '<span class="nex-commerce-mark" aria-hidden="true">◇</span><span class="nex-commerce-primary__copy"><small>' . htmlspecialchars($shopMeta, ENT_QUOTES, 'UTF-8') . '</small><strong>' . htmlspecialchars($shopLabel, ENT_QUOTES, 'UTF-8') . '</strong></span></a>';
	echo '<details class="nex-commerce-more">';
	echo '<summary aria-label="' . htmlspecialchars(nexora_lang('nexora_commerce_open', 'Abrir economía Nexora'), ENT_QUOTES, 'UTF-8') . '"><span aria-hidden="true">⌄</span></summary>';
	echo '<div class="nex-commerce-panel">';
	echo '<header><span>' . htmlspecialchars($economyLabel, ENT_QUOTES, 'UTF-8') . '</span><strong>' . htmlspecialchars($realmLabel, ENT_QUOTES, 'UTF-8') . '</strong></header>';
	echo '<a class="nex-commerce-panel__item nex-commerce-panel__item--primary' . ($isShop ? ' is-active' : '') . '" href="' . htmlspecialchars($shop['url'], ENT_QUOTES, 'UTF-8') . '"><span class="nex-commerce-panel__icon" aria-hidden="true">◇</span><span><strong>' . htmlspecialchars($shopLabel, ENT_QUOTES, 'UTF-8') . '</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_arsenal_panel', 'Catálogo, stock y entrega al baúl'), ENT_QUOTES, 'UTF-8') . '</small></span></a>';
	if (is_array($benefits)) {
		echo '<a class="nex-commerce-panel__item' . ($isBenefits ? ' is-active' : '') . '" href="' . htmlspecialchars($benefits['url'], ENT_QUOTES, 'UTF-8') . '"><span class="nex-commerce-panel__icon" aria-hidden="true">◆</span><span><strong>' . htmlspecialchars(nexora_lang('menu_txt_benefits', 'Beneficios'), ENT_QUOTES, 'UTF-8') . '</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_benefits_meta', 'Ventajas activas'), ENT_QUOTES, 'UTF-8') . '</small></span></a>';
	}
	if (is_array($credits)) {
		echo '<a class="nex-commerce-panel__item' . ($isDonation ? ' is-active' : '') . '" href="' . htmlspecialchars($credits['url'], ENT_QUOTES, 'UTF-8') . '"><span class="nex-commerce-panel__icon" aria-hidden="true">◈</span><span><strong>' . htmlspecialchars(nexora_lang('nexora_commerce_credits', 'WCoinC'), ENT_QUOTES, 'UTF-8') . '</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_credits_meta', 'Saldo del reino'), ENT_QUOTES, 'UTF-8') . '</small></span></a>';
	}
	if (isLoggedIn()) {
		echo '<a class="nex-commerce-panel__item' . ($isVip ? ' is-active' : '') . '" href="' . htmlspecialchars(__BASE_URL__ . 'vip/info/', ENT_QUOTES, 'UTF-8') . '"><span class="nex-commerce-panel__icon" aria-hidden="true">✦</span><span><strong>VIP</strong><small>' . htmlspecialchars(nexora_lang('nexora_commerce_vip_meta', 'Membresía de cuenta'), ENT_QUOTES, 'UTF-8') . '</small></span></a>';
	}
	echo '<footer>' . htmlspecialchars($realmNote, ENT_QUOTES, 'UTF-8') . '</footer></div></details></div>';
}

/** Acceso comercial principal de Home. No ejecuta consultas adicionales. */
function nexoraCommerceHomeCard() {
	$shop = nexoraCommerceNavbarEntry('shop');
	if (!is_array($shop)) {
		return;
	}
	$benefits = nexoraCommerceNavbarEntry('benefits');
	$credits = nexoraCommerceNavbarEntry('donation');
	$realmLabel = class_exists('NexoraRealm') ? NexoraRealm::label() : 'Nexora';
	echo '<section class="nex-home-commerce" aria-label="' . htmlspecialchars(nexora_lang('nexora_commerce_economy', 'Economía Nexora'), ENT_QUOTES, 'UTF-8') . '">';
	echo '<a class="nex-home-commerce__primary" href="' . htmlspecialchars($shop['url'], ENT_QUOTES, 'UTF-8') . '"><span class="nex-home-commerce__sigil" aria-hidden="true">◇</span><span class="nex-home-commerce__copy"><small>' . htmlspecialchars($realmLabel . ' · ' . nexora_lang('nexora_commerce_economy', 'Economía Nexora'), ENT_QUOTES, 'UTF-8') . '</small><strong>' . htmlspecialchars(nexora_lang('nexora_commerce_arsenal', 'Arsenal'), ENT_QUOTES, 'UTF-8') . '</strong><em>' . htmlspecialchars(nexora_lang('nexora_commerce_home_meta', 'Equipamiento publicado con stock y entrega controlada al baúl.'), ENT_QUOTES, 'UTF-8') . '</em></span><b aria-hidden="true">→</b></a>';
	echo '<div class="nex-home-commerce__rail">';
	if (is_array($benefits)) {
		echo '<a href="' . htmlspecialchars($benefits['url'], ENT_QUOTES, 'UTF-8') . '"><span>◆</span><strong>' . htmlspecialchars(nexora_lang('menu_txt_benefits', 'Beneficios'), ENT_QUOTES, 'UTF-8') . '</strong></a>';
	}
	if (is_array($credits)) {
		echo '<a href="' . htmlspecialchars($credits['url'], ENT_QUOTES, 'UTF-8') . '"><span>◈</span><strong>' . htmlspecialchars(nexora_lang('nexora_commerce_credits', 'WCoinC'), ENT_QUOTES, 'UTF-8') . '</strong></a>';
	}
	echo '</div></section>';
}

function templateBuildUsercp() {
	$cfg = loadConfig('usercp');
	if (!is_array($cfg)) {
		return;
	}
	uasort($cfg, function ($a, $b) {
		$o1 = isset($a['order']) ? (int) $a['order'] : 999;
		$o2 = isset($b['order']) ? (int) $b['order'] : 999;
		return $o1 <=> $o2;
	});

	$p = isset($_REQUEST['page']) ? (string) $_REQUEST['page'] : '';
	$s = isset($_REQUEST['subpage']) ? (string) $_REQUEST['subpage'] : '';
	$current = strtolower(trim($s !== '' ? $p . '/' . $s : $p, '/'));

	$aria = htmlspecialchars(nexora_lang('nexora_ucp_nav_aria', 'Opciones del panel de cuenta'), ENT_QUOTES, 'UTF-8');
	echo '<nav class="nex-usercp-nav" aria-label="' . $aria . '">';
	echo '<ul class="nex-usercp-nav-list">';
	foreach ($cfg as $element) {
		if (!is_array($element) || empty($element['active'])) {
			continue;
		}
		if ($element['visibility'] == 'guest' && isLoggedIn()) {
			continue;
		}
		if ($element['visibility'] == 'user' && !isLoggedIn()) {
			continue;
		}
		$link = ($element['type'] == 'internal' ? __BASE_URL__ . $element['link'] : $element['link']);
		$tRaw = lang($element['phrase'], true);
		$title = (check_value($tRaw) && $tRaw !== 'ERROR') ? $tRaw : '—';
		$icon = nexoraUsercpIcon($element);
		$tgt = !empty($element['newtab']) ? ' target="_blank" rel="noopener"' : '';
		$pathEl = strtolower(trim((string) $element['link'], '/'));
		$isActive = ($pathEl !== '' && $pathEl === $current);
		$cls = 'nex-usercp-nav-link' . ($isActive ? ' is-active' : '');
		echo '<li class="nex-usercp-nav-item">';
		echo '<a class="' . htmlspecialchars($cls, ENT_QUOTES, 'UTF-8') . '" href="' . htmlspecialchars($link, ENT_QUOTES, 'UTF-8') . '"' . $tgt . '>';
		echo '<span class="nex-usercp-nav-ico"><img src="' . htmlspecialchars($icon, ENT_QUOTES, 'UTF-8') . '" alt="" width="22" height="22" loading="lazy"></span>';
		echo '<span class="nex-usercp-nav-txt">' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '</span>';
		echo '</a></li>';
	}
	echo '</ul></nav>';
}

function templateCastleSiegeWidget() {
	$castleSiege = new CastleSiege();
	if (!$castleSiege->showWidget()) {
		return;
	}
	$siegeData = $castleSiege->siegeData();
	if (!is_array($siegeData) || !is_array($siegeData['castle_data'])) {
		return;
	}
	if ($siegeData['castle_data'][_CLMN_MCD_OCCUPY_] == 1) {
		$guildOwner = guildProfile($siegeData['castle_data'][_CLMN_MCD_GUILD_OWNER_]);
		$guildOwnerMark = $siegeData['castle_owner_alliance'][0][_CLMN_GUILD_LOGO_];
		$guildMaster = playerProfile($siegeData['castle_owner_alliance'][0][_CLMN_GUILD_MASTER_]);
	} else {
		$guildOwner = '-';
		$guildOwnerMark = '1111111111111111111111111114411111144111111111111111111111111111';
		$guildMaster = '-';
	}
	echo '<div class="nex-widget nex-widget--castle">';
	echo '<h3>' . htmlspecialchars(lang('castlesiege_widget_title'), ENT_QUOTES, 'UTF-8') . '</h3>';
	echo '<div class="nex-castle-grid">';
	echo '<div class="nex-castle-emblem">' . returnGuildLogo($guildOwnerMark, 100) . '</div>';
	echo '<div class="nex-castle-meta">';
	echo '<div class="nex-castle-meta-row"><span class="nex-castle-meta-lbl">' . htmlspecialchars(lang('castlesiege_txt_2'), ENT_QUOTES, 'UTF-8') . '</span><span class="nex-castle-meta-val">' . $guildOwner . '</span></div>';
	echo '<div class="nex-castle-meta-row"><span class="nex-castle-meta-lbl">' . htmlspecialchars(lang('castlesiege_txt_12'), ENT_QUOTES, 'UTF-8') . '</span><span class="nex-castle-meta-val">' . $guildMaster . '</span></div>';
	echo '</div></div>';
	echo '<p class="nex-castle-stage">' . htmlspecialchars($siegeData['current_stage']['title'], ENT_QUOTES, 'UTF-8') . '</p>';
	echo '<a class="nex-btn nex-btn--ghost nex-castle-more" href="' . htmlspecialchars(rtrim(__BASE_URL__, '/') . '/castlesiege', ENT_QUOTES, 'UTF-8') . '">' . htmlspecialchars(lang('castlesiege_txt_7'), ENT_QUOTES, 'UTF-8') . '</a>';
	echo '</div>';
}

function nexoraLanguageSelector() {
	if (!function_exists('lang') || !config('language_switch_active', true)) {
		return;
	}
	$langList = array(
		'en' => array('English', 'US'),
		'es' => array('Español', 'ES'),
		'ph' => array('Filipino', 'PH'),
		'br' => array('Português', 'BR'),
		'ro' => array('Romanian', 'RO'),
		'cn' => array('Simplified Chinese', 'CN'),
		'ru' => array('Russian', 'RU'),
		'lt' => array('Lithuanian', 'LT'),
	);
	if (isset($_SESSION['language_display'])) {
		$lang = $_SESSION['language_display'];
	} else {
		$lang = config('language_default', true);
	}
	if (!isset($langList[$lang])) {
		$lang = 'en';
	}
	echo '<div class="nex-lang">';
	echo '<button type="button" class="nex-lang-trigger" aria-expanded="false" aria-haspopup="listbox" id="nex-lang-btn">';
	echo '<img src="' . htmlspecialchars(getCountryFlag($langList[$lang][1]), ENT_QUOTES, 'UTF-8') . '" alt="" width="18" height="12" loading="lazy">';
	echo '<span>' . strtoupper(htmlspecialchars($lang, ENT_QUOTES, 'UTF-8')) . '</span>';
	echo '</button>';
	echo '<ul class="nex-lang-menu" role="listbox" aria-labelledby="nex-lang-btn">';
	foreach ($langList as $code => $languageInfo) {
		if ($code === $lang) {
			continue;
		}
		$url = __BASE_URL__ . 'language/switch/to/' . strtolower($code);
		echo '<li role="option">';
		echo '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">';
		echo '<img src="' . htmlspecialchars(getCountryFlag($languageInfo[1]), ENT_QUOTES, 'UTF-8') . '" alt="" width="18" height="12" loading="lazy">';
		echo '<span>' . htmlspecialchars($languageInfo[0], ENT_QUOTES, 'UTF-8') . '</span>';
		echo '</a></li>';
	}
	echo '</ul>';
	echo '</div>';
}

function nexoraHomeRealmSnapshot($realm) {
	$realm = NexoraRealm::validate($realm);
	$serverCache = LoadCacheData('server_info.cache', $realm);
	$serverInfo = array();
	if (is_array($serverCache) && isset($serverCache[1][0])) {
		$serverInfo = explode('|', $serverCache[1][0]);
	}

	$resetsRanking = LoadCacheData('rankings_resets.cache', $realm);
	$levelRanking = LoadCacheData('rankings_level.cache', $realm);
	$hasResets = is_array($resetsRanking) && count($resetsRanking) > 1;
	$hasLevel = is_array($levelRanking) && count($levelRanking) > 1;
	$rankingMode = $hasResets ? 'resets' : 'level';
	$rankingData = $hasResets ? $resetsRanking : ($hasLevel ? $levelRanking : array());

	$updatedAt = null;
	if (isset($rankingData[0][0]) && is_numeric($rankingData[0][0])) {
		$updatedAt = (int) $rankingData[0][0];
	} elseif (isset($serverCache[0][0]) && is_numeric($serverCache[0][0])) {
		$updatedAt = (int) $serverCache[0][0];
	}

	return array(
		'realm' => $realm,
		'info' => NexoraRealm::info($realm),
		'accounts' => isset($serverInfo[0]) && is_numeric($serverInfo[0]) ? (int) $serverInfo[0] : null,
		'characters' => isset($serverInfo[1]) && is_numeric($serverInfo[1]) ? (int) $serverInfo[1] : null,
		'guilds' => isset($serverInfo[2]) && is_numeric($serverInfo[2]) ? (int) $serverInfo[2] : null,
		'online' => isset($serverInfo[3]) && is_numeric($serverInfo[3]) ? (int) $serverInfo[3] : null,
		'ranking_mode' => $rankingMode,
		'ranking' => $rankingData,
		'updated_at' => $updatedAt,
	);
}

function nexoraRenderHomeRealmRanking($realm) {
	$snapshot = nexoraHomeRealmSnapshot($realm);
	$realm = $snapshot['realm'];
	$info = $snapshot['info'];
	$h = function ($value) {
		return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
	};
	$metric = function ($value) {
		return $value === null ? '—' : number_format((int) $value);
	};

	$realmNarrative = $realm === 'legacy'
		? 'Progreso permanente: lo que la comunidad construye permanece y gana historia.'
		: 'Ritmo competitivo: cada objetivo cumplido fortalece el valor que regresa a Legado.';
	$onlineText = $snapshot['online'] === null
		? 'Actividad pendiente'
		: number_format($snapshot['online']) . ($snapshot['online'] === 1 ? ' conectado' : ' conectados');
	$rankingPath = $snapshot['ranking_mode'] === 'resets' ? 'rankings/resets/' : 'rankings/level/';
	$rankingUrl = NexoraRealm::appendToUrl(rtrim(__BASE_URL__, '/') . '/' . $rankingPath, $realm);
	$canOpenRanking = !isLoggedIn() || NexoraRealm::accountRealm() === $realm;

	echo '<article class="nex-dual-rank-card nex-dual-rank-card--' . $h($realm) . '">';
	echo '<div class="nex-dual-rank-card__glow" aria-hidden="true"></div>';
	echo '<header class="nex-dual-rank-card__head">';
	echo '<div class="nex-dual-rank-card__identity">';
	echo '<span class="nex-dual-rank-card__type">' . $h(isset($info['short_label']) ? $info['short_label'] : '') . '</span>';
	echo '<h3>' . $h(isset($info['label']) ? $info['label'] : ucfirst($realm)) . '</h3>';
	echo '</div>';
	echo '<span class="nex-dual-rank-card__activity"><i aria-hidden="true"></i>' . $h($onlineText) . '</span>';
	echo '</header>';
	echo '<p class="nex-dual-rank-card__narrative">' . $h($realmNarrative) . '</p>';

	echo '<div class="nex-dual-rank-metrics" aria-label="Actividad de ' . $h($info['label']) . '">';
	echo '<div><strong>' . $metric($snapshot['accounts']) . '</strong><span>Cuentas</span></div>';
	echo '<div><strong>' . $metric($snapshot['characters']) . '</strong><span>Personajes</span></div>';
	echo '<div><strong>' . $metric($snapshot['guilds']) . '</strong><span>Guilds</span></div>';
	echo '</div>';

	echo '<div class="nex-dual-rank-card__table-head">';
	echo '<strong>Top progreso</strong>';
	echo '<span>' . ($snapshot['ranking_mode'] === 'resets' ? 'Resets + nivel' : 'Nivel actual') . '</span>';
	echo '</div>';

	$rankingRows = is_array($snapshot['ranking']) ? array_slice($snapshot['ranking'], 1, 5) : array();
	if (count($rankingRows) > 0) {
		echo '<div class="nex-dual-rank-table-wrap"><table class="nex-dual-rank-table">';
		echo '<thead><tr><th>#</th><th>Jugador</th><th>Clase</th><th>Resets</th><th>Nivel</th></tr></thead><tbody>';
		$position = 0;
		foreach ($rankingRows as $row) {
			if (!is_array($row) || !isset($row[0])) continue;
			$position++;
			$classId = isset($row[1]) ? (int) $row[1] : 0;
			$resets = $snapshot['ranking_mode'] === 'resets' && isset($row[2]) && is_numeric($row[2]) ? number_format($row[2]) : '—';
			$levelIndex = $snapshot['ranking_mode'] === 'resets' ? 3 : 2;
			$level = isset($row[$levelIndex]) && is_numeric($row[$levelIndex]) ? number_format($row[$levelIndex]) : '—';
			$classImg = function_exists('getPlayerClassAvatar')
				? getPlayerClassAvatar($classId, true, true, 'rankings-class-image nex-dual-rank-class-image')
				: $h(function_exists('getPlayerClass') ? getPlayerClass($classId) : '—');
			echo '<tr data-class-id="' . $classId . '">';
			echo '<td class="nex-dual-rank-pos' . ($position <= 3 ? ' is-podium' : '') . '">' . $position . '</td>';
			echo '<td class="nex-dual-rank-player">' . $h($row[0]) . '</td>';
			echo '<td class="nex-dual-rank-class">' . $classImg . '</td>';
			echo '<td class="nex-dual-rank-value">' . $resets . '</td>';
			echo '<td class="nex-dual-rank-value">' . $level . '</td>';
			echo '</tr>';
		}
		echo '</tbody></table></div>';
	} else {
		echo '<div class="nex-dual-rank-empty">Esperando la primera actualización de progreso.</div>';
	}

	echo '<footer class="nex-dual-rank-card__foot">';
	if ($snapshot['updated_at'] !== null) {
		echo '<span>Actualizado ' . $h(date('d/m/Y · h:i A', $snapshot['updated_at'])) . '</span>';
	} else {
		echo '<span>Datos disponibles después del próximo ciclo.</span>';
	}
	if ($canOpenRanking) {
		echo '<a href="' . $h($rankingUrl) . '">Ver ranking del reino</a>';
	} else {
		echo '<span class="nex-dual-rank-card__scope">Vista comparativa</span>';
	}
	echo '</footer>';
	echo '</article>';
}

function nexoraHomeRankings() {
	echo '<section class="nex-rankings-section nex-dual-rankings nex-reveal" aria-labelledby="nex-dual-rankings-title">';
	echo '<header class="nex-dual-rankings__intro">';
	echo '<span class="nex-dual-rankings__eyebrow">Actividad del ecosistema</span>';
	echo '<h2 id="nex-dual-rankings-title">Dos caminos. Un mismo avance.</h2>';
	echo '<p>Legado conserva el valor construido. Seasonal concentra el pulso competitivo de cada etapa. Aquí ambos progresos se leen de frente.</p>';
	echo '</header>';
	echo '<div class="nex-dual-rankings__grid">';
	nexoraRenderHomeRealmRanking('legacy');
	nexoraRenderHomeRealmRanking('seasonal');
	echo '</div>';
	echo '<div class="nex-dual-rankings__events">';
	nexora_home_events_card();
	echo '</div>';
	echo '</section>';
}

function nexoraCastleSiegeEpic() {
	$castleSiege = new CastleSiege();
	if (!$castleSiege->showWidget()) {
		return;
	}
	$siegeData = $castleSiege->siegeData();
	if (!is_array($siegeData) || !is_array($siegeData['castle_data'])) {
		return;
	}

	if ($siegeData['castle_data'][_CLMN_MCD_OCCUPY_] == 1) {
		$guildOwner = guildProfile($siegeData['castle_data'][_CLMN_MCD_GUILD_OWNER_]);
		$guildOwnerMark = $siegeData['castle_owner_alliance'][0][_CLMN_GUILD_LOGO_];
		$guildMaster = playerProfile($siegeData['castle_owner_alliance'][0][_CLMN_GUILD_MASTER_]);
	} else {
		$guildOwner = '-';
		$guildOwnerMark = '1111111111111111111111111114411111144111111111111111111111111111';
		$guildMaster = '-';
	}

	$bgFs = __PATH_TEMPLATE_ROOT__ . 'img/castle_owner_bg.jpg';
	$hasCastleBg = is_file($bgFs);
	$bgUrl = $hasCastleBg ? (__PATH_TEMPLATE_IMG__ . 'castle_owner_bg.jpg') : '';

	$stageTitle = isset($siegeData['current_stage']['title']) ? $siegeData['current_stage']['title'] : '';
	$countdown = isset($siegeData['warfare_stage_countdown']) ? $siegeData['warfare_stage_countdown'] : '';

	$secClass = 'nex-castle-epic nex-reveal' . ($hasCastleBg ? ' nex-castle-epic--bg' : '');
	$secStyle = $hasCastleBg ? ' style="--nex-cs-bg:url(\'' . htmlspecialchars($bgUrl, ENT_QUOTES, 'UTF-8') . '\')"' : '';
	echo '<section class="' . $secClass . '"' . $secStyle . ' aria-labelledby="nex-castle-epic-title">';
	echo '<div class="nex-castle-epic-shine" aria-hidden="true"></div>';
	echo '<div class="nex-castle-epic-panel">';
	echo '<div class="nex-castle-epic-head">';
	echo '<p class="nex-castle-epic-tag">' . htmlspecialchars(nexora_lang('nexora_cs_tag', 'Season War'), ENT_QUOTES, 'UTF-8') . '</p>';
	echo '<h2 id="nex-castle-epic-title" class="nex-castle-epic-title">' . htmlspecialchars(lang('castlesiege_widget_title'), ENT_QUOTES, 'UTF-8') . '</h2>';
	echo '</div>';
	echo '<div class="nex-castle-epic-layout">';
	echo '<aside class="nex-castle-epic-aside">';
	echo '<div class="nex-castle-epic-emblem">' . returnGuildLogo($guildOwnerMark, 128) . '</div>';
	echo '<a class="nex-castle-epic-btn" href="' . htmlspecialchars(rtrim(__BASE_URL__, '/') . '/castlesiege', ENT_QUOTES, 'UTF-8') . '">' . htmlspecialchars(lang('castlesiege_txt_7'), ENT_QUOTES, 'UTF-8') . '</a>';
	echo '</aside>';
	echo '<div class="nex-castle-epic-rows">';
	echo '<div class="nex-castle-row"><span class="nex-castle-lbl">' . htmlspecialchars(lang('castlesiege_txt_2'), ENT_QUOTES, 'UTF-8') . '</span><span class="nex-castle-val">' . $guildOwner . '</span></div>';
	echo '<div class="nex-castle-row"><span class="nex-castle-lbl">' . htmlspecialchars(lang('castlesiege_txt_12'), ENT_QUOTES, 'UTF-8') . '</span><span class="nex-castle-val">' . $guildMaster . '</span></div>';
	echo '<div class="nex-castle-row"><span class="nex-castle-lbl">' . htmlspecialchars(lang('castlesiege_txt_21'), ENT_QUOTES, 'UTF-8') . '</span><span class="nex-castle-val">' . htmlspecialchars($stageTitle, ENT_QUOTES, 'UTF-8') . '</span></div>';
	echo '<div class="nex-castle-row nex-castle-row--highlight"><span class="nex-castle-lbl">' . htmlspecialchars(lang('castlesiege_txt_1'), ENT_QUOTES, 'UTF-8') . '</span><span class="nex-castle-val nex-castle-countdown">' . htmlspecialchars($countdown, ENT_QUOTES, 'UTF-8') . '</span></div>';
	echo '</div></div></div></section>';
}

/**
 * Carga módulos PHP del CMS con override en inc/modules/ del template (misma lógica que Ezio / Nexora V6).
 *
 * @param object $handler Instancia del handler WebEngine.
 * @param string $page Página solicitada.
 * @param string $subpage Subpágina (vacío si no aplica).
 */
function nexora_template_load_module($handler, $page, $subpage) {
	if (is_object($handler) && method_exists($handler, 'parseRequestParameter')) {
		$handler->parseRequestParameter();
	}

	$tplModules = __PATH_TEMPLATE_ROOT__ . 'inc/modules/';

	// News Studio usa un mismo renderer para el archivo y el detalle /news/{id}/.
	if ($page === 'news' && is_file($tplModules . 'news.php')) {
		@loadModuleConfigs('news');
		include $tplModules . 'news.php';
		return;
	}

	if (check_value($subpage)) {
		if ($page === 'usercp' && isset($_REQUEST['request'])) {
			$reqSeg = preg_replace('/[^a-zA-Z0-9_-]/', '', (string) $_REQUEST['request']);
			if ($reqSeg !== '') {
				$nestedTpl = $tplModules . $page . '/' . $subpage . '/' . $reqSeg . '.php';
				if (is_file($nestedTpl)) {
					@loadModuleConfigs($page);
					@loadModuleConfigs($page . '.' . $subpage);
					include $nestedTpl;
					return;
				}
				$nestedCore = __PATH_MODULES_USERCP__ . $subpage . '/' . $reqSeg . '.php';
				if (is_file($nestedCore)) {
					@loadModuleConfigs($page);
					@loadModuleConfigs($page . '.' . $subpage);
					include $nestedCore;
					return;
				}
			}
		}
		$customSubPath = $tplModules . $page . '/' . $subpage . '.php';
		if (is_file($customSubPath)) {
			if ($page === 'profile') {
				@loadModuleConfigs('profiles');
			} else {
				@loadModuleConfigs($page);
				@loadModuleConfigs($page . '.' . $subpage);
			}
			include $customSubPath;
			return;
		}
		$handler->loadModule($page, $subpage);
		return;
	}

	$customModulePath = $tplModules . $page . '.php';
	if (is_file($customModulePath)) {
		@loadModuleConfigs($page);
		include $customModulePath;
	} else {
		$handler->loadModule($page, $subpage);
	}
}
