<?php

namespace classes;

require_once WD . '/modules/fxratesapi/fxratesapi_c.php';
/** Actions */
require_once WD . '/modules/fxratesapi/actions/convert_rate_a.php';

use Exception;
use fxratesapi\actions\convert_rate_a;
use fxratesapi\fxratesapi_c;
use interfaces\fxratesapi_i;
use objects\fxratesapi_conversion_rates_o;

class fxratesapi implements fxratesapi_i
{
    /**
     * Configuration of the fxratesapi module
     * @var fxratesapi_c
     */
    public fxratesapi_c $config;

    /**
     * API URL
     * @var string
     */
    private string $api_url = 'https://api.fxratesapi.com/';

    /**
     * ACTION: CONVERT RATE
     * @see convert_rate_a
     * @notation This action is when a currency conversion request is made, and logs the request in the database
     * @var convert_rate_a
     */
    private convert_rate_a $convert_rate;

    public function __construct()
    {
        $this->config = new fxratesapi_c();
        /** Actions */
        $this->convert_rate = new convert_rate_a();
    }

    /**
     * @inheritDoc
     */
    function isCached(string $base, string $target): bool
    {
        // Check if the base currency is valid
        self::requireValidCurrency($base);
        // Check if the target currency is valid
        self::requireValidCurrency($base);
        // Check if the request is cached
        $fxratesapi_lookups = new fxratesapi_conversion_rates_o();
        return $fxratesapi_lookups->isCached($base, $target);
    }

    /**
     * @inheritDoc
     */
    function requireValidCurrency(string $currency): void
    {
        // Check if the currency is valid
        if (!preg_match('/^[A-Z]{3}$/', $currency)) {
            throw new Exception('Invalid currency');
        }
    }

    /**
     * @inheritDoc
     */
    function getCachedResponse(string $base, string $target): object
    {
        // Check if the base and target currencies are valid
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);

