<?php

declare(strict_types=1);

namespace App\Infrastructure;

use App\Entity\ModelGenerationConfig;
use App\Service\ModelGenerationConfigProvider;
use Generator;
use JsonException;
use RuntimeException;
use Throwable;

final class OllamaClient
{
    private const CONNECT_TIMEOUT_SECONDS = 10;
    private const LOW_SPEED_LIMIT_BYTES = 1;
    private const LOW_SPEED_TIME_SECONDS = 45;
    private ?ModelGenerationConfig $cachedConfig = null;
    private $config = null;

    public function __construct(
        private string $apiUrl,
        private string $timeoutSeconds,
        private ModelGenerationConfigProvider $configProvider
    ) {}

    /**
     * Public Streaming API
     */
    public function stream(string $prompt): Generator
    {
        $this->config = $this->getConfig();

        if ($this->config->isStream()) {
            yield from $this->streamInternal($prompt);
            return;
        }

        // Fallback: Blocking generate → Generator-kompatibel ausgeben
        yield $this->generateInternal($prompt);
    }

    /**
     * Public Blocking API
     */
    public function generate(string $prompt): string
    {
        return $this->generateInternal($prompt);
    }

    /**
     * Internal streaming transport
     */
    private function streamInternal(string $prompt): Generator
    {
        $payload = $this->buildPayload($prompt, true);

        $buffer = '';
        $done = false;

        $ch = curl_init($this->apiUrl);
        if ($ch === false) {
            throw new RuntimeException('Failed to initialize cURL');
        }

        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_RETURNTRANSFER => false,
            CURLOPT_TIMEOUT => $this->requestTimeoutSeconds(),
            CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT_SECONDS,
            CURLOPT_LOW_SPEED_LIMIT => self::LOW_SPEED_LIMIT_BYTES,
            CURLOPT_LOW_SPEED_TIME => self::LOW_SPEED_TIME_SECONDS,
            CURLOPT_WRITEFUNCTION => function ($curl, string $data) use (&$buffer): int {
                $buffer .= $data;
                return strlen($data);
            },
        ]);

        $mh = curl_multi_init();
        if ($mh === false) {
            curl_close($ch);
            throw new RuntimeException('Failed to initialize cURL multi handle');
        }

        curl_multi_add_handle($mh, $ch);

        try {
            do {
                do {
                    $status = curl_multi_exec($mh, $running);
                } while ($status === CURLM_CALL_MULTI_PERFORM);

                while (($pos = strpos($buffer, "\n")) !== false) {
                    $line = trim(substr($buffer, 0, $pos));
                    $buffer = substr($buffer, $pos + 1);

                    if ($line === '') {
                        continue;
                    }

                    try {
                        $json = json_decode($line, true, flags: JSON_THROW_ON_ERROR);
                    } catch (Throwable) {
                        continue;
                    }

                    if (isset($json['response'])) {
                        yield $json['response'];
                    }

                    if (!empty($json['done'])) {
                        $done = true;
                    }
                }

                if ($running) {
                    curl_multi_select($mh, 0.2);
                }

            } while ($running && !$done);

            if (curl_errno($ch)) {
                throw new RuntimeException('LLM connection error: ' . curl_error($ch));
            }

        } finally {
            curl_multi_remove_handle($mh, $ch);
            curl_multi_close($mh);
            curl_close($ch);
        }
    }

    /**
     * Internal blocking transport
     */
    private function generateInternal(string $prompt): string
    {
        $payload = $this->buildPayload($prompt, false);

        $ch = curl_init($this->apiUrl);
        if ($ch === false) {
            throw new RuntimeException('Failed to initialize cURL');
        }

        curl_setopt_array($ch, [
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS => $payload,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->requestTimeoutSeconds(),
            CURLOPT_CONNECTTIMEOUT => self::CONNECT_TIMEOUT_SECONDS,
            CURLOPT_LOW_SPEED_LIMIT => self::LOW_SPEED_LIMIT_BYTES,
            CURLOPT_LOW_SPEED_TIME => self::LOW_SPEED_TIME_SECONDS,
        ]);

        $response = curl_exec($ch);

        if ($response === false) {
            throw new RuntimeException('LLM error: ' . curl_error($ch));
        }

        curl_close($ch);

        $json = json_decode($response, true, flags: JSON_THROW_ON_ERROR);

        return $json['response'] ?? '';
    }

    /**
     * Central Payload Builder (DRY)
     */
    private function buildPayload(string $prompt, bool $stream): string
    {
        return json_encode([
            'model' => $this->config->getModelName(),
            'prompt' => $prompt,
            'stream' => $stream,
            'options' => $this->buildOptions()
        ], JSON_THROW_ON_ERROR);
    }

    /**
     * Central Options Builder (DRY)
     */
    private function buildOptions(): array
    {
        $this->config = $this->getConfig();
        return [
            'temperature' => $this->config->getTemperature(),
            'top_k' => $this->config->getTopK(),
            'top_p' => $this->config->getTopP(),
            'repeat_penalty' => $this->config->getRepeatPenalty(),
            'num_ctx' => $this->config->getNumCtx(),
        ];
    }

    private function requestTimeoutSeconds(): int
    {
        $timeout = (int) $this->timeoutSeconds;

        return $timeout > 0 ? $timeout : 300;
    }

    /**
     * Config caching per request
     */
    private function getConfig(): ModelGenerationConfig
    {
        if ($this->cachedConfig === null) {
            $this->cachedConfig = $this->configProvider->getActiveForModel();
        }

        return $this->cachedConfig;
    }
}
