<?php

/**
 * Manages an Onboarding Url / Token to preserve /v2/customer/partner-referrals action_url
 * integrity.
 *
 * @package WooCommerce\PayPalCommerce\Settings\Service
 */
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\Settings\Service;

use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use Throwable;
class OnboardingUrl
{
    /**
     * The user ID to associate with the cache key
     */
    private int $user_id;
    /**
     * The cryptographically secure secret
     */
    private ?string $secret = null;
    /**
     * Unix Timestamp when token was generated
     */
    private ?int $time = null;
    /**
     * The "action_url" from /v2/customer/partner-referrals
     */
    private ?string $url = null;
    /**
     * The cryptographically random seller nonce (PKCE code_verifier)
     */
    private ?string $nonce = null;
    private Cache $cache;
    private string $cache_key_prefix;
    private int $cache_ttl = MONTH_IN_SECONDS;
    /**
     * The TTL for the previous token cache.
     */
    private int $previous_cache_ttl = 60;
    /**
     * The constructor
     *
     * @param Cache  $cache            The cache object to store the URL.
     * @param string $cache_key_prefix The prefix for the cache entry.
     * @param int    $user_id          User ID to associate the link with.
     */
    public function __construct(Cache $cache, string $cache_key_prefix, int $user_id)
    {
        $this->cache = $cache;
        $this->cache_key_prefix = $cache_key_prefix;
        $this->user_id = $user_id;
    }
    /**
     * Instances the object with a $token.
     *
     * @param Cache  $cache   The cache object where the URL is stored.
     * @param string $token   The token to validate.
     * @param int    $user_id User ID to associate the link with.
     * @return false|self
     */
    public static function make_from_token(Cache $cache, string $token, int $user_id)
    {
        if (!$token) {
            return \false;
        }
        try {
            $json_string = self::url_safe_base64_decode($token) ?: '';
            $token_data = json_decode($json_string, \true, 512, \JSON_THROW_ON_ERROR);
        } catch (Throwable $exception) {
            return \false;
        }
        if (!$token_data) {
            return \false;
        }
        if (!isset($token_data['u'], $token_data['k'])) {
            return \false;
        }
        if ($token_data['u'] !== $user_id) {
            return \false;
        }
        return new self($cache, $token_data['k'], $token_data['u']);
    }
    /**
     * Validates the token, if it's valid then delete it.
     * If it's invalid don't delete it, to prevent malicious requests from invalidating the token.
     *
     * @param Cache  $cache   The cache object where the URL is stored.
     * @param string $token   The token to validate.
     * @param int    $user_id User ID to associate the link with.
     * @return bool
     */
    public static function validate_token_and_delete(Cache $cache, string $token, int $user_id): bool
    {
        $onboarding_url = self::make_from_token($cache, $token, $user_id);
        if ($onboarding_url === \false) {
            return \false;
        }
        if (!$onboarding_url->load()) {
            return \false;
        }
        $expected_token = $onboarding_url->onboarding_token();
        if (!$expected_token || $expected_token !== $token) {
            return \false;
        }
        $onboarding_url->replace_previous_token($token);
        $onboarding_url->delete();
        return \true;
    }
    /**
     * Validates the token against the previous token.
     * Useful to don't throw errors on burst calls to endpoints.
     *
     * @param Cache  $cache   The cache object where the URL is stored.
     * @param string $token   The token to validate.
     * @param int    $user_id User ID to associate the link with.
     * @return bool
     */
    public static function validate_previous_token(Cache $cache, string $token, int $user_id): bool
    {
        $onboarding_url = self::make_from_token($cache, $token, $user_id);
        if ($onboarding_url === \false) {
            return \false;
        }
        return $onboarding_url->matches_previous_token($token);
    }
    /**
     * Load cached data if is valid and initialize object.
     *
     * @return bool
     */
    public function load(): bool
    {
        $key = $this->cache_key();
        if (!$this->cache->has($key)) {
            return \false;
        }
        $cached_data = $this->cache->get($key);
        if (!is_array($cached_data) || !$this->validate_cache_data($cached_data)) {
            return \false;
        }
        $this->secret = $cached_data['secret'];
        $this->time = $cached_data['time'];
        $this->url = $cached_data['url'];
        $this->nonce = $cached_data['nonce'];
        return \true;
    }
    public function init(): void
    {
        try {
            $this->secret = bin2hex(random_bytes(16));
            $this->nonce = bin2hex(random_bytes(32));
        } catch (Throwable $e) {
            $this->secret = wp_generate_password(16);
            $this->nonce = wp_generate_password(64, \false);
        }
        $this->time = time();
        $this->url = null;
    }
    private function validate_cache_data(array $cache_data): bool
    {
        if (!($cache_data['user_id'] ?? \false) || !($cache_data['hash_check'] ?? \false) || !($cache_data['secret'] ?? \false) || !($cache_data['time'] ?? \false) || !($cache_data['url'] ?? \false) || !($cache_data['nonce'] ?? \false)) {
            return \false;
        }
        if ($cache_data['user_id'] !== $this->user_id) {
            return \false;
        }
        // Detect if salt has changed.
        if ($cache_data['hash_check'] !== wp_hash('')) {
            return \false;
        }
        return \true;
    }
    /**
     * Returns the Token
     *
     * @return string Empty string on failure, otherwise a base64 encoded payload.
     */
    public function onboarding_token(): string
    {
        if (null === $this->secret || null === $this->time) {
            return '';
        }
        // Trim the hash to make sure the token isn't too long.
        $hash = substr(wp_hash(implode('|', array($this->cache_key_prefix, $this->user_id, $this->secret, $this->time))), 0, 32);
        $token = wp_json_encode(array('k' => $this->cache_key_prefix, 'u' => $this->user_id, 'h' => $hash));
        if (!$token) {
            return '';
        }
        return self::url_safe_base64_encode($token);
    }
    public function get_onboarding_url(): string
    {
        return $this->url ?? '';
    }
    public function set_onboarding_url(string $url): void
    {
        $this->url = $url;
    }
    /**
     * Persists the URL and related data in cache
     *
     * @return void
     */
    public function persist(): void
    {
        if (null === $this->secret || null === $this->time || null === $this->url || null === $this->nonce) {
            return;
        }
        $this->cache->set($this->cache_key(), array(
            'hash_check' => wp_hash(''),
            // To detect if salt has changed.
            'secret' => $this->secret,
            'time' => $this->time,
            'user_id' => $this->user_id,
            'url' => $this->url,
            'nonce' => $this->nonce,
        ), $this->cache_ttl);
    }
    public function seller_nonce(): string
    {
        return $this->nonce ?? '';
    }
    /**
     * Deletes the token from cache
     *
     * @return void
     */
    public function delete(): void
    {
        $this->cache->delete($this->cache_key());
    }
    private function cache_key(): string
    {
        return implode('_', array($this->cache_key_prefix, $this->user_id));
    }
    private function previous_cache_key(): string
    {
        return $this->cache_key() . '_previous';
    }
    private function matches_previous_token(string $previous_token): bool
    {
        if (!$this->cache->has($this->previous_cache_key())) {
            return \false;
        }
        $cached_token = $this->cache->get($this->previous_cache_key());
        return $cached_token === $previous_token;
    }
    private function replace_previous_token(string $previous_token): void
    {
        $this->cache->set($this->previous_cache_key(), $previous_token, $this->previous_cache_ttl);
    }
    private static function url_safe_base64_encode(string $string): string
    {
        //phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
        $encoded_string = base64_encode($string);
        $url_safe_string = str_replace(array('+', '/'), array('-', '_'), $encoded_string);
        return rtrim($url_safe_string, '=');
    }
    /** @phpstan-ignore missingType.return */
    private static function url_safe_base64_decode(string $url_safe_string)
    {
        $padded_string = str_pad($url_safe_string, strlen($url_safe_string) % 4, '=', \STR_PAD_RIGHT);
        $encoded_string = str_replace(array('-', '_'), array('+', '/'), $padded_string);
        //phpcs:disable WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
        return base64_decode($encoded_string);
    }
}
