<?php
/**
 * BaseCloud Boost — Metrics Collector
 *
 * Tracks performance metrics (cache hits/misses, bytes saved) using
 * a lightweight JSON file — no database writes on the critical path.
 *
 * File: BCBOOST_CACHE_DIR/bcboost-stats.json
 * Thread-safety: flock() with LOCK_NB so busy traffic skips rather
 * than queues, keeping overhead near-zero on high-traffic sites.
 *
 * @package BaseCloud\Boost\Core
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Core;

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

/**
 * Collects and persists lightweight performance metrics.
 */
class Metrics_Collector {

	/** @var string Absolute path to the stats JSON file. */
	private string $stats_file;

	public function __construct() {
		$this->stats_file = BCBOOST_CACHE_DIR . 'bcboost-stats.json';
	}

	// ─── Write Operations ─────────────────────────────────────────────────

	/**
	 * Atomically increment a numeric counter.
	 * Uses non-blocking lock so concurrent requests skip rather than stall.
	 *
	 * @param string $key    Stats key (e.g. 'hits', 'html_saved').
	 * @param int    $amount Amount to add.
	 * @return void
	 */
	public function increment( string $key, int $amount = 1 ): void {
		if ( ! is_dir( BCBOOST_CACHE_DIR ) ) {
			return;
		}

		$fp = @fopen( $this->stats_file, 'c+' );
		if ( ! $fp ) {
			return;
		}

		if ( flock( $fp, LOCK_EX | LOCK_NB ) ) {
			$data         = $this->read_from_handle( $fp );
			$data[ $key ] = ( (int) ( $data[ $key ] ?? 0 ) ) + $amount;
			$this->write_to_handle( $fp, $data );
			flock( $fp, LOCK_UN );
		}

		fclose( $fp );
	}

	/**
	 * Set a stats value (blocking write).
	 *
	 * @param string $key   Stats key.
	 * @param mixed  $value Value to store.
	 * @return void
	 */
	public function set( string $key, $value ): void {
		if ( ! is_dir( BCBOOST_CACHE_DIR ) ) {
			return;
		}

		$fp = @fopen( $this->stats_file, 'c+' );
		if ( ! $fp ) {
			return;
		}

		if ( flock( $fp, LOCK_EX ) ) {
			$data         = $this->read_from_handle( $fp );
			$data[ $key ] = $value;
			$this->write_to_handle( $fp, $data );
			flock( $fp, LOCK_UN );
		}

		fclose( $fp );
	}

	/**
	 * Reset all counters, preserving activated_at timestamp.
	 *
	 * @return void
	 */
	public function reset(): void {
		$existing     = $this->get_all();
		$fresh        = $this->defaults();
		$fresh['activated_at'] = $existing['activated_at'] ?? time();
		file_put_contents( $this->stats_file, (string) wp_json_encode( $fresh ), LOCK_EX );
	}

	// ─── Read Operations ──────────────────────────────────────────────────

	/**
	 * Return all stats merged with defaults.
	 *
	 * @return array<string, mixed>
	 */
	public function get_all(): array {
		if ( ! file_exists( $this->stats_file ) ) {
			return $this->defaults();
		}

		$json = @file_get_contents( $this->stats_file );
		if ( false === $json || '' === $json ) {
			return $this->defaults();
		}

		$data = json_decode( $json, true );
		return is_array( $data ) ? array_merge( $this->defaults(), $data ) : $this->defaults();
	}

	/**
	 * Calculate the cache hit rate as a percentage (0–100).
	 *
	 * @return float
	 */
	public function get_hit_rate(): float {
		$stats = $this->get_all();
		$total = (int) $stats['hits'] + (int) $stats['misses'];

		if ( $total === 0 ) {
			return 0.0;
		}

		return round( ( (int) $stats['hits'] / $total ) * 100, 1 );
	}

	/**
	 * Total bytes saved across HTML, CSS, and JS minification.
	 *
	 * @return int
	 */
	public function get_total_saved_bytes(): int {
		$stats = $this->get_all();
		return (int) $stats['html_saved'] + (int) $stats['css_saved'] + (int) $stats['js_saved'];
	}

	// ─── Private Helpers ──────────────────────────────────────────────────

	/**
	 * Read JSON stats from an already-open file handle (after seek to 0).
	 *
	 * @param resource $fp Open file handle.
	 * @return array<string, mixed>
	 */
	private function read_from_handle( $fp ): array {
		rewind( $fp );
		$raw  = (string) fread( $fp, 65536 );
		$data = json_decode( $raw, true );
		return is_array( $data ) ? array_merge( $this->defaults(), $data ) : $this->defaults();
	}

	/**
	 * Write JSON stats to an open file handle (truncates first).
	 *
	 * @param resource             $fp   Open file handle.
	 * @param array<string, mixed> $data Data to write.
	 * @return void
	 */
	private function write_to_handle( $fp, array $data ): void {
		ftruncate( $fp, 0 );
		rewind( $fp );
		fwrite( $fp, (string) wp_json_encode( $data ) );
	}

	/**
	 * Return the default stats schema.
	 *
	 * @return array<string, mixed>
	 */
	private function defaults(): array {
		return [
			'hits'                   => 0,
			'misses'                 => 0,
			'html_saved'             => 0,
			'css_saved'              => 0,
			'js_saved'               => 0,
			'last_purge'             => null,
			'last_webhook_sent'      => null,
			'webhook_errors'         => 0,
			'last_lighthouse_sent'   => null,
			'lighthouse_errors'      => 0,
			'activated_at'           => time(),
		];
	}
}
