<?php

namespace routes;

use classes\licenseplaterecognizer;
use classes\response;
use traits\route_t;

class moduleScannerRoute
{
    use route_t;

    private const LPR_IMAGE_UPLOAD_FIELD = 'image';
    private const LPR_IMAGE_UPLOAD_MAX_BYTES = 4194304;
    private const LPR_CLIENT_CAPTURE_MS_FIELD = 'client_capture_ms';
    private const LPR_CLIENT_CAPTURE_MAX_MS = 10000;
    private const LPR_CLIENT_DRAW_MS_FIELD = 'client_draw_ms';
    private const LPR_CLIENT_ENCODE_MS_FIELD = 'client_encode_ms';
    private const LPR_CLIENT_FRAME_WIDTH_FIELD = 'client_frame_width';
    private const LPR_CLIENT_FRAME_HEIGHT_FIELD = 'client_frame_height';
    private const LPR_CLIENT_FRAME_BYTES_FIELD = 'client_frame_bytes';
    private const LPR_CLIENT_FRAME_MAX_DIMENSION = 4096;
    private const LPR_CLIENT_PREFLIGHT_MS_FIELD = 'client_preflight_ms';
    private const LPR_CLIENT_VISUAL_FINGERPRINT_MS_FIELD = 'client_visual_fingerprint_ms';
    private const LPR_CLIENT_CAPTURE_MS_HEADER = 'HTTP_X_LPR_CLIENT_CAPTURE_MS';
    private const LPR_CLIENT_DRAW_MS_HEADER = 'HTTP_X_LPR_CLIENT_DRAW_MS';
    private const LPR_CLIENT_ENCODE_MS_HEADER = 'HTTP_X_LPR_CLIENT_ENCODE_MS';
    private const LPR_CLIENT_FRAME_WIDTH_HEADER = 'HTTP_X_LPR_CLIENT_FRAME_WIDTH';
    private const LPR_CLIENT_FRAME_HEIGHT_HEADER = 'HTTP_X_LPR_CLIENT_FRAME_HEIGHT';
    private const LPR_CLIENT_FRAME_BYTES_HEADER = 'HTTP_X_LPR_CLIENT_FRAME_BYTES';
    private const LPR_CLIENT_PREFLIGHT_MS_HEADER = 'HTTP_X_LPR_CLIENT_PREFLIGHT_MS';
    private const LPR_CLIENT_VISUAL_FINGERPRINT_MS_HEADER = 'HTTP_X_LPR_CLIENT_VISUAL_FINGERPRINT_MS';

    public function run(): void
    {
        global /** @var response $response */
        $router, $response;
        /** Modules > Scanner > License Plate Recognition > POST */
        $this->post('/modules/scanner/lpr', function () {
            global $response;
            $route_started_at = microtime(true);
            $image_upload = self::getLPRImageUpload();
            $raw_image_upload = $image_upload === null ? self::getLPRRawImageUpload() : null;
            $client_timings = self::getLPRClientTimings();
            $base64_image = null;
            //self::requirePermission('modules_scanner_lpr');
            if ($image_upload === null && $raw_image_upload === null) {
                $base64_image = self::getParameter('base64_image');
                if (!is_string($base64_image) || trim($base64_image) === '') {
                    $response->error('Image is required.');
                }
            }
            $recognizer = new licenseplaterecognizer(false);
            try {
                if ($image_upload !== null) {
                    $lpr_result = $recognizer->licenseplaterecognizerUploadFile($image_upload['path'], $image_upload['mime_type']);
                } elseif ($raw_image_upload !== null) {
                    $lpr_result = $recognizer->licenseplaterecognizerUploadUncached($raw_image_upload['data'], $raw_image_upload['mime_type']);
                } else {
                    $lpr_result = $recognizer->licenseplaterecognizer((string)$base64_image);
                }
            } finally {
                self::sendLPRServerTiming(array_merge($client_timings, $recognizer->getLastTimings()), $route_started_at);
            }
            if ($lpr_result['success']) {
                // Set the LICENSE_PLATE_NUMBER to uppercase and remove spaces.
                $lpr_result['license_plate_number'] = strtoupper(str_replace(' ', '', $lpr_result['license_plate_number']));
                // Make sure the scanned plate is more than 3 characters long.
                $scannedPlateIsTooShort = strlen($lpr_result['license_plate_number']) <= 3;
                // If the confidence is below 90%, treat it as a recoverable scanner miss.
                if (isset($lpr_result['confidence']) && $lpr_result['confidence'] < 0.9 && !$scannedPlateIsTooShort) {
                    $response->response(false, [
                        'message' => 'License plate recognition confidence too low.',
                        'reason' => 'low_confidence_license_plate',
                        'confidence' => $lpr_result['confidence'],
                        'license_plate_number' => $lpr_result['license_plate_number'],
                    ], 200);
                }
                // Success
                $response->success(['success' => true, 'license_plate_number' => $lpr_result['license_plate_number']]);
            } else {
                $response->response(false, [
                    'message' => $lpr_result['message'] ?? 'No license plate detected.',
                    'reason' => 'no_license_plate_detected',
                ], 200);
            }
        },
            [
                'modules_scanner_lpr' => 'License Plate Recognition',
            ]
        );
    }

