<?php

namespace classes;
require_once WD . '/modules/licenseplaterecognizer/licenseplaterecognizer_c.php';

use Exception;
use interfaces\licenseplaterecognizer_i;
use licenseplaterecognizer\helpers\licenseplaterecognizer_info;
use licenseplaterecognizer\licenseplaterecognizer_c;

class licenseplaterecognizer implements licenseplaterecognizer_i
{
    private const DEFAULT_API_URL = 'https://vs4sws0kg4sog4ssw8kwowk4.coolify.truckwash.dk';
    private const PLATE_READER_CONFIG_JSON = '{"mode":"fast","plates_per_vehicle":1,"zoom_in_vehicles":0}';
    private const RESULT_CACHE_CONTEXT = '{"config":{"mode":"fast","plates_per_vehicle":1,"zoom_in_vehicles":0},"regions":"dk,de,se,no"}';
    private const PLATE_READER_REGIONS = 'dk,de,se,no';
    private const DEFAULT_UPLOAD_FILE_NAME = 'license-plate.jpg';
    private const RUNTIME_CONFIG_CACHE_TTL_SECONDS = 15;
    private const RUNTIME_CONFIG_REDIS_CACHE_KEY = 'licenseplaterecognizer:runtime_config:v1';
    private const RESULT_CACHE_TTL_SECONDS = 10;
    private const RESULT_CACHE_REDIS_KEY_PREFIX = 'licenseplaterecognizer:result:v1:';
    private const PLATE_READER_CONNECT_TIMEOUT_MS = 1000;
    private const PLATE_READER_TOTAL_TIMEOUT_MS = 4500;
    /**
     * @var array<string, float>
     */
    private array $last_timings = [];

    /**
     * The configuration of the module
     * @var licenseplaterecognizer_c
     */
    public licenseplaterecognizer_c $config;
    /**
     * API URL
     * @var string
     */
    private string $api_url;

    /**
     * @var array{enabled: bool, api_key: string}|null
     */
    private ?array $runtime_config = null;

    /**
     * @var array{values: array{enabled: bool, api_key: string}, cached_at: float}|null
     */
    private static ?array $runtime_config_cache = null;

    public function __construct(bool $load_config = true, ?string $api_url = null)
    {
        $this->api_url = self::normalizeApiUrl($api_url ?? self::configuredApiUrl());

        if ($load_config) {
            $this->config = new licenseplaterecognizer_c();
        }
    }


    /**
     * @inheritDoc
     */
    public function requireModuleEnabled(): void
    {
        if (!$this->runtimeConfig()['enabled']) {
            throw new Exception('licenseplaterecognizer module is not enabled.');
        }
    }

    /**
     * @inheritDoc
     * @throws Exception If the module is not enabled or if there is an error in the API request
     * @throws Exception If the API response cannot be parsed
     */
    public function licenseplaterecognizer(string $base64_image): array
    {
        return $this->recognizePlate(
            fn () => $this->buildPlateReaderPayload($base64_image),
            fn () => $this->buildResultCacheKeyFromUploadString($base64_image)
        );
    }

    public function licenseplaterecognizerUpload(string $image_data, string $mime_type = 'image/jpeg'): array
    {
        return $this->recognizePlate(
            fn () => $this->buildPlateReaderPayloadFromUpload(
                $this->buildUploadValueFromBytes($image_data, $mime_type)
            ),
            fn () => $this->buildResultCacheKeyFromBytes($image_data)
        );
    }

    public function licenseplaterecognizerUploadUncached(string $image_data, string $mime_type = 'image/jpeg'): array
    {
        return $this->recognizePlate(
            fn () => $this->buildPlateReaderPayloadFromUpload(
                $this->buildUploadValueFromBytes($image_data, $mime_type)
            )
        );
    }

    public function licenseplaterecognizerUploadFile(string $image_path, string $mime_type = 'image/jpeg'): array
    {
        return $this->recognizePlate(
            fn () => $this->buildPlateReaderPayloadFromUpload(
                $this->buildUploadValueFromFile($image_path, $mime_type)
            )
        );
    }

