<?php
/**
 * WP Dashboard widget showing a Last-7-Days visitor chart, key KPIs,
 * top page, top referrer, and the realtime online count.
 *
 * Rendered server-side from cache only:
 *   - dashboard report: 1h transient, shared with the SPA through
 *     SimpleStats_Report::get_cached_dashboard_report().
 *   - realtime online:  30s transient (set in SimpleStats_Report).
 *
 * Neither fetch happens while the Dashboard is rendering. On a cold cache the
 * widget ships a loading state and the same AJAX endpoint that powers the range
 * switcher fills in the body — so a slow or unreachable app can never hold up
 * wp-admin's index.php, which is what a blocking 15s report fetch plus a 3s
 * realtime fetch on wp_dashboard_setup used to risk.
 *
 * Gated on manage_options + connected + show_dashboard_widget setting.
 */

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

class SimpleStats_Dashboard_Widget {

	const WIDGET_ID    = 'simplestats_dashboard_widget';
	const AJAX_ACTION  = 'simplestats_widget_chart';
	const NONCE_ACTION = 'simplestats_widget_chart';

	/**
	 * Supported range presets and their day counts.
	 */
	const RANGES = array(
		'7d'  => 7,
		'30d' => 30,
	);

	const DEFAULT_RANGE = '7d';

	private static $instance;

	/**
	 * Resolved data for the current render. Populated in register() so the
	 * postbox title (with the online badge) and the body share one fetch.
	 *
	 * @var array
	 */
	private $data = array();

	/**
	 * Active range preset key for this render. One of self::RANGES keys.
	 *
	 * @var string
	 */
	private $range = self::DEFAULT_RANGE;

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

	/**
	 * Asset handles for the widget's inline CSS/JS.
	 */
	const STYLE_HANDLE  = 'simplestats-dashboard-widget';
	const SCRIPT_HANDLE = 'simplestats-dashboard-widget';

	private function __construct() {
		add_action( 'wp_dashboard_setup', array( $this, 'register' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
		add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax_chart' ) );
	}

	/**
	 * Whether the widget should be registered for the current user.
	 */
	private function should_render() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}
		if ( ! simplestats_is_connected() ) {
			return false;
		}
		$settings = simplestats_get_settings();
		return ! empty( $settings['show_dashboard_widget'] );
	}

	/**
	 * Hook callback — reads whatever is cached, builds the title with the online
	 * badge, and registers the widget. Never contacts the app; see the file
	 * docblock.
	 */
	public function register() {
		if ( ! $this->should_render() ) {
			return;
		}

		$this->range = self::DEFAULT_RANGE;
		$this->data  = $this->read_cached_data( self::RANGES[ $this->range ] );

		wp_add_dashboard_widget(
			self::WIDGET_ID,
			$this->build_title_html(),
			array( $this, 'render' )
		);
	}

	/**
	 * Resolve the inclusive date range for a "last N days" window.
	 *
	 * @param int $days Number of days back from today (inclusive).
	 * @return array { from:string, to:string }
	 */
	private function range_dates( $days ) {
		$days  = max( 1, (int) $days );
		$today = current_time( 'Y-m-d' );

		return array(
			'from' => gmdate( 'Y-m-d', strtotime( $today . ' -' . ( $days - 1 ) . ' days' ) ),
			'to'   => $today,
		);
	}

	/**
	 * Cache-only data for the render path. `cold` is true when there is no
	 * cached report to draw, which is the signal to render the loading state and
	 * let the hydration script fetch it.
	 *
	 * @param int $days Number of days back from today (inclusive).
	 * @return array { online:int|null, report:array|null, from:string, to:string, cold:bool }
	 */
	private function read_cached_data( $days ) {
		$dates  = $this->range_dates( $days );
		$report = SimpleStats_Report::get_instance()->get_cached_dashboard_report_only( $dates['from'], $dates['to'] );

		return array(
			'from'   => $dates['from'],
			'to'     => $dates['to'],
			'report' => is_array( $report ) ? $report : null,
			'online' => $this->cached_online(),
			'cold'   => ! is_array( $report ),
		);
	}

