<?php
/**
 * Bounded local history of PSI runs (free tier, never leaves the site).
 *
 * Stored in a single non-autoloaded option. Only measured values are kept; a
 * metric the API did not return stays null so the trend shows a gap instead of
 * a fabricated zero.
 *
 * @package MarketingConsole
 */

namespace MarketingConsole\Admin\Free;

use MarketingConsole\Core\OptionLock;

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

/**
 * Append-only, capped log of successful PSI measurements.
 */
final class PsiHistory {

	public const OPTION_HISTORY = 'speedlyst_free_psi_history';
	public const OPTION_LOCK    = 'speedlyst_free_psi_history_lock';
	public const MAX_ENTRIES    = 60;

	/**
	 * Metric key => history key.
	 */
	private const TRACKED = array(
		'lab_performance' => 'score',
		'lab_lcp_ms'      => 'lcp_ms',
		'lab_cls'         => 'cls',
		'field_inp_ms'    => 'inp_ms',
	);

	/**
	 * Store one successful measurement. Failed runs are not recorded.
	 *
	 * @param array<string,mixed> $result Normalized PSI result.
	 * @return bool True when the entry was stored.
	 */
	public function record( array $result ): bool {
		if ( 'ok' !== ( isset( $result['status'] ) ? $result['status'] : '' ) ) {
			return false;
		}

		$metrics = isset( $result['metrics'] ) && is_array( $result['metrics'] ) ? $result['metrics'] : array();
		$entry   = array(
			'date'        => function_exists( 'wp_date' ) ? wp_date( 'Y-m-d' ) : gmdate( 'Y-m-d' ),
			'observed_at' => gmdate( 'Y-m-d\TH:i:s\Z' ),
		);

		foreach ( self::TRACKED as $metric_key => $history_key ) {
			$entry[ $history_key ] = $this->measured_value( $metrics, $metric_key );
		}

		// Stejný read-modify-write zámek jako u denní historie: dvě souběžná
		// měření by si jinak přepsala seznam načtený před cizím zápisem.
		$token = '';
		for ( $attempt = 0; $attempt < 3 && '' === $token; $attempt++ ) {
			$token = OptionLock::acquire( self::OPTION_LOCK, 5 );
			if ( '' === $token ) {
				usleep( 100000 );
			}
		}

		if ( '' === $token ) {
			return false;
		}

		try {
			$entries   = $this->entries();
			$entries[] = $entry;
			$entries   = array_slice( $entries, -self::MAX_ENTRIES );

			$stored = update_option( self::OPTION_HISTORY, $entries, false );

			return $stored || $entries === get_option( self::OPTION_HISTORY, array() );
		} finally {
			OptionLock::release( self::OPTION_LOCK, $token );
		}
	}

	/**
	 * Valid entries only; a tampered option can never break the dashboard.
	 *
	 * @return array<int,array<string,mixed>>
	 */
	public function entries(): array {
		$stored = get_option( self::OPTION_HISTORY, array() );
		$valid  = array();

		if ( ! is_array( $stored ) ) {
			return $valid;
		}

		foreach ( $stored as $entry ) {
			if ( ! is_array( $entry ) || ! isset( $entry['date'] ) || ! is_string( $entry['date'] ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/D', $entry['date'] ) ) {
				continue;
			}

			$clean = array(
				'date'        => $entry['date'],
				'observed_at' => isset( $entry['observed_at'] ) && is_string( $entry['observed_at'] ) ? $entry['observed_at'] : '',
			);

			foreach ( self::TRACKED as $history_key ) {
				$value                 = isset( $entry[ $history_key ] ) ? $entry[ $history_key ] : null;
				$clean[ $history_key ] = ( is_int( $value ) || is_float( $value ) ) && $value >= 0 ? $value + 0 : null;
			}

			$valid[] = $clean;
		}

		return array_slice( $valid, -self::MAX_ENTRIES );
	}

	/**
	 * @param array<string,mixed> $metrics Normalized metrics.
	 * @param string              $key     Metric key.
	 * @return float|null
	 */
	private function measured_value( array $metrics, string $key ): ?float {
		$field = isset( $metrics[ $key ] ) && is_array( $metrics[ $key ] ) ? $metrics[ $key ] : array();

		if ( 'ok' !== ( isset( $field['status'] ) ? $field['status'] : '' ) || ! isset( $field['value'] ) || ! ( is_int( $field['value'] ) || is_float( $field['value'] ) ) ) {
			return null;
		}

		return (float) $field['value'];
	}
}