        // Get the cached response from the fxratesapi request log
        $fxratesapi_lookups = new fxratesapi_conversion_rates_o();
        $fxratesapi_lookups->getCachedResponse($base, $target);
        return $fxratesapi_lookups;
    }

    /**
     * @inheritDoc
     * @throws Exception If the base or target currency is invalid
     * @throws Exception If the endpoint is invalid
     * @throws Exception If the response is invalid
     * @throws Exception If the response is not valid JSON
     * @throws Exception If the request fails
     */
    function getConversionRate(string $base, string $target, string $endpoint, array $data): object
    {
        // Check if the base and target currencies are valid
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);

        // Send the request
        // Return the conversion rate
        return self::sendRequest($base, $target, $endpoint, $data);
    }

    /**
     * @inheritDoc
     * @throws Exception If the module is not enabled, the license plate is invalid, the daily limit is exceeded, or the secret key is invalid
     */
    function sendRequest(
        string $base,
        string $target,
        string $endpoint,
        array  $data = [],
        string $method = 'GET'
    ): object
    {
        // Validate the module is enabled
        self::requireModuleEnabled();
        // Validate the base and target currencies
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);
        // Validate the daily limit
        self::requireDailyLimitNotExceeded();
        // Validate the secret key
        self::requireValidSecretKey();
        // Send the request
        $response = match ($method) {
            'GET' => self::sendGetRequest($base, $target, $endpoint, $data),
            'POST' => self::sendPostRequest($base, $target, $endpoint, $data),
            'PUT' => self::sendPutRequest($base, $target, $endpoint, $data),
            'DELETE' => self::sendDeleteRequest($base, $target, $endpoint, $data),
            default => self::exception(
                $base,
                $target,
                [
                    'method' => $method,
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'license_plate' => $base,
                    'response' => null,
                    'status_code' => 400,
                    'error' => 'Invalid request method',
                ],
                500
            ),
        };
        // Add the usage to the request log
        $this->convert_rate->convert_rate($base, $target, $response, 200);
        // Add the request to the log
        self::addRequestToLog($base, $target, $endpoint, $response);
        // Return the response
        return $response;
    }

    /**
     * @inheritDoc
     */
    function requireModuleEnabled(): void
    {
        // Check if the module is enabled
        if (!$this->config->enabled->isTrue()) {
            throw new Exception('The fxratesapi module is not enabled');
        }
    }

    /**
     * @inheritDoc
     */
    function requireDailyLimitNotExceeded(): void
    {
        // Check if the daily limit is exceeded
        if ($this->getDailyRequestCounter() >= $this->config->daily_limit->getVariableValue()) {
            throw new Exception('Daily limit exceeded');
        }
    }

    /**
     * @inheritDoc
     */
    function getDailyRequestCounter(): int
    {
        // Count the rows from the fxratesapi request log that was made today
        $fxratesapi_lookups = new fxratesapi_conversion_rates_o();
        $fxratesapi_lookups->getTodayCount();
        return $fxratesapi_lookups->getTodayCount();
    }

    /**
     * @inheritDoc
     */
    function requireValidSecretKey(): void
    {
        // Check if the secret key is valid
        if ($this->config->secret_key->getVariableValue() === null) {
            self::exception(
                '',
                '',
                [
                    'status_code' => 500,
                    'error' => 'Invalid secret key defined in the config (fxratesapi_secret_key_c)',
                ],
                500
            );
        }
    }

    /**
     * @throws Exception
     */
    function exception(string $base, string $target, array $data, int $status_code = 500): object
    {
        // Check if the response is valid JSON
        if (!json_decode($data['response'])) {
            throw new Exception('Invalid response');
        }
        // Add the request to the log
        $this->convert_rate->convert_rate($base, $target, $data, $status_code);
        return throw new Exception($data['error'] ?? 'An error occurred while processing the request, in ' . $this->config->getModuleName() . ' module');
    }

    /**
     * @inheritDoc
     * @throws Exception
     */
    function sendGetRequest(string $base, string $target, string $endpoint, array $data): object
    {
        // Check if the base and target currencies are valid
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);

        // Add the base and target currencies to the data array
        $data['base'] = $base;

        // Build the query string if any data is provided
        $query_string = '';
        if (!empty($data)) {
            $query_string = '?' . http_build_query($data);
        }

        // Initialize cURL session
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $query_string);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        // Set HTTP headers including the X-AUTH-TOKEN
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
        ]);

        // Execute the request and handle response
        $output = curl_exec($ch);

        // Check for errors
        if (curl_errno($ch)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'GET',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => 500,
                    'error' => curl_error($ch),
                ],
                500
            );
        }

        // Get the status code
        $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);
        // Check if the response is valid JSON
        if (!json_decode($output)) {
            $exception_message = match ($status_code) {
                401 => 'Unauthorized',
                404 => 'Not found',
                429 => 'Too many requests',
                default => 'Invalid response',
            };
            self::exception(
                $base,
                $target,
                [
                    'method' => 'GET',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => $status_code,
                    'error' => $exception_message,
                ],
                $status_code
            );
        }
        return json_decode($output);
    }

    /**
     * @inheritDoc
     * @throws Exception If the request fails
     * @throws Exception If the response is invalid
     * @throws Exception If the response is not valid JSON
     * @throws Exception If the base or target currency is invalid
     * @throws Exception If the endpoint is invalid
     */
    function sendPostRequest(string $base, string $target, string $endpoint, array $data): object
    {
        // Check if the base and target currencies are valid
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);

        // Initialize cURL session
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $base . '/' . $target);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        // Set HTTP headers including the X-AUTH-TOKEN
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
        ]);

        // Execute the request and handle response
        $output = curl_exec($ch);

        // Check for errors
        if (curl_errno($ch)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'POST',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => 500,
                    'error' => curl_error($ch),
                ],
                500
            );
        }

        // Get the status code
        $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);
        // Check if the response is valid JSON
        if (!json_decode($output)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'POST',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => $status_code,
                    'error' => 'Invalid response',
                ],
                500
            );
        }
        return json_decode($output);
    }

    /**
     * @inheritDoc
     * @throws Exception If the request fails
     * @throws Exception If the response is invalid
     * @throws Exception If the response is not valid JSON
     * @throws Exception If the base or target currency is invalid
     * @throws Exception If the endpoint is invalid
     */
    function sendPutRequest(string $base, string $target, string $endpoint, array $data): object
    {
        // Check if the base and target currencies are valid
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);

        // Initialize cURL session
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $base . '/' . $target);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        // Set HTTP headers including the X-AUTH-TOKEN
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
        ]);

        // Execute the request and handle response
        $output = curl_exec($ch);

        // Check for errors
        if (curl_errno($ch)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'PUT',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => 500,
                    'error' => curl_error($ch),
                ],
                500
            );
        }

        // Get the status code
        $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);
        // Check if the response is valid JSON
        if (!json_decode($output)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'PUT',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => $status_code,
                    'error' => 'Invalid response',
                ],
                500
            );
        }
        return json_decode($output);
    }

    /**
     * @inheritDoc
     * @throws Exception If the request fails
     * @throws Exception If the response is invalid
     * @throws Exception If the response is not valid JSON
     * @throws Exception If the base or target currency is invalid
     * @throws Exception If the endpoint is invalid
     */
    function sendDeleteRequest(string $base, string $target, string $endpoint, array $data): object
    {
        // Check if the base and target currencies are valid
        self::requireValidCurrency($base);
        self::requireValidCurrency($target);

        // Initialize cURL session
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $base . '/' . $target);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        // Set HTTP headers including the X-AUTH-TOKEN
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
        ]);

        // Execute the request and handle response
        $output = curl_exec($ch);

        // Check for errors
        if (curl_errno($ch)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'DELETE',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => 500,
                    'error' => curl_error($ch),
                ],
                500
            );
        }

        // Get the status code
        $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);
        // Check if the response is valid JSON
        if (!json_decode($output)) {
            self::exception(
                $base,
                $target,
                [
                    'method' => 'DELETE',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => null,
                    'status_code' => $status_code,
                    'error' => 'Invalid response',
                ],
                500
            );
        }
        return json_decode($output);
    }

    /**
     * @inheritDoc
     * @throws Exception
     */
    function addRequestToLog(string $base, string $target, string $endpoint, object $response): void
    {
        // Add the request to the fxratesapi request log
        $fxratesapi_lookups = new fxratesapi_conversion_rates_o();
        $fxratesapi_lookups->add($base, $target, json_encode($response), $endpoint);
        // Check if the request was added successfully
        if (!$fxratesapi_lookups->id) {
            throw new Exception('Failed to add request to log');
        }
    }
}