<?php

namespace classes;

require_once WD . '/modules/shelly/shelly_c.php';
/** Actions */
require_once WD . '/modules/shelly/actions/shelly_search_a.php';

use Exception;
use interfaces\shelly_i;
use shelly\actions\shelly_search_a;
use shelly\shelly_c;

class shelly implements shelly_i
{
    private const SHELLY_RATE_LIMIT_WAIT_TIMEOUT_SECONDS = 20;
    private const SHELLY_RATE_LIMIT_WINDOW_MILLISECONDS = 2000;
    private const SHELLY_RATE_LIMIT_GATE_KEY = 'shelly_cloud_rate_limit_gate';
    private const SHELLY_CONNECT_TIMEOUT_SECONDS = 2;
    private const SHELLY_REQUEST_TIMEOUT_SECONDS = 5;
    /**
     * @var array<int,array<string,mixed>>
     */
    private static array $blocked_request_log = [];

    /**
     * Configuration of the shelly module
     * @var shelly_c
     */
    public shelly_c $config;

    /**
     * ACTION: shelly_SEARCH
     * @see shelly_search_a
     * @notation This action is when a company search request is made, and logs the request in the database
     * @var shelly_search_a $shelly_search
     */
    private shelly_search_a $shelly_search;

    public function __construct()
    {
        $this->config = new shelly_c();
        /** Actions */
        $this->shelly_search = new shelly_search_a();
    }