	/**
	 * Online count from the realtime cache, or null when nothing is cached.
	 *
	 * @return int|null
	 */
	private function cached_online() {
		$realtime = SimpleStats_Report::get_instance()->get_realtime_cached();

		return ( is_array( $realtime ) && isset( $realtime['online'] ) ) ? (int) $realtime['online'] : null;
	}

	/**
	 * Remote-allowed data for the AJAX path, where the user is already watching
	 * a loading state. Report caching (including the refusal to cache a
	 * zero-activity report) is owned by SimpleStats_Report.
	 *
	 * @param int $days Number of days back from today (inclusive).
	 * @return array { online:int|null, report:array|null, from:string, to:string, cold:bool }
	 */
	private function fetch_data( $days ) {
		$dates    = $this->range_dates( $days );
		$report   = SimpleStats_Report::get_instance()->get_cached_dashboard_report( $dates['from'], $dates['to'] );
		$realtime = SimpleStats_Report::get_instance()->get_realtime();

		return array(
			'from'   => $dates['from'],
			'to'     => $dates['to'],
			'report' => is_array( $report ) ? $report : null,
			'online' => ( is_array( $realtime ) && isset( $realtime['online'] ) ) ? (int) $realtime['online'] : null,
			'cold'   => false,
		);
	}

	/**
	 * Build the title used inside the WP postbox header — "SimpleStats"
	 * plus the online badge on the right. WordPress prints the title HTML
	 * raw inside the postbox <h2>.
	 */
	private function build_title_html() {
		$title = '<span class="simplestats-dw__title-text">' . esc_html__( 'SimpleStats', 'simplestats-friendly-website-analytics' ) . '</span>';

		// No cached count yet: omit the badge rather than claim zero. The
		// hydration script inserts it once the real number arrives.
		if ( null === $this->data['online'] ) {
			return $title;
		}

		return $title . $this->build_badge_html( (int) $this->data['online'] );
	}

	/**
	 * The "N online now" badge shown in the postbox header.
	 *
	 * @param int $count Visitors currently online.
	 * @return string
	 */
	private function build_badge_html( $count ) {
		return sprintf(
			'<span class="simplestats-dw__online-badge" aria-label="%1$s"><span class="simplestats-dw__online-dot" aria-hidden="true"></span>%2$s</span>',
			esc_attr( sprintf(
				/* translators: %d: visitors currently online. */
				_n( '%d visitor online now', '%d visitors online now', $count, 'simplestats-friendly-website-analytics' ),
				$count
			) ),
			esc_html( sprintf(
				/* translators: %d: visitors currently online. */
				_n( '%d online now', '%d online now', $count, 'simplestats-friendly-website-analytics' ),
				$count
			) )
		);
	}

	/**
	 * Widget body render callback.
	 */
	public function render() {
		if ( ! empty( $this->data['cold'] ) ) {
			$this->render_loading();
			return;
		}

		$report = $this->data['report'];

		if ( ! is_array( $report ) ) {
			$this->render_error();
			return;
		}

		$this->render_body( $report );
	}

	/**
	 * The widget body proper. Also captured verbatim by the AJAX handler, so the
	 * hydrated markup is identical to the server-rendered markup.
	 *
	 * @param array $report Report payload.
	 */
	private function render_body( $report ) {
		echo '<div class="simplestats-dw">';
			$this->render_chart_section( $report );
			$this->render_metrics_section( $report );
			$this->render_footer();
		echo '</div>';
	}

	/**
	 * Capture render_body() as a string for the AJAX response.
	 *
	 * @param array $report Report payload.
	 * @return string
	 */
	private function capture_body( $report ) {
		ob_start();
		$this->render_body( $report );

		return (string) ob_get_clean();
	}

	/**
	 * Placeholder shown while the hydration script fetches the first report.
	 */
	private function render_loading() {
		printf(
			'<div class="simplestats-dw simplestats-dw--loading" data-simplestats-cold="1"><p>%s</p></div>',
			esc_html__( 'Loading your SimpleStats data…', 'simplestats-friendly-website-analytics' )
		);
	}

