<?php

/**
 * API core functions.
 *
 * @package northmill-online-shared
 */
declare (strict_types=1);
namespace Northmill_Online_Shared_Sync\Northmill_Online_Shared\Abstracts\Hub_API;

use InvalidArgumentException;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Helpers\General_Helpers;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Helpers\Hook_Helpers;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Helpers\HTTP_Request_Helpers;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Abstract_Shared_Base;
/**
 * Class Abstract_API_Base
 */
abstract class Abstract_API_Base
{
    /**
     * Default time to sleep before API request, in milliseconds.
     */
    const DEFAULT_PRE_API_SLEEP_MS = 200;
    const API_URL_PROD_INTERNAL = 'https://intapi.prod.moreflo.com/';
    const API_URL_TEST_INTERNAL = 'https://intapi.beta.moreflo.com/';
    const API_URL_PROD_EXTERNAL = 'https://api.moreflo.com/';
    const API_URL_TEST_EXTERNAL = 'https://test.api.moreflo.com/';
    /**
     * API URL.
     *
     * @var string $url
     */
    protected string $url;
    /**
     * API account username.
     *
     * @var string $user
     */
    protected string $user;
    /**
     * API account password.
     *
     * @var string $pass
     */
    protected string $pass;
    /**
     * Log channel.
     *
     * @var string $log_channel
     */
    protected string $log_channel;
    /**
     * Construct the API object.
     *
     * @param string $url         The API URL.
     * @param string $user        The API account username.
     * @param string $password    The API account password.
     * @param string $log_channel The log channel.
     */
    public function __construct(string $url, string $user, string $password, string $log_channel = 'sync')
    {
        $this->log_channel = $log_channel;
        $this->url = $url;
        $this->user = $user;
        $this->pass = $password;
    }
    /**
     * Make API request.
     *
     * @param string            $method   HTTP Method. One of: GET, POST, PUT, PATCH, DELETE.
     * @param string            $endpoint API endpoint. For example, 'articles'.
     * @param string|array|null $body     Request body. Strings are passed in the request body as is, arrays are converted to query string.
     * @param null|string       $version  API version. For example, 'v2'.
     * @param array             $headers  Request headers.
     *
     * @return false|string     Response body, or false on failure.
     */
    protected function request(string $method, string $endpoint, null|string|array $body = null, ?string $version = null, array $headers = array()): false|string
    {
        if (empty($this->url) || empty($this->user) || empty($this->pass)) {
            if (!empty($this->url) || !Abstract_Shared_Base::base_instance()->nm_hosting || !is_multisite() || !is_main_site()) {
                Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'Invalid API credentials', array('url' => $this->url, 'user' => $this->user, 'method' => $method, 'endpoint' => $endpoint), Log_Level_Enum::ERROR);
            }
            return \false;
        }
        $method = strtoupper($method);
        $endpoint = strtolower($endpoint);
        $version = is_string($version) ? strtolower($version) : $this->get_default_api_version();
        if ($version) {
            $version .= '/';
        }
        $request_url = $this->build_request_url($version, $endpoint);
        $headers['Authorization'] = $this->get_basic_auth();
        $headers['Content-Type'] = 'application/json; charset=utf-8';
        $params = array('method' => $method, 'headers' => $headers, 'timeout' => 30);
        if (!empty($body)) {
            if (is_string($body)) {
                $params['body'] = $body;
            } else {
                // Is array.
                $request_url .= '?' . http_build_query($body);
            }
        }
        Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'API request start - ' . $method . ' ' . $endpoint, array('method' => $method, 'endpoint' => $endpoint, 'body' => $body, 'headers' => $headers, 'url' => $request_url, 'user' => $this->user), Log_Level_Enum::DEBUG);
        usleep(self::DEFAULT_PRE_API_SLEEP_MS * 1000);
        // Quiet down api calls.
        $response = $this->get_request_response($request_url, $params);
        Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'API request end - ' . $method . ' ' . $endpoint, null, Log_Level_Enum::DEBUG);
        return $response;
    }
    /**
     * Builds the full request URL.
     *
     * @param string $version  API version to use in the request URL.
     * @param string $endpoint API endpoint to use in the request URL.
     *
     * @return string The fully constructed request URL.
     */
    protected function build_request_url(string $version, string $endpoint): string
    {
        return sprintf('%s%s%s/', $this->url, $version, $endpoint);
    }
    /**
     * Retrieve the default API version.
     *
     * @return string The default API version.
     */
    protected function get_default_api_version(): string
    {
        return 'v2';
    }
    /**
     * Get the response from the API request, decoded as JSON to array.
     *
     * @param string            $method   HTTP Method. One of: GET, POST, PUT, PATCH, DELETE.
     * @param string            $endpoint API endpoint. For example, 'articles'.
     * @param null|string|array $body     Request body.
     * @param null|string       $version  API version. For example, 'v2'.
     * @param array             $headers  Request headers.
     *
     * @return ?array
     */
    protected function request_array(string $method, string $endpoint, null|string|array $body = '', ?string $version = null, array $headers = array()): ?array
    {
        $response = $this->request($method, $endpoint, $body, $version, $headers);
        if ($response) {
            $data = json_decode($response, \true);
            if (is_array($data)) {
                return $data;
            }
        }
        return null;
    }
    /**
     * Get the response from the API request, decoded as JSON to array.
     *
     * @param string            $method   HTTP Method. One of: GET, POST, PUT, PATCH, DELETE.
     * @param string            $endpoint API endpoint. For example 'articles'.
     * @param null|string|array $body     Request body.
     * @param null|string       $version  API version. For example 'v2'.
     * @param array             $headers  Request headers.
     *
     * @return ?object
     */
    protected function request_object(string $method, string $endpoint, null|string|array $body = '', ?string $version = null, array $headers = array()): ?object
    {
        $response = $this->request($method, $endpoint, $body, $version, $headers);
        if ($response) {
            $data = json_decode($response);
            if (is_object($data)) {
                /**
                 * Data is an object.
                 *
                 * @var object $data
                 */
                return $data;
            }
        }
        return null;
    }
    /**
     * Get API request response.
     *
     * @param string $request_url Request URL.
     * @param array  $params      Request parameters.
     * @param int    $retry_count Retry count.
     *
     * @return false|string Response body, or false on failure.
     */
    private function get_request_response(string $request_url, array $params, int $retry_count = 0): false|string
    {
        $request = wp_remote_request(esc_url_raw($request_url), $params);
        $status_code = wp_remote_retrieve_response_code($request);
        if (is_wp_error($request)) {
            Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'API Request failed', array('request_url' => $request_url, 'params' => $params, 'error' => $request->get_error_messages()), Log_Level_Enum::ERROR);
        } elseif (!empty($request['body']) && is_string($request['body']) && 200 === $status_code) {
            Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'API successful response.', array('response' => $request['body']), Log_Level_Enum::DEBUG);
            return $request['body'];
        } else {
            $silent_404 = apply_filters('northmill_online_shared_get_request_response_silent_404_' . Abstract_Shared_Base::base_instance()->package_info->get_service_name_underscored(), \false);
            if (404 === $status_code && $silent_404) {
                Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'Silent 404.', null, Log_Level_Enum::DEBUG);
            } else {
                Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'API Request failed with response code', array('status_code' => $status_code, 'request_url' => $request_url, 'params' => $params, 'request_body' => $request['body'] ?? null, 'retry_count' => $retry_count), Log_Level_Enum::ERROR);
            }
        }
        $retry_limit = 5;
        $retry_codes = array(
            // Array of request response codes that should trigger retry.
            429 => 'API Rate limit reached',
        );
        if (!empty($retry_codes[$status_code])) {
            if ($retry_count <= $retry_limit) {
                $seconds_to_sleep = 1;
                Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'API Rate limit reached, retrying after short pause', array('code' => $status_code, 'message' => $retry_codes[$status_code], 'error' => $request->get_error_messages(), 'seconds_to_sleep' => $seconds_to_sleep, 'retry_count' => $retry_count), Log_Level_Enum::ERROR);
                usleep(1000 * 1000 * $seconds_to_sleep);
                ++$retry_count;
                return $this->get_request_response($request_url, $params, $retry_count);
            } else {
                Abstract_Shared_Base::base_instance()->logger->log($this->log_channel, 'Request retry limit reached', array(), Log_Level_Enum::ERROR);
                return \false;
            }
        }
        return \false;
    }
    /**
     * Get an image file from API.
     *
     * @param string      $image_url  Image URL.
     * @param string|null $image_type Image type.
     * @param int         $sleep      Sleep time in milliseconds before API request.
     *
     * @return false|string
     */
    public function get_image_file(string $image_url, ?string $image_type = null, int $sleep = self::DEFAULT_PRE_API_SLEEP_MS): false|string
    {
        if (empty($this->user) || empty($this->pass)) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'No valid API credentials when trying to get image file', array('user' => $this->user ?? null), Log_Level_Enum::ERROR);
            return \false;
        }
        usleep($sleep * 1000);
        // Quiet down api calls.
        $headers = array('Authorization' => $this->get_basic_auth());
        if ($image_type) {
            $headers['Content-Type'] = $image_type;
        }
        $response = wp_remote_get($image_url, array('headers' => $headers, 'timeout' => 30));
        if (is_wp_error($response) || empty($response['body'])) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Failed to fetch image file', array('user' => $this->user, 'error' => is_wp_error($response) ? $response->get_error_message() : 'Empty body'), Log_Level_Enum::ERROR);
            return \false;
        }
        return $response['body'];
    }
    /**
     * Get API URL options.
     *
     * @param string $env Environment to get options for.
     *
     * @return array
     */
    public static function get_api_url_options(string $env): array
    {
        $urls = match ($env) {
            'prod' => array(self::API_URL_PROD_EXTERNAL),
            'test' => array(self::API_URL_TEST_EXTERNAL),
            default => array(),
        };
        if (Abstract_Shared_Base::base_instance()->nm_hosting) {
            switch ($env) {
                case 'prod':
                    array_unshift($urls, self::API_URL_PROD_INTERNAL);
                    break;
                case 'test':
                    array_unshift($urls, self::API_URL_TEST_INTERNAL);
                    break;
            }
        }
        $custom_urls = null;
        switch ($env) {
            case 'prod':
                $custom_urls = General_Helpers::get_constant('NM_SHARED_HUB_ACCOUNTS_PROD_API_URLS', array());
                break;
            case 'test':
                $custom_urls = General_Helpers::get_constant('NM_SHARED_HUB_ACCOUNTS_TEST_API_URLS', array());
                break;
        }
        if (!empty($custom_urls)) {
            $custom_urls = explode(',', $custom_urls);
            $urls = array_merge($urls, $custom_urls);
        }
        $urls = Hook_Helpers::apply_scoped_filters('northmill_online_shared_hub_accounts_api_url_options', $urls, array($env));
        $urls = Hook_Helpers::apply_scoped_filters('northmill_online_shared_hub_accounts_api_url_options_' . $env, $urls);
        foreach ($urls as &$url) {
            $url = General_Helpers::ensure_trailing_slash($url);
        }
        return $urls;
    }
    /**
     * Get default API URL.
     *
     * @param string $env Environment to get URL for.
     *
     * @return string
     *
     * @throws InvalidArgumentException On invalid environment.
     *
     * @used Northmill Booking
     */
    public static function get_default_api_url(string $env = 'prod'): string
    {
        if (Abstract_Shared_Base::base_instance()->nm_hosting) {
            switch ($env) {
                case 'prod':
                    return self::API_URL_PROD_INTERNAL;
                case 'test':
                    return self::API_URL_TEST_INTERNAL;
            }
        } else {
            switch ($env) {
                case 'prod':
                    return self::API_URL_PROD_EXTERNAL;
                case 'test':
                    return self::API_URL_TEST_EXTERNAL;
            }
        }
        throw new InvalidArgumentException('Invalid environment: ' . esc_html($env));
    }
    /**
     * Generate Basic Auth header value for API.
     *
     * @return string
     */
    protected function get_basic_auth(): string
    {
        return HTTP_Request_Helpers::basic_auth($this->user, $this->pass);
    }
}
