<?php
/**
 * OAuth state (CSRF) — opaque token + optional post-login path + account linking intent.
 *
 * @package WP_PowerSuite\OAuth
 */

namespace WP_PowerSuite\OAuth;

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

final class StateManager {

	private const TRANSIENT_PREFIX = 'wpps_oauth_st_';
	private const LINK_PREFIX      = 'wpps_oauth_lnk_';
	private const TTL              = 600; // 10 minutes.
	private const SEP              = '|';
	/** @var int Payload format: store redirect + link id in DB; send a short `state` only (fixes long URL / encoding issues with Connect flows). */
	private const PAYLOAD_V2 = 2;

	/**
	 * @param int $link_user_id If set, this OAuth run links the provider to an existing user (Connect flow).
	 * @return array{state: string, store_key: string}
	 */
	public function createState( string $provider_key, string $redirect_after = '', int $link_user_id = 0 ): array {
		try {
			$token = bin2hex( random_bytes( 16 ) );
		} catch ( \Throwable $e ) {
			$token = substr( hash( 'sha256', (string) microtime( true ) . wp_generate_password( 32, true, true ) ), 0, 32 );
		}
		$link_user_id = (int) $link_user_id;
		$store_key    = self::TRANSIENT_PREFIX . $provider_key . '_' . $token;
		$payload      = [
			'v' => self::PAYLOAD_V2,
			'r' => (string) $redirect_after,
			'l' => $link_user_id,
		];
		// Opaque `state` sent to the provider: token only. Avoids 2k+ OAuth URLs and fragile hash_equals on embedded redirects.
		set_transient( $store_key, wp_json_encode( $payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), self::TTL );
		return [ 'state' => $token, 'store_key' => $store_key ];
	}

	/**
	 * @return array{ verified: string, link_user_id: int, redirect?: string }|false
	 */
	public function verifyAndConsume( string $provider_key, string $state_param ): array|false {
		$state_param = (string) $state_param;
		$pos         = strpos( $state_param, self::SEP );
		$token       = $pos !== false ? substr( $state_param, 0, $pos ) : $state_param;
		$token       = preg_replace( '/[^a-f0-9]/', '', $token ) ?? '';
		if ( strlen( $token ) < 8 ) {
			return false;
		}
		$store_key = self::TRANSIENT_PREFIX . $provider_key . '_' . $token;
		$stored    = get_transient( $store_key );
		delete_transient( $store_key );
		if ( ! is_string( $stored ) || $stored === '' ) {
			return false;
		}
		$trim = ltrim( $stored );
		if ( str_starts_with( $trim, '{' ) ) {
			$data = json_decode( $stored, true );
			if ( is_array( $data ) && (int) ( $data['v'] ?? 0 ) === self::PAYLOAD_V2 ) {
				// v2: no second transient; clean up if an old link row still exists.
				delete_transient( self::LINK_PREFIX . $provider_key . '_' . $token );
				return [
					'verified'     => $state_param,
					'link_user_id' => (int) ( $data['l'] ?? 0 ),
					'redirect'     => (string) ( $data['r'] ?? '' ),
				];
			}
		}
		// Legacy: stored string matched the full `state` sent to the provider.
		if ( ! hash_equals( $stored, $state_param ) ) {
			return false;
		}
		$link_key = self::LINK_PREFIX . $provider_key . '_' . $token;
		$link_raw = get_transient( $link_key );
		delete_transient( $link_key );
		$link_uid = ( is_string( $link_raw ) || is_numeric( $link_raw ) ) ? (int) $link_raw : 0;
		return [
			'verified'     => $state_param,
			'link_user_id' => $link_uid,
		];
	}

	/**
	 * @return string Raw redirect path/URL to use after login (unvalidated string).
	 */
	public function getRedirectFromState( string $verified_state_full ): string {
		$pos = strpos( $verified_state_full, self::SEP );
		if ( $pos === false ) {
			return '';
		}
		return rawurldecode( substr( $verified_state_full, $pos + 1 ) );
	}
}