	private function render_error() {
		echo '<div class="simplestats-dw simplestats-dw--error">';
		echo '<p>' . esc_html__( 'Could not load SimpleStats data right now. Please try again in a moment.', 'simplestats-friendly-website-analytics' ) . '</p>';
		echo '</div>';
	}

	/**
	 * "Visitors (Last 7 Days)" chart row.
	 */
	private function render_chart_section( $report ) {
		$series = isset( $report['series']['visitors'] ) && is_array( $report['series']['visitors'] )
			? $report['series']['visitors']
			: array();

		$days      = self::RANGES[ $this->range ];
		$select_id = self::WIDGET_ID . '-range';
		?>
		<section class="simplestats-dw__chart-section">
			<div class="simplestats-dw__label-row">
				<h3 class="simplestats-dw__chart-title"><?php echo esc_html( $this->chart_title_for_days( $days ) ); ?></h3>
				<label class="screen-reader-text" for="<?php echo esc_attr( $select_id ); ?>"><?php esc_html_e( 'Date range', 'simplestats-friendly-website-analytics' ); ?></label>
				<?php // The nonce is localized with the script, not duplicated here — this markup is also served over AJAX. ?>
				<select
					id="<?php echo esc_attr( $select_id ); ?>"
					class="simplestats-dw__range"
				>
					<?php foreach ( self::RANGES as $key => $count ) : ?>
						<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $this->range, $key ); ?>>
							<?php echo esc_html( $this->range_option_label( $count ) ); ?>
						</option>
					<?php endforeach; ?>
				</select>
			</div>
			<div class="simplestats-dw__chart-canvas">
				<?php
				// Inline SVG built entirely from locally-computed geometry and
				// esc_html()'d labels — see build_chart_svg(). Not run through
				// kses because wp_kses_post() strips <svg>/<polyline>/<circle>.
				echo $this->build_chart_svg( $series ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
				?>
			</div>
		</section>
		<?php
	}

	/**
	 * "Visitors (Last N Days)" for a given day count.
	 */
	private function chart_title_for_days( $days ) {
		/* translators: %d: number of days in the selected range. */
		return sprintf( __( 'Visitors (Last %d Days)', 'simplestats-friendly-website-analytics' ), (int) $days );
	}

	/**
	 * "Last N Days" used as the <select> option label.
	 */
	private function range_option_label( $days ) {
		/* translators: %d: number of days. */
		return sprintf( __( 'Last %d Days', 'simplestats-friendly-website-analytics' ), (int) $days );
	}

	/**
	 * Analytics summary section — Visitors / Pageviews / Top Page / Top Referrer.
	 */
	private function render_metrics_section( $report ) {
		$kpis        = isset( $report['kpis'] ) && is_array( $report['kpis'] ) ? $report['kpis'] : array();
		$top_pages   = isset( $report['top_pages'] ) && is_array( $report['top_pages'] ) ? $report['top_pages'] : array();
		$referrers   = isset( $report['referrers'] ) && is_array( $report['referrers'] ) ? $report['referrers'] : array();

		$visitors_value  = isset( $kpis['visitors']['value'] )       ? (int) $kpis['visitors']['value']       : 0;
		$visitors_delta  = isset( $kpis['visitors']['delta_pct'] )   ? $kpis['visitors']['delta_pct']         : null;
		$pageviews_value = isset( $kpis['pageviews']['value'] )      ? (int) $kpis['pageviews']['value']      : 0;
		$pageviews_delta = isset( $kpis['pageviews']['delta_pct'] )  ? $kpis['pageviews']['delta_pct']        : null;

		$top_page      = $top_pages[0]  ?? null;
		$top_referrer  = $referrers[0]  ?? null;

		?>
		<section class="simplestats-dw__metrics">
			<header class="simplestats-dw__metrics-header">
				<h3 class="simplestats-dw__metrics-title"><?php esc_html_e( 'Analytics Summary', 'simplestats-friendly-website-analytics' ); ?></h3>
			</header>

			<?php
			$this->render_metric_row(
				__( 'Visitors', 'simplestats-friendly-website-analytics' ),
				$this->render_value_with_delta( $visitors_value, $visitors_delta )
			);

			$this->render_metric_row(
				__( 'Pageviews', 'simplestats-friendly-website-analytics' ),
				$this->render_value_with_delta( $pageviews_value, $pageviews_delta )
			);

			$this->render_metric_row(
				__( 'Top Page', 'simplestats-friendly-website-analytics' ),
				$this->render_top_page( $top_page )
			);

			$this->render_metric_row(
				__( 'Top Referrer', 'simplestats-friendly-website-analytics' ),
				$this->render_top_referrer( $top_referrer ),
				true
			);
			?>
		</section>
		<?php
	}

