<?php
/**
 * User engagement / health scorer.
 *
 * Scores a user 0–100 from generic signals that work on ANY WordPress site:
 *   - Login recency (last login within 60 days)
 *   - Content activity (posts authored + approved comments, last 90 days)
 *   - Tenure (account age)
 *   - Subscription health (only when MemberPress is present — otherwise the
 *     remaining factors are re-normalised, so the score stays meaningful).
 *
 * Designed to be cheap at scale: activity is fetched in a handful of grouped
 * queries via prime(), then every user is scored in PHP — no per-user SQL.
 *
 * @package Platform\Helm\Modules\Member_Health
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Member_Health;

use WP_User;

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

final class HealthScorer {

	/** Meta key where the last successful login timestamp is stored. */
	public const LAST_LOGIN_META = '_platform_helm_last_login';

	private const W_LOGIN        = 40.0;
	private const W_ACTIVITY     = 30.0;
	private const W_TENURE       = 15.0;
	private const W_SUBSCRIPTION = 15.0;

	private const LOGIN_WINDOW_DAYS = 60;
	private const ACTIVITY_DAYS     = 90;
	private const ACTIVITY_TARGET   = 8.0;
	private const TENURE_TARGET     = 180.0;

	/** @var array<int,int>|null author id => published post count */
	private ?array $posts = null;
	/** @var array<int,int>|null user id => approved comment count */
	private ?array $comments = null;
	/** @var array<int,int> user id => last-login timestamp */
	private array $logins = [];
	/** @var array<int,string>|null user id => best MemberPress status */
	private ?array $subs = null;
	private bool $has_memberpress = false;

	/**
	 * Pre-load activity once (grouped queries). Call before scoring a batch.
	 */
	public function prime(): void {
		global $wpdb;
		$post_cut = gmdate( 'Y-m-d H:i:s', time() - self::ACTIVITY_DAYS * DAY_IN_SECONDS );
		$com_cut  = gmdate( 'Y-m-d H:i:s', time() - self::ACTIVITY_DAYS * DAY_IN_SECONDS );

		$this->posts = [];
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		foreach ( (array) $wpdb->get_results( $wpdb->prepare( "SELECT post_author AS u, COUNT(*) AS c FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_date >= %s GROUP BY post_author", $post_cut ), ARRAY_A ) as $r ) {
			$this->posts[ (int) $r['u'] ] = (int) $r['c'];
		}

		$this->comments = [];
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		foreach ( (array) $wpdb->get_results( $wpdb->prepare( "SELECT user_id AS u, COUNT(*) AS c FROM {$wpdb->comments} WHERE comment_approved = '1' AND user_id > 0 AND comment_date >= %s GROUP BY user_id", $com_cut ), ARRAY_A ) as $r ) {
			$this->comments[ (int) $r['u'] ] = (int) $r['c'];
		}

		// Last-login timestamps in bulk (avoids a meta query per user).
		$this->logins = [];
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		foreach ( (array) $wpdb->get_results( $wpdb->prepare( "SELECT user_id AS u, meta_value AS v FROM {$wpdb->usermeta} WHERE meta_key = %s", self::LAST_LOGIN_META ), ARRAY_A ) as $r ) {
			$this->logins[ (int) $r['u'] ] = (int) $r['v'];
		}

		// MemberPress subscription statuses, only if its table exists.
		$mp = $wpdb->prefix . 'mepr_subscriptions';
		$this->subs            = [];
		$this->has_memberpress = false;
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $mp ) ) === $mp ) {
			$this->has_memberpress = true;
			$priority              = [ 'active' => 5, 'trialing' => 4, 'pending' => 3, 'suspended' => 2, 'cancelled' => 1 ];
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			foreach ( (array) $wpdb->get_results( "SELECT user_id AS u, status AS s FROM `{$mp}`", ARRAY_A ) as $r ) {
				$uid = (int) $r['u'];
				$s   = (string) $r['s'];
				if ( ! isset( $this->subs[ $uid ] ) || ( $priority[ $s ] ?? 0 ) > ( $priority[ $this->subs[ $uid ] ] ?? 0 ) ) {
					$this->subs[ $uid ] = $s;
				}
			}
		}
	}

	/**
	 * @return array{score:int,risk:string,last_login:int,factors:array<string,float>}
	 */
	public function score( WP_User $user ): array {
		if ( null === $this->posts ) {
			$this->prime();
		}
		$uid = (int) $user->ID;

		// Last login (tracked on wp_login); fall back to registration date.
		$login_ts = (int) ( $this->logins[ $uid ] ?? 0 );
		if ( $login_ts <= 0 ) {
			$reg      = strtotime( (string) $user->user_registered );
			$login_ts = false === $reg ? 0 : (int) $reg;
		}

		$factors = [];
		$weights = [];

		// Login recency.
		$days                 = $login_ts > 0 ? max( 0.0, ( time() - $login_ts ) / DAY_IN_SECONDS ) : self::LOGIN_WINDOW_DAYS;
		$factors['login']     = $days >= self::LOGIN_WINDOW_DAYS ? 0.0 : 1.0 - ( $days / self::LOGIN_WINDOW_DAYS );
		$weights['login']     = self::W_LOGIN;

		// Content activity.
		$acts                 = (int) ( ( $this->posts[ $uid ] ?? 0 ) + ( $this->comments[ $uid ] ?? 0 ) );
		$factors['activity']  = min( 1.0, $acts / self::ACTIVITY_TARGET );
		$weights['activity']  = self::W_ACTIVITY;

		// Tenure.
		$reg                  = strtotime( (string) $user->user_registered );
		$tdays                = false === $reg ? 0.0 : max( 0.0, ( time() - $reg ) / DAY_IN_SECONDS );
		$factors['tenure']    = min( 1.0, $tdays / self::TENURE_TARGET );
		$weights['tenure']    = self::W_TENURE;

		// Subscription — only counts when MemberPress is present.
		if ( $this->has_memberpress ) {
			$status                  = $this->subs[ $uid ] ?? '';
			$factors['subscription'] = match ( $status ) {
				'active'    => 1.0,
				'trialing'  => 0.7,
				'pending'   => 0.5,
				'suspended' => 0.3,
				'cancelled' => 0.2,
				default     => 0.0,
			};
			$weights['subscription'] = self::W_SUBSCRIPTION;
		}

		// Weighted score, normalised over the factors actually in play.
		$sum_w = array_sum( $weights );
		$score = 0.0;
		foreach ( $factors as $k => $v ) {
			$score += $v * $weights[ $k ];
		}
		$value = $sum_w > 0 ? (int) round( ( $score / $sum_w ) * 100 ) : 0;

		return [
			'score'      => max( 0, min( 100, $value ) ),
			'risk'       => self::risk_for( $value ),
			'last_login' => $login_ts,
			'factors'    => $factors,
		];
	}

	public static function risk_for( int $score ): string {
		return match ( true ) {
			$score >= 75 => 'healthy',
			$score >= 50 => 'steady',
			$score >= 30 => 'at_risk',
			default      => 'critical',
		};
	}

	/** @return array{0:string,1:string} [label, badge-tone] */
	public static function risk_meta( string $risk ): array {
		return match ( $risk ) {
			'healthy'  => [ __( 'Healthy', 'platform-helm' ), 'success' ],
			'steady'   => [ __( 'Steady', 'platform-helm' ), 'info' ],
			'at_risk'  => [ __( 'At risk', 'platform-helm' ), 'warning' ],
			default    => [ __( 'Critical', 'platform-helm' ), 'danger' ],
		};
	}
}
