<?php
/**
 * Proxy to the SaaS app's report endpoints.
 */

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

class SimpleStats_Report {

	private static $instance;

	/**
	 * How long a failed realtime lookup is remembered, so an unreachable app
	 * does not cost a blocking request on every page load. An empty array is
	 * stored as the sentinel — get_realtime() maps it back to false.
	 */
	const FAILURE_CACHE_SECONDS = 60;

	/**
	 * How long a dashboard report is cached.
	 */
	const REPORT_CACHE_SECONDS = HOUR_IN_SECONDS;

	/**
	 * How long a realtime lookup is cached.
	 */
	const REALTIME_CACHE_SECONDS = 30;

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

	/**
	 * Transient key for a dashboard report range.
	 *
	 * Every consumer of the report cache — the SPA's AJAX handler and the WP
	 * dashboard widget — must build its key here so the two genuinely share one
	 * cache entry instead of drifting apart.
	 *
	 * @param string $from YYYY-MM-DD.
	 * @param string $to   YYYY-MM-DD.
	 * @return string
	 */
	public static function report_cache_key( $from, $to ) {
		return "simplestats_report_{$from}_{$to}";
	}

	/**
	 * Read a cached dashboard report without ever contacting the app.
	 *
	 * Used on page-render paths (the dashboard widget) so a cold cache costs a
	 * loading state rather than a blocking HTTP round trip.
	 *
	 * @param string $from YYYY-MM-DD.
	 * @param string $to   YYYY-MM-DD.
	 * @return array|false Report, or false when nothing is cached.
	 */
	public function get_cached_dashboard_report_only( $from, $to ) {
		$cached = get_transient( self::report_cache_key( $from, $to ) );

		return is_array( $cached ) ? $cached : false;
	}

	/**
	 * Cache-aware dashboard report: serve the transient when warm, otherwise
	 * fetch and (conditionally) store it.
	 *
	 * A report with no visitors and no pageviews is deliberately NOT cached. A
	 * freshly connected site that has not recorded a visit yet would otherwise
	 * be pinned to "no data" for a full hour; we want the next dashboard load
	 * to ask again so the first pageview surfaces immediately. Both the AJAX
	 * handler and the dashboard widget go through here, so neither can write an
	 * empty report into the cache the other reads.
	 *
	 * @param string $from YYYY-MM-DD.
	 * @param string $to   YYYY-MM-DD.
	 * @return array|false Report, or false when the app could not be reached.
	 */
	public function get_cached_dashboard_report( $from, $to ) {
		$cached = $this->get_cached_dashboard_report_only( $from, $to );

		if ( false !== $cached ) {
			return $cached;
		}

		$data = $this->get_dashboard_report( $from, $to );

		if ( false === $data || ! is_array( $data ) ) {
			return false;
		}

		if ( self::report_has_activity( $data ) ) {
			set_transient( self::report_cache_key( $from, $to ), $data, self::REPORT_CACHE_SECONDS );
		}

		return $data;
	}

	/**
	 * Whether a report recorded any activity at all.
	 *
	 * @param array $data Report payload.
	 * @return bool
	 */
	private static function report_has_activity( $data ) {
		$visitors  = (int) ( $data['kpis']['visitors']['value'] ?? 0 );
		$pageviews = (int) ( $data['kpis']['pageviews']['value'] ?? 0 );

		return $visitors > 0 || $pageviews > 0;
	}

	/**
	 * POST to /wp-connector/report-dashboard with a date range.
	 *
	 * @param string $from YYYY-MM-DD
	 * @param string $to   YYYY-MM-DD
	 * @return array|false
	 */
	public function get_dashboard_report( $from, $to ) {
		$connection = simplestats_get_connection();
		if ( empty( $connection['api_key'] ) || empty( $connection['pixel_key'] ) ) {
			return false;
		}

		$response = wp_remote_post( simplestats_get_app_url() . '/wp-connector/report-dashboard', array(
			// Generous, but this only ever runs from an explicit admin AJAX
			// request where the user is already watching a loading state — it is
			// never in a page-render path.
			// phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
			'timeout' => 15,
			'headers' => array(
				'Authorization' => 'Bearer ' . $connection['api_key'],
			),
			'body'    => array(
				'pixel_key' => $connection['pixel_key'],
				'from'      => $from,
				'to'        => $to,
			),
		) );

		if ( is_wp_error( $response ) ) {
			return false;
		}

		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );
		return isset( $decoded['data'] ) ? $decoded['data'] : false;
	}

	/**
	 * Transient key for the realtime lookup.
	 *
	 * @param array $connection Connection record.
	 * @return string
	 */
	private static function realtime_cache_key( $connection ) {
		return 'simplestats_report_realtime_' . md5( $connection['pixel_key'] );
	}

	/**
	 * Read the cached realtime payload without ever contacting the app.
	 *
	 * The admin-bar indicator renders on every admin AND front-end page load, so
	 * it uses this and hydrates over AJAX when the cache is cold — a page render
	 * must never wait on a third-party HTTP request.
	 *
	 * @return array|false Payload, or false when nothing usable is cached.
	 */
	public function get_realtime_cached() {
		$connection = simplestats_get_connection();
		if ( empty( $connection['api_key'] ) || empty( $connection['pixel_key'] ) ) {
			return false;
		}

		$cached = get_transient( self::realtime_cache_key( $connection ) );

		// Empty array is the "we tried and it failed" sentinel.
		return ( is_array( $cached ) && ! empty( $cached ) ) ? $cached : false;
	}

	/**
	 * POST to /wp-connector/report-realtime. Cached as
	 * `simplestats_report_realtime_<hash>` for 30 seconds. Disconnect busts every
	 * transient with the `simplestats_report_` prefix.
	 *
	 * Only ever called from an AJAX request the user is already waiting on —
	 * never from a page-render path; see get_realtime_cached().
	 *
	 * @return array|false
	 */
	public function get_realtime() {
		$connection = simplestats_get_connection();
		if ( empty( $connection['api_key'] ) || empty( $connection['pixel_key'] ) ) {
			return false;
		}

		$cache_key = self::realtime_cache_key( $connection );
		$cached    = get_transient( $cache_key );
		if ( false !== $cached ) {
			// Empty array is the "we tried and it failed" sentinel.
			return empty( $cached ) ? false : $cached;
		}

		$response = wp_remote_post( simplestats_get_app_url() . '/wp-connector/report-realtime', array(
			// Deliberately short. Small payload, so 3s is ample; failures are
			// negative-cached above so an unreachable app cannot turn every
			// indicator refresh into a slow request.
			'timeout' => 3,
			'headers' => array(
				'Authorization' => 'Bearer ' . $connection['api_key'],
			),
			'body'    => array(
				'pixel_key' => $connection['pixel_key'],
			),
		) );

		if ( is_wp_error( $response ) ) {
			// Cache the failure briefly so a down app cannot turn the indicator's
			// refresh into a slow request on every single page load while the
			// admin keeps browsing.
			set_transient( $cache_key, array(), self::FAILURE_CACHE_SECONDS );
			return false;
		}

		$decoded = json_decode( wp_remote_retrieve_body( $response ), true );
		$data    = isset( $decoded['data'] ) ? $decoded['data'] : false;

		if ( false === $data ) {
			set_transient( $cache_key, array(), self::FAILURE_CACHE_SECONDS );
			return false;
		}

		set_transient( $cache_key, $data, self::REALTIME_CACHE_SECONDS );

		return $data;
	}
}