	private function render_metric_row( $label, $value_html, $is_last = false ) {
		$class = 'simplestats-dw__row' . ( $is_last ? ' simplestats-dw__row--last' : '' );
		echo '<div class="' . esc_attr( $class ) . '">';
		echo '<span class="simplestats-dw__row-label">' . esc_html( $label ) . '</span>';
		// $value_html is small markup (span / a) built by the render_* helpers
		// below, each of which escapes its own remote-sourced values. kses is a
		// second line of defence at the sink, so a helper that ever forgets to
		// escape cannot turn app-controlled report data into admin XSS.
		echo '<span class="simplestats-dw__row-value">' . wp_kses_post( $value_html ) . '</span>';
		echo '</div>';
	}

	private function render_value_with_delta( $value, $delta_pct ) {
		$pill = '';

		if ( null !== $delta_pct && '' !== $delta_pct ) {
			$n         = (float) $delta_pct;
			$pill_dir  = $n > 0 ? 'up' : ( $n < 0 ? 'down' : 'flat' );
			$sign      = $n > 0 ? '+' : ( $n < 0 ? '-' : '' );
			$abs       = abs( $n );
			$formatted = $abs === floor( $abs ) ? (string) (int) $abs : number_format_i18n( $abs, 1 );

			$pill = sprintf(
				'<span class="simplestats-dw__pill simplestats-dw__pill--%1$s">%2$s%3$s%%</span>',
				esc_attr( $pill_dir ),
				esc_html( $sign ),
				esc_html( $formatted )
			);
		}

		return $pill . '<span class="simplestats-dw__metric-number">' . esc_html( number_format_i18n( $value ) ) . '</span>';
	}

	private function render_top_page( $row ) {
		if ( ! is_array( $row ) ) {
			return '<span class="simplestats-dw__muted">—</span>';
		}

		$label = $row['title'] ?? ( $row['path'] ?? '' );
		$path  = $row['path']  ?? '';

		if ( '' === $label && '' === $path ) {
			return '<span class="simplestats-dw__muted">—</span>';
		}

		if ( '' !== $path ) {
			return sprintf(
				'<a class="simplestats-dw__link" href="%1$s" target="_blank" rel="noopener">%2$s</a>',
				esc_url( home_url( $path ) ),
				esc_html( $this->truncate( (string) $label, 28 ) )
			);
		}

		return '<span class="simplestats-dw__link">' . esc_html( $this->truncate( (string) $label, 28 ) ) . '</span>';
	}

	private function render_top_referrer( $row ) {
		if ( ! is_array( $row ) ) {
			return '<span class="simplestats-dw__muted">—</span>';
		}

		$host = isset( $row['host'] ) ? (string) $row['host'] : '';
		if ( '' === $host ) {
			return '<span class="simplestats-dw__muted">—</span>';
		}

		return '<span class="simplestats-dw__metric-number">' . esc_html( $this->truncate( $host, 28 ) ) . '</span>';
	}

	private function render_footer() {
		$url = admin_url( 'admin.php?page=simplestats#/dashboard' );
		?>
		<footer class="simplestats-dw__footer">
			<a class="simplestats-dw__view-all" href="<?php echo esc_url( $url ); ?>">
				<?php esc_html_e( 'View Full Dashboard', 'simplestats-friendly-website-analytics' ); ?>
				<span aria-hidden="true">&rarr;</span>
			</a>
		</footer>
		<?php
	}

