<?php
/**
 * Encryption controller.
 *
 * @since 2.5.0
 */

declare( strict_types=1 );
namespace FeData\FastEvents\Storage;

defined( 'ABSPATH' ) || exit;

/**
 * Encryption class.
 */
final class Encryption {

	private const FEVT_KEY    = 'FAST_EVENTS_KEY';
	private const FEVT_SALT   = 'FAST_EVENTS_SALT';
	private const FEVT_METHOD = 'aes-256-ctr';
	private const FEVT_PREFIX = 'fevt:';

	/**
	 * Key to use for encryption
	 *
	 * @since 2.5.0
	 * @var string
	 */
	private string $key = '';

	/**
	 * Salt to use for encryption
	 *
	 * @since 2.5.0
	 * @var string
	 */
	private string $salt = '';

	/**
	 * Flag to check if openssl is available.
	 *
	 * @since 2.5.0
	 * @var bool
	 */
	private bool $is_openssl_available;

	/**
	 * Flag to check if FEVT_KEY and FEVT_SALT are set in wp-config.php.
	 *
	 * @since 2.5.0
	 * @var bool
	 */
	private bool $is_key_salt_available;


	/**
	 * Encryption constructor.
	 *
	 * @since 2.5.0
	 */
	public function __construct() {
		$this->is_openssl_available = extension_loaded( 'openssl' );

		if ( defined( self::FEVT_KEY ) && '' !== constant( self::FEVT_KEY ) ) {
			$this->key = constant( self::FEVT_KEY );
		}

		if ( defined( self::FEVT_SALT ) && '' !== constant( self::FEVT_SALT ) ) {
			$this->salt = constant( self::FEVT_SALT );
		}

		$this->is_key_salt_available = ! empty( $this->key ) && ! empty( $this->salt );
	}


	/**
	 * Singletons should not be cloneable.
	 *
	 * @since 2.5.0
	 */
	private function __clone(): void {
	}


	/**
	 * Prevent from being unserialized.
	 *
	 * @since 2.5.0
	 */
	public function __wakeup(): void {
	}


	/**
	 * Is the openssl extension supported?
	 *
	 * @since 2.5.0
	 */
	public function has_openssl(): bool {
		return $this->is_openssl_available;
	}


	/**
	 * Are FEVT_KEY and FEVT_SALT set in wp-config.php?
	 *
	 * @since 2.5.0
	 */
	public function has_key_salt(): bool {
		return $this->is_key_salt_available;
	}


	/**
	 * Encrypt a value.
	 *
	 * @since 2.5.0
	 *
	 * @param string $value Value to encrypt.
	 *
	 * @return string
	 */
	public function encrypt( string $value ): string {
		if ( ! $this->is_openssl_available || ! $this->is_key_salt_available ) {
			return $value;
		}

		$ivlen = openssl_cipher_iv_length( self::FEVT_METHOD );
		$iv    = openssl_random_pseudo_bytes( $ivlen );

		$raw_value = openssl_encrypt( $value . $this->salt, self::FEVT_METHOD, $this->key, 0, $iv );
		if ( ! $raw_value ) {
			return $value;
		}

		return self::FEVT_PREFIX . base64_encode( $iv . $raw_value ); // phpcs:ignore
	}


	/**
	 * Decrypt a value.
	 *
	 * @since 2.5.0
	 *
	 * @param mixed $raw_value Value to decrypt.
	 *
	 * @return string
	 */
	public function decrypt( mixed $raw_value ): string {
		if ( ! $this->is_openssl_available || ! $this->is_key_salt_available || ! is_string( $raw_value ) || ! str_starts_with( $raw_value, self::FEVT_PREFIX ) ) {
			return $raw_value;
		}

		$decoded_value = base64_decode( substr( $raw_value, strlen( self::FEVT_PREFIX ) ), true ); // phpcs:ignore
		if ( false === $decoded_value ) {
			return $raw_value;
		}

		$ivlen         = openssl_cipher_iv_length( self::FEVT_METHOD );
		$iv            = substr( $decoded_value, 0, $ivlen );
		$decoded_value = substr( $decoded_value, $ivlen );

		$value = openssl_decrypt( $decoded_value, self::FEVT_METHOD, $this->key, 0, $iv );
		if ( ! $value || ! str_ends_with( $value, $this->salt ) ) {
			return $raw_value;
		}

		return substr( $value, 0, - strlen( $this->salt ) );
	}
}
