<?php

namespace DigitalApps\PaymentsForAdyen;

defined('ABSPATH') || exit;

/**
 * Encrypts sensitive settings while preserving the plugin's existing option API.
 */
final class Settings_Store
{
	const OPTION_NAME = 'digitalapps_payments_for_adyen_settings';
	const STORAGE_VERSION_OPTION = 'digitalapps_payments_for_adyen_settings_storage_version';
	const STORAGE_VERSION = 2;
	const PREFIX = 'dapa1:';
	const CIPHER = 'aes-256-gcm';

	/** @var string[] */
	private static $secret_keys = [
		'apiKey',
		'hmacKey',
		'previousHmacKey',
		'testApiKey',
		'testHmacKey',
		'testPreviousHmacKey',
		'liveApiKey',
		'liveHmacKey',
		'livePreviousHmacKey',
		'webhookPassword',
		'webhookToken',
	];

	/** Environment-specific setting aliases retained for shared Free/Pro consumers. */
	private static $environment_keys = [
		'merchantAccount' => 'MerchantAccount',
		'apiKey' => 'ApiKey',
		'clientKey' => 'ClientKey',
		'hmacKey' => 'HmacKey',
		'previousHmacKey' => 'PreviousHmacKey',
		'liveUrlPrefix' => 'UrlPrefix',
	];

	/**
	 * Register filters before any settings consumers run.
	 */
	public static function register_hooks()
	{
		add_filter('option_' . self::OPTION_NAME, [__CLASS__, 'decrypt_option']);
		add_filter('pre_update_option_' . self::OPTION_NAME, [__CLASS__, 'encrypt_option'], 10, 3);
	}

	/**
	 * Return decrypted settings.
	 *
	 * @return array
	 */
	public static function get()
	{
		$settings = get_option(self::OPTION_NAME, []);
		return is_array($settings) ? $settings : [];
	}

	/**
	 * Return the selected environment profile with legacy aliases.
	 *
	 * @param bool|null $test_mode True for test, false for live, null for selected.
	 * @param array|null $settings Optional decrypted settings.
	 * @return array
	 */
	public static function get_environment($test_mode = null, $settings = null)
	{
		$settings = is_array($settings) ? $settings : self::get();
		if (null === $test_mode) {
			$test_mode = !empty($settings['testMode']);
		}

		$prefix = $test_mode ? 'test' : 'live';
		$profile = ['testMode' => (bool) $test_mode];
		foreach (self::$environment_keys as $alias => $suffix) {
			$profile[$alias] = $settings[$prefix . $suffix] ?? '';
		}

		return $profile;
	}

	/**
	 * Merge values into one environment without changing the other profile.
	 *
	 * @param array $settings Decrypted settings.
	 * @param bool  $test_mode Environment selector.
	 * @param array $values Environment values using legacy aliases.
	 * @return array
	 */
	public static function set_environment(array $settings, $test_mode, array $values)
	{
		$prefix = $test_mode ? 'test' : 'live';
		foreach (self::$environment_keys as $alias => $suffix) {
			if (array_key_exists($alias, $values)) {
				$settings[$prefix . $suffix] = $values[$alias];
			}
		}

		return self::apply_active_aliases($settings);
	}

	/**
	 * Persist settings without autoloading them.
	 *
	 * @param array $settings Settings containing plaintext values at runtime.
	 * @return bool
	 */
	public static function save(array $settings)
	{
		$settings = self::apply_active_aliases($settings);
		$updated = update_option(self::OPTION_NAME, $settings, false);
		self::disable_autoload(self::OPTION_NAME);
		return $updated;
	}

	/**
	 * Encrypt legacy plaintext settings after an upgrade.
	 */
	public static function maybe_migrate()
	{
		$version = (int) get_option(self::STORAGE_VERSION_OPTION, 0);
		if ($version >= self::STORAGE_VERSION) {
			return;
		}

		$settings = self::get();
		if ($version < 2 && !empty($settings)) {
			$test_mode = !empty($settings['testMode']);
			$profile = [];
			foreach (array_keys(self::$environment_keys) as $alias) {
				if (array_key_exists($alias, $settings)) {
					$profile[$alias] = $settings[$alias];
				}
			}
			$settings = self::set_environment($settings, $test_mode, $profile);
		}
		if (!empty($settings)) {
			self::save($settings);
		} else {
			self::disable_autoload(self::OPTION_NAME);
		}

		update_option(self::STORAGE_VERSION_OPTION, self::STORAGE_VERSION, false);
		self::disable_autoload(self::STORAGE_VERSION_OPTION);
	}

