<?php
/**
 * License server service for remote license server communication.
 *
 * @package RewardsWP
 */

namespace Awesomemotive\Rewardswp\Services;

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

class LicenseServerService {
	/**
	 * The license server endpoint.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const REQUEST_URL = 'https://affiliatewp.com/edd-sl-api';

	/**
	 * The request timeout limit.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const REQUEST_TIMEOUT = 35;

	/**
	 * The name of the transient used to check the license periodically.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const LICENSE_CHECK_KEY = 'rewardswp_license_check';

	/**
	 * The period of time that the license must be checked again.
	 *
	 * @since 1.0.0
	 *
	 * @var int
	 */
	const LICENSE_CHECK_TIMEOUT = DAY_IN_SECONDS;

	/**
	 * The limit in hours to re-check the license.
	 *
	 * This will only be used in case the first attempt to check the license fails.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	const LICENSE_RECHECK_TIMEOUT = 3 * HOUR_IN_SECONDS;

	/**
	 * The license statuses.
	 *
	 * @since 1.0.0
	 */
	const LICENSE_STATUSES = [ 'expired', 'invalid', 'valid' ];

	/**
	 * The error code generated by the last request to the license server.
	 *
	 * @since 1.0.0
	 *
	 * @var int|null
	 */
	private ?int $last_request_code = null;

	/**
	 * The error message generated by the last request to the license server.
	 *
	 * @since 1.0.0
	 *
	 * @var string
	 */
	private string $last_request_error = '';

	/**
	 * The license data retrieved from the last request.
	 *
	 * @since 1.0.0
	 *
	 * @var array|null
	 */
	private ?array $license_data = null;

	/**
	 * Retrieve the HTTP response code from the last license server request.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null The HTTP response code, or null if no request has been made.
	 */
	public function get_last_request_code(): ?int {
		return $this->last_request_code;
	}

	/**
	 * Retrieve the error message from the last license server request.
	 *
	 * @since 1.0.0
	 *
	 * @return string The error message, or an empty string if no error occurred.
	 */
	public function get_last_request_error(): string {
		return $this->last_request_error;
	}

	/**
	 * Retrieve the license data from the last license server request.
	 *
	 * @since 1.0.0
	 *
	 * @return array|null The license data, or null if no request has been made.
	 */
	public function get_license_data(): ?array {
		return $this->license_data;
	}

