<?php

namespace CleantalkSP\Common;

use CleantalkSP\SpbctWP\Helpers\IP;
use CleantalkSP\Variables\Request;
use CleantalkSP\SpbctWP\State;

class RemoteCalls
{
    /**
     * State object
     *
     * @var \CleantalkSP\SpbctWP\State
     */
    protected $state;

    /**
     * Daughter class name
     *
     * @var string
     */
    protected $class_name;

    /**
     * Is the remote call fires without token, is this remote call will be allowed?
     *
     * @var bool
     */
    protected $without_token;

    protected static $allowedActionsWithoutToken = [
        'post_api_key',
    ];

    const COOLDOWN = 10;

    /**
     * Checking if the current request is the Remote Call
     *
     * @return bool
     */
    public static function check()
    {
        return Request::getString('spbc_remote_call_token')
            ? self::checkWithToken()
            : self::checkWithoutToken();
    }

    public static function checkWithToken()
    {
        return Request::getString('spbc_remote_call_token') &&
               Request::getString('spbc_remote_call_action') &&
               in_array(Request::getString('plugin_name'), array('security', 'spbc'));
    }

    public static function checkWithoutToken()
    {
        global $spbc;

        $rc_servers = [
            'netserv3.cleantalk.org',
            'netserv4.cleantalk.org',
        ];

        return ! $spbc->key_is_ok &&
               Request::getString('spbc_remote_call_action') &&
               in_array(Request::getString('plugin_name'), array('security', 'spbc')) &&
               in_array(IP::resolve(IP::get('remote_addr')), $rc_servers, true);
    }

    private static function isAllowedWithoutToken($rc)
    {
        return in_array($rc, self::$allowedActionsWithoutToken, true);
    }

    /**
     * Execute corresponding method of RemoteCalls if exists
     *
     * @return void
     */
    public function process()
    {
        $action = strtolower(Request::getString('spbc_remote_call_action'));
        $token  = strtolower(Request::getString('spbc_remote_call_token'));
        $method = 'action__' . $action;

        if (isset($this->state->remote_calls[$action])) {
            $cooldown = isset($this->state->remote_calls[$action]['cooldown'])
                ? $this->state->remote_calls[$action]['cooldown']
                : self::COOLDOWN;

            // Return OK for test remote calls
            if (Request::getString('test')) {
                die('OK');
            }

            $is_locked = static::hasRateOverlimit(
                (int)$cooldown,
                $action,
                (int)$this->state->remote_calls[$action]['last_call']
            );

            if (!$is_locked) {
                $this->state->remote_calls[$action]['last_call'] = time();
                $this->state->save('remote_calls', true, false);

                // Check API key
                if (
                    ($this->state->data['key_is_ok'] !== false && $this->state->api_key !== '') &&
                    (
                        ( $token === strtolower(md5($this->state->api_key)) ||
                        $token === strtolower(hash('sha256', $this->state->api_key)) ) ||
                        ( $this->without_token && self::isAllowedWithoutToken($action) )
                    )
                ) {
                    // Flag to let plugin know that Remote Call is running.
                    $this->state->rc_running = true;

                    if (method_exists($this->class_name, $method)) {
                        // Perform action from a daughter class
                        $out = static::filter_before_action();
                        if (! $out) {
                            // Every remote call action handler should implement output or
                            // If out is empty(), the execution will continue
                            $out = static::$method();
                        }
                    } else {
                        $out = 'FAIL ' . json_encode(array('error' => 'UNKNOWN_ACTION_METHOD'));
                    }
                } else {
                    $out = 'FAIL ' . json_encode(array('error' => 'WRONG_TOKEN'));
                }
            } else {
                $out = 'FAIL ' . json_encode(array('error' => 'TOO_MANY_ATTEMPTS'));
            }
        } else {
            $out = 'FAIL ' . json_encode(array('error' => 'UNKNOWN_ACTION'));
        }

        if ($out) {
            die((string)$out);
        }
    }

    /**
     * @return bool|string[] This string[] return made for Psalm competition to avoid invalid inherited return type
     */
    protected static function filter_before_action() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps
    {
        return false;
    }

    public static function buildParameters($rc_action, $plugin_name, $api_key, $additional_params)
    {
        return array_merge(
            array(
                'spbc_remote_call_token'  => md5($api_key),
                'spbc_remote_call_action' => $rc_action,
                'plugin_name'             => $plugin_name,
            ),
            $additional_params
        );
    }

    /**
     * Performs remote call to the current website
     *
     * @param string $host
     * @param string $rc_action
     * @param string $plugin_name
     * @param string $api_key
     * @param array  $params
     * @param array  $patterns
     * @param bool   $do_check Perform check before main remote call or not
     *
     * @return bool|string[]
     * @psalm-suppress PossiblyUnusedMethod
     */
    public static function perform($host, $rc_action, $plugin_name, $api_key, $params, $patterns = array(), $do_check = true)
    {
        $params = static::buildParameters($rc_action, $plugin_name, $api_key, $params);

        // Async RC is fire-and-forget; a sync test needs a 2nd PHP worker and deadlocks on low pm.max_children (e.g. Local WP).
        if ($do_check && ! in_array('async', $patterns, true)) {
            $result__rc_check_website = static::performTest($host, $params, $patterns);
            if (! empty($result__rc_check_website['error'])) {
                return $result__rc_check_website;
            }
        }

        $http = new \CleantalkSP\Common\HTTP\Request();

        return $http
            ->setUrl($host)
            ->setData($params)
            ->setPresets($patterns)
            ->request();
    }

    /**
     * Performs test remote call to the current website
     * Expects 'OK' string as good response
     *
     * @param string $host
     * @param array  $params
     * @param array  $patterns
     *
     * @return array|bool|string
     */
    public static function performTest($host, $params, $patterns = array())
    {
        // Delete async pattern to get the result in this process
        $key = array_search('async', $patterns, true);
        if ($key) {
            unset($patterns[$key]);
        }

        // Adding test flag
        $params = array_merge($params, array('test' => 'test'));

        // Perform test request
        $http   = new \CleantalkSP\Common\HTTP\Request();
        $result = $http
            ->setUrl($host)
            ->setData($params)
            ->setPresets($patterns)
            ->request();

        $error_template = 'WRONG_SITE_RESPONSE TEST_ACTION FOR RC %s, ERROR: %s';

        // Considering empty response as error
        if ($result === '') {
            $error = sprintf($error_template, $params['spbc_remote_call_action'], 'EMPTY RESPONSE');
        // Wrap and pass error
        } elseif (! empty($result['error'])) {
            $error = sprintf($error_template, $params['spbc_remote_call_action'], $result['error']);
            // Expects 'OK' string as good response otherwise - error
        } elseif (is_string($result) && ! preg_match('@^.*?OK$@', $result)) {
            $unexpected_response = 'UNEXPECTED RESPONSE: "' . json_encode(substr($result, 0, 400)) . '"';
            $error = sprintf($error_template, $params['spbc_remote_call_action'], $unexpected_response);
        }

        if (!empty($error)) {
            $result = array('error' => $error);
        }

        return $result;
    }

    /**
     * Check if calls limit reached.
     * @param int $cooldown timeframe in seconds
     * @param string $_action action name
     * @param int $_last_call last call timestamp
     * @return bool True if difference between current timestamp and last call is greater than cooldown, false otherwise.
     */
    public static function hasRateOverlimit($cooldown, $_action = '', $_last_call = 0)
    {
        return (time() - $_last_call) < $cooldown;
    }
}
