<?php

namespace classes;

require_once WD . '/modules/motorapi/motorapi_c.php';
require_once WD . '/modules/motorapi/helpers/motorapi_vehicle_types.php';
/** Actions */
require_once WD . '/modules/motorapi/actions/license_plate_lookup_a.php';

use Exception;
use interfaces\motorapi_i;
use motorapi\actions\license_plate_lookup_a;
use motorapi\helpers\motorapi_vehicle_types;
use motorapi\motorapi_c;
use objects\motorapi_lookups_o;

class motorapi implements motorapi_i
{
    /**
     * Configuration of the motorapi module
     * @var motorapi_c
     */
    public motorapi_c $config;
    /**
     * Vehicle types
     * @var motorapi_vehicle_types
     */
    public motorapi_vehicle_types $vehicle_types;

    /**
     * API URL
     * @var string
     */
    private string $api_url = 'https://v1.motorapi.dk/';

    /**
     * ACTION: LICENSE_PLATE_LOOKUP
     * @see license_plate_lookup_a
     * @notation This action is when a license plate lookup request is made, and logs the request in the database
     * @var license_plate_lookup_a
     */
    private license_plate_lookup_a $license_plate_lookup;

    public function __construct()
    {
        $this->config = new motorapi_c();
        $this->vehicle_types = new motorapi_vehicle_types();
        /** Actions */
        $this->license_plate_lookup = new license_plate_lookup_a();
    }

    private static function cleanJSON(mixed $value): string
    {
        if (is_string($value)) {
            // Remove backslashes
            $value = stripslashes($value);
            $value = stripslashes($value);
            // Ensure the string is valid UTF-8
            $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
            // Optionally, you can use regex to remove any non-printable characters
            $value = preg_replace('/[^\x20-\x7E]/', '', $value);
        } else {
            echo 'Unable to perform cleaning on value of type ' . gettype($value) . ': ' . print_r($value, true) . PHP_EOL;
        }
        // Remove any invalid UTF-8 characters
        return $value;
    }

    /**
     * Get the recommended products for a license plate
     * @param string $plate The license plate
     * @return array The recommended products
     * @throws Exception If the module is not enabled
     * @throws Exception If the license plate is invalid
     * @throws Exception If the daily limit is exceeded
     * @throws Exception If the secret key is invalid
     */
    public function getRecommendedProducts(string $plate): array
    {
        self::requireModuleEnabled();
        self::requireValidLicensePlate($plate);
        $data = self::getLicensePlateInformation($plate, false);
        // Get the recommended products for the license plate
        return match ($data->type) {
            // TODO: Add recommended products for each vehicle type, and convert this into an object type.
            $this->vehicle_types->PLATE_TYPE_CAR => [
                1,
                2,
            ],
            $this->vehicle_types->PLATE_TYPE_TRUCK => [
                3,
                4,
            ],
            default => [],
        };
    }

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

    /**
     * @inheritDoc
     */
    function requireValidLicensePlate(string $licensePlate): void
    {
        // Check if the license plate is valid
        if (!preg_match('/^[A-Z0-9]{1,10}$/', $licensePlate)) {
            throw new Exception('Invalid license plate');
        }
    }

    /**
     * @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 getLicensePlateInformation(string $licensePlate, bool $ignoreCache = true): object
    {
        // Check if the license plate information is cached
        if (!$ignoreCache && self::isCached($licensePlate)) {
            // Get the cached result
            $tmp_res = self::getCachedResult($licensePlate);
            if ($tmp_res !== null) {
                return $tmp_res;
            }
        }
        // Get the license plate information from the motorapi
        return $this->sendRequest($licensePlate, 'vehicles', [], 'GET');
    }

    /**
     * @inheritDoc
     */
    function isCached(string $licensePlate): bool
    {
        // Check if the license plate response is stored in the log/local database/cache
        $motorapi_lookups = new motorapi_lookups_o();
        return $motorapi_lookups->isCached($licensePlate);
    }

