<?php
/**
 * Health Scorer — computes the site health score (0-100) on demand from
 * wp_conflict_radar_logs and wp_conflict_radar_events. Stateless; never persisted.
 *
 * Algorithm is specified verbatim in docs/01-FEATURES.md F3. The arithmetic lives
 * in the pure computeScore() so it can be verified without DB state.
 *
 * @package ConflictRadar\Monitor
 */

declare( strict_types=1 );

namespace ConflictRadar\Monitor;

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

class HealthScorer {

	/**
	 * The health score as an int 0-100.
	 */
	public function calculate(): int {
		return $this->calculateWithBreakdown()['score'];
	}

	/**
	 * Score plus the component counts behind it, so the dashboard widget can show
	 * "3 fatals, 2 updates" without recomputing.
	 *
	 * @return array{
	 *   score:int, fatal_count:int, warning_count:int, errors_last_7_days:int,
	 *   new_error_count:int, recent_update_count:int, clean_update_count:int
	 * }
	 */
	public function calculateWithBreakdown(): array {
		// UTC bounds matching the UTC-stored logged_at / created_at columns.
		$since_24h = gmdate( 'Y-m-d H:i:s', time() - DAY_IN_SECONDS );
		$since_7d  = gmdate( 'Y-m-d H:i:s', time() - WEEK_IN_SECONDS );

		// Last 24h.
		$fatal_count   = $this->countLogsSince( $since_24h, 'fatal' );
		$warning_count = $this->countLogsSince( $since_24h, 'warning' );

		// Last 7 days.
		$errors_last_7_days  = $this->countLogsSince( $since_7d, null );
		$recent_update_count = $this->countUpdatesSince( $since_7d );

		// v1: "new errors since recent updates" is gated on the 7-day error total.
		$new_error_count    = $errors_last_7_days;
		$clean_update_count = ( 0 === $new_error_count ) ? $recent_update_count : 0;

		$score = $this->computeScore( $fatal_count, $warning_count, $errors_last_7_days, $recent_update_count );

		return array(
			'score'               => $score,
			'fatal_count'         => $fatal_count,
			'warning_count'       => $warning_count,
			'errors_last_7_days'  => $errors_last_7_days,
			'new_error_count'     => $new_error_count,
			'recent_update_count' => $recent_update_count,
			'clean_update_count'  => $clean_update_count,
		);
	}

	/**
	 * Pure scoring formula — docs/01-FEATURES.md F3, implemented verbatim.
	 * No DB, no side effects; this is the unit under test.
	 *
	 * Sequence is load-bearing: penalties, then floor at 0, then bonuses, then
	 * cap at 100. For v1, new_error_count === errors_last_7_days and
	 * clean_update_count === recent_update_count (the bonus block only runs when
	 * there are zero new errors, so the two coincide there).
	 */
	public function computeScore( int $fatal_count, int $warning_count, int $errors_last_7_days, int $recent_update_count ): int {
		$new_error_count    = $errors_last_7_days;
		$clean_update_count = $recent_update_count;

		$score  = 100;
		$score -= min( 60, $fatal_count * 20 );    // fatals last 24h, 3+ = -60 max.
		$score -= min( 20, $warning_count * 5 );   // warnings last 24h, 4+ = -20 max.
		$score  = max( 0, $score );                // Floor at 0 BEFORE bonuses.
		if ( $recent_update_count > 0 && 0 === $new_error_count ) {
			$score += min( 20, $clean_update_count * 5 );
		}
		if ( 0 === $errors_last_7_days ) {
			$score += 20;
		}
		$score = min( 100, $score );               // Cap at 100 LAST.

		return $score;
	}

	/**
	 * Count log rows at/after $since (UTC mysql datetime). Null $error_type counts
	 * every type; otherwise restricts to one type.
	 */
	private function countLogsSince( string $since, ?string $error_type ): int {
		global $wpdb;

		if ( null === $error_type ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table (wp_conflict_radar_*); WP options API / object cache not applicable to dedicated event/reporting data.
			return (int) $wpdb->get_var(
				$wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}conflict_radar_logs WHERE logged_at >= %s", $since )
			);
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table (wp_conflict_radar_*); WP options API / object cache not applicable to dedicated event/reporting data.
		return (int) $wpdb->get_var(
			$wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}conflict_radar_logs WHERE error_type = %s AND logged_at >= %s", $error_type, $since )
		);
	}

	/**
	 * Count plugin/theme/core update events at/after $since (UTC mysql datetime).
	 */
	private function countUpdatesSince( string $since ): int {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom plugin table (wp_conflict_radar_*); WP options API / object cache not applicable to dedicated event/reporting data.
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}conflict_radar_events
				 WHERE event_type IN ( 'plugin_update', 'theme_update', 'core_update' )
				 AND created_at >= %s",
				$since
			)
		);
	}
}