	/**
	 * Attempt to activate a product license.
	 *
	 * This method sends a request to the license server to activate a license.
	 * If successful, it caches the license status for the configured timeout period.
	 *
	 * @since 1.0.0
	 *
	 * @param string $product_name The product name.
	 * @param string $license_key  The license key.
	 *
	 * @return bool Whether the product was activated successfully.
	 */
	public function activate_product( string $product_name, string $license_key ): bool {

		$response = wp_remote_get(
			self::REQUEST_URL,
			[
				'timeout'   => self::REQUEST_TIMEOUT,
				'sslverify' => false,
				'body'      => [
					'edd_action' => 'activate_license',
					'license'    => sanitize_text_field( $license_key ),
					'item_name'  => sanitize_text_field( $product_name ),
					'url'        => home_url(),
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			$this->last_request_error = $response->get_error_message();
			return false;
		}

		// Cast to int: wp_remote_retrieve_response_code() returns '' for a malformed (non-WP_Error) response, which would fatal on the ?int property.
		$this->last_request_code = (int) wp_remote_retrieve_response_code( $response );

		// Register the errors and return false in case it fails to reach the external API.
		if ( 200 !== $this->last_request_code ) {
			$this->last_request_error = sprintf( 'HTTP %d', $this->last_request_code );
			return false;
		}

		// Decode the license data.
		$this->license_data = json_decode(
			wp_remote_retrieve_body( $response ),
			true
		);

		// Handle invalid JSON response.
		if ( ! is_array( $this->license_data ) ) {
			$this->license_data = [
				'error'   => 'invalid_response',
				'license' => 'invalid',
			];

			return false;
		}

		// Default to invalid when there is an error or the license status is not valid.
		if (
			isset( $this->license_data['error'] ) &&
			(
				! isset( $this->license_data['license'] ) ||
				! in_array( $this->license_data['license'], self::LICENSE_STATUSES, true )
			)
		) {
			$this->license_data['error']   = 'item_name_mismatch';
			$this->license_data['license'] = 'invalid';
		}

		// Set license check transient.
		if ( isset( $this->license_data['license'] ) ) {
			set_transient(
				self::LICENSE_CHECK_KEY,
				$this->license_data['license'],
				self::LICENSE_CHECK_TIMEOUT
			);
		}

		return true;
	}

	/**
	 * Attempt to deactivate a product license.
	 *
	 * This method sends a request to the license server to deactivate a license.
	 *
	 * @since 1.0.0
	 *
	 * @param string $product_name The product name.
	 * @param string $license_key  The license key.
	 *
	 * @return bool Whether the product was deactivated successfully.
	 */
	public function deactivate_product( string $product_name, string $license_key ): bool {
		$response = wp_remote_get(
			self::REQUEST_URL,
			[
				'timeout'   => self::REQUEST_TIMEOUT,
				'sslverify' => false,
				'body'      => [
					'edd_action' => 'deactivate_license',
					'license'    => sanitize_text_field( $license_key ),
					'item_name'  => sanitize_text_field( $product_name ),
					'url'        => home_url(),
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			$this->last_request_error = $response->get_error_message();
			return false;
		}

		// Cast to int: wp_remote_retrieve_response_code() returns '' for a malformed (non-WP_Error) response, which would fatal on the ?int property.
		$this->last_request_code = (int) wp_remote_retrieve_response_code( $response );

		// Make sure the response came back okay.
		if ( 200 !== $this->last_request_code ) {
			$this->last_request_error = sprintf( 'HTTP %d', $this->last_request_code );
			return false;
		}

		delete_transient( self::LICENSE_CHECK_KEY );

		return true;
	}

	/**
	 * Check the product license status.
	 *
	 * This method checks the status of a product license, using a transient to cache
	 * the result for a set period. It performs a remote request to the license server
	 * if the transient is expired, if `$force` is true, or if the current site URL
	 * does not match the license server URL.
	 *
	 * When a request fails or returns an unexpected response, the cached status is preserved
	 * to prevent accidental license deactivation.
	 *
	 * @since 1.0.0
	 *
	 * @param string $product_name The product name.
	 * @param string $license_key  The license key.
	 * @param bool   $force        Whether to force a recheck, bypassing the transient.
	 *
	 * @return string The license status ('valid', 'invalid', 'expired', or 'request-failed').
	 */
	public function check_product_status( string $product_name, string $license_key, bool $force = false ): string {
		$cached_status = get_transient( self::LICENSE_CHECK_KEY );

		// If forcing a check, clear cache first.
		if ( $force ) {
			delete_transient( self::LICENSE_CHECK_KEY );
			$cached_status = false;
		}

		// Return cached status if it exists and no force flag is set.
		if ( false !== $cached_status && ! $force ) {
			return $cached_status;
		}

		// Return early if the current site URL matches the license server URL to prevent self-checks.
		if ( site_url() === self::REQUEST_URL ) {
			return 'invalid';
		}

		// Perform a remote request to check the license status.
		$response = wp_remote_get(
			self::REQUEST_URL,
			[
				'timeout'   => self::REQUEST_TIMEOUT,
				'sslverify' => false,
				'body'      => [
					'edd_action' => 'check_license',
					'license'    => sanitize_text_field( $license_key ),
					'item_name'  => sanitize_text_field( $product_name ),
					'url'        => home_url(),
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			if ( false !== $cached_status ) {
				set_transient( self::LICENSE_CHECK_KEY, $cached_status, self::LICENSE_RECHECK_TIMEOUT );
			}
			$this->last_request_error = $response->get_error_message();
			return 'request-failed';
		}

		// Cast to int: wp_remote_retrieve_response_code() returns '' for a malformed (non-WP_Error) response, which would fatal on the ?int property.
		$this->last_request_code = (int) wp_remote_retrieve_response_code( $response );

		// Handle request errors or invalid response codes.
		if ( 200 !== $this->last_request_code ) {
			// Preserve cached status if it exists to avoid accidental deactivation.
			if ( false !== $cached_status ) {
				set_transient( self::LICENSE_CHECK_KEY, $cached_status, self::LICENSE_RECHECK_TIMEOUT );
			}
			$this->last_request_error = sprintf( 'HTTP %d', $this->last_request_code );
			return 'request-failed';
		}

		// Decode the response body and extract the license status.
		$this->license_data = json_decode(
			wp_remote_retrieve_body( $response ),
			true
		);

		// Handle invalid JSON response.
		if ( ! is_array( $this->license_data ) ) {
			if ( false !== $cached_status ) {
				set_transient( self::LICENSE_CHECK_KEY, $cached_status, self::LICENSE_RECHECK_TIMEOUT );
			}

			return 'request-failed';
		}

		$status = $this->license_data['license'] ?? '';

		// Validate that the status is a known value to prevent unexpected status changes.
		if ( ! in_array( $status, self::LICENSE_STATUSES, true ) ) {
			// Unknown status - preserve cached status and retry later.
			if ( false !== $cached_status ) {
				set_transient( self::LICENSE_CHECK_KEY, $cached_status, self::LICENSE_RECHECK_TIMEOUT );
			}

			return 'request-failed';
		}

		// Cache the valid license status with the specified timeout.
		set_transient(
			self::LICENSE_CHECK_KEY,
			$status,
			self::LICENSE_CHECK_TIMEOUT
		);

		return $status;
	}
}
