<?php
declare(strict_types=1);

namespace DMRS\MBS\Api\License;

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

/**
 * File Path: includes/api/license/class-mbs-license-ping.php
 *
 * License Ping & Validation Handler
 *
 * Performs the daily HMAC-signed validation ping against the remote API and
 * handles nonce rotation on success.
 *
 * @package MyBookShowroom
 * @subpackage API\License
 * @since 1.0.0
 */

use WP_Error;

/**
 * Class MBS_License_Ping
 */
final class MBS_License_Ping {

	private MBS_License_Credentials $credentials;

	public function __construct() {
		$this->credentials = new MBS_License_Credentials();
	}

	/**
	 * Validate current license status by pinging the remote API.
	 *
	 * Signs the request with HMAC-SHA256 using the per-activation api_secret.
	 * On success, rotates the nonce and updates last-ping timestamp.
	 * On transient failure (network error), does nothing — the grace period
	 * in mbs_is_pro() covers temporary outages.
	 *
	 * @return array|WP_Error
	 */
	public function validate(): array|WP_Error {
		if ( ! $this->credentials->is_registered() ) {
			return new WP_Error( 'no_license', __( 'No license key found.', 'my-book-showroom' ) );
		}

		$raw_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     = wp_parse_url( home_url(), PHP_URL_HOST ) ?: '';
		$timestamp  = time();

		if ( empty( $raw_key ) || empty( $api_secret ) ) {
			return new WP_Error( 'missing_credentials', __( 'Stored credentials could not be decrypted.', 'my-book-showroom' ) );
		}

		$hmac = \MBS_HMAC::sign_request( $raw_key, $domain, $timestamp, $nonce, $api_secret );

		// Build file hashes for tamper detection (mirroring original validate()).
		$tamper_detector = new \MBS_Tamper_Detector();
		if ( defined( 'MBS_VERSION' ) && MBS_VERSION !== get_option( 'mbs_manifest_version' ) ) {
			$tamper_detector->fetch_and_verify_manifest();
		}
		$file_hashes = $tamper_detector->build_file_hashes();

		$request_body = array(
			'license_key'    => $raw_key,
			'domain'         => $domain,
			'plugin_version' => defined( 'MBS_VERSION' ) ? MBS_VERSION : '1.0.0',
			'wp_version'     => get_bloginfo( 'version' ),
			'php_version'    => PHP_VERSION,
			'timestamp'      => $timestamp,
			'nonce'          => $nonce,
			'hmac'           => $hmac,
			'file_hashes'    => $file_hashes,
		);

		// SECURITY.md Rule 8: all API calls carrying credentials must use HTTPS.
		if ( ! str_starts_with( MBS_API_BASE, 'https://' ) ) {
			return new WP_Error(
				'mbs_https_required',
				'License API call aborted: MBS_API_BASE must use https:// in production.'
			);
		}

		$start    = microtime( true );
		$response = wp_remote_post( MBS_API_BASE . '/license/validate', array(
			'headers' => array( 'Content-Type' => 'application/json' ),
			'body'    => wp_json_encode( $request_body ),
			'timeout' => 20,
		) );
		MBS_License_Request_Log::log_and_return( 'validate', $request_body, $response, microtime( true ) - $start );

		if ( is_wp_error( $response ) ) {
			// Transient failure — do not clear license; grace period applies.
			\MBS_Telemetry_Failure_Tracker::record_api_failure( 'license_validate' ); // F-006
			return $response;
		}

		$http_code = wp_remote_retrieve_response_code( $response );
		$data      = json_decode( wp_remote_retrieve_body( $response ), true );

		// B-005: Non-200 responses return WP_Error. Previously the final
		// `return $data ?? array('success' => false)` would return a 4xx/5xx
		// JSON error body (e.g. {"error_code":"invalid_hmac"}) as a plain array,
		// causing callers that check $result['success'] to misread the failure.
		if ( $http_code !== 200 ) {
			$error_code = $data['error_code'] ?? 'ping_failed';
			$message    = $data['message']    ?? __( 'License validation failed.', 'my-book-showroom' );
			\MBS_Telemetry_Failure_Tracker::record_api_failure( 'license_validate' ); // F-006
			return new WP_Error( $error_code, $message );
		}

		if ( ! empty( $data['success'] ) ) {
			// F-02: authenticate the response BEFORE trusting tier / tamper_flag /
			// new_nonce. Reconstruct the canonical payload exactly as the server signed
			// it (manifest contract: canonical JSON via
			// JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE, hmac sha256) and verify the
			// signature with the per-activation api_secret. On failure: trust nothing,
			// log, and bail (the mbs_is_pro grace period covers the transient gap).
			$resp_ts           = isset( $data['response_ts'] ) ? (int) $data['response_ts'] : 0;
			$canonical_payload = array(
				'tier'          => (string) ( $data['tier'] ?? '' ),
				'tamper_flag'   => (int) ( $data['tamper_flag'] ?? 0 ),
				'new_nonce'     => (string) ( $data['new_nonce'] ?? '' ),
				'sites_used'    => (int) ( $data['sites_used'] ?? -1 ),
				'sites_allowed' => (int) ( $data['sites_allowed'] ?? -1 ),
				'response_ts'   => $resp_ts,
			);
			$canonical_json = wp_json_encode( $canonical_payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
			$expected_sig   = hash_hmac( 'sha256', $canonical_json, $api_secret );
			if ( ! \MBS_HMAC::verify_response( $expected_sig, $data['signature'] ?? null, $resp_ts ) ) {
				\MBS_Telemetry_Failure_Tracker::record_api_failure( 'license_validate_unsigned' );
				return new WP_Error(
					'invalid_response_signature',
					__( 'License response signature verification failed; ignoring untrusted response.', 'my-book-showroom' )
				);
			}

			// Rotate nonce, update stored state, and refresh site-usage counters.
			// PLG-055: pass sites_used / sites_allowed so the License tab counter
			// stays current after each daily ping.
			$new_nonce = $data['new_nonce'] ?? $nonce;
			$new_tier  = $data['tier']      ?? get_option( 'mbs_license_tier', 'free' ); // R3-authorized: ping response applier -- not a gate check.
			$this->credentials->update_after_ping(
				$new_nonce,
				$new_tier,
				isset( $data['sites_used'] )    ? (int) $data['sites_used']    : -1,
				isset( $data['sites_allowed'] ) ? (int) $data['sites_allowed'] : -1
			);

			// Apply any tamper flag the server returned.
			if ( isset( $data['tamper_flag'] ) ) {
				$tamper_detector->apply_tamper_flag( (int) $data['tamper_flag'] );
			}
		}
		// Grace period: do not clear tier or last_ping on transient failures.

		return $data ?? array( 'success' => false );
	}

	/**
	 * Schedule the daily validation cron if not already scheduled.
	 *
	 * Can be called safely on every admin_init or plugin activation — it is a
	 * no-op when the event is already queued.
	 *
	 * @return void
	 */
	public function schedule_daily_ping(): void {
		// V2-D-042: Site-wide cron -- only schedule on main site in multisite.
		if ( ! is_multisite() || is_main_site() ) {
			if ( ! wp_next_scheduled( 'mbs_daily_license_ping' ) ) {
				wp_schedule_event( time(), 'daily', 'mbs_daily_license_ping' );
			}
		}
	}
}
