<?php
/**
 * Decorator that overrides GiveWP's MerchantDetails repository with a
 * pre-built credential object for a specific PayPal account.
 *
 * @since   1.0.0
 * @package DonationRouter
 */

declare( strict_types=1 );
namespace DonationRouter;

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

use Give\PaymentGateways\PayPalCommerce\Models\MerchantDetail;
use Give\PaymentGateways\PayPalCommerce\Repositories\MerchantDetails;

/**
 * Wraps a fixed MerchantDetail object inside the MerchantDetails repository
 * contract so that AuthorizationInjector::fetchAccessToken() — which calls
 * give(MerchantDetails::class)->getDetails() directly — returns the correct
 * per-account credentials.
 *
 * The parent constructor is intentionally not called; this class is only ever
 * bound into the IoC container via PayPal_Credential_Router and is never
 * auto-wired by GiveWP. Because the parent constructor never runs, the
 * inherited `HasMode::$mode` is never set — so every inherited method that
 * reads it (via `getAccountKey()`) would operate on a `null` mode. Two rules
 * therefore govern this class:
 *
 *  1. Signatures MUST match the parent's exactly for parameters. PHP forbids
 *     narrowing an untyped parent parameter (e.g. `HasMode::setMode($mode)`);
 *     declaring `setMode( string $mode )` is a fatal "Declaration must be
 *     compatible" error raised the moment this class is loaded — which happens
 *     on every routed donation, breaking the entire money path. Adding a
 *     *return* type is allowed (covariant); narrowing a *parameter* type is not.
 *
 *  2. Every state-mutating / storage-reading method is overridden to a no-op.
 *     GiveWP's `AuthorizationInjector::registerRefreshTokenCronJob()` calls
 *     `give(MerchantDetails::class)->save(...)` after fetching a fresh access
 *     token on each routed payment. The inherited `save()` writes the
 *     per-account credentials to `get_option( "give_paypal_commerce_{$mode}_account" )`
 *     — with `$mode` null that becomes a malformed `give_paypal_commerce__account`
 *     option holding decrypted clientId / clientSecret / token in PLAINTEXT,
 *     bypassing this plugin's AES-256-GCM storage (see ADR-002). Sealing these
 *     methods keeps credentials out of `wp_options` entirely.
 *
 * @since 1.0.0
 */
class Merchant_Details_Overrider extends MerchantDetails {

	/**
	 * The pre-built credential object to return from getDetails().
	 *
	 * @since 1.0.0
	 *
	 * @var MerchantDetail
	 */
	private MerchantDetail $detail;

	/**
	 * Initialise with the fixed credential object to serve.
	 *
	 * @since 1.0.0
	 *
	 * @param MerchantDetail $detail  Account-specific credentials to serve.
	 */
	public function __construct( MerchantDetail $detail ) {
		$this->detail = $detail;
	}

	/**
	 * Return the fixed MerchantDetail without reading from the database.
	 *
	 * @since 1.0.0
	 *
	 * @return MerchantDetail
	 */
	public function getDetails(): MerchantDetail { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return $this->detail;
	}

	/**
	 * Mode is baked into the credential object; this is a no-op.
	 *
	 * Parameter is intentionally left untyped to match the parent
	 * `HasMode::setMode($mode)` signature — adding a type would raise a fatal
	 * incompatible-declaration error when the class is loaded.
	 *
	 * @since 1.0.0
	 *
	 * @param string $mode  Ignored.
	 *
	 * @return static
	 */
	public function setMode( $mode ) { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return $this;
	}

	/**
	 * Report the current PayPal environment.
	 *
	 * The parent constructor never runs on this decorator, so the inherited
	 * `HasMode::$mode` is never initialised — calling the parent `getMode(): string`
	 * would return an uninitialised value and raise a TypeError. Overriding it to
	 * return the site's current mode keeps any GiveWP path that reads the mode off
	 * the rebound repository safe. Return type matches the parent (covariant).
	 *
	 * @since 1.1.0
	 *
	 * @return string  'sandbox' when GiveWP test mode is on, otherwise 'live'.
	 */
	public function getMode(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return give_is_test_mode() ? 'sandbox' : 'live';
	}

	/**
	 * No-op: never persist through the overrider.
	 *
	 * GiveWP's token-refresh path calls this after minting a fresh access
	 * token; the inherited implementation would leak the per-account
	 * credentials to `wp_options` in plaintext. See the class docblock.
	 *
	 * @since 1.0.0
	 *
	 * @param MerchantDetail $merchant_detail  Ignored.
	 *
	 * @return bool  Always false — nothing was persisted.
	 */
	public function save( MerchantDetail $merchant_detail ): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return false;
	}

	/**
	 * No-op: the overrider never deletes the real stored account.
	 *
	 * @since 1.0.0
	 *
	 * @return bool  Always false.
	 */
	public function delete(): bool {
		return false;
	}

	/**
	 * The routed account always has credentials — report it connected.
	 *
	 * @since 1.0.0
	 *
	 * @return bool
	 */
	public function accountIsConnected(): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return true;
	}

	/**
	 * No-op: client tokens are not served by the overrider.
	 *
	 * Return type must remain `string` to match the parent's declared return.
	 *
	 * @since 1.0.0
	 *
	 * @return string  Always empty.
	 */
	public function getClientToken(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return '';
	}

	/**
	 * No-op: nothing to delete.
	 *
	 * @since 1.0.0
	 *
	 * @return bool  Always false.
	 */
	public function deleteClientToken(): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return false;
	}

	/**
	 * No stored errors for the transient routed account.
	 *
	 * @since 1.0.0
	 *
	 * @return array<int,string>|null  Always null.
	 */
	public function getAccountErrors(): ?array { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return null;
	}

	/**
	 * No-op: never persist account errors through the overrider.
	 *
	 * Parameter left untyped to match the parent signature.
	 *
	 * @since 1.0.0
	 *
	 * @param string $error_message  Ignored.
	 *
	 * @return bool  Always false.
	 */
	public function saveAccountErrors( $error_message ): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return false;
	}

	/**
	 * No-op: nothing to delete.
	 *
	 * @since 1.0.0
	 *
	 * @return bool  Always false.
	 */
	public function deleteAccountErrors(): bool { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return false;
	}

	/**
	 * Return an empty option key so no inherited path can touch a real
	 * `give_paypal_commerce_*` option even if reached.
	 *
	 * @since 1.0.0
	 *
	 * @return string  Always empty.
	 */
	public function getAccountKey(): string { // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
		return '';
	}
}
