<?php
/**
 * Anomaly Detector.
 *
 * Reads Platform Helm's Activity Log and, per action type, compares the most
 * recent COMPLETE day (yesterday, local time) against the preceding 30-day
 * rolling baseline (mean + standard deviation). Anything beyond 2σ is flagged
 * as a spike or drop; beyond 3σ is "critical".
 *
 * No second event store — it analyses the existing activity table directly,
 * and degrades to an empty result if that table doesn't exist yet.
 *
 * @package Platform\Helm\Modules\Activity_Intelligence
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Activity_Intelligence;

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

final class AnomalyDetector {

	private const BASELINE_DAYS = 30;
	private const MIN_BASELINE  = 7;
	private const WARNING_Z     = 2.0;
	private const CRITICAL_Z    = 3.0;
	private const MIN_STDDEV    = 0.5;

	private function table(): string {
		global $wpdb;
		return $wpdb->prefix . 'platform_helm_activity';
	}

	/**
	 * Detect anomalies on yesterday's activity vs the 30 days before it.
	 *
	 * @return array<int,array<string,mixed>> Sorted by |Z| descending.
	 */
	public function detect(): array {
		global $wpdb;
		$table = $this->table();

		// Bail quietly if the Activity Log table hasn't been created.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
			return [];
		}

		// Anchor on the site's local "today", then analyse yesterday.
		$today       = substr( (string) current_time( 'mysql' ), 0, 10 );
		$current_day = gmdate( 'Y-m-d', (int) strtotime( $today . ' -1 day' ) );
		$window_from = gmdate( 'Y-m-d 00:00:00', (int) strtotime( $current_day . ' -' . self::BASELINE_DAYS . ' days' ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT DATE(logged_at) AS d, action AS a, COUNT(*) AS c
				 FROM {$table} WHERE logged_at >= %s AND logged_at < %s
				 GROUP BY DATE(logged_at), action", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$window_from,
				$current_day . ' 23:59:59'
			),
			ARRAY_A
		);
		if ( empty( $rows ) ) {
			return [];
		}

		// series[action][Y-m-d] = count
		$series = [];
		foreach ( $rows as $r ) {
			$series[ (string) $r['a'] ][ (string) $r['d'] ] = (int) $r['c'];
		}

		$anomalies = [];
		foreach ( $series as $action => $days ) {
			$found = $this->check_action( (string) $action, $days, $current_day );
			if ( null !== $found ) {
				$anomalies[] = $found;
			}
		}

		usort( $anomalies, static fn( $x, $y ) => abs( (float) $y['z'] ) <=> abs( (float) $x['z'] ) );
		return $anomalies;
	}

	/**
	 * @param array<string,int> $days Y-m-d => count for one action.
	 * @return array<string,mixed>|null
	 */
	private function check_action( string $action, array $days, string $current_day ): ?array {
		$count = (int) ( $days[ $current_day ] ?? 0 );

		// Baseline = the 30 calendar days before current_day, missing days = 0.
		$baseline = [];
		for ( $i = 1; $i <= self::BASELINE_DAYS; $i++ ) {
			$d          = gmdate( 'Y-m-d', (int) strtotime( $current_day . ' -' . $i . ' days' ) );
			$baseline[] = (int) ( $days[ $d ] ?? 0 );
		}
		if ( count( $baseline ) < self::MIN_BASELINE ) {
			return null;
		}

		$n    = count( $baseline );
		$mean = array_sum( $baseline ) / $n;
		$var  = 0.0;
		foreach ( $baseline as $v ) {
			$var += ( $v - $mean ) ** 2;
		}
		$stddev = sqrt( $var / $n );

		// A nearly-flat metric would flag on any blip — require real variance.
		if ( $stddev < self::MIN_STDDEV ) {
			return null;
		}

		$z = ( $count - $mean ) / $stddev;
		if ( abs( $z ) < self::WARNING_Z ) {
			return null;
		}

		return [
			'action'    => $action,
			'label'     => $this->action_label( $action ),
			'severity'  => abs( $z ) >= self::CRITICAL_Z ? 'critical' : 'warning',
			'direction' => $z > 0 ? 'spike' : 'drop',
			'day'       => $current_day,
			'count'     => $count,
			'mean'      => round( $mean, 1 ),
			'stddev'    => round( $stddev, 2 ),
			'z'         => round( $z, 1 ),
		];
	}

	private function action_label( string $action ): string {
		$map = [
			'login'              => __( 'Logins', 'platform-helm' ),
			'logout'             => __( 'Logouts', 'platform-helm' ),
			'post_published'     => __( 'Posts published', 'platform-helm' ),
			'post_updated'       => __( 'Posts updated', 'platform-helm' ),
			'post_trashed'       => __( 'Posts trashed', 'platform-helm' ),
			'post_deleted'       => __( 'Posts deleted', 'platform-helm' ),
			'role_changed'       => __( 'Role changes', 'platform-helm' ),
			'user_registered'    => __( 'Registrations', 'platform-helm' ),
			'link_inserted'      => __( 'Links applied', 'platform-helm' ),
			'link_restored'      => __( 'Links restored', 'platform-helm' ),
			'settings_changed'   => __( 'Settings changes', 'platform-helm' ),
			'plugin_activated'   => __( 'Plugin activations', 'platform-helm' ),
			'plugin_deactivated' => __( 'Plugin deactivations', 'platform-helm' ),
		];
		return $map[ $action ] ?? ucfirst( str_replace( '_', ' ', $action ) );
	}
}
