<?php
/**
 * WP admin bar live-visitor indicator.
 *
 * Renders a node on the right side of the admin bar with the number of visitors
 * currently online.
 *
 * The realtime figure is never fetched during page render. This node appears on
 * every admin AND front-end page load for administrators, so a blocking HTTP
 * call to the app here would put a third party on the critical path of every one
 * of those requests. Instead:
 *
 *   - render reads the 30-second cache only (SimpleStats_Report::get_realtime_cached())
 *   - on a cache miss the node ships hidden, and a small script fills it in over
 *     admin-ajax, which is where the actual fetch (and the cache write) happens
 *
 * Gated on:
 *   - is_admin_bar_showing()
 *   - user can manage_options (same gate as the dashboard page)
 *   - site is connected
 *   - `show_admin_bar_indicator` setting is enabled
 *
 * When the setting is off, neither the node nor the request happens at all.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class SimpleStats_Admin_Bar {

	private static $instance;

	public static function get_instance() {
		if ( ! self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Style handle carrying the admin bar node's inline CSS.
	 */
	const STYLE_HANDLE = 'simplestats-admin-bar';

	/**
	 * Script handle carrying the node's inline hydration JS.
	 */
	const SCRIPT_HANDLE = 'simplestats-admin-bar';

	/**
	 * admin-ajax action + nonce action used to hydrate a cold node.
	 */
	const AJAX_ACTION  = 'simplestats_admin_bar_online';
	const NONCE_ACTION = 'simplestats_admin_bar_online';

	/**
	 * Node id, and the CSS class that keeps it hidden until hydrated.
	 */
	const NODE_ID       = 'simplestats-online';
	const PENDING_CLASS = 'simplestats-admin-bar--pending';

	/**
	 * Cached realtime payload for this render, or false. Resolved once so
	 * add_node() and enqueue_assets() agree on whether hydration is needed.
	 *
	 * @var array|false|null
	 */
	private $realtime;

	private function __construct() {
		add_action( 'admin_bar_menu', array( $this, 'add_node' ), 999 );
		// The admin bar renders on both the front end and in wp-admin, so the
		// assets are enqueued on both enqueue hooks.
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
		add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax_online' ) );
	}

	/**
	 * Cached realtime payload for this request, resolved at most once.
	 *
	 * @return array|false
	 */
	private function cached_realtime() {
		if ( null === $this->realtime ) {
			$this->realtime = SimpleStats_Report::get_instance()->get_realtime_cached();
		}

		return $this->realtime;
	}

	/**
	 * Whether the indicator should render in this request.
	 *
	 * @return bool
	 */
	private function should_render() {
		if ( ! is_admin_bar_showing() ) {
			return false;
		}

		if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
			return false;
		}

		if ( ! simplestats_is_connected() ) {
			return false;
		}

		$settings = simplestats_get_settings();
		return ! empty( $settings['show_admin_bar_indicator'] );
	}

	/**
	 * Render the admin bar node.
	 *
	 * Uses only what is already cached. On a miss the node is still emitted, but
	 * hidden and empty, for the hydration script to fill — never a blocking
	 * request on the render path.
	 *
	 * @param WP_Admin_Bar $wp_admin_bar
	 */
	public function add_node( $wp_admin_bar ) {
		if ( ! $this->should_render() ) {
			return;
		}

		$data      = $this->cached_realtime();
		$has_count = is_array( $data ) && isset( $data['online'] );
		$label     = $has_count ? $this->online_label( (int) $data['online'] ) : '';

		$title = sprintf(
			'<span class="simplestats-admin-bar__icon" aria-hidden="true">%1$s</span><span class="simplestats-admin-bar__label">%2$s</span>',
			$this->get_icon_svg(),
			esc_html( $label )
		);

		$meta = array(
			'title' => esc_attr__( 'View SimpleStats dashboard', 'simplestats-friendly-website-analytics' ),
		);

		if ( ! $has_count ) {
			$meta['class'] = self::PENDING_CLASS;
		}

		$wp_admin_bar->add_node( array(
			'id'    => self::NODE_ID,
			'title' => $title,
			'href'  => admin_url( 'admin.php?page=simplestats#/dashboard' ),
			'meta'  => $meta,
		) );
	}

	/**
	 * The "N online" label.
	 *
	 * Built server-side (including by the AJAX handler) so _n() picks the right
	 * plural form for the locale — something the hydration script could not do
	 * on the front end, where wp.i18n is not loaded.
	 *
	 * @param int $count Visitors currently online.
	 * @return string Plain text. The node render escapes it; the AJAX response
	 *                sends it raw because the script assigns it via textContent.
	 */
	private function online_label( $count ) {
		return sprintf(
			/* translators: %d: number of visitors currently online. */
			_n( '%d online', '%d online', $count, 'simplestats-friendly-website-analytics' ),
			$count
		);
	}

	/**
	 * AJAX: resolve the realtime count and return the rendered label.
	 *
	 * This is the only path that actually contacts the app for the indicator, and
	 * it also warms the 30-second cache that subsequent renders read.
	 */
	public function ajax_online() {
		check_ajax_referer( self::NONCE_ACTION );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'code' => 'forbidden' ), 403 );
		}

		if ( ! simplestats_is_connected() ) {
			wp_send_json_error( array( 'code' => 'not_connected' ), 409 );
		}

		$data = SimpleStats_Report::get_instance()->get_realtime();

		if ( ! is_array( $data ) || ! isset( $data['online'] ) ) {
			wp_send_json_error( array( 'code' => 'upstream' ), 502 );
		}

		$count = (int) $data['online'];

		wp_send_json_success( array(
			'online' => $count,
			'label'  => $this->online_label( $count ),
		) );
	}

	/**
	 * Inline trending-up icon (from Figma node 240:1328).
	 * `currentColor` lets CSS drive the fill.
	 *
	 * @return string
	 */
	private function get_icon_svg() {
		return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false"><path d="M8 0C3.58187 0 0 3.58187 0 8C0 12.4181 3.58187 16 8 16C12.4181 16 16 12.4181 16 8C16 3.58187 12.4181 0 8 0ZM12.3706 8.50375C12.3687 8.80312 12.1416 9.02063 11.8306 9.02063H11.8244C11.5156 9.01781 11.2966 8.8 11.2916 8.49125C11.2878 8.24906 11.2884 8.00281 11.2894 7.765C11.2894 7.66083 11.2894 7.55667 11.2894 7.4525V7.39C11.2816 7.39687 11.2744 7.40375 11.2675 7.41062C10.5912 8.08979 9.91531 8.76917 9.23969 9.44875L8.95844 9.73C8.78969 9.89969 8.63188 9.98219 8.47594 9.98219C8.32 9.98219 8.16656 9.90156 8.00219 9.73594L6.55656 8.29062L6.4375 8.41156C6.3125 8.53656 6.19312 8.65406 6.0775 8.77031L5.53438 9.31688C5.22188 9.63188 4.89875 9.95781 4.57969 10.2772C4.52664 10.3337 4.4628 10.379 4.39196 10.4105C4.32111 10.4419 4.24469 10.4589 4.16719 10.4603C4.0804 10.46 3.99487 10.4396 3.91719 10.4009C3.71125 10.3 3.59344 10.0819 3.63063 9.86969C3.65133 9.73659 3.70991 9.61226 3.79938 9.51156C4.65281 8.64469 5.50781 7.79281 6.1575 7.14813C6.20888 7.09378 6.27059 7.05024 6.339 7.02003C6.40741 6.98982 6.48116 6.97355 6.55594 6.97219C6.70687 6.97219 6.8525 7.03781 6.97687 7.16219L8.44563 8.63094L10.4456 6.63094H10.2437C9.98719 6.63094 9.63563 6.62781 9.43406 6.62594C9.11656 6.62344 8.89406 6.40219 8.8925 6.08781C8.89071 6.0158 8.90316 5.94415 8.92911 5.87696C8.95506 5.80977 8.99402 5.74836 9.04375 5.69625C9.0946 5.64666 9.15479 5.60765 9.22082 5.58147C9.28685 5.55529 9.35742 5.54247 9.42844 5.54375C9.83469 5.54219 10.2409 5.54156 10.6322 5.54156C11.04 5.54156 11.4466 5.54156 11.8406 5.54375C12.1447 5.54375 12.3669 5.7625 12.3687 6.06125C12.3766 6.89281 12.375 7.71531 12.3706 8.50375Z" fill="currentColor"/></svg>';
	}

	/**
	 * Enqueue the admin bar node's CSS, plus the hydration JS when the realtime
	 * cache was cold for this render.
	 *
	 * Both are static and scoped to #wp-admin-bar-simplestats-online, so they are
	 * attached as inline assets to src-less registered handles (the
	 * WordPress.org-sanctioned way to ship inline-only assets through the
	 * enqueue API).
	 */
	public function enqueue_assets() {
		if ( ! $this->should_render() ) {
			return;
		}

		wp_register_style( self::STYLE_HANDLE, false, array(), SIMPLESTATS_PLUGIN_VERSION );
		wp_enqueue_style( self::STYLE_HANDLE );
		wp_add_inline_style( self::STYLE_HANDLE, $this->get_inline_styles() );

		// Cache is warm — the node already carries its number, so no script and
		// no request at all.
		if ( false !== $this->cached_realtime() ) {
			return;
		}

		// wp-util supplies wp.ajax, and localizes its own admin-ajax URL, so this
		// works unchanged on the front end where the admin bar also renders.
		wp_register_script( self::SCRIPT_HANDLE, false, array( 'wp-util' ), SIMPLESTATS_PLUGIN_VERSION, true );
		wp_enqueue_script( self::SCRIPT_HANDLE );

		wp_localize_script( self::SCRIPT_HANDLE, 'simplestatsAdminBar', array(
			'action'  => self::AJAX_ACTION,
			'nonce'   => wp_create_nonce( self::NONCE_ACTION ),
			'nodeId'  => 'wp-admin-bar-' . self::NODE_ID,
			'pending' => self::PENDING_CLASS,
		) );

		wp_add_inline_script( self::SCRIPT_HANDLE, $this->get_inline_script() );
	}

	/**
	 * The node's inline hydration JS.
	 *
	 * Transport is wp.ajax, which unwraps the wp_send_json_* envelope and resolves
	 * with the payload itself. Runs on DOMContentLoaded because footer scripts
	 * print at wp_footer priority 20 while the front-end admin bar renders at
	 * priority 1000 — the node does not exist yet when this executes.
	 *
	 * @return string
	 */
	private function get_inline_script() {
		return <<<'JS'
( function () {
	var cfg = window.simplestatsAdminBar;

	if ( ! cfg || ! window.wp || ! wp.ajax ) {
		return;
	}

	function hydrate() {
		var node = document.getElementById( cfg.nodeId );

		if ( ! node ) {
			return;
		}

		var label = node.querySelector( '.simplestats-admin-bar__label' );

		if ( ! label ) {
			return;
		}

		wp.ajax.post( cfg.action, {
			_ajax_nonce: cfg.nonce
		} ).done( function ( data ) {
			if ( ! data || typeof data.label !== 'string' ) {
				return;
			}

			label.textContent = data.label;
			node.classList.remove( cfg.pending );
		} ).fail( function () {
			// Leave the node hidden — the indicator is not worth an error state.
		} );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', hydrate );
	} else {
		hydrate();
	}
}() );
JS;
	}

	/**
	 * The admin bar node's inline CSS.
	 *
	 * @return string
	 */
	private function get_inline_styles() {
		return '
			#wpadminbar #wp-admin-bar-simplestats-online.' . self::PENDING_CLASS . ' {
				display: none;
			}
			#wpadminbar #wp-admin-bar-simplestats-online > .ab-item {
				display: flex;
				align-items: center;
				gap: 8px;
			}
			#wpadminbar #wp-admin-bar-simplestats-online .simplestats-admin-bar__icon {
				display: inline-flex;
				align-items: center;
				color: #fff;
			}
			#wpadminbar #wp-admin-bar-simplestats-online .simplestats-admin-bar__icon svg {
				width: 16px;
				height: 16px;
				display: block;
			}
			#wpadminbar #wp-admin-bar-simplestats-online .simplestats-admin-bar__label {
				color: #27AE60;
				font-weight: 600;
			}
			#wpadminbar #wp-admin-bar-simplestats-online:hover > .ab-item .simplestats-admin-bar__label,
			#wpadminbar #wp-admin-bar-simplestats-online > .ab-item:focus .simplestats-admin-bar__label {
				color: #27AE60;
			}
		';
	}
}