    public static function resetBlockedRequestLog(): void
    {
        self::$blocked_request_log = [];
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public static function blockedRequestLog(): array
    {
        return self::$blocked_request_log;
    }

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

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


    /**
     * @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 (shelly_secret_key_c)',
                ],
                500
            );
        }
    }

    /**
     * @throws Exception
     */
    function exception(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->shelly_search->shelly_search($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 requireValidServerURL(): void
    {
        // Check if the server URL is valid
        if ($this->config->server_url->getVariableValue() === null) {
            self::exception(
                [
                    'status_code' => 500,
                ]
            );
        }
    }

    /**
     * @inheritDoc
     * @throws Exception
     */
    function sendPostRequest(string $endpoint, array $data): array|object|null
    {
        /**
         * curl -X POST 'https://<HOST>/v2/devices/api/get?auth_key=<AUTH_KEY>' \
         * -H 'Content-Type: application/json' \
         * -d '<BODY>'
         */
        $this->guardRealShellyRequest('POST', $endpoint, $data);
        // Require the module to be enabled
        self::requireModuleEnabled();
        self::requireValidSecretKey();
        self::requireValidServerURL();
        $this->waitForShellyRateLimitWindow();
        // Send the request
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, self::appendAuthKeyToQuery($this->config->server_url->getVariableValue() . $endpoint));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
        ]);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::SHELLY_CONNECT_TIMEOUT_SECONDS);
        curl_setopt($ch, CURLOPT_TIMEOUT, self::SHELLY_REQUEST_TIMEOUT_SECONDS);
        curl_setopt($ch, CURLOPT_NOSIGNAL, true);
        // Execute the request
        $response = curl_exec($ch);
        // Get the status code
        $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        // Check for errors
        if (curl_errno($ch)) {
            self::exception(
                [
                    'method' => 'POST',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => $response,
                ],
                $status_code
            );
        }
        // Close the cURL session
        curl_close($ch);
        // Check if there is a response (some endpoints don't return a response)
        if ($response === false) {
            return null;
        }
        // Return the response
        return json_decode($response);
    }

    /**
     * @inheritDoc
     * @throws Exception
     */
    function sendGetRequest(string $endpoint, array $data): array|object|null
    {
        $this->guardRealShellyRequest('GET', $endpoint, $data);
        self::requireModuleEnabled();
        self::requireValidSecretKey();
        self::requireValidServerURL();
        $this->waitForShellyRateLimitWindow();

        $ch = curl_init();
        curl_setopt(
            $ch,
            CURLOPT_URL,
            self::appendAuthKeyToQuery($this->config->server_url->getVariableValue() . $endpoint, $data)
        );
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPGET, true);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::SHELLY_CONNECT_TIMEOUT_SECONDS);
        curl_setopt($ch, CURLOPT_TIMEOUT, self::SHELLY_REQUEST_TIMEOUT_SECONDS);
        curl_setopt($ch, CURLOPT_NOSIGNAL, true);

        $response = curl_exec($ch);
        $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        if (curl_errno($ch)) {
            self::exception(
                [
                    'method' => 'GET',
                    'endpoint' => $endpoint,
                    'data' => $data,
                    'response' => $response,
                ],
                $status_code
            );
        }

        curl_close($ch);

        if ($response === false) {
            return null;
        }

        return json_decode($response);
    }

    function appendAuthKeyToQuery(string $url, array $query = []): string
    {
        $query['auth_key'] = $this->config->secret_key->getVariableValue();
        $separator = str_contains($url, '?') ? '&' : '?';

        return $url . $separator . http_build_query($query);
    }

    /**
     * @param array<string,mixed> $data
     * @throws Exception
     */
    private function guardRealShellyRequest(string $method, string $endpoint, array $data): void
    {
        if (!$this->shouldBlockRealShellyRequest()) {
            return;
        }

        $record = [
            'method' => strtoupper($method),
            'endpoint' => $endpoint,
            'data' => $data,
            'at' => date('c'),
        ];
        self::$blocked_request_log[] = $record;

        $log_path = trim((string)(getenv('TRUCKWASH_TEST_SHELLY_GUARD_LOG') ?: ''));
        if ($log_path !== '') {
            $encoded = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if (is_string($encoded)) {
                @file_put_contents($log_path, $encoded . PHP_EOL, FILE_APPEND | LOCK_EX);
            }
        }

        throw new Exception('Real Shelly requests are blocked in test mode: ' . strtoupper($method) . ' ' . $endpoint);
    }

    private function shouldBlockRealShellyRequest(): bool
    {
        return trim((string)(getenv('TRUCKWASH_TEST_BLOCK_REAL_SHELLY') ?: '')) === '1';
    }

    /**
     * @throws Exception
     */
    private function waitForShellyRateLimitWindow(): void
    {
        $deadline = $this->nowTimestamp() + self::SHELLY_RATE_LIMIT_WAIT_TIMEOUT_SECONDS;

        do {
            if ($this->tryAcquireShellyRateLimitSlot()) {
                return;
            }

            if ($this->nowTimestamp() >= $deadline) {
                throw new Exception('Shelly rate limit gate wait timed out');
            }

            $remaining_ms = $this->getShellyRateLimitSlotRemainingMs();
            if ($remaining_ms <= 0) {
                $remaining_ms = 50;
            }
            $this->sleepMicroseconds(min($remaining_ms, 250) * 1000);
        } while (true);
    }

    private function tryAcquireShellyRateLimitSlot(): bool
    {
        $redis = $this->redisFacade();
        if ($redis === null) {
            return true;
        }

        try {
            $result = $redis->get_client()->set(
                self::SHELLY_RATE_LIMIT_GATE_KEY,
                (string)$this->nowTimestamp(),
                'PX',
                self::SHELLY_RATE_LIMIT_WINDOW_MILLISECONDS,
                'NX'
            );
            return $result === true || strtoupper((string)$result) === 'OK';
        } catch (\Throwable) {
            // If Redis gate can't be evaluated, fail open to avoid blocking API traffic completely.
            return true;
        }
    }

    private function getShellyRateLimitSlotRemainingMs(): int
    {
        $redis = $this->redisFacade();
        if ($redis === null) {
            return 0;
        }

        try {
            $ttl = $redis->get_client()->pttl(self::SHELLY_RATE_LIMIT_GATE_KEY);
            if (!is_numeric($ttl)) {
                return 0;
            }
            $ttl = (int)$ttl;
            return $ttl > 0 ? $ttl : 0;
        } catch (\Throwable) {
            return 0;
        }
    }

    protected function redisFacade(): mixed
    {
        return defined('redis') ? redis : null;
    }

    protected function nowTimestamp(): float
    {
        return microtime(true);
    }

    protected function sleepMicroseconds(int $microseconds): void
    {
        if ($microseconds > 0) {
            usleep($microseconds);
        }
    }
}
