<?php

namespace App\Ocr;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;

/**
 * Client OCR Google Gemini (vision). Port de app/api/ocr/route.ts.
 * Renvoie le texte brut détecté et son type (plaque / VIN).
 */
final class GeminiOcrClient
{
    private const PLATE_PROMPT = <<<TXT
        You are a license plate OCR system. The image shows a French vehicle license plate.
        Extract ONLY the plate text. Return ONLY the raw characters visible, nothing else.
        No explanation. No punctuation except hyphens already on the plate.
        If unreadable, return exactly: UNREADABLE
        TXT;

    private const VIN_PROMPT = <<<TXT
        You are a VIN (Vehicle Identification Number) OCR system.
        Extract ONLY the 17-character VIN from the image. Return ONLY the 17 characters, no spaces or hyphens.
        VINs never contain the letters I, O, or Q.
        If unreadable, return exactly: UNREADABLE
        TXT;

    private const AUTO_PROMPT = <<<TXT
        You are a vehicle document OCR system. The image shows either a French license plate or a VIN (Vehicle Identification Number).
        First determine: is this a LICENSE_PLATE or a VIN?
        Then extract the value, reading EVERY character carefully from left to right.
        A VIN has EXACTLY 17 characters and never contains the letters I, O or Q. Do not omit or merge any character.
        Respond in this exact format (two lines only):
        TYPE: VIN
        VALUE: [the 17 characters]
        OR:
        TYPE: LICENSE_PLATE
        VALUE: [plate text]
        If unreadable, VALUE: UNREADABLE
        TXT;

    private const MODEL_PROMPT = <<<TXT
        You are a vehicle identification expert. You are given the VIN of a vehicle and one or more photos of that SAME vehicle.
        Use BOTH sources: the VIN (its first characters encode the manufacturer / WMI, and the following characters often encode the model) AND the visual evidence in the photos.
        Identify the vehicle's MAKE (brand) and its MODEL (commercial model name, no trim/finish level).
        Be conservative: if you are unsure of the model, lower the confidence rather than guessing.
        Respond in EXACTLY three lines, nothing else:
        MAKE: [brand, or UNKNOWN]
        MODEL: [model name, or UNKNOWN]
        CONFIDENCE: [integer 0-100 = your confidence that the MODEL above is correct]
        TXT;

    public function __construct(
        private readonly HttpClientInterface $httpClient,
        #[Autowire('%env(GEMINI_API_KEY)%')]
        private readonly string $apiKey,
        #[Autowire('%env(GEMINI_MODEL)%')]
        private readonly string $model,
        #[Autowire('%env(GEMINI_THINKING_BUDGET)%')]
        private readonly string $thinkingBudget,
        private readonly LoggerInterface $logger,
    ) {
    }

    /**
     * @param string $type 'plate' | 'vin' | 'auto'
     *
     * @return array{raw: string, type: 'plate'|'vin'}
     */
    public function recognize(string $base64Image, string $type): array
    {
        if ('auto' === $type) {
            $response = $this->callGeminiParts([$this->imagePart($base64Image)], self::AUTO_PROMPT);

            return $this->parseAutoResponse($response);
        }

        $prompt = 'plate' === $type ? self::PLATE_PROMPT : self::VIN_PROMPT;
        $raw = $this->callGeminiParts([$this->imagePart($base64Image)], $prompt);

        return ['raw' => $raw, 'type' => 'plate' === $type ? 'plate' : 'vin'];
    }

    /**
     * Identifie marque + modèle d'un véhicule à partir de son VIN (texte) et d'une
     * ou deux photos (les photos NON-VIN du véhicule : plaque, vue d'ensemble…).
     *
     * @param list<string> $base64Images photos non-VIN (compressées, base64 sans préfixe data:)
     * @param string       $vin          VIN lu (peut être vide si illisible)
     *
     * @return array{make: ?string, model: ?string, confidence: ?int}
     */
    public function recognizeModel(array $base64Images, string $vin): array
    {
        $images = array_values(array_filter($base64Images, static fn ($b) => \is_string($b) && '' !== $b));
        if ([] === $images) {
            return ['make' => null, 'model' => null, 'confidence' => null];
        }

        $vin = trim($vin);
        $prompt = self::MODEL_PROMPT . "\n\nVIN: " . ('' !== $vin ? $vin : 'UNKNOWN');

        $parts = [];
        foreach ($images as $img) {
            $parts[] = $this->imagePart($img);
        }
        $response = $this->callGeminiParts($parts, $prompt);

        return $this->parseModelResponse($response);
    }

