<?php

declare( strict_types=1 );

namespace StrykeWP\Modules\Connector;

/**
 * Authentication & API Key Management for the Hub Connector module.
 *
 * Ported from StrykeHub_Auth (stryke-connector v1.0.0).
 * Option names preserved for backward compatibility with existing installations.
 *
 * @package StrykeWP\Modules\Connector
 */
class Auth {

	// -----------------------------------------------------------------------
	// Constants
	// -----------------------------------------------------------------------

	/**
	 * wp_options key that stores the SHA-256 hash of the API key.
	 * Kept for backward compatibility with existing stryke-hub installations.
	 *
	 * @var string
	 */
	const OPTION_HASH = 'stryke_hub_api_key_hash';

	/**
	 * Transient key used to surface the raw key for one-time display.
	 *
	 * @var string
	 */
	const TRANSIENT_RAW_KEY = 'stryke_hub_raw_key_once';

	/**
	 * How many seconds the one-time raw-key transient lives (1 minute).
	 *
	 * @var int
	 */
	const TRANSIENT_TTL = 60;

	/**
	 * SHA-256 for performance in a per-request validation loop.
	 *
	 * @var string
	 */
	const HASH_ALGO = 'sha256';

	/**
	 * Max failed auth attempts before rate-limiting kicks in.
	 */
	const RATE_LIMIT_MAX = 5;

	/**
	 * Rate-limit window in seconds (15 minutes).
	 */
	const RATE_LIMIT_WINDOW = 900;

	/**
	 * Transient key prefix for rate-limiting.
	 */
	const RATE_LIMIT_PREFIX = 'stryke_rate_';

	// -----------------------------------------------------------------------
	// Key Generation & Storage
	// -----------------------------------------------------------------------

	/**
	 * Generates a new cryptographically secure API key.
	 *
	 * @return string The raw (plaintext) generated key — 64 lowercase hex chars.
	 * @throws \RuntimeException If the system cannot generate random bytes.
	 */
	public function generate_and_store_key(): string {
		$raw_key    = bin2hex( random_bytes( 32 ) );
		$hashed_key = hash( self::HASH_ALGO, $raw_key );
		update_option( self::OPTION_HASH, $hashed_key, false );
		set_transient( self::TRANSIENT_RAW_KEY, $raw_key, self::TRANSIENT_TTL );
		return $raw_key;
	}

	/**
	 * Checks whether an API key has been configured.
	 *
	 * @return bool True if a hash is stored in wp_options.
	 */
	public function has_key(): bool {
		return ! empty( get_option( self::OPTION_HASH, '' ) );
	}

	/**
	 * Retrieves the one-time plaintext key from the transient (if still valid).
	 *
	 * @return string|false The raw key string, or false if expired/not available.
	 */
	public function get_raw_key_once(): string|false {
		$key = get_transient( self::TRANSIENT_RAW_KEY );
		if ( false !== $key ) {
			delete_transient( self::TRANSIENT_RAW_KEY );
		}
		return $key;
	}

	/**
	 * Immediately revokes the stored API key.
	 *
	 * @return void
	 */
	public function revoke_key(): void {
		delete_option( self::OPTION_HASH );
		delete_transient( self::TRANSIENT_RAW_KEY );
	}

	// -----------------------------------------------------------------------
	// Stealth Mode — Plugin List Visibility
	// -----------------------------------------------------------------------

	/**
	 * Filter callback: `all_plugins` (priority 10).
	 *
	 * Removes the STRYKE WP master plugin from the WordPress Plugins list page.
	 * Visibility is controlled by the STRYKE_HUB_VISIBLE constant in wp-config.php.
	 *
	 * @param array $all_plugins All installed plugins.
	 * @return array Modified plugin list.
	 */
	public function maybe_hide_from_plugin_list( array $all_plugins ): array {
		if ( is_multisite() && is_super_admin() ) {
			return $all_plugins;
		}

		if ( defined( 'STRYKE_HUB_VISIBLE' ) && true === STRYKE_HUB_VISIBLE ) {
			return $all_plugins;
		}

		unset( $all_plugins[ STRYKE_WP_BASENAME ] );
		return $all_plugins;
	}

	// -----------------------------------------------------------------------
	// Request Validation (REST Permission Callback)
	// -----------------------------------------------------------------------

