<?php /** @noinspection HttpUrlsUsage */

namespace WPDRMS\ASP\Misc;

use WP_Error;
use WPDRMS\ASP\Cache\NamespaceShard;
use WPDRMS\Utils\Functions;
use WPDRMS\Utils\Server;

if ( !defined('ABSPATH') ) {
	die('-1');
}


class PluginLicense {

	static $url = 'https://update.wp-dreams.com/';

	private const OPTION_NAME = 'asp_license_data';

	private const SIG_SYNC_THROTTLE = '_asp_license_sig_sync';

	/**
	 * Ed25519 public key (base64) used to verify the bound activation token.
	 * The matching private key lives only on the activation server.
	 *
	 * @var string
	 */
	private static string $pk = 'CHfHVKna8xvyNnLfT0xVt9PGO4Icw7IfoXJBGOQLTFo=';

	public static function activate( $license_key ) {
		$url      = rawurlencode( Server::getHTTPHost() );
		$key      = rawurlencode( $license_key );
		$env      = rawurlencode( Server::getEnvironmentType() );
		$url      = self::$url . "license/activate/$key/?url=" . $url . '&env_type=' . $env;
		$response = wp_remote_post( $url );
		if ( $response instanceof WP_Error ) {
			return false;
		}

		$data = json_decode( $response['body'], true );

		// something went wrong
		if ( empty($data) ) {
			return false;
		}

		if ( isset($data['status']) && $data['status'] == 1 ) {
			update_site_option(
				self::OPTION_NAME,
				array(
					'key'  => $license_key,
					'host' => Server::getHTTPHost(),
					'sig'  => ( isset($data['signature']) && is_string($data['signature']) ) ? $data['signature'] : '',
				)
			);
			NamespaceShard::flush();
		}

		return $data;
	}

	public static function deactivate( $remote_check = true ) {
		$data = false;
		if ( $remote_check ) {
			$key = self::isActivated();
			if ( false !== $key ) {
				$url      = rawurlencode( Server::getHTTPHost() );
				$key      = rawurlencode($key);
				$url      = self::$url . "license/deactivate/$key?url=" . $url;
				$response = wp_remote_request($url, array( 'method' =>'PATCH' ));
				if ( $response instanceof WP_Error ) {
					return false;
				}
				$data = json_decode($response['body'], true);
			}
		}
		delete_site_option(self::OPTION_NAME);
		NamespaceShard::flush();
		return $data;
	}

	/**
	 * When a record has a key + host but no bound token, asks the server to
	 * (re)issue one. Genuine installs become signed (offline-verifiable);
	 * unknown/forced keys are rejected and deactivated. Throttled and skipped
	 * on dev/demo so it never adds a per-load remote call.
	 */
	public static function syncSignature(): void {
		$data = get_site_option( self::OPTION_NAME );
		if ( ! is_array($data) || empty($data['key']) || empty($data['host']) ) {
			return;
		}
		if ( ! empty($data['sig']) ) {
			return;
		}
		if ( get_transient( self::SIG_SYNC_THROTTLE ) ) {
			return;
		}
		set_transient( self::SIG_SYNC_THROTTLE, 1, HOUR_IN_SECONDS );
		self::isActivated( true );
	}

