<?php
/**
 * PeachPay Authorize.net Credentials Manager.
 *
 * Handles secure storage and retrieval of merchant's Authorize.net API credentials.
 * Credentials are stored per-mode (test/live) using AES-256-CBC encryption for sensitive fields.
 *
 * @package PeachPay
 */

defined( 'PEACHPAY_ABSPATH' ) || exit;

/**
 * Manages Authorize.net API credentials for client-side integration.
 *
 * This class replaces the legacy proxy-based credential model
 * (`peachpay_connected_authnet_account` / `peachpay_connected_authnet_config`)
 * with a self-hosted, per-mode encrypted storage pattern that mirrors
 * PeachPay_Stripe_Credentials.
 */
final class PeachPay_Authnet_Credentials {

	/**
	 * Option key for test-mode credentials.
	 */
	const OPTION_KEY_TEST = 'peachpay_authnet_credentials_test';

	/**
	 * Option key for live-mode credentials.
	 */
	const OPTION_KEY_LIVE = 'peachpay_authnet_credentials_live';

	/**
	 * Encryption method used for sensitive fields.
	 */
	const ENCRYPTION_METHOD = 'aes-256-cbc';

	// -------------------------------------------------------------------------
	// Private encryption helpers
	// -------------------------------------------------------------------------

	/**
	 * Derives the encryption key from WordPress salts.
	 *
	 * @return string 32-byte raw binary key.
	 */
	private static function get_encryption_key() {
		if ( ! defined( 'LOGGED_IN_SALT' ) ) {
			peachpay_log( 'error', 'LOGGED_IN_SALT is not defined. Authorize.net credentials cannot be encrypted or decrypted.' );
			return '';
		}
		return hash( 'sha256', LOGGED_IN_SALT, true );
	}