	/**
	 * REST API permission_callback — validates every incoming request.
	 *
	 * @param \WP_REST_Request $request The incoming REST request object.
	 * @return bool|\WP_Error True on success; WP_Error otherwise.
	 */
	public function validate_request( \WP_REST_Request $request ): bool|\WP_Error {
		// Rate-limit check first — block brute-force attempts.
		$rate_check = $this->check_rate_limit();
		if ( is_wp_error( $rate_check ) ) {
			return $rate_check;
		}

		$stored_hash = get_option( self::OPTION_HASH, '' );

		if ( empty( $stored_hash ) ) {
			return new \WP_Error(
				'stryke_not_configured',
				__(
					'The STRYKE Hub Connector has no API key configured. '
					. 'Please generate one in the WordPress admin.',
					'stryke-wp'
				),
				array( 'status' => 503 )
			);
		}

		$provided_key = $request->get_header( 'X-STRYKE-API-KEY' );

		if ( empty( $provided_key ) ) {
			$this->record_failed_attempt();
			return new \WP_Error(
				'stryke_missing_key',
				__(
					'Authentication required. '
					. 'Provide a valid X-STRYKE-API-KEY header.',
					'stryke-wp'
				),
				array( 'status' => 401 )
			);
		}

		$sanitized_key = sanitize_text_field( wp_unslash( $provided_key ) );
		$provided_hash = hash( self::HASH_ALGO, $sanitized_key );

		if ( ! hash_equals( $stored_hash, $provided_hash ) ) {
			$this->record_failed_attempt();
			return new \WP_Error(
				'stryke_invalid_key',
				__( 'Invalid API key. Access denied.', 'stryke-wp' ),
				array( 'status' => 403 )
			);
		}

		return $this->maybe_validate_hmac( $request, $sanitized_key );
	}

	// -----------------------------------------------------------------------
	// Rate Limiting
	// -----------------------------------------------------------------------

	/**
	 * Checks if the current IP has exceeded the allowed number of failed attempts.
	 *
	 * @return bool|\WP_Error True if within limits; WP_Error with 429 if exceeded.
	 */
	private function check_rate_limit(): bool|\WP_Error {
		$ip   = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) );
		$key  = self::RATE_LIMIT_PREFIX . md5( $ip );
		$data = get_transient( $key );

		if ( false !== $data ) {
			$parts = explode( '|', (string) $data );
			$count = (int) ( $parts[0] ?? 0 );
			$first = (int) ( $parts[1] ?? 0 );

			if ( $count >= self::RATE_LIMIT_MAX && ( time() - $first ) < self::RATE_LIMIT_WINDOW ) {
				return new \WP_Error(
					'stryke_rate_limited',
					__( 'Too many failed authentication attempts. Please try again later.', 'stryke-wp' ),
					array( 'status' => 429 )
				);
			}
		}

		return true;
	}

	/**
	 * Records a failed authentication attempt for the current IP.
	 */
	private function record_failed_attempt(): void {
		$ip   = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0' ) );
		$key  = self::RATE_LIMIT_PREFIX . md5( $ip );
		$data = get_transient( $key );

		if ( false !== $data ) {
			$parts = explode( '|', (string) $data );
			$count = (int) ( $parts[0] ?? 0 ) + 1;
			$first = (int) ( $parts[1] ?? time() );
		} else {
			$count = 1;
			$first = time();
		}

		set_transient( $key, "{$count}|{$first}", self::RATE_LIMIT_WINDOW );
	}

	// -----------------------------------------------------------------------
	// Optional HMAC-SHA256 Body Signature
	// -----------------------------------------------------------------------

	/**
	 * Optional HMAC-SHA256 body-signature validation (opt-in via X-STRYKE-SIGNATURE header).
	 *
	 * @param \WP_REST_Request $request       The incoming REST request.
	 * @param string           $sanitized_key The verified, sanitized raw API key.
	 * @return bool|\WP_Error  True on pass or absent header; WP_Error on mismatch.
	 */
	private function maybe_validate_hmac( \WP_REST_Request $request, string $sanitized_key ): bool|\WP_Error {

		$provided_sig = $request->get_header( 'X-STRYKE-SIGNATURE' );

		if ( empty( $provided_sig ) ) {
			$require_hmac = (bool) get_option( 'stryke_wp_connector_require_hmac', false );
			if ( $require_hmac ) {
				return new \WP_Error(
					'stryke_missing_signature',
					__( 'HMAC signature is required. Provide X-STRYKE-SIGNATURE header.', 'stryke-wp' ),
					array( 'status' => 403 )
				);
			}
			return true;
		}

		$body         = (string) $request->get_body();
		$expected_sig = hash_hmac( 'sha256', $body, $sanitized_key );
		$provided_sig = sanitize_text_field( wp_unslash( $provided_sig ) );

		if ( ! hash_equals( $expected_sig, $provided_sig ) ) {
			return new \WP_Error(
				'stryke_invalid_signature',
				__(
					'Request body signature mismatch. '
					. 'Ensure X-STRYKE-SIGNATURE = HMAC-SHA256(raw_body, api_key).',
					'stryke-wp'
				),
				array( 'status' => 403 )
			);
		}

		return true;
	}
}