    private static function getLPRImageUpload(): ?array
    {
        global $response;

        if (!isset($_FILES[self::LPR_IMAGE_UPLOAD_FIELD]) || !is_array($_FILES[self::LPR_IMAGE_UPLOAD_FIELD])) {
            return null;
        }

        $file = $_FILES[self::LPR_IMAGE_UPLOAD_FIELD];
        if (is_array($file['error'] ?? null)) {
            $response->error('Only one image can be uploaded.', 400);
        }

        $upload_error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($upload_error === UPLOAD_ERR_NO_FILE) {
            return null;
        }

        if ($upload_error !== UPLOAD_ERR_OK) {
            $response->error('Image upload failed.', 400);
        }

        $size = (int)($file['size'] ?? 0);
        if ($size <= 0) {
            $response->error('Image upload is empty.', 400);
        }

        if ($size > self::LPR_IMAGE_UPLOAD_MAX_BYTES) {
            $response->error('Image upload is too large.', 413);
        }

        $tmp_name = (string)($file['tmp_name'] ?? '');
        if ($tmp_name === '' || !is_uploaded_file($tmp_name)) {
            $response->error('Image upload is invalid.', 400);
        }

        if (!is_readable($tmp_name)) {
            $response->error('Image upload could not be read.', 400);
        }

        $mime_type = self::detectLPRImageMimeType($file, $tmp_name);
        if (!str_starts_with($mime_type, 'image/')) {
            $response->error('Image upload must be an image.', 400);
        }

        return [
            'path' => $tmp_name,
            'mime_type' => $mime_type,
        ];
    }

    private static function getLPRRawImageUpload(): ?array
    {
        global $response;

        $mime_type = self::getRequestContentType();
        if (!str_starts_with($mime_type, 'image/')) {
            return null;
        }

        $content_length = isset($_SERVER['CONTENT_LENGTH']) && is_numeric($_SERVER['CONTENT_LENGTH'])
            ? (int)$_SERVER['CONTENT_LENGTH']
            : null;
        if ($content_length !== null && $content_length > self::LPR_IMAGE_UPLOAD_MAX_BYTES) {
            $response->error('Image upload is too large.', 413);
        }

        $image_data = file_get_contents('php://input');
        if (!is_string($image_data) || $image_data === '') {
            $response->error('Image upload is empty.', 400);
        }

        if (strlen($image_data) > self::LPR_IMAGE_UPLOAD_MAX_BYTES) {
            $response->error('Image upload is too large.', 413);
        }

        return [
            'data' => $image_data,
            'mime_type' => $mime_type,
        ];
    }

    private static function getRequestContentType(): string
    {
        $content_type = (string)($_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '');
        $content_type = strtolower(trim(explode(';', $content_type, 2)[0] ?? ''));

        return $content_type;
    }

    private static function detectLPRImageMimeType(array $file, string $tmp_name): string
    {
        $mime_type = trim((string)($file['type'] ?? ''));
        if ($mime_type !== '') {
            return $mime_type;
        }

        if (class_exists(\finfo::class)) {
            $finfo = new \finfo(FILEINFO_MIME_TYPE);
            $detected = $finfo->file($tmp_name);
            if (is_string($detected) && $detected !== '') {
                return $detected;
            }
        }

        return 'image/jpeg';
    }

