<?php
/**
 * File Path: includes/api/class-mbs-telemetry-client.php
 *
 * Telemetry Client — My Book Showroom
 *
 * Sends an anonymous usage ping to the MBS API server.
 * Called from the `mbs_telemetry_weekly_ping` WP Cron action.
 *
 * Payload is strictly limited to the server's allowlisted fields:
 *   wp_version     — WordPress version string
 *   php_version    — PHP version string
 *   mbs_version    — MBS plugin version constant
 *   active_layouts — array of layout slugs currently in use
 *   feature_flags  — boolean map of enabled feature toggles
 *   book_count     — count of published mbs_book posts (integer)
 *
 * SECURITY:
 *   - Never include: book titles, author names, cover URLs, ISBNs, ASINs,
 *     purchase links, license keys, api_secret, or user-identifying data.
 *   - All credentials are retrieved from encrypted storage and never logged raw.
 *   - `send_ping()` bails silently when credentials are unavailable (no active
 *     license, empty api_secret, etc.) — no error is shown to the user.
 *
 * Cron event name: `mbs_telemetry_weekly_ping`  ← CANONICAL — never vary this name.
 * The name must match exactly in both this file and class-mbs-telemetry.php.
 *
 * `require_once` in class-mbs-plugin.php (alongside MBS_Telemetry):
 *   require_once MBS_INCLUDES_DIR . 'api/class-mbs-telemetry-client.php';
 *
 * Story 14-9 — Sprint 14.
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Api
 * @since      1.0.0
 */

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

class MBS_Telemetry_Client { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/** Canonical cron event name — must match class-mbs-telemetry.php. */
	const CRON_HOOK = 'mbs_telemetry_weekly_ping';

	/** Allowlisted payload keys accepted by the server. */
	const ALLOWED_PAYLOAD_KEYS = array(
		'wp_version',
		'php_version',
		'mbs_version',
		'active_layouts',
		'feature_flags',
		'book_count',
		'request_failures', // F-006
		'import_failures',  // F-006
	);

	// ── Public cron callback ──────────────────────────────────────────────────

	/**
	 * Send an anonymous telemetry ping to the MBS API.
	 *
	 * This is the static callback bound to `mbs_telemetry_weekly_ping` by
	 * MBS_Telemetry::schedule_cron(). It is a pure static method with no
	 * dependency on instantiation — safe to call from cron in any context.
	 *
	 * @return void  Silent on success. Logs errors via MBS_License_Request_Log.
	 */
	public static function send_ping(): void {
		// V7-E-001/EXP-054: Positive consent guard — only send if explicitly opted in.
		// Uses the same option key as MBS_Telemetry::CONSENT_KEY ('mbs_telemetry_consent').
		if ( get_option( 'mbs_telemetry_consent' ) !== 'opted_in' ) {  // V7-E-001: Positive consent check — bail unless explicitly opted in.
			return;
		}

		// ── Retrieve credentials from encrypted storage ───────────────────────
		$license_key = MBS_Encryption::decrypt( get_option( 'mbs_license_key_encrypted', '' ) ); // R1-authorized: license credential machinery -- value not logged.
		$api_secret  = MBS_Encryption::decrypt( get_option( 'mbs_api_secret_encrypted', '' ) ); // R1-authorized: license credential machinery -- value not logged.
		$nonce       = get_option( 'mbs_api_nonce', '' );
		$domain      = (string) wp_parse_url( home_url(), PHP_URL_HOST );

		// Bail silently if credentials are unavailable (no active license).
		if ( '' === $license_key || '' === $api_secret || '' === $nonce || '' === $domain ) {
			return;
		}

		// ── Build allowlisted payload ─────────────────────────────────────────
		$timestamp = time();

		$payload = array(
			// Required for server-side license lookup — stripped from
			// log at the unset() call below per SECURITY.md Rule S14-1.
			'license_key'    => $license_key,    // Required for server-side lookup.
			'domain'         => $domain,
			'timestamp'      => $timestamp,
			'nonce'          => $nonce,
			'wp_version'     => get_bloginfo( 'version' ),
			'php_version'    => PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION,
			'mbs_version'    => MBS_VERSION,
			'active_layouts' => self::get_active_layouts(),
			'feature_flags'  => self::get_feature_flags(),
			'book_count'     => self::get_book_count(),
		);

		// F-006: Snapshot failure counts (non-destructive). Flush deferred until 200 OK.
		$failure_counts              = MBS_Telemetry_Failure_Tracker::get_counts();
		$payload['request_failures'] = $failure_counts['api'];
		$payload['import_failures']  = $failure_counts['import'];

		// ── Sign with HMAC ────────────────────────────────────────────────────
		$payload['signature'] = MBS_HMAC::sign_request(
			$license_key,
			$domain,
			$timestamp,
			$nonce,
			$api_secret
		);

		// ── POST to server ────────────────────────────────────────────────────
		$start    = microtime( true );
		$response = wp_remote_post(
			MBS_API_BASE . '/telemetry/ping',
			array(
				'body'    => $payload,
				'timeout' => 15,
			)
		);
		$elapsed = microtime( true ) - $start;

		// ── Log (strips sensitive fields before persistence) ──────────────────
		// Remove credentials from the logged request body — never log raw keys.
		$log_body = $payload;
		unset( $log_body['license_key'], $log_body['signature'] );

		\DMRS\MBS\Api\License\MBS_License_Request_Log::log_and_return(
			'telemetry_ping',
			$log_body,
			$response,
			$elapsed
		);

		// F-006: Flush failure counts on confirmed success; record ping failure otherwise.
		if ( ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200 ) {
			MBS_Telemetry_Failure_Tracker::flush_and_reset();
		} else {
			MBS_Telemetry_Failure_Tracker::record_api_failure( 'telemetry_ping' );
		}
	}

	// ── Private payload helpers ───────────────────────────────────────────────

	/**
	 * Return an array of layout slugs currently active in the plugin settings.
	 *
	 * Includes the configured default layout plus any layouts referenced in
	 * registered shortcodes' mbs_setting_default_layout option.
	 * No book-level data (titles, ISBNs, etc.) is ever included.
	 *
	 * @return array<string>
	 */
	private static function get_active_layouts(): array {
		$layouts = array();

		// Default layout from Formatting settings.
		$default = get_option( 'mbs_setting_formatting_default_layout', 'grid' );
		if ( is_string( $default ) && '' !== $default ) {
			$layouts[] = sanitize_key( $default );
		}

		// De-duplicate and return sorted for consistent server-side comparison.
		$layouts = array_unique( $layouts );
		sort( $layouts );

		return array_values( $layouts );
	}

	/**
	 * Return a boolean map of enabled feature toggles.
	 *
	 * Only plugin-level option toggles are included — never per-book data.
	 *
	 * @return array<string, bool>
	 */
	private static function get_feature_flags(): array {
		return array(
			'universe_enabled'  => (bool) get_option( 'mbs_setting_advanced_universe_enabled', 0 ),
			'telemetry_opted_in' => true, // Always true when this method is reached.
		);
	}

	/**
	 * Return the count of published mbs_book posts.
	 *
	 * @return int
	 */
	private static function get_book_count(): int {
		$counts = wp_count_posts( MBS_POST_TYPE );
		return isset( $counts->publish ) ? (int) $counts->publish : 0;
	}
}