    /**
     * @inheritDoc
     */
    function getCachedResult(string $licensePlate): ?object
    {
        global /** @var response $response */
        $response;
        // Get the cached result from the log/local database/cache
        $motorapi_lookups = new motorapi_lookups_o();
        // Add the cached value to the meta
        $response->add_meta('cached', true);
        $cleaned_result = self::cleanJSON($motorapi_lookups->getCachedResult($licensePlate)->result->value());
        $object = json_decode($cleaned_result);
        if ($object === null) {
            print_r($cleaned_result);
            throw new Exception('Cached result is not valid JSON');
        }
        return json_decode($cleaned_result);
    }

    /**
     * @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 $licensePlate, string $endpoint, array $data = [], string $method = 'GET'): object
    {
        // Validate the module is enabled
        self::requireModuleEnabled();
        // Validate the license plate
        self::requireValidLicensePlate($licensePlate);
        // Validate the daily limit
        self::requireDailyLimitNotExceeded();
        // Validate the secret key
        self::requireValidSecretKey();
        // Send the request
        $response = match ($method) {
            'GET' => self::sendGetRequest($licensePlate, $endpoint, $data),
            'POST' => self::sendPostRequest($licensePlate, $endpoint, $data),
            'PUT' => self::sendPutRequest($licensePlate, $endpoint, $data),
            'DELETE' => self::sendDeleteRequest($licensePlate, $endpoint, $data),
            default => self::exception(
                $licensePlate, [
                'method' => $method,
                'endpoint' => $endpoint,
                'data' => $data,
                'license_plate' => $licensePlate,
                'response' => null,
                'status_code' => 400,
                'error' => 'Invalid request method',
            ],
                500
            ),
        };
        // Add the request to the log
        self::addRequestToLog($licensePlate, $endpoint, $response);
        // Return the response
        return $response;
    }

    /**
     * @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 motorapi request log that was made today
        $motorapi_lookups = new motorapi_lookups_o();
        $motorapi_lookups->getTodayCount();
        return $motorapi_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 (motorapi_secret_key_c)',
                ],
                500
            );
        }
    }

    /**
     * @throws Exception
     */
    function exception(string $licensePlate, array $data = [], int $status_code = 500): exception
    {
        // Add the request to the log
        $this->license_plate_lookup->license_plate_lookup($licensePlate, $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 $licensePlate, string $endpoint, array $data): object
    {
        // 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 . '/' . $licensePlate . $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(
                $licensePlate, [
                'method' => 'GET',
                'endpoint' => $endpoint,
                'data' => $data,
                'license_plate' => $licensePlate,
                '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(
                $licensePlate, [
                'method' => 'GET',
                'endpoint' => $endpoint,
                'data' => $data,
                'license_plate' => $licensePlate,
                'response' => null,
                'status_code' => $status_code,
                'error' => $exception_message,
            ],
                $status_code
            );
        }
        $this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), $status_code);
        return json_decode($output);
    }

    /**
     * @inheritDoc
     */
    function sendPostRequest(string $licensePlate, string $endpoint, array $data): object
    {
        // Send a POST request to the motorapi
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        curl_close($ch);
        $this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), 200);
        return json_decode($output);
    }

    /**
     * @inheritDoc
     */
    function sendPutRequest(string $licensePlate, string $endpoint, array $data): object
    {
        // Send a PUT request to the motorapi
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        curl_close($ch);
        $this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), 200);
        return json_decode($output);
    }

    /**
     * @inheritDoc
     */
    function sendDeleteRequest(string $licensePlate, string $endpoint, array $data): object
    {
        // Send a DELETE request to the motorapi
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $output = curl_exec($ch);
        curl_close($ch);
        $this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), 200);
        return json_decode($output);
    }

    /**
     * @inheritDoc
     * @throws Exception
     */
    function addRequestToLog(string $licensePlate, string $endpoint, object $response): void
    {
        // Add the request to the motorapi request log
        $motorapi_lookups = new motorapi_lookups_o();
        $motorapi_lookups->add($licensePlate, json_encode($response), $endpoint);
    }
}