    private static function getLPRClientTimings(): array
    {
        $timings = [];
        $client_capture_ms = self::getNumericClientField(
            self::LPR_CLIENT_CAPTURE_MS_FIELD,
            self::LPR_CLIENT_CAPTURE_MS_HEADER,
            0,
            self::LPR_CLIENT_CAPTURE_MAX_MS
        );
        if ($client_capture_ms !== null) {
            $timings['client_capture'] = $client_capture_ms;
        }

        $client_preflight_ms = self::getNumericClientField(
            self::LPR_CLIENT_PREFLIGHT_MS_FIELD,
            self::LPR_CLIENT_PREFLIGHT_MS_HEADER,
            0,
            self::LPR_CLIENT_CAPTURE_MAX_MS
        );
        if ($client_preflight_ms !== null) {
            $timings['client_preflight'] = $client_preflight_ms;
        }

        $client_draw_ms = self::getNumericClientField(
            self::LPR_CLIENT_DRAW_MS_FIELD,
            self::LPR_CLIENT_DRAW_MS_HEADER,
            0,
            self::LPR_CLIENT_CAPTURE_MAX_MS
        );
        if ($client_draw_ms !== null) {
            $timings['client_draw'] = $client_draw_ms;
        }

        $client_encode_ms = self::getNumericClientField(
            self::LPR_CLIENT_ENCODE_MS_FIELD,
            self::LPR_CLIENT_ENCODE_MS_HEADER,
            0,
            self::LPR_CLIENT_CAPTURE_MAX_MS
        );
        if ($client_encode_ms !== null) {
            $timings['client_encode'] = $client_encode_ms;
        }

        $client_visual_fingerprint_ms = self::getNumericClientField(
            self::LPR_CLIENT_VISUAL_FINGERPRINT_MS_FIELD,
            self::LPR_CLIENT_VISUAL_FINGERPRINT_MS_HEADER,
            0,
            self::LPR_CLIENT_CAPTURE_MAX_MS
        );
        if ($client_visual_fingerprint_ms !== null) {
            $timings['client_visual_fingerprint'] = $client_visual_fingerprint_ms;
        }

        $client_frame_width = self::getNumericClientField(
            self::LPR_CLIENT_FRAME_WIDTH_FIELD,
            self::LPR_CLIENT_FRAME_WIDTH_HEADER,
            1,
            self::LPR_CLIENT_FRAME_MAX_DIMENSION
        );
        if ($client_frame_width !== null) {
            $timings['client_frame_width'] = $client_frame_width;
        }

        $client_frame_height = self::getNumericClientField(
            self::LPR_CLIENT_FRAME_HEIGHT_FIELD,
            self::LPR_CLIENT_FRAME_HEIGHT_HEADER,
            1,
            self::LPR_CLIENT_FRAME_MAX_DIMENSION
        );
        if ($client_frame_height !== null) {
            $timings['client_frame_height'] = $client_frame_height;
        }

        $client_frame_bytes = self::getNumericClientField(
            self::LPR_CLIENT_FRAME_BYTES_FIELD,
            self::LPR_CLIENT_FRAME_BYTES_HEADER,
            1,
            self::LPR_IMAGE_UPLOAD_MAX_BYTES
        );
        if ($client_frame_bytes !== null) {
            $timings['client_frame_bytes'] = $client_frame_bytes;
        }

        return $timings;
    }

    private static function getNumericClientField(string $field, string $server_header, float $min, float $max): ?float
    {
        $value = $_GET[$field] ?? null;
        if ($value === null) {
            $value = $_POST[$field] ?? null;
        }
        if ($value === null) {
            $value = $_SERVER[$server_header] ?? null;
        }
        if (is_array($value) || !is_numeric($value)) {
            return null;
        }

        $value = (float)$value;
        if ($value < $min || $value > $max) {
            return null;
        }

        return $value;
    }

    private static function sendLPRServerTiming(array $timings, float $route_started_at): void
    {
        if (headers_sent()) {
            return;
        }

        $parts = [];
        $timings['route_total'] = max(0, (microtime(true) - $route_started_at) * 1000);
        $timings['local'] = self::getLPRLocalDuration($timings);
        if (isset($_SERVER['REQUEST_TIME_FLOAT']) && is_numeric($_SERVER['REQUEST_TIME_FLOAT'])) {
            $timings['request_total'] = max(0, (microtime(true) - (float)$_SERVER['REQUEST_TIME_FLOAT']) * 1000);
        }

        foreach ([
            'client_capture',
            'client_preflight',
            'client_visual_fingerprint',
            'client_draw',
            'client_encode',
            'client_frame_width',
            'client_frame_height',
            'client_frame_bytes',
            'config',
            'cache',
            'cache_hit',
            'cache_miss',
            'local',
            'payload',
            'upstream_dns',
            'upstream_connect',
            'upstream_tls',
            'upstream_pretransfer',
            'upstream_ttfb',
            'upstream_total',
            'upstream_processing',
            'upstream',
            'parse',
            'total',
        ] as $name) {
            if (!isset($timings[$name]) || !is_numeric($timings[$name])) {
                continue;
            }

            $parts[] = sprintf('lpr_%s;dur=%.3F', $name, (float)$timings[$name]);
        }
        foreach (['route_total', 'request_total'] as $name) {
            if (!isset($timings[$name]) || !is_numeric($timings[$name])) {
                continue;
            }

            $parts[] = sprintf('lpr_%s;dur=%.3F', $name, (float)$timings[$name]);
        }

        if ($parts !== []) {
            header('Server-Timing: ' . implode(', ', $parts));
        }
    }

    private static function getLPRLocalDuration(array $timings): float
    {
        $upstream = null;
        foreach (['upstream', 'upstream_total'] as $name) {
            if (isset($timings[$name]) && is_numeric($timings[$name])) {
                $upstream = max(0, (float)$timings[$name]);
                break;
            }
        }

        return max(0, (float)$timings['route_total'] - ($upstream ?? 0));
    }
}