	/**
	 * Encrypt sensitive values before WordPress writes the option.
	 *
	 * @param mixed  $value New option value.
	 * @param mixed  $old_value Previous option value.
	 * @param string $option Option name.
	 * @return mixed
	 */
	public static function encrypt_option($value, $old_value = null, $option = '')
	{
		unset($old_value, $option);
		if (!is_array($value)) {
			return $value;
		}

		$value = self::initialize_environment_profile($value);
		$value = self::apply_active_aliases($value);

		foreach (self::$secret_keys as $key) {
			if (isset($value[$key]) && is_string($value[$key]) && '' !== $value[$key]) {
				$value[$key] = self::encrypt($value[$key]);
			}
		}

		return $value;
	}

	/**
	 * Decrypt sensitive values after WordPress reads the option.
	 *
	 * @param mixed $value Stored option value.
	 * @return mixed
	 */
	public static function decrypt_option($value)
	{
		if (!is_array($value)) {
			return $value;
		}

		foreach (self::$secret_keys as $key) {
			if (isset($value[$key]) && is_string($value[$key])) {
				$value[$key] = self::decrypt($value[$key]);
			}
		}

		return self::apply_active_aliases($value);
	}

	/**
	 * Populate a selected profile when a legacy option is written directly.
	 *
	 * @param array $settings Settings before storage.
	 * @return array
	 */
	private static function initialize_environment_profile(array $settings)
	{
		$prefix = !empty($settings['testMode']) ? 'test' : 'live';
		$profile_exists = false;
		foreach (self::$environment_keys as $suffix) {
			if (array_key_exists($prefix . $suffix, $settings)) {
				$profile_exists = true;
				break;
			}
		}

		if (!$profile_exists) {
			foreach (self::$environment_keys as $alias => $suffix) {
				if (array_key_exists($alias, $settings)) {
					$settings[$prefix . $suffix] = $settings[$alias];
				}
			}
		}

		return $settings;
	}

	/**
	 * Keep the legacy settings contract mapped to the active environment only.
	 *
	 * @param array $settings Decrypted settings.
	 * @return array
	 */
	private static function apply_active_aliases(array $settings)
	{
		$prefix = !empty($settings['testMode']) ? 'test' : 'live';
		foreach (self::$environment_keys as $alias => $suffix) {
			$profile_key = $prefix . $suffix;
			if (array_key_exists($profile_key, $settings)) {
				$settings[$alias] = $settings[$profile_key];
			}
		}

		return $settings;
	}

	/**
	 * Encrypt a credential using authenticated encryption.
	 *
	 * @param string $plaintext Plaintext credential.
	 * @return string
	 */
	private static function encrypt($plaintext)
	{
		if (0 === strpos($plaintext, self::PREFIX) || !function_exists('openssl_encrypt')) {
			return $plaintext;
		}

		$iv_length = openssl_cipher_iv_length(self::CIPHER);
		if (false === $iv_length || $iv_length < 1) {
			return $plaintext;
		}

		try {
			$iv = random_bytes($iv_length);
		} catch (\Exception $exception) {
			return $plaintext;
		}

		$tag = '';
		$ciphertext = openssl_encrypt($plaintext, self::CIPHER, self::key(), OPENSSL_RAW_DATA, $iv, $tag);
		if (false === $ciphertext) {
			return $plaintext;
		}

		return self::PREFIX . base64_encode($iv . $tag . $ciphertext);
	}

	/**
	 * Decrypt a credential, while accepting legacy plaintext values.
	 *
	 * @param string $stored Stored credential.
	 * @return string
	 */
	private static function decrypt($stored)
	{
		if (0 !== strpos($stored, self::PREFIX)) {
			return $stored;
		}

		$decoded = base64_decode(substr($stored, strlen(self::PREFIX)), true);
		$iv_length = openssl_cipher_iv_length(self::CIPHER);
		$tag_length = 16;
		if (false === $decoded || false === $iv_length || strlen($decoded) <= ($iv_length + $tag_length)) {
			return '';
		}

		$iv = substr($decoded, 0, $iv_length);
		$tag = substr($decoded, $iv_length, $tag_length);
		$ciphertext = substr($decoded, $iv_length + $tag_length);
		$plaintext = openssl_decrypt($ciphertext, self::CIPHER, self::key(), OPENSSL_RAW_DATA, $iv, $tag);

		return false === $plaintext ? '' : $plaintext;
	}

	/**
	 * Derive a site-specific key from WordPress secret material.
	 *
	 * @return string Binary key.
	 */
	private static function key()
	{
		$material = wp_salt('auth') . '|digital-apps-payments-for-adyen|settings';
		return hash('sha256', $material, true);
	}

	/**
	 * Ensure an option is not loaded into memory on every request.
	 *
	 * @param string $option Option name.
	 */
	private static function disable_autoload($option)
	{
		if (function_exists('wp_set_option_autoload_values')) {
			wp_set_option_autoload_values([$option => false]);
			return;
		}

		global $wpdb;
		$wpdb->update(
			$wpdb->options,
			['autoload' => 'no'],
			['option_name' => $option],
			['%s'],
			['%s']
		);
	}
}