	/**
	 * Build an SVG line chart from a series of { date, value } points.
	 * Renders an empty-state pill when there is no data.
	 *
	 * Geometry is taken from the Figma canvas (480×123) so dot sizes, padding,
	 * and axis labels match without estimation:
	 *   - chart area:  x 28→474, y 6→95
	 *   - dots:        size 8, color #1470AF
	 *   - y labels:    "1" at top, "0" at bottom
	 *   - x labels:    short date per point, baseline y=110
	 *
	 * @param array $series Each point: { date: 'YYYY-MM-DD', value: number }.
	 */
	private function build_chart_svg( array $series ) {
		if ( empty( $series ) ) {
			return '<div class="simplestats-dw__chart-empty">' . esc_html__( 'No activity in this range.', 'simplestats-friendly-website-analytics' ) . '</div>';
		}

		$x0    = 28.0;
		$x1    = 474.0;
		$y_top = 6.0;
		$y_bot = 95.0;

		// Long-form closure, not an arrow function: `fn()` is PHP 7.4+ and this
		// file is loaded on every request, so it would be a parse error — a
		// site-wide fatal — on the 7.2 this plugin declares support for.
		$values   = array_map(
			static function ( $p ) {
				return (float) ( isset( $p['value'] ) ? $p['value'] : 0 );
			},
			$series
		);
		$max      = max( $values );
		$max_safe = $max > 0 ? $max : 1.0;

		$count = count( $series );
		$step  = $count > 1 ? ( $x1 - $x0 ) / ( $count - 1 ) : 0;

		// Cap to ~8 visible date labels regardless of how many points we plot.
		$max_labels   = 8;
		$label_stride = max( 1, (int) ceil( $count / $max_labels ) );
		$dot_radius   = $count > 14 ? 2.5 : 4;

		$points   = array();
		$dots     = '';
		$x_labels = '';

		foreach ( $series as $i => $p ) {
			$x = $x0 + $step * $i;
			$y = $y_bot - ( ( (float) ( $p['value'] ?? 0 ) / $max_safe ) * ( $y_bot - $y_top ) );

			$points[] = sprintf( '%.2f,%.2f', $x, $y );

			$dots .= sprintf(
				'<circle cx="%.2f" cy="%.2f" r="%s" fill="#1470AF" />',
				$x, $y, $dot_radius
			);

			$is_label_tick = ( 0 === $i % $label_stride ) || ( $i === $count - 1 );
			if ( ! $is_label_tick ) {
				continue;
			}

			$date_label = '';
			if ( ! empty( $p['date'] ) ) {
				$ts         = strtotime( (string) $p['date'] );
				$date_label = $ts ? date_i18n( 'M j', $ts ) : '';
			}

			$x_labels .= sprintf(
				'<text x="%.2f" y="110" text-anchor="middle" class="simplestats-dw__chart-xlabel">%s</text>',
				$x,
				esc_html( $date_label )
			);
		}

		$polyline = sprintf(
			'<polyline points="%s" fill="none" stroke="#1470AF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />',
			implode( ' ', $points )
		);

		$top_axis = ( $max > 0 ? (int) ceil( $max ) : 1 );

		$aria = sprintf(
			/* translators: %d: number of days plotted. */
			__( 'Visitors over the last %d days', 'simplestats-friendly-website-analytics' ),
			$count
		);

		return sprintf(
			'<svg class="simplestats-dw__chart-svg" viewBox="0 0 480 123" preserveAspectRatio="none" role="img" aria-label="%s">'
				. '<text x="0" y="10" class="simplestats-dw__chart-ylabel">%d</text>'
				. '<text x="0" y="99" class="simplestats-dw__chart-ylabel">0</text>'
				. '<line x1="28" y1="6" x2="474" y2="6" stroke="#EDEDED" stroke-width="1" />'
				. '<line x1="28" y1="95" x2="474" y2="95" stroke="#EDEDED" stroke-width="1" />'
				. '%s%s%s'
				. '</svg>',
			esc_attr( $aria ),
			$top_axis,
			$polyline,
			$dots,
			$x_labels
		);
	}