	public static function isActivated( bool $remote_check = false ) {
		self::convertToSiteOption();
		$data = get_site_option(self::OPTION_NAME);
		if ( $data === false || !isset($data['host']) || !isset($data['key']) ) {
			return false;
		}

		if ( $remote_check ) {
			$url      = rawurlencode( Server::getHTTPHost() );
			$key      = rawurlencode( $data['key'] );
			$env      = rawurlencode( Server::getEnvironmentType() );
			$url      = self::$url . "license/is_active/$key/?url=" . $url . '&env_type=' . $env;
			$response = wp_remote_get( $url );
			if ( $response instanceof WP_Error ) {
				return false;
			}
			$rdata = json_decode( $response['body'], true );
			// Only act on a definite server answer. A malformed/empty body (transient outage,
			// proxy error) must NOT deactivate — otherwise a server hiccup would log everyone out.
			if ( ! is_array( $rdata ) ) {
				return false;
			}
			if (
				isset($rdata['message']) && (
					str_contains($rdata['message'], 'invalid format') ||
					str_contains($rdata['message'], 'refunded') ||
					str_contains($rdata['message'], 'canceled') ||
					stripos($rdata['message'], 'invalid purchase') !== false
				)
			) {
				self::deactivate( false );
				return false;
			}
			if ( $rdata['status'] ?? false ) {
				// Lazily bind the token to installs activated before it existed.
				if (
					isset($rdata['signature']) && is_string($rdata['signature']) && $rdata['signature'] !== '' &&
					( !isset($data['sig']) || $data['sig'] !== $rdata['signature'] )
				) {
					$data['sig'] = $rdata['signature'];
					update_site_option( self::OPTION_NAME, $data );
					NamespaceShard::flush();
				}
				return $data['key'];
			}
			// "Activated for a different website" is a domain mismatch (e.g. a staging clone
			// querying with the production host on file), not a deactivation. Keep the local
			// record so the user can re-activate; only gate the admin for this request.
			if ( isset($rdata['message']) && stripos( $rdata['message'], 'different website' ) !== false ) {
				return false;
			}
			// The server responded that this site is no longer active (e.g. deactivated from the
			// account). Mirror that locally so the admin re-gates. The front-end is unaffected —
			// that only locks for forged records (a separate check), not for a deactivated one.
			self::deactivate( false );
			return false;
		}

		return $data['key'];
	}

	public static function getData(): bool {
		$data = get_site_option(self::OPTION_NAME);
		if ( $data === false || !isset($data['host']) || !isset($data['key']) ) {
			return true;
		}
		if ( !Functions::uv($data['key']) ) {
			return false;
		}
		return true;
	}

	/**
	 * Stored install handle state: 0 when the handle is recognised, 1 otherwise.
	 * Consumed by the cache layer for shard selection.
	 *
	 * Scope is forged/non-signed records only — NOT the mere absence of a
	 * license. A site with no license record returns 0 here; activation is
	 * enforced separately by the gate (/check + the server timed-modal), which
	 * already exempts local/dev. A record that EXISTS must verify against its
	 * bound token (a tampered/directly-written record fails) or, lacking a token
	 * (legacy installs, re-bound on their next server round-trip), be well-formed.
	 */
	public static function fingerprint(): int {
		$data   = get_site_option(self::OPTION_NAME);
		$backup = get_site_option('_asp_la');

		// No license record at all -> not a forgery; leave enforcement to the gate.
		if ( ( ! is_array($data) || empty($data['key']) ) && ( ! is_string($backup) || $backup === '' ) ) {
			return 0;
		}

		$key = ( is_array($data) && isset($data['key']) && is_string($data['key']) ) ? $data['key'] : '';

		// A bound token is verified only when verification is actually possible.
		// If it can't be (no libsodium on the host), fall through to the format
		// check instead of penalising a legitimate, signed install.
		if (
			is_array($data) && isset($data['sig']) && is_string($data['sig']) && $data['sig'] !== '' &&
			function_exists('sodium_crypto_sign_verify_detached')
		) {
			return self::verify($key, $data['sig']) ? 0 : 1;
		}

		if ( Functions::uv($key) ) {
			return 0;
		}
		if ( is_string($backup) && Functions::uv($backup) ) {
			return 0;
		}
		return 1;
	}

	private static function verify( string $key, string $sig ): bool {
		if ( $key === '' ) {
			return false;
		}
		$raw = base64_decode($sig, true);
		$pub = base64_decode(self::$pk, true);
		if ( $raw === false || $pub === false ) {
			return false;
		}
		if ( strlen($raw) !== SODIUM_CRYPTO_SIGN_BYTES || strlen($pub) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES ) {
			return false;
		}
		try {
			return sodium_crypto_sign_verify_detached($raw, $key, $pub);
		} catch ( \Throwable $e ) {
			return false;
		}
	}

	private static function convertToSiteOption() {
		// Old option with the old key
		$data = get_option('asp_update_data');
		if ( $data !== false ) {
			update_site_option(self::OPTION_NAME, $data);
			delete_option('asp_update_data');
		}
	}
}
