<?php
/**
 * Self-contained Freemius license verifier via direct REST API call.
 *
 * Hits api.freemius.com directly to verify the plugin license, intentionally
 * bypassing the in-process Freemius SDK which may be hijacked by another
 * plugin's dev-version SDK (v999.99.99). See support sprint Group A /
 * SDK hijack defence.
 *
 * Constraints:
 * - MUST NOT import or depend on vendor/freemius/ code.
 * - MUST NOT call aipt_fs() or any Freemius singleton accessor.
 * - MUST NOT log secret_key, public_key, or Authorization header content.
 * - MUST NOT cache failures longer than NEGATIVE_TTL.
 * - MUST NOT store unencrypted result in the transient.
 *
 * @package AIPT\Core\Licensing
 * @since   2.6.0
 */

namespace AIPT\Core\Licensing;

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

/**
 * Verifies the plugin's Freemius license by querying api.freemius.com
 * directly, with AES-256-GCM encrypted transient caching of results.
 */
final class DirectApiVerifier {

	/**
	 * Transient key used to cache the verification result.
	 */
	public const CACHE_KEY = 'aipt_direct_license_verification';

	/**
	 * Transient TTL for a successfully verified result (positive or negative auth).
	 */
	public const CACHE_TTL = HOUR_IN_SECONDS;

	/**
	 * Shorter TTL used when the API returns 401 / 403 (auth-specific negative).
	 */
	public const NEGATIVE_TTL = 5 * MINUTE_IN_SECONDS;

	/**
	 * Freemius plugin ID for ai-product-tools.
	 */
	public const PLUGIN_ID = 15570;

	/**
	 * Freemius REST API base URL.
	 */
	public const API_BASE = 'https://api.freemius.com';

	/**
	 * Plugin slug as it appears in the fs_accounts option.
	 */
	public const PLUGIN_SLUG = 'ai-product-tools';

	/**
	 * Verify the Freemius license.
	 *
	 * Returns a cached result if available and $force is false.
	 * On a fresh fetch, hits the Freemius API and caches the result.
	 *
	 * @param bool $force If true, bypass the transient cache and re-fetch.
	 * @return array{
	 *     plan:string,
	 *     license_active:bool,
	 *     expiration:string|null,
	 *     verified_at:int,
	 *     source:string
	 * }|null  Returns null on any transient failure (network error, missing or
	 *         incomplete credentials, unknown plan, malformed response).
	 */
	public function verify( bool $force = false ): ?array {

		if ( ! $force ) {
			$cached = $this->readCache();
			if ( null !== $cached ) {
				return $cached;
			}
		}

		$accounts = get_option( 'fs_accounts', [] );
		if ( ! is_array( $accounts ) ) {
			return null;
		}

		$creds = $this->resolveCredentials( $accounts );
		if ( null === $creds ) {
			return null;
		}

		$path = sprintf(
			'/v1/plugins/%d/installs/%d.json',
			self::PLUGIN_ID,
			$creds['install_id']
		);

		$auth = HmacSigner::sign(
			'GET',
			$path,
			$creds['install_id'],
			$creds['public_key'],
			$creds['secret_key']
		);

		$response = wp_remote_get(
			self::API_BASE . $path,
			[
				'timeout' => 10,
				'headers' => [
					'Date'          => $auth['date'],
					'Authorization' => $auth['authorization'],
					'Accept'        => 'application/json',
				],
			]
		);

		if ( is_wp_error( $response ) ) {

			return null;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );

		if ( 401 === $code || 403 === $code ) {

			$negative = [
				'plan'           => 'free',
				'license_active' => false,
				'expiration'     => null,
				'verified_at'    => time(),
				'source'         => 'api',
			];
			$this->writeCache( $negative, self::NEGATIVE_TTL );
			return $negative;
		}

		if ( $code < 200 || $code >= 300 ) {

			return null;
		}

		$raw  = wp_remote_retrieve_body( $response );
		$body = json_decode( $raw );

		if ( null === $body || JSON_ERROR_NONE !== json_last_error() ) {
			return null;
		}

		if ( ! isset( $body->plan_id, $body->is_active ) ) {
			return null;
		}

		$plan_name = $this->resolvePlanName( (int) $body->plan_id, $accounts );
		if ( null === $plan_name ) {
			return null;
		}

		$is_active = ! empty( $body->is_active ) && empty( $body->is_uninstalled );

		$result = [
			'plan'           => $plan_name,
			'license_active' => $is_active && 'free' !== $plan_name,
			'expiration'     => $body->trial_ends ?? null,
			'verified_at'    => time(),
			'source'         => 'api',
		];

		$this->writeCache( $result, self::CACHE_TTL );

		return $result;
	}

	/**
	 * Delete the cached verification transient.
	 */
	public function clearCache(): void {
		delete_transient( self::CACHE_KEY );
	}

	/**
	 * WP AJAX handler: force re-verify and return fresh license status.
	 *
	 * Expects POST field 'nonce' verified against 'aipt_force_license_reverify'.
	 * Requires manage_options capability.
	 */
	public static function handleForceReverifyAjax(): void {
		check_ajax_referer( 'aipt_force_license_reverify', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				[ 'message' => __( 'Insufficient permissions.', 'ai-product-tools' ) ],
				403
			);
		}

		$verifier = new self();
		$verifier->clearCache();
		$result = $verifier->verify( true );