	private function truncate( $str, $len ) {
		if ( function_exists( 'mb_strlen' ) && mb_strlen( $str ) > $len ) {
			return mb_substr( $str, 0, $len - 1 ) . '…';
		}
		if ( strlen( $str ) > $len ) {
			return substr( $str, 0, $len - 1 ) . '…';
		}
		return $str;
	}

	/**
	 * AJAX handler — resolves the report for the requested range and returns
	 * everything the widget might need to repaint: the chart title and SVG (for a
	 * range switch), the full body markup (for the initial cold-cache hydration)
	 * and the header badge.
	 *
	 * This is the only path allowed to contact the app, because here the user is
	 * already looking at a loading state.
	 */
	public function ajax_chart() {
		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 );
		}

		$range = isset( $_POST['range'] ) ? sanitize_text_field( wp_unslash( $_POST['range'] ) ) : '';
		if ( ! isset( self::RANGES[ $range ] ) ) {
			wp_send_json_error( array( 'code' => 'bad_range' ), 400 );
		}

		$days = self::RANGES[ $range ];

		// Both are read back by the render helpers below, so set them before
		// rendering: the captured body must reflect the range just requested.
		$this->range = $range;
		$this->data  = $this->fetch_data( $days );

		$report = $this->data['report'];

		if ( ! is_array( $report ) ) {
			wp_send_json_error( array( 'code' => 'upstream' ), 502 );
		}

		$series = isset( $report['series']['visitors'] ) && is_array( $report['series']['visitors'] )
			? $report['series']['visitors']
			: array();

		wp_send_json_success( array(
			'title' => $this->chart_title_for_days( $days ),
			'svg'   => $this->build_chart_svg( $series ),
			'body'  => $this->capture_body( $report ),
			'badge' => ( null === $this->data['online'] ) ? '' : $this->build_badge_html( (int) $this->data['online'] ),
		) );
	}

	/**
	 * Enqueue the widget's CSS + JS, dashboard screen only.
	 *
	 * Both are inline-only, so they hang off src-less registered handles (the
	 * WordPress.org-sanctioned way to ship inline assets through the enqueue
	 * API). The script declares jquery as a dependency rather than being bolted
	 * onto the jquery handle itself, so it can carry its own localized config.
	 *
	 * @param string $hook_suffix Current admin page.
	 */
	public function enqueue_assets( $hook_suffix ) {
		if ( 'index.php' !== $hook_suffix || ! $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() );

		// wp-util supplies wp.ajax (and pulls in jquery, used for the DOM work).
		wp_register_script( self::SCRIPT_HANDLE, false, array( 'jquery', 'wp-util' ), SIMPLESTATS_PLUGIN_VERSION, true );
		wp_enqueue_script( self::SCRIPT_HANDLE );

		wp_localize_script( self::SCRIPT_HANDLE, 'simplestatsDashboardWidget', array(
			'action'       => self::AJAX_ACTION,
			'nonce'        => wp_create_nonce( self::NONCE_ACTION ),
			'widgetId'     => self::WIDGET_ID,
			'defaultRange' => self::DEFAULT_RANGE,
			'errorText'    => __( 'Could not load SimpleStats data right now. Please try again in a moment.', 'simplestats-friendly-website-analytics' ),
		) );

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

	/**
	 * The widget's inline JS.
	 *
	 * Two jobs:
	 *   1. Hydration — when the body was rendered as a cold-cache placeholder,
	 *      fetch the report and swap the real body in.
	 *   2. Range switching — repaint the chart title and SVG.
	 *
	 * The change handler is delegated from the widget container because
	 * hydration replaces the <select> that would otherwise be bound directly.
	 *
	 * Transport is wp.ajax, which unwraps the wp_send_json_* envelope, so the
	 * done handlers receive the payload itself rather than { success, data }.
	 *
	 * A NOWDOC, so nothing here is interpolated by PHP and jQuery's $-prefixed
	 * locals survive untouched; all configuration arrives via the localized
	 * object instead of being substituted into the source.
	 *
	 * @return string
	 */
	private function get_inline_script() {
		return <<<'JS'
( function( $ ) {
	$( function() {
		var cfg = window.simplestatsDashboardWidget;

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

		var $widget = $( '#' + cfg.widgetId );

		if ( ! $widget.length ) {
			return;
		}

		function request( range ) {
			return wp.ajax.post( cfg.action, {
				_ajax_nonce: cfg.nonce,
				range:       range
			} );
		}

		function paintBadge( html ) {
			if ( ! html ) {
				return;
			}

			var $badge = $widget.find( '.simplestats-dw__online-badge' );

			if ( $badge.length ) {
				$badge.replaceWith( html );
			} else {
				$widget.find( '.hndle .simplestats-dw__title-text' ).after( html );
			}
		}

		function showError( $target ) {
			$target
				.removeAttr( 'data-simplestats-cold' )
				.removeClass( 'simplestats-dw--loading' )
				.addClass( 'simplestats-dw--error' )
				.empty()
				.append( $( '<p/>' ).text( cfg.errorText ) );
		}

		// 1. Cold cache: the rendered body is a placeholder, so fetch for real.
		var $cold = $widget.find( '.simplestats-dw[data-simplestats-cold="1"]' );

		if ( $cold.length ) {
			request( cfg.defaultRange ).done( function( data ) {
				if ( data && data.body ) {
					$cold.replaceWith( data.body );
					paintBadge( data.badge );
				} else {
					showError( $cold );
				}
			} ).fail( function() {
				showError( $cold );
			} );
		}

		// 2. Range switch on an already-rendered body.
		$widget.on( 'change', '.simplestats-dw__range', function() {
			var $select = $( this );
			var $title  = $widget.find( '.simplestats-dw__chart-title' );
			var $canvas = $widget.find( '.simplestats-dw__chart-canvas' );

			$select.prop( 'disabled', true );
			$canvas.css( 'opacity', '0.45' );

			request( $select.val() ).done( function( data ) {
				if ( data ) {
					$title.text( data.title );
					$canvas.html( data.svg );
					paintBadge( data.badge );
				}
			} ).always( function() {
				$select.prop( 'disabled', false );
				$canvas.css( 'opacity', '' );
			} );
		} );
	} );
} )( jQuery );
JS;
	}

	/**
	 * The widget's inline CSS, scoped under the widget id so unrelated
	 * dashboards stay untouched. Built from a NOWDOC with the (safe, constant)
	 * widget id substituted into the id selectors.
	 *
	 * @return string
	 */
	private function get_inline_styles() {
		$css = <<<'CSS'
			#__WIDGET_ID__ .hndle {
				display: flex;
				align-items: center;
				gap: 12px;
			}
			#__WIDGET_ID__ .hndle .simplestats-dw__title-text {
				font-weight: 700;
				color: #1D2327;
			}
			#__WIDGET_ID__ .hndle .simplestats-dw__online-badge {
				display: inline-flex;
				align-items: center;
				gap: 8px;
				margin-left: auto;
				padding: 2px 10px;
				background: #EDF7EE;
				border: 1px solid #4AB866;
				border-radius: 100px;
				color: #1E4620;
				font-size: 13px;
				font-weight: 500;
				line-height: 20px;
			}
			#__WIDGET_ID__ .hndle .simplestats-dw__online-dot {
				width: 8px;
				height: 8px;
				border-radius: 50%;
				background: #4AB866;
				flex: 0 0 8px;
			}
			#__WIDGET_ID__ .inside {
				margin: 0;
				padding: 0;
				background: #fff;
			}
			.simplestats-dw__chart-section {
				padding: 12px 16px 8px;
				border-bottom: 1px solid #C3C4C7;
			}
			.simplestats-dw__label-row {
				display: flex;
				align-items: center;
				justify-content: space-between;
				margin-bottom: 8px;
			}
			#dashboard-widgets h3.simplestats-dw__chart-title {
				margin: 0;
				font-size: 14px;
				font-weight: 700;
				color: #2F2F2F;
			}
			.simplestats-dw__range {
				appearance: none;
				-webkit-appearance: none;
				-moz-appearance: none;
				display: inline-flex;
				align-items: center;
				height: 32px;
				min-height: 32px;
				padding: 3px 26px 3px 10px;
				background-color: #fff;
				background-image: url( "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8' fill='none'><path d='M6.90106 1.45312L4 4.35419L1.09894 1.45312L0 2.55206L4 6.55206L8 2.55206L6.90106 1.45312Z' fill='%232F2F2F'/></svg>" );
				background-repeat: no-repeat;
				background-position: right 10px center;
				background-size: 8px 8px;
				border: 1px solid #C3C4C7;
				border-radius: 3px;
				box-shadow: none;
				font-size: 12px;
				font-weight: 500;
				color: #2F2F2F;
				line-height: 16px;
				cursor: pointer;
			}
			.simplestats-dw__range:focus {
				border-color: #1470AF;
				outline: none;
				box-shadow: 0 0 0 1px #1470AF;
			}
			.simplestats-dw__chart-canvas {
				width: 100%;
				height: 123px;
			}
			.simplestats-dw__chart-svg {
				display: block;
				width: 100%;
				height: 100%;
				overflow: visible;
			}
			.simplestats-dw__chart-ylabel,
			.simplestats-dw__chart-xlabel {
				fill: #757575;
				font-family: inherit;
			}
			.simplestats-dw__chart-ylabel { font-size: 10px; }
			.simplestats-dw__chart-xlabel { font-size: 9px; }
			.simplestats-dw__chart-empty {
				display: flex;
				align-items: center;
				justify-content: center;
				height: 100%;
				color: #757575;
				font-size: 12px;
			}
			.simplestats-dw__metrics-header {
				display: flex;
				align-items: center;
				height: 65px;
				padding: 0 16px;
				border-bottom: 1px solid #C3C4C7;
			}
			#dashboard-widgets h3.simplestats-dw__metrics-title {
				margin: 0;
				font-size: 14px;
				font-weight: 700;
				color: #2F2F2F;
			}
			.simplestats-dw__row {
				display: flex;
				align-items: center;
				justify-content: space-between;
				height: 40px;
				padding: 0 16px;
				background: #F9F9F9;
				border-bottom: 1px solid #CACFD2;
			}
			.simplestats-dw__row--last { border-bottom: 0; }
			.simplestats-dw__row-label {
				font-size: 13px;
				font-weight: 600;
				color: #2F2F2F;
			}
			.simplestats-dw__row-value {
				display: inline-flex;
				align-items: center;
				gap: 7px;
			}
			.simplestats-dw__pill {
				display: inline-flex;
				align-items: center;
				padding: 2px 7px;
				border-radius: 100px;
				font-size: 11px;
				font-weight: 600;
				line-height: 16px;
			}
			.simplestats-dw__pill--up   { background: #DCFCE8; color: #27AC60; }
			.simplestats-dw__pill--down { background: #FEE2E2; color: #B91C1C; }
			.simplestats-dw__pill--flat { background: #F1F5F9; color: #475569; }
			.simplestats-dw__metric-number {
				font-size: 12px;
				font-weight: 600;
				color: #1E1E1E;
			}
			.simplestats-dw__link {
				font-size: 12px;
				font-weight: 600;
				color: #1470AF;
				text-decoration: none;
			}
			.simplestats-dw__link:hover,
			.simplestats-dw__link:focus { text-decoration: underline; color: #1470AF; }
			.simplestats-dw__muted { color: #757575; font-size: 12px; }
			.simplestats-dw__footer {
				display: flex;
				align-items: center;
				justify-content: center;
				height: 40px;
				background: #F2F3F4;
				border-top: 1px solid #C3C4C7;
			}
			.simplestats-dw__view-all {
				font-size: 12px;
				font-weight: 600;
				color: #1470AF;
				text-decoration: none;
			}
			.simplestats-dw__view-all:hover,
			.simplestats-dw__view-all:focus { text-decoration: underline; color: #1470AF; }
			.simplestats-dw--error p {
				margin: 0;
				padding: 16px;
				color: #757575;
				font-size: 13px;
			}
CSS;

		return strtr( $css, array( '__WIDGET_ID__' => self::WIDGET_ID ) );
	}
}