    /**
     * @return array{inline_data: array{mime_type: string, data: string}}
     */
    private function imagePart(string $base64Image): array
    {
        // Retirer le préfixe data URL si présent.
        $imageData = preg_replace('/^data:[^;]+;base64,/', '', $base64Image) ?? $base64Image;

        return ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => $imageData]];
    }

    /**
     * Appel Gemini generateContent avec un prompt texte + N parties image.
     *
     * @param list<array<string, mixed>> $imageParts parties inline_data déjà construites
     */
    private function callGeminiParts(array $imageParts, string $prompt, int $retries = 3): string
    {
        if ('' === $this->apiKey || 'changeme' === $this->apiKey) {
            throw new \RuntimeException('GEMINI_API_KEY not configured');
        }

        $url = sprintf(
            'https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent',
            rawurlencode($this->model)
        );

        // Config de génération :
        // - GEMINI_THINKING_BUDGET (env) > 0 : raisonnement activé avec ce budget + maxOutputTokens
        //   élargi (sinon la réflexion tronque la réponse). Permet d'activer le raisonnement même
        //   sur un modèle « flash » (meilleure lecture des VIN gravés, un peu plus lent).
        // - = 0 sur un modèle non-pro : rapide, sans raisonnement.
        // - vide / « auto » : pro = raisonnement obligatoire (512, car thinkingBudget=0 renvoie HTTP 400) ;
        //   flash = rapide (0). Repli sur l'auto-pro si on demande 0 sur un modèle pro.
        $isPro = str_contains(strtolower($this->model), 'pro');
        $budget = trim($this->thinkingBudget);
        if (is_numeric($budget) && (int) $budget > 0) {
            $b = (int) $budget;
            $generationConfig = ['maxOutputTokens' => max(1024, $b * 4), 'temperature' => 0, 'thinkingConfig' => ['thinkingBudget' => $b]];
        } elseif (is_numeric($budget) && 0 === (int) $budget && !$isPro) {
            $generationConfig = ['maxOutputTokens' => 256, 'temperature' => 0, 'thinkingConfig' => ['thinkingBudget' => 0]];
        } else {
            $generationConfig = $isPro
                ? ['maxOutputTokens' => 2048, 'temperature' => 0, 'thinkingConfig' => ['thinkingBudget' => 512]]
                : ['maxOutputTokens' => 256, 'temperature' => 0, 'thinkingConfig' => ['thinkingBudget' => 0]];
        }

        $payload = [
            'contents' => [[
                'parts' => array_merge([['text' => $prompt]], $imageParts),
            ]],
            'generationConfig' => $generationConfig,
        ];

        $lastError = null;
        for ($attempt = 0; $attempt < $retries; ++$attempt) {
            try {
                $response = $this->httpClient->request('POST', $url, [
                    'headers' => [
                        'Content-Type' => 'application/json',
                        'x-goog-api-key' => $this->apiKey,
                    ],
                    'json' => $payload,
                    'timeout' => 30,
                ]);

                $data = $response->toArray(false);

                if (200 !== $response->getStatusCode()) {
                    throw new \RuntimeException(sprintf(
                        'Gemini API error %d: %s',
                        $response->getStatusCode(),
                        json_encode($data['error'] ?? $data)
                    ));
                }

                // Concatène les parties de texte en ignorant les éventuelles parties « thought »
                // (modèles à raisonnement) pour ne renvoyer que la réponse finale.
                $parts = $data['candidates'][0]['content']['parts'] ?? [];
                $text = '';
                foreach ($parts as $p) {
                    if (isset($p['text']) && empty($p['thought'])) {
                        $text .= $p['text'];
                    }
                }

                return trim($text);
            } catch (\Throwable $e) {
                $lastError = $e;
                if ($attempt < $retries - 1) {
                    // Backoff exponentiel : 500ms, 1000ms, 2000ms
                    usleep((int) (500_000 * 2 ** $attempt));
                }
            }
        }

        $this->logger->error('Gemini OCR failed', ['error' => $lastError?->getMessage()]);
        throw $lastError ?? new \RuntimeException('Unknown error calling Gemini');
    }

    /**
     * @return array{raw: string, type: 'plate'|'vin'}
     */
    private function parseAutoResponse(string $response): array
    {
        $type = 'plate';
        $value = 'UNREADABLE';

        foreach (explode("\n", $response) as $line) {
            $line = trim($line);
            if (str_starts_with($line, 'TYPE:')) {
                $t = strtoupper(trim(substr($line, strlen('TYPE:'))));
                $type = 'VIN' === $t ? 'vin' : 'plate';
            }
            if (str_starts_with($line, 'VALUE:')) {
                $value = trim(substr($line, strlen('VALUE:')));
            }
        }

        return ['raw' => $value, 'type' => $type];
    }

    /**
     * @return array{make: ?string, model: ?string, confidence: ?int}
     */
    private function parseModelResponse(string $response): array
    {
        $make = null;
        $model = null;
        $confidence = null;

        foreach (explode("\n", $response) as $line) {
            $line = trim($line);
            if (str_starts_with(strtoupper($line), 'MAKE:')) {
                $make = $this->cleanValue(substr($line, 5));
            } elseif (str_starts_with(strtoupper($line), 'MODEL:')) {
                $model = $this->cleanValue(substr($line, 6));
            } elseif (str_starts_with(strtoupper($line), 'CONFIDENCE:')) {
                if (preg_match('/\d+/', $line, $m)) {
                    $confidence = max(0, min(100, (int) $m[0]));
                }
            }
        }

        return ['make' => $make, 'model' => $model, 'confidence' => $confidence];
    }

    private function cleanValue(string $v): ?string
    {
        $v = trim($v);
        if ('' === $v || 0 === strcasecmp($v, 'UNKNOWN') || 0 === strcasecmp($v, 'UNREADABLE')) {
            return null;
        }

        return mb_substr($v, 0, 60);
    }
}