		if ( null === $result ) {
			wp_send_json_error(
				[
					'message' => __( 'License re-verification failed. The Freemius API was unreachable or local credentials are missing.', 'ai-product-tools' ),
				],
				500
			);
		}

		wp_send_json_success(
			[
				'result'         => $result,
				'plan'           => $result['plan'] ?? null,
				'license_active' => $result['license_active'] ?? false,
			]
		);
	}

	/**
	 * Read and decrypt cached verification result from transient.
	 *
	 * @return array|null Decoded result (with source overridden to 'cache'),
	 *                    or null on cache miss, decrypt failure, or shape mismatch.
	 */
	private function readCache(): ?array {
		try {
			$encrypted = get_transient( self::CACHE_KEY );
			if ( ! is_string( $encrypted ) || '' === $encrypted ) {
				return null;
			}

			$json = \AIPT\Utils\Encryption::decrypt( $encrypted );
			if ( empty( $json ) ) {
				return null;
			}

			$data = json_decode( $json, true );
			if ( ! is_array( $data ) ) {
				return null;
			}

			if ( ! isset( $data['plan'], $data['license_active'], $data['verified_at'] ) ) {
				return null;
			}

			$data['source'] = 'cache';
			return $data;

		} catch ( \Throwable $e ) {

			return null;
		}
	}

	/**
	 * Encrypt and store a result array in the transient.
	 *
	 * @param array $result The result array to cache.
	 * @param int   $ttl    Transient lifetime in seconds.
	 */
	private function writeCache( array $result, int $ttl ): void {
		$json      = wp_json_encode( $result );
		$encrypted = \AIPT\Utils\Encryption::encrypt( (string) $json );
		if ( ! empty( $encrypted ) ) {
			set_transient( self::CACHE_KEY, $encrypted, $ttl );
		}
	}

	/**
	 * Extract install credentials from the raw fs_accounts option value.
	 *
	 * Handles three shapes:
	 * (a) FS_Site object (Freemius SDK fully loaded): object with public properties.
	 * (b) stdClass (SDK not fully loaded or serialised): same property access.
	 * (c) Plain associative array (rare defensive case): array key access.
	 *
	 * @param array $accounts Raw value of the 'fs_accounts' WordPress option.
	 * @return array{install_id:int,public_key:string,secret_key:string,plan_id:int|null}|null
	 *         Null when install_id or secret_key are missing.
	 */
	private function resolveCredentials( array $accounts ): ?array {
		$sites = $accounts['sites'] ?? null;
		if ( empty( $sites ) || ! is_array( $sites ) ) {
			return null;
		}

		$site = null;
		if ( isset( $sites[ self::PLUGIN_SLUG ] ) ) {
			$site = $sites[ self::PLUGIN_SLUG ];
		} else {
			foreach ( $sites as $entry ) {
				$slug = is_object( $entry )
					? ( $entry->slug ?? null )
					: ( is_array( $entry ) ? ( $entry['slug'] ?? null ) : null );
				if ( self::PLUGIN_SLUG === $slug ) {
					$site = $entry;
					break;
				}
			}
		}

		if ( null === $site ) {
			return null;
		}

		if ( is_object( $site ) ) {
			$install_id = (int) ( $site->id ?? 0 );
			$public_key = (string) ( $site->public_key ?? '' );
			$secret_key = (string) ( $site->secret_key ?? '' );
			$plan_id    = isset( $site->plan_id ) ? (int) $site->plan_id : null;
		} elseif ( is_array( $site ) ) {
			$install_id = (int) ( $site['id'] ?? 0 );
			$public_key = (string) ( $site['public_key'] ?? '' );
			$secret_key = (string) ( $site['secret_key'] ?? '' );
			$plan_id    = isset( $site['plan_id'] ) ? (int) $site['plan_id'] : null;
		} else {
			return null;
		}

		if ( empty( $install_id ) || empty( $secret_key ) ) {
			return null;
		}

		return [
			'install_id' => $install_id,
			'public_key' => $public_key,
			'secret_key' => $secret_key,
			'plan_id'    => $plan_id,
		];
	}

	/**
	 * Resolve a plan name string from the fs_accounts plans sub-array.
	 *
	 * Accepts FS_Plugin_Plan objects, stdClass, or plain arrays.
	 *
	 * @param int   $plan_id  The plan_id returned by the Freemius API response.
	 * @param array $accounts Raw fs_accounts option value.
	 * @return string|null Lowercase plan name (e.g. 'free', 'pro', 'business'),
	 *                     or null if the plan ID is not found locally.
	 */
	private function resolvePlanName( int $plan_id, array $accounts ): ?string {
		$plans = $accounts['plans'][ self::PLUGIN_SLUG ] ?? null;
		if ( empty( $plans ) ) {
			return null;
		}

		if ( ! is_array( $plans ) ) {
			$plans = (array) $plans;
		}

		foreach ( $plans as $plan ) {
			if ( is_object( $plan ) ) {
				if ( isset( $plan->id ) && (int) $plan->id === $plan_id ) {
					return strtolower( (string) $plan->name );
				}
			} elseif ( is_array( $plan ) ) {
				if ( isset( $plan['id'] ) && (int) $plan['id'] === $plan_id ) {
					return strtolower( (string) $plan['name'] );
				}
			}
		}

		return null;
	}
}
