<?php declare( strict_types=1 );

namespace FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Actions;

use FernleafSystems\Utilities\Data\Adapter\DynPropertiesClass;
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\ActionNonce;
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\ActionResponse;
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Constants;
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\RoutedResponse;
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Exceptions\{
	InvalidActionNonceException,
	IpBlockedException,
	SecurityAdminRequiredException,
	UserAuthRequiredException
};
use FernleafSystems\Wordpress\Plugin\Shield\ActionRouter\Exceptions\ActionException;
use FernleafSystems\Wordpress\Plugin\Shield\Modules\PluginControllerConsumer;
use FernleafSystems\Wordpress\Services\Services;

/**
 * @property array $action_data
 */
abstract class BaseAction extends DynPropertiesClass {

	use PluginControllerConsumer;

	public const SLUG = '';

	private ActionResponse $response;

	private RoutedResponse $routedResponse;

	public function __construct( array $data = [], ?ActionResponse $response = null ) {
		$this->action_data = $data;
		$this->response = $response instanceof ActionResponse ? $response : new ActionResponse();
	}

	public function __get( string $key ) {
		$value = parent::__get( $key );

		switch ( $key ) {
			case 'action_data':
				$value = \array_merge( $this->getDefaults(), \is_array( $value ) ? $value : [] );
				break;
			default:
				break;
		}

		return $value;
	}

	/**
	 * @throws ActionException
	 * @throws InvalidActionNonceException
	 * @throws IpBlockedException
	 * @throws SecurityAdminRequiredException
	 * @throws UserAuthRequiredException
	 */
	public function process() {
		$this->checkAccess();
		$this->checkAvailableData();
		$this->preExec();
		$this->exec();
		$this->postExec();
	}

	/**
	 * @throws InvalidActionNonceException
	 * @throws IpBlockedException
	 * @throws SecurityAdminRequiredException
	 * @throws UserAuthRequiredException
	 */
	protected function checkAccess() {
		$con = self::con();
		$thisReq = $con->this_req;
		if ( !$thisReq->request_bypasses_all_restrictions && $thisReq->is_ip_blocked && !$this->canBypassIpAddressBlock() ) {
			throw new IpBlockedException( sprintf( __( 'IP Address blocked so cannot process action: %s', 'wp-simple-firewall' ), static::SLUG ) );
		}

		$WPU = Services::WpUsers();
		if ( $this->isUserAuthRequired()
			 && ( !$WPU->isUserLoggedIn() || !user_can( $WPU->getCurrentWpUser(), $this->getMinimumUserAuthCapability() ) ) ) {
			throw new UserAuthRequiredException( sprintf( __( 'Must be logged-in to execute this action: %s', 'wp-simple-firewall' ), static::SLUG ) );
		}

		if ( !$thisReq->is_security_admin && $this->isSecurityAdminRequired() ) {
			throw new SecurityAdminRequiredException( sprintf( __( 'Security admin required for action: %s', 'wp-simple-firewall' ), static::SLUG ) );
		}

		if ( $this->isNonceVerifyRequired() && !ActionNonce::VerifyFromRequest() ) {
			throw new InvalidActionNonceException( __( 'Invalid Action Nonce Exception.', 'wp-simple-firewall' ) );
		}
	}

	protected function preExec() {
	}

	protected function postExec() {
	}

	/**
	 * @throws ActionException
	 */
	abstract protected function exec();

	public function response() :ActionResponse {
		$this->response->action_slug = static::SLUG;
		$this->response->action_data = $this->action_data;
		return $this->response;
	}

	public function setResponse( RoutedResponse $response ) {
		$this->routedResponse = $response;
		$this->response = $this->routedResponse->actionResponse();
	}

	protected function getDefaults() :array {
		return [];
	}

	protected function getMinimumUserAuthCapability() :string {
		return self::con()->cfg->properties[ 'base_permissions' ] ?? 'manage_options';
	}

	protected function canBypassIpAddressBlock() :bool {
		return false;
	}

	protected function isNonceVerifyRequired() :bool {
		return (bool)( $this->getActionOverrides()[ Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED ] ?? self::con()->this_req->wp_is_ajax );
	}

	protected function isUserAuthRequired() :bool {
		return !empty( $this->getMinimumUserAuthCapability() );
	}

	protected function isSecurityAdminRequired() :bool {
		return $this->getMinimumUserAuthCapability() === 'manage_options';
	}

	protected function getActionOverrides() :array {
		return $this->action_data[ 'action_overrides' ] ?? [];
	}

	/**
	 * Set action override programmatically (for trusted integrations like MainWP)
	 *
	 * This method allows trusted integrations to set action overrides AFTER action creation,
	 * ensuring security controls never come from user input paths.
	 *
	 * SECURITY NOTE: This method should ONLY be called in trusted contexts where:
	 * - The caller has verified authentication (e.g., MainWP authenticated requests)
	 * - The override is set programmatically, not from user input
	 * - The action object has already been created via ActionProcessor::getAction()
	 *
	 * @param string $overrideKey Override key constant (e.g., Constants::ACTION_OVERRIDE_IS_NONCE_VERIFY_REQUIRED)
	 * @param mixed  $value       Override value (typically boolean for is_nonce_verify_required)
	 * @return self For method chaining
	 */
	public function setActionOverride( string $overrideKey, $value ) :self {
		$actionData = $this->getRawData()[ 'action_data' ] ?? [];
		if ( !\is_array( $actionData ) ) {
			$actionData = [];
		}
		$overrides = \array_merge(
			\is_array( $actionData[ 'action_overrides' ] ?? null ) ? $actionData[ 'action_overrides' ] : [],
			[ $overrideKey => $value ]
		);
		$actionData[ 'action_overrides' ] = $overrides;
		$this->action_data = $actionData;
		return $this;
	}

	/**
	 * @throws ActionException
	 */
	protected function checkAvailableData() {
		$missing = \array_diff( \array_unique( $this->getRequiredDataKeys() ), \array_keys( $this->action_data ) );
		if ( !empty( $missing ) ) {
			throw new ActionException( sprintf( 'Missing action (%s) data for the following keys: %s', static::SLUG, \implode( ', ', $missing ) ) );
		}
	}

	protected function getRequiredDataKeys() :array {
		return [];
	}

	public static function NonceCfg() :array {
		return [
			'ip'  => false,
			'ttl' => 12,
		];
	}
}
