<?php

namespace Bryceandy\Laravel_Pesapal\OAuth;

class OAuthServer
{
    protected int $timestamp_threshold = 300; // in seconds, five minutes
    protected float $version = 1.0;             // hi blaine
    protected array $signature_methods = array();

    protected $data_store;

    public function __construct($data_store)
    {
        $this->data_store = $data_store;
    }

    public function add_signature_method($signature_method)
    {
        $this->signature_methods[$signature_method->get_name()] = $signature_method;
    }

    // high level functions

    /**
     * process a request_token request &
     * returns request token on success
     *
     * @param $request
     * @return string
     * @throws OAuthException
     */
    public function fetch_request_token(&$request)
    {
        $this->get_version($request);

        $consumer = $this->get_consumer($request);

        // no token required for the initial token request
        $token = NULL;

        $this->check_signature($request, $consumer, $token);

        return $this->data_store->new_request_token($consumer);
    }

    /**
     * Process an access_token request &
     * returns access token on success
     *
     * @param $request
     * @return string
     * @throws OAuthException
     */
    public function fetch_access_token(&$request)
    {
        $this->get_version($request);

        $consumer = $this->get_consumer($request);

        // requires authorized request token
        $token = $this->get_token($request, $consumer, "request");

        $this->check_signature($request, $consumer, $token);

        return $this->data_store->new_access_token($token, $consumer);
    }

    /**
     * Verify an api call, checks all the parameters
     *
     * @param $request
     * @return array
     * @throws OAuthException
     */
    public function verify_request(&$request)
    {
        $this->get_version($request);
        $consumer = $this->get_consumer($request);
        $token = $this->get_token($request, $consumer, "access");
        $this->check_signature($request, $consumer, $token);

        return array($consumer, $token);
    }

    // Internals from here

    /**
     * Version 1
     *
     * @param $request
     * @return float
     * @throws OAuthException
     */
    private function get_version(&$request)
    {
        $version = $request->get_parameter("oauth_version");
        if (!$version) {
            $version = 1.0;
        }
        if ($version && $version != $this->version) {
            throw new OAuthException("OAuth version '$version' not supported");
        }
        return $version;
    }

    /**
     * Figure out the signature with some defaults
     * @param $request
     * @return mixed
     * @throws OAuthException
     */
    private function get_signature_method(&$request)
    {
        $signature_method =
            @$request->get_parameter("oauth_signature_method");
        if (!$signature_method) {
            $signature_method = "PLAINTEXT";
        }
        if (!in_array($signature_method,
            array_keys($this->signature_methods))) {
            throw new OAuthException(
                "Signature method '$signature_method' not supported " .
                "try one of the following: " .
                implode(", ", array_keys($this->signature_methods))
            );
        }

        return $this->signature_methods[$signature_method];
    }

    /**
     * Try to find the consumer for the provided request's consumer key
     * @param $request
     * @noinspection PhpDocMissingReturnTagInspection
     * @throws OAuthException
     */
    private function get_consumer(&$request)
    {
        $consumer_key = @$request->get_parameter("oauth_consumer_key");
        if (!$consumer_key) {
            throw new OAuthException("Invalid consumer key");
        }

        $consumer = $this->data_store->lookup_consumer($consumer_key);
        if (!$consumer) {
            throw new OAuthException("Invalid consumer");
        }

        return $consumer;
    }

    /**
     * Try to find the token for the provided request's token key
     *
     * @param $request
     * @param $consumer
     * @param string $token_type
     * @throws OAuthException
     * @noinspection PhpDocMissingReturnTagInspection
     */
    private function get_token(&$request, $consumer, $token_type="access")
    {
        $token_field = @$request->get_parameter('oauth_token');
        $token = $this->data_store->lookup_token(
            $consumer, $token_type, $token_field
        );
        if (!$token) {
            throw new OAuthException("Invalid $token_type token: $token_field");
        }

        return $token;
    }

    /**
     * All-in-one function to check the signature on a request
     * should guess the signature method appropriately
     *
     * @param $request
     * @param $consumer
     * @param $token
     * @throws OAuthException
     */
    private function check_signature(&$request, $consumer, $token)
    {
        // this should probably be in a different method
        $timestamp = @$request->get_parameter('oauth_timestamp');
        $nonce = @$request->get_parameter('oauth_nonce');

        $this->check_timestamp($timestamp);
        $this->check_nonce($consumer, $token, $nonce, $timestamp);

        $signature_method = $this->get_signature_method($request);

        $signature = $request->get_parameter('oauth_signature');
        $valid_sig = $signature_method->check_signature(
            $request,
            $consumer,
            $token,
            $signature
        );

        if (!$valid_sig) {
            throw new OAuthException("Invalid signature");
        }
    }

    /**
     * Check that the timestamp is new enough
     *
     * @param $timestamp
     * @throws OAuthException
     */
    private function check_timestamp($timestamp)
    {
        // verify that timestamp is recentish
        $now = time();
        if ($now - $timestamp > $this->timestamp_threshold) {
            throw new OAuthException(
                "Expired timestamp, yours $timestamp, ours $now"
            );
        }
    }

    /**
     * Check that the nonce is not repeated
     *
     * @param $consumer
     * @param $token
     * @param $nonce
     * @param $timestamp
     * @throws OAuthException
     */
    private function check_nonce($consumer, $token, $nonce, $timestamp)
    {
        // verify that the nonce is uniqueish
        $found = $this->data_store->lookup_nonce(
            $consumer,
            $token,
            $nonce,
            $timestamp
        );
        if ($found) {
            throw new OAuthException("Nonce already used: $nonce");
        }
    }
}
