<?php
/**
 * Panic_Button_AJAX — admin-only AJAX endpoints for manual lockdown toggle.
 *
 * @package TrustLens
 * @since   1.2.2
 */

defined( 'ABSPATH' ) || exit;

class TrustLens_Panic_Button_AJAX {

	public const ACTION_ACTIVATE   = 'trustlens_panic_activate';
	public const ACTION_DEACTIVATE = 'trustlens_panic_deactivate';
	public const NONCE_NAME        = 'trustlens_panic_button';

	public function register_hooks(): void {
		add_action( 'wp_ajax_' . self::ACTION_ACTIVATE, array( $this, 'ajax_activate' ) );
		add_action( 'wp_ajax_' . self::ACTION_DEACTIVATE, array( $this, 'ajax_deactivate' ) );
	}

	public function ajax_activate(): void {
		wp_send_json( $this->handle_activate() );
	}

	public function ajax_deactivate(): void {
		wp_send_json( $this->handle_deactivate() );
	}

	/**
	 * Maximum server-enforced panic-freeze duration in seconds.
	 *
	 * 30 minutes is enough for an admin to investigate and respond to a real
	 * card-testing wave. Beyond that, the chance of a stale freeze accidentally
	 * blocking real customers exceeds the chance of an actual attack still
	 * needing the freeze — admins can re-activate if they need more time.
	 *
	 * Filterable for sites that genuinely need a longer ceiling.
	 *
	 * @since 1.2.3
	 */
	private const MAX_DURATION = 1800;

	/**
	 * Default freeze duration when the request omits one, in seconds.
	 *
	 * Always clamped to the active [60, $max_duration] bounds before use, so a
	 * lowered max_duration ceiling applies to the default too.
	 *
	 * @since 1.3.0
	 */
	private const DEFAULT_DURATION = 900;

	public function handle_activate(): array {
		if ( ! $this->authorized() ) {
			return array( 'success' => false, 'message' => 'forbidden' );
		}

		/**
		 * Filter the upper bound on a single Panic Freeze activation, in seconds.
		 *
		 * Default 1800 (30 min). Lower values trade investigation headroom for
		 * tighter safety against an admin accidentally blocking real customers.
		 *
		 * @since 1.2.3
		 * @param int $max Maximum allowed duration in seconds.
		 */
		$max_duration = (int) apply_filters( 'trustlens/card_testing/panic_max_duration', self::MAX_DURATION );
		$max_duration = max( 60, $max_duration ); // Never below 60s, regardless of filter value.

		// Clamp both an explicit request and the fallback default to the same
		// [60, $max_duration] bounds so the default can never exceed the ceiling.
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in the authorized() guard at the top of handle_activate().
		$requested = isset( $_POST['duration'] ) ? (int) $_POST['duration'] : self::DEFAULT_DURATION;
		$duration  = max( 60, min( $max_duration, $requested ) );

		$state = new TrustLens_Lockdown_State();
		$state->activate_panic_freeze( $duration );

		/**
		 * Fires when an admin activates the panic-freeze button.
		 *
		 * Stable Pro contract since 1.5.0. Subscribers may:
		 * - Send notifications (Slack, email, SMS, pager).
		 * - Record the event in external analytics.
		 * - Trigger correlated infrastructure actions (CDN rules, firewall).
		 *
		 * Callers must handle the action asynchronously when the work is
		 * non-trivial; this action fires synchronously from an admin AJAX
		 * request and will block the response.
		 *
		 * @since 1.2.2
		 * @stable-contract 1.5.0 — Pro tier documents this hook as a public
		 *                 integration surface. Free builds may call it but
		 *                 the contract is not guaranteed.
		 * @param int $duration Seconds the freeze will last (min 60, max 3600).
		 */
		do_action( 'trustlens/panic_button_activated', $duration );

		return array(
			'success'    => true,
			'expires_at' => $state->panic_expires_at(),
		);
	}

	public function handle_deactivate(): array {
		if ( ! $this->authorized() ) {
			return array( 'success' => false, 'message' => 'forbidden' );
		}
		( new TrustLens_Lockdown_State() )->deactivate_panic_freeze();
		return array( 'success' => true );
	}

	private function authorized(): bool {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return false;
		}
		$nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_REQUEST['_wpnonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, self::NONCE_NAME ) ) {
			return false;
		}
		return true;
	}
}