	/**
	 * Encrypts a value using AES-256-CBC with a random IV.
	 *
	 * The output is base64-encoded and stores the IV prepended to the ciphertext
	 * so it can be extracted during decryption.
	 *
	 * @param string $value The plaintext value to encrypt.
	 * @return string Base64-encoded string (IV + ciphertext), or empty string if input is empty.
	 */
	private static function encrypt( $value ) {
		if ( empty( $value ) ) {
			return '';
		}

		$iv        = openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::ENCRYPTION_METHOD ) );
		$encrypted = openssl_encrypt( $value, self::ENCRYPTION_METHOD, self::get_encryption_key(), 0, $iv );

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
		return base64_encode( $iv . $encrypted );
	}

	/**
	 * Decrypts a value previously encrypted with self::encrypt().
	 *
	 * Extracts the IV from the first 16 bytes of the base64-decoded payload,
	 * then decrypts the remainder.
	 *
	 * @param string $value The base64-encoded encrypted value.
	 * @return string The decrypted plaintext, or empty string if input is empty.
	 */
	private static function decrypt( $value ) {
		if ( empty( $value ) ) {
			return '';
		}

		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
		$data = base64_decode( $value );
		if ( false === $data ) {
			return '';
		}
		$iv_length = openssl_cipher_iv_length( self::ENCRYPTION_METHOD );
		$iv        = substr( $data, 0, $iv_length );
		$encrypted = substr( $data, $iv_length );

		return openssl_decrypt( $encrypted, self::ENCRYPTION_METHOD, self::get_encryption_key(), 0, $iv );
	}

	// -------------------------------------------------------------------------
	// Public API
	// -------------------------------------------------------------------------

	/**
	 * Returns the current active mode ('test' or 'live').
	 *
	 * Reads the global PeachPay test-mode admin toggle directly so that the
	 * admin UI always reflects what the merchant has chosen, regardless of
	 * whether the site is running on a local or staging URL.
	 * (PeachPay_Authnet_Integration::mode() also coerces local/staging sites
	 * to 'test' for payment processing, but that should not affect the
	 * credential section title or which credential set is displayed in admin.)
	 *
	 * @return string 'test' or 'live'.
	 */
	public static function current_mode() {
		return peachpay_is_test_mode() ? 'test' : 'live';
	}

	/**
	 * Checks whether credentials are configured for the given (or current) mode.
	 *
	 * Requires both login_id and transaction_key to be present.
	 *
	 * @param string|null $mode 'test', 'live', or null to use current mode.
	 * @return bool True if fully configured, false otherwise.
	 */
	public static function is_configured( $mode = null ) {
		if ( null === $mode ) {
			$mode = self::current_mode();
		}

		$account = self::get_account( $mode );

		return ! empty( $account['login_id'] ) && ! empty( $account['transaction_key'] );
	}

	/**
	 * Gets the API Login ID for the given (or current) mode.
	 *
	 * @param string|null $mode 'test', 'live', or null to use current mode.
	 * @return string The login ID, or empty string if not set.
	 */
	public static function get_login_id( $mode = null ) {
		if ( null === $mode ) {
			$mode = self::current_mode();
		}

		$account = self::get_account( $mode );

		return isset( $account['login_id'] ) ? $account['login_id'] : '';
	}

	/**
	 * Gets the Public Client Key for Accept.js for the given (or current) mode.
	 *
	 * The public client key is NOT sensitive and is stored in plaintext.
	 *
	 * @param string|null $mode 'test', 'live', or null to use current mode.
	 * @return string The public client key, or empty string if not set.
	 */
	public static function get_public_client_key( $mode = null ) {
		if ( null === $mode ) {
			$mode = self::current_mode();
		}

		$account = self::get_account( $mode );

		return isset( $account['public_client_key'] ) ? $account['public_client_key'] : '';
	}

	/**
	 * Gets the Transaction Key for the given (or current) mode.
	 *
	 * The transaction key is stored encrypted; this method decrypts on read.
	 *
	 * @param string|null $mode 'test', 'live', or null to use current mode.
	 * @return string The decrypted transaction key, or empty string if not set.
	 */
	public static function get_transaction_key( $mode = null ) {
		if ( null === $mode ) {
			$mode = self::current_mode();
		}

		$account = self::get_account( $mode );

		if ( empty( $account['transaction_key'] ) ) {
			return '';
		}

		return self::decrypt( $account['transaction_key'] );
	}

	/**
	 * Gets the Webhook Signature Key for the given (or current) mode.
	 *
	 * The webhook signature key is stored encrypted; this method decrypts on read.
	 *
	 * @param string|null $mode 'test', 'live', or null to use current mode.
	 * @return string The decrypted webhook signature key, or empty string if not set.
	 */
	public static function get_webhook_signature_key( $mode = null ) {
		if ( null === $mode ) {
			$mode = self::current_mode();
		}

		$account = self::get_account( $mode );

		if ( empty( $account['webhook_sig_key'] ) ) {
			return '';
		}

		return self::decrypt( $account['webhook_sig_key'] );
	}

	/**
	 * Returns the full raw stored credentials array for the given (or current) mode.
	 *
	 * Note: transaction_key and webhook_sig_key are returned still encrypted.
	 * Use the dedicated getters to retrieve decrypted values.
	 *
	 * @param string|null $mode 'test', 'live', or null to use current mode.
	 * @return array The credentials array, or empty array if not configured.
	 */
	public static function get_account( $mode = null ) {
		if ( null === $mode ) {
			$mode = self::current_mode();
		}

		$option_key = 'test' === $mode ? self::OPTION_KEY_TEST : self::OPTION_KEY_LIVE;

		return get_option( $option_key, array() );
	}

	/**
	 * Validates credentials against the Authorize.net authenticateTestRequest endpoint
	 * and stores them if valid.
	 *
	 * Validation flow:
	 *  1. Require login_id, transaction_key, public_client_key.
	 *  2. Call authenticateTestRequest via wp_remote_post() to verify the pair.
	 *  3. Encrypt sensitive fields (transaction_key, webhook_sig_key).
	 *  4. Persist to the mode-specific option key.
	 *  5. On first successful save, delete legacy proxy-era options.
	 *
	 * NOTE: This method currently issues the authenticateTestRequest call inline
	 * because PeachPay_Authnet utility class has not been rewritten yet.
	 * Once PeachPay_Authnet is updated, extract this to
	 * PeachPay_Authnet::authenticate($mode, $login_id, $transaction_key).
	 *
	 * @param string $mode   'test' or 'live'.
	 * @param array  $fields {
	 *     Required credential fields.
	 *
	 *     @type string $login_id          API Login ID.
	 *     @type string $transaction_key   Transaction Key (will be encrypted).
	 *     @type string $public_client_key Public Client Key for Accept.js (plaintext).
	 *     @type string $webhook_sig_key   Optional. Webhook Signature Key (will be encrypted).
	 * }
	 * @return array {
	 *     Result array.
	 *
	 *     @type bool   $success Whether the save succeeded.
	 *     @type string $message Human-readable result message.
	 * }
	 */
	public static function save( $mode, $fields ) {
		// 1. Validate required fields.
		$login_id          = isset( $fields['login_id'] ) ? sanitize_text_field( $fields['login_id'] ) : '';
		$transaction_key   = isset( $fields['transaction_key'] ) ? sanitize_text_field( $fields['transaction_key'] ) : '';
		$public_client_key = isset( $fields['public_client_key'] ) ? sanitize_text_field( $fields['public_client_key'] ) : '';
		$webhook_sig_key   = isset( $fields['webhook_sig_key'] ) ? sanitize_text_field( $fields['webhook_sig_key'] ) : '';

		if ( empty( $login_id ) ) {
			return array(
				'success' => false,
				'message' => __( 'API Login ID is required.', 'peachpay-for-woocommerce' ),
			);
		}

		if ( empty( $transaction_key ) ) {
			return array(
				'success' => false,
				'message' => __( 'Transaction Key is required.', 'peachpay-for-woocommerce' ),
			);
		}

		if ( empty( $public_client_key ) ) {
			return array(
				'success' => false,
				'message' => __( 'Public Client Key is required.', 'peachpay-for-woocommerce' ),
			);
		}

		// 2. Validate credentials against Authorize.net authenticateTestRequest.
		$auth_result = PeachPay_Authnet::authenticate( $mode, $login_id, $transaction_key );

		if ( ! $auth_result['success'] ) {
			return $auth_result;
		}

		// Validate public_client_key against Authorize.net merchant details.
		$merchant_details = PeachPay_Authnet::get_merchant_details( $mode, $login_id, $transaction_key );
		if ( null === $merchant_details ) {
			peachpay_log( 'warning', 'Authorize.net: public client key validation skipped because getMerchantDetailsRequest returned null (API unavailable). Credentials will be saved without key validation.' );
		} elseif ( isset( $merchant_details['publicClientKey'] ) ) {
			if ( $merchant_details['publicClientKey'] !== $public_client_key ) {
				return array(
					'success' => false,
					'message' => __( 'The Public Client Key does not match your Authorize.net account. Please copy it from your merchant portal under Account > Security Settings > General Security Settings > Manage Public Client Key.', 'peachpay-for-woocommerce' ),
				);
			}
		}

		// 3. Encrypt sensitive fields.
		$data_to_store = array(
			'login_id'          => $login_id,
			'public_client_key' => $public_client_key,
			'transaction_key'   => self::encrypt( $transaction_key ),
		);

		if ( ! empty( $webhook_sig_key ) ) {
			$data_to_store['webhook_sig_key'] = self::encrypt( $webhook_sig_key );
		}

		// 4. Persist.
		$option_key = 'test' === $mode ? self::OPTION_KEY_TEST : self::OPTION_KEY_LIVE;
		update_option( $option_key, $data_to_store );

		// 5. On first successful save, clean up legacy proxy-era options.
		if ( get_option( 'peachpay_connected_authnet_account', false ) !== false
			|| get_option( 'peachpay_connected_authnet_config', false ) !== false
		) {
			delete_option( 'peachpay_connected_authnet_account' );
			delete_option( 'peachpay_connected_authnet_config' );
		}

		return array(
			'success' => true,
			'message' => __( 'Credentials saved.', 'peachpay-for-woocommerce' ),
		);
	}

	/**
	 * Deletes stored credentials for the given mode, or both modes if null.
	 *
	 * @param string|null $mode 'test', 'live', or null to delete both.
	 * @return bool Always returns true.
	 */
	public static function delete( $mode = null ) {
		if ( null === $mode || 'test' === $mode ) {
			delete_option( self::OPTION_KEY_TEST );
		}

		if ( null === $mode || 'live' === $mode ) {
			delete_option( self::OPTION_KEY_LIVE );
		}

		return true;
	}
}