    /**
     * @throws Exception
     */
    private function recognizePlate(callable $payload_factory, ?callable $result_cache_key_factory = null): array
    {
        $started_at = microtime(true);
        $this->last_timings = [];
        $result_cache = null;
        $result_cache_key = null;

        try {
            $config_started_at = microtime(true);
            $runtime_config = $this->runtimeConfig();
            if (!$runtime_config['enabled']) {
                throw new Exception('licenseplaterecognizer module is not enabled.');
            }
            $api_key = $runtime_config['api_key'];
            $this->last_timings['config'] = $this->elapsedMs($config_started_at);

            if ($result_cache_key_factory !== null) {
                $cache_started_at = microtime(true);
                try {
                    $result_cache = $this->resultCacheStore();
                    if ($result_cache !== null) {
                        $result_cache_key = $result_cache_key_factory();
                        if ($result_cache_key !== null) {
                            $cached_result = $this->readRecognitionResultCache($result_cache, $result_cache_key);
                            if ($cached_result !== null) {
                                $this->last_timings['cache_hit'] = 1;
                                return $this->completeRecognition($started_at, $cached_result);
                            }
                        }
                    }
                    $this->last_timings['cache_miss'] = 1;
                } finally {
                    $this->last_timings['cache'] = $this->elapsedMs($cache_started_at);
                }
            }

            $payload_started_at = microtime(true);
            $data = $payload_factory();
            $this->last_timings['payload'] = $this->elapsedMs($payload_started_at);

            $ch = curl_init($this->api_url . '/v1/plate-reader/');
            if (!$ch instanceof \CurlHandle) {
                throw new Exception('Error initializing API request.');
            }

            $curl_options = [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_POSTFIELDS => $data,
                CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS,
                CURLOPT_CONNECTTIMEOUT_MS => self::PLATE_READER_CONNECT_TIMEOUT_MS,
                CURLOPT_TIMEOUT_MS => self::PLATE_READER_TOTAL_TIMEOUT_MS,
                CURLOPT_NOSIGNAL => true,
                CURLOPT_NOPROGRESS => false,
                CURLOPT_XFERINFOFUNCTION => self::clientDisconnectAbortCallback(),
                CURLOPT_HTTPHEADER => [
                    "Authorization: Token " . $api_key,
                    'Expect:',
                ],
            ];
            if (defined('CURLOPT_TCP_NODELAY')) {
                $curl_options[(int)constant('CURLOPT_TCP_NODELAY')] = true;
            }
            curl_setopt_array($ch, $curl_options);

            // Submit the POST request and close cURL session handle
            $upstream_started_at = microtime(true);
            $result = curl_exec($ch);
            $this->last_timings['upstream'] = $this->elapsedMs($upstream_started_at);
            $this->recordCurlTimings($ch);
            curl_close($ch);

            // Print the response from the server
            if ($result === false) {
                throw new Exception('Error in API request.');
            }

            $parse_started_at = microtime(true);
            $response_data = json_decode($result, true);
            $this->last_timings['parse'] = $this->elapsedMs($parse_started_at);
            $this->recordResponseTimings($response_data);

            if (isset($response_data['results']) && count($response_data['results']) > 0) {
                $recognized_result = [
                    'success' => true,
                    'license_plate_number' => $response_data['results'][0]['plate'] ?? null,
                    'confidence' => $response_data['results'][0]['score'] ?? null,
                ];

                $this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);

                return $this->completeRecognition($started_at, $recognized_result);
            }

            $recognized_result = [
                'success' => false,
                'message' => 'No license plate detected.',
            ];
            $this->writeRecognitionResultCache($result_cache, $result_cache_key, $recognized_result);

            return $this->completeRecognition($started_at, $recognized_result);
        } catch (\Throwable $exception) {
            $this->last_timings['total'] = $this->elapsedMs($started_at);
            throw $exception;
        }
    }

    private function completeRecognition(float $started_at, array $result): array
    {
        $this->last_timings['total'] = $this->elapsedMs($started_at);
        return $result;
    }

    private static function clientDisconnectAbortCallback(): callable
    {
        return static function (): int {
            return connection_aborted() ? 1 : 0;
        };
    }

    public function getLastTimings(): array
    {
        return $this->last_timings;
    }

    private function elapsedMs(float $started_at): float
    {
        return (microtime(true) - $started_at) * 1000;
    }

    private static function configuredApiUrl(): string
    {
        $configured = getenv('PLATE_RECOGNIZER_API_URL');
        if ($configured === false || trim((string)$configured) === '') {
            $configured = $_ENV['PLATE_RECOGNIZER_API_URL'] ?? $_SERVER['PLATE_RECOGNIZER_API_URL'] ?? self::DEFAULT_API_URL;
        }

        return (string)$configured;
    }

    public static function configuredApiBaseUrl(): string
    {
        return self::normalizeApiUrl(self::configuredApiUrl());
    }

    private static function normalizeApiUrl(string $api_url): string
    {
        $api_url = trim($api_url);
        if ($api_url === '') {
            return self::DEFAULT_API_URL;
        }

        return rtrim($api_url, '/');
    }

    private function recordCurlTimings(\CurlHandle $curl_handle): void
    {
        $mapping = [
            CURLINFO_NAMELOOKUP_TIME => 'upstream_dns',
            CURLINFO_CONNECT_TIME => 'upstream_connect',
            CURLINFO_APPCONNECT_TIME => 'upstream_tls',
            CURLINFO_PRETRANSFER_TIME => 'upstream_pretransfer',
            CURLINFO_STARTTRANSFER_TIME => 'upstream_ttfb',
            CURLINFO_TOTAL_TIME => 'upstream_total',
        ];

        foreach ($mapping as $curl_info_option => $timing_key) {
            $value = curl_getinfo($curl_handle, $curl_info_option);
            if (!is_numeric($value)) {
                continue;
            }

            $this->last_timings[$timing_key] = max(0, (float)$value * 1000);
        }
    }

    private function recordResponseTimings(mixed $response_data): void
    {
        if (!is_array($response_data) || !isset($response_data['processing_time']) || !is_numeric($response_data['processing_time'])) {
            return;
        }

        $this->last_timings['upstream_processing'] = max(0, (float)$response_data['processing_time']);
    }

    private function buildResultCacheKeyFromUploadString(string $base64_image): string
    {
        $base64_image = trim($base64_image);
        if (preg_match('/^data:image\/[a-zA-Z0-9.+-]+;base64,(.*)$/s', $base64_image, $matches) === 1) {
            $image_data = base64_decode((string)$matches[1], true);
            if (is_string($image_data)) {
                return $this->buildResultCacheKeyFromBytes($image_data);
            }
        }

        return $this->buildResultCacheKeyFromBytes($base64_image);
    }

    private function buildResultCacheKeyFromBytes(string $image_data): string
    {
        $context = hash_init('sha256');
        hash_update($context, $this->resultCacheContext());
        hash_update($context, "\0");
        hash_update($context, $image_data);

        return self::RESULT_CACHE_REDIS_KEY_PREFIX . hash_final($context);
    }

    private function resultCacheContext(): string
    {
        return self::RESULT_CACHE_CONTEXT;
    }

    protected function resultCacheStore(): ?object
    {
        return $this->runtimeConfigCacheStore();
    }

    private function readRecognitionResultCache(?object $cache, ?string $key): ?array
    {
        if ($cache === null || $key === null || !method_exists($cache, 'get')) {
            return null;
        }

        try {
            $cached = $cache->get($key);
        } catch (\Throwable) {
            return null;
        }

        if (!is_string($cached) || trim($cached) === '') {
            return null;
        }

        $decoded = json_decode($cached, true);
        if (!is_array($decoded) || !array_key_exists('success', $decoded)) {
            return null;
        }

        return $decoded;
    }

    private function writeRecognitionResultCache(?object $cache, ?string $key, array $result): void
    {
        if ($cache === null || $key === null || !method_exists($cache, 'setEx')) {
            return;
        }

        try {
            $encoded = json_encode($result, JSON_UNESCAPED_SLASHES);
            if (is_string($encoded)) {
                $cache->setEx($key, $encoded, self::RESULT_CACHE_TTL_SECONDS);
            }
        } catch (\Throwable) {
            // Scanner result cache is best-effort; Plate Recognizer remains the source of truth.
        }
    }

    protected function buildPlateReaderPayload(string $base64_image): array
    {
        return $this->buildPlateReaderPayloadFromUpload($this->buildUploadValue($base64_image));
    }

    protected function buildPlateReaderPayloadFromUpload(string|\CURLFile|\CURLStringFile $upload): array
    {
        return [
            'upload' => $upload,
            'config' => self::PLATE_READER_CONFIG_JSON,
            'regions' => self::PLATE_READER_REGIONS,
        ];
    }

    private function buildUploadValue(string $base64_image): string|\CURLStringFile
    {
        $base64_image = trim($base64_image);
        if (preg_match('/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.*)$/s', $base64_image, $matches) !== 1) {
            return $base64_image;
        }

        $image_data = base64_decode((string)$matches[2], true);
        if ($image_data === false || !class_exists(\CURLStringFile::class)) {
            return (string)$matches[2];
        }

        return new \CURLStringFile($image_data, self::DEFAULT_UPLOAD_FILE_NAME, (string)$matches[1]);
    }

    private function buildUploadValueFromBytes(string $image_data, string $mime_type): string|\CURLStringFile
    {
        $mime_type = trim($mime_type) !== '' ? trim($mime_type) : 'image/jpeg';
        if (!str_starts_with($mime_type, 'image/')) {
            $mime_type = 'image/jpeg';
        }

        if (!class_exists(\CURLStringFile::class)) {
            return $image_data;
        }

        return new \CURLStringFile($image_data, self::DEFAULT_UPLOAD_FILE_NAME, $mime_type);
    }

    /**
     * @throws Exception
     */
    private function buildUploadValueFromFile(string $image_path, string $mime_type): \CURLFile
    {
        $image_path = trim($image_path);
        $mime_type = trim($mime_type) !== '' ? trim($mime_type) : 'image/jpeg';
        if (!str_starts_with($mime_type, 'image/')) {
            $mime_type = 'image/jpeg';
        }

        if ($image_path === '' || !is_file($image_path) || !class_exists(\CURLFile::class)) {
            throw new Exception('Image upload file is invalid.');
        }

        return new \CURLFile($image_path, $mime_type, self::DEFAULT_UPLOAD_FILE_NAME);
    }

    protected function runtimeConfig(): array
    {
        if ($this->runtime_config !== null) {
            return $this->runtime_config;
        }

        if ($this->shouldUseSharedRuntimeConfigCache()) {
            $cached_config = self::getSharedRuntimeConfigCache();
            if ($cached_config !== null) {
                $this->runtime_config = $cached_config;

                return $this->runtime_config;
            }

            $cached_config = $this->readRuntimeConfigCacheStore();
            if ($cached_config !== null) {
                self::$runtime_config_cache = [
                    'values' => $cached_config,
                    'cached_at' => microtime(true),
                ];
                $this->runtime_config = $cached_config;

                return $this->runtime_config;
            }
        }

        $values = $this->readRuntimeModuleConfig();

        $this->runtime_config = [
            'enabled' => $this->parseModuleConfigBool($values['enabled'] ?? false),
            'api_key' => (string)($values['api_key'] ?? ''),
        ];

        if ($this->shouldUseSharedRuntimeConfigCache()) {
            self::$runtime_config_cache = [
                'values' => $this->runtime_config,
                'cached_at' => microtime(true),
            ];
            $this->writeRuntimeConfigCacheStore($this->runtime_config);
        }

        return $this->runtime_config;
    }

    protected function shouldUseSharedRuntimeConfigCache(): bool
    {
        return static::class === self::class;
    }

    private static function getSharedRuntimeConfigCache(): ?array
    {
        if (self::$runtime_config_cache === null) {
            return null;
        }

        $cache_age_seconds = microtime(true) - self::$runtime_config_cache['cached_at'];
        if ($cache_age_seconds > self::RUNTIME_CONFIG_CACHE_TTL_SECONDS) {
            self::$runtime_config_cache = null;

            return null;
        }

        return self::$runtime_config_cache['values'];
    }

    protected function runtimeConfigCacheStore(): ?object
    {
        return defined('redis') ? constant('redis') : null;
    }

    private function readRuntimeConfigCacheStore(): ?array
    {
        $cache = $this->runtimeConfigCacheStore();
        if ($cache === null || !method_exists($cache, 'get')) {
            return null;
        }

        try {
            $cached = $cache->get(self::RUNTIME_CONFIG_REDIS_CACHE_KEY);
        } catch (\Throwable) {
            return null;
        }

        if (!is_string($cached) || trim($cached) === '') {
            return null;
        }

        $decoded = json_decode($cached, true);
        if (!is_array($decoded)) {
            return null;
        }

        if (!array_key_exists('enabled', $decoded) || !array_key_exists('api_key', $decoded)) {
            return null;
        }

        return [
            'enabled' => $this->parseModuleConfigBool($decoded['enabled']),
            'api_key' => (string)$decoded['api_key'],
        ];
    }

    /**
     * @param array{enabled: bool, api_key: string} $config
     */
    private function writeRuntimeConfigCacheStore(array $config): void
    {
        $cache = $this->runtimeConfigCacheStore();
        if ($cache === null || !method_exists($cache, 'setEx')) {
            return;
        }

        try {
            $encoded = json_encode($config, JSON_UNESCAPED_SLASHES);
            if (is_string($encoded)) {
                $cache->setEx(self::RUNTIME_CONFIG_REDIS_CACHE_KEY, $encoded, self::RUNTIME_CONFIG_CACHE_TTL_SECONDS);
            }
        } catch (\Throwable) {
            // Scanner config cache is best-effort; DB remains the source of truth.
        }
    }

    private function parseModuleConfigBool(mixed $value): bool
    {
        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return (int)$value === 1;
        }

        return strtolower(trim((string)$value)) === 'true';
    }

    protected function readRuntimeModuleConfig(): array
    {
        global $db;

        if ($db instanceof db) {
            $module = $db->escape_string('licenseplaterecognizer');
            $result = $db->query("SELECT variable, value FROM module_config WHERE module = '$module' AND variable IN ('enabled', 'api_key')");
            $values = [];

            if ($result instanceof \mysqli_result) {
                while ($row = $result->fetch_assoc()) {
                    $variable = (string)($row['variable'] ?? '');
                    if ($variable !== '') {
                        $values[$variable] = (string)($row['value'] ?? '');
                    }
                }
            }

            return $values;
        }

        if (!isset($this->config)) {
            $this->config = new licenseplaterecognizer_c();
        }

        return [
            'enabled' => (string)$this->config->enabled->getVariableValue(),
            'api_key' => (string)$this->config->api_key->getVariableValue(),
        ];
    }

    /**
     * @inheritDoc
     * @throws Exception If the module is not enabled or if there is an error in the API request
     * @throws Exception If the API response cannot be parsed
     */
    public function get_usage(): licenseplaterecognizer_info
    {
        $this->requireModuleEnabled();
        $api_key = $this->runtimeConfig()['api_key'];
        $curl = curl_init();
        curl_setopt_array($curl, array(
            CURLOPT_URL => $this->api_url . '/info/',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2TLS,
            CURLOPT_CUSTOMREQUEST => 'GET',
            CURLOPT_HTTPHEADER => [
                'Authorization: Token ' . $api_key,
            ],
        ));
        $response = curl_exec($curl);
        curl_close($curl);
        $response_data = json_decode($response, true);
        if ($response_data === null) {
            throw new Exception('Error parsing API response.');
        }
        return new licenseplaterecognizer_info($response_data);
    }
}
