<?php

declare(strict_types=1);

namespace App\Agent;

use App\Commerce\SearchRepairService;
use App\Commerce\ShopSearchService;
use App\Config\AgentRunnerConfig;
use App\Context\ContextService;
use App\Context\UrlAnalyzer;
use App\Infrastructure\OllamaClient;
use App\Intent\CommerceIntentLite;
use App\Knowledge\Retrieval\RetrieverInterface;
use Generator;
use Psr\Log\LoggerInterface;
use Throwable;

final readonly class AgentRunner
{
    private const COMMERCE_HISTORY_BUDGET_CHARS = 1000;

    private bool $systemMsgOn;

    public function __construct(
        private PromptBuilder $promptBuilder,
        private ThinkSuppressor $thinkSuppressor,
        private ContextService $contextService,
        private UrlAnalyzer $urlAnalyzer,
        private RetrieverInterface $retriever,
        private ShopSearchService $shopSearchService,
        private SearchRepairService $searchRepairService,
        private CommerceIntentLite $commerceIntentLite,
        private OllamaClient $ollamaClient,
        private LoggerInterface $agentLogger,
        private AgentRunnerConfig $agentRunnerConfig,
        private bool $debug,
        private bool $logPrompt,
        private bool $logContext,
    ) {
        $this->systemMsgOn = true;
    }

    public function run(string $prompt, string $userId, bool $forceFullContext = false): Generator
    {
        $prompt = trim($prompt);

        if ($prompt === '') {
            yield $this->systemMsg('❌ Empty prompt.', 'err');
            return;
        }

        $shopResults = [];
        $primaryShopResults = [];
        $sources = [];
        $optimizedShopQuery = '';
        $shopSearchQuery = '';
        $commerceIntent = CommerceIntentLite::NONE;
        $commerceHistoryContext = '';
        $attemptedShopRepair = false;
        $usedShopRepair = false;
        $shopRepairQueries = [];

        $this->agentLogger->info('Agent run started', [
            'userId' => $userId,
        ]);

        try {
            if ($forceFullContext) {
                // Full context mode is already passed to PromptBuilder.
                // Additional context strategies can be added here later.
            }

            yield $this->systemMsg('Ich analysiere deine Anfrage...', 'think');

            yield $this->systemMsg('Ich prüfe auf Internetquellen...', 'think');

            $urlContent = $this->urlAnalyzer->extractContentFromPrompt($prompt);
            if ($urlContent !== '') {
                $this->addSource($sources, 'Externe URL');
            }

            yield $this->systemMsg('Ich hole relevante Daten aus meinem RAG-Wissen...', 'think');

            $knowledgeChunks = $this->retriever->retrieve($prompt);
            if ($knowledgeChunks !== []) {
                $this->addSource($sources, 'RAG Wissen');
            }

            $commerceIntent = $this->detectCommerceIntent($prompt);

            if ($this->isCommerceIntent($commerceIntent)) {
                yield $this->systemMsg('Ich optimiere die Recherche...', 'think');

                $commerceHistoryContext = $this->buildCommerceHistoryContext($userId);

                if ($commerceHistoryContext !== '') {
                    $this->addSource($sources, 'Chatverlauf');
                }

                $optimizedShopQuery = $this->buildOptimizedShopQuery(
                    $prompt,
                    $userId,
                    $commerceHistoryContext
                );

                $shopSearchQuery = $optimizedShopQuery !== '' ? $optimizedShopQuery : $prompt;

                $this->agentLogger->info('Commerce search prepared', [
                    'userId' => $userId,
                    'commerceIntent' => $commerceIntent,
                    'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
                    'optimizedShopQuery' => $optimizedShopQuery,
                    'shopSearchQuery' => $shopSearchQuery,
                    'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
                    'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
                ]);

                yield $this->systemMsg(
                    'Ich rufe Recherchedaten ab (type: ' . $commerceIntent . ')',
                    'think'
                );

                $primaryShopResults = $this->searchShop(
                    $shopSearchQuery,
                    $commerceIntent,
                    $userId,
                    $commerceHistoryContext
                );

                $repairPayload = $this->repairShopResults(
                    prompt: $prompt,
                    userId: $userId,
                    commerceIntent: $commerceIntent,
                    commerceHistoryContext: $commerceHistoryContext,
                    primaryQuery: $shopSearchQuery,
                    primaryShopResults: $primaryShopResults,
                    knowledgeChunks: $knowledgeChunks
                );

                $shopResults = $repairPayload['results'];
                $attemptedShopRepair = $repairPayload['attemptedRepair'];
                $usedShopRepair = $repairPayload['usedRepair'];
                $shopRepairQueries = $repairPayload['repairQueries'];

                if ($shopResults !== []) {
                    $this->addSource($sources, 'Shopsystem');
                }

                if ($attemptedShopRepair) {
                    $this->addSource($sources, 'Erweiterte Shopsuche');
                }
            }

            if ($shopResults !== []) {
                $knowledgeChunks = $this->limitKnowledgeChunks($knowledgeChunks, $commerceIntent);
            }

            yield $this->systemMsg('Ich analysiere alle Informationen...', 'think');

            $finalPrompt = $this->promptBuilder->build(
                prompt: $prompt,
                userId: $userId,
                urlContent: $urlContent,
                knowledgeChunks: $knowledgeChunks,
                shopResults: $shopResults,
                fullContext: $forceFullContext,
                swagFullOutPut: $optimizedShopQuery
            );

            if ($this->debug && $this->logPrompt) {
                $this->agentLogger->debug('Final prompt', [
                    'userId' => $userId,
                    'finalPrompt' => $finalPrompt,
                    'optimizedShopQuery' => $optimizedShopQuery,
                    'shopSearchQuery' => $shopSearchQuery,
                    'primaryShopResultsCount' => count($primaryShopResults),
                    'shopResultsCount' => count($shopResults),
                    'attemptedShopRepair' => $attemptedShopRepair,
                    'usedShopRepair' => $usedShopRepair,
                    'shopRepairQueries' => $shopRepairQueries,
                ]);
            }

            if ($this->debug && $this->logContext) {
                $this->agentLogger->debug('Conversation context snapshot', [
                    'userId' => $userId,
                    'context' => $this->contextService->buildUserContext(
                        $userId,
                        $forceFullContext
                    ),
                ]);
            }

            if ($sources !== []) {
                yield $this->emitSources($sources, 'Genutzte Quellen: ');
            }

            $fullOutput = yield from $this->streamFinalAnswer($finalPrompt);

            if ($sources !== []) {
                yield $this->emitSources($sources, 'Quellen: ');
            }

            if ($this->debug) {
                yield $this->systemMsg($finalPrompt, 'debug');
            }

            if ($fullOutput !== '') {
                $this->contextService->appendHistory(
                    $userId,
                    $prompt,
                    $fullOutput
                );
            }

            $this->agentLogger->info('Agent run finished', [
                'userId' => $userId,
                'outputLength' => mb_strlen($fullOutput),
                'contextMode' => $forceFullContext ? 'full' : 'recent',
                'commerceIntent' => $commerceIntent,
                'primaryShopResultsCount' => count($primaryShopResults),
                'shopResultsCount' => count($shopResults),
                'attemptedShopRepair' => $attemptedShopRepair,
                'usedShopRepair' => $usedShopRepair,
                'shopRepairQueries' => $shopRepairQueries,
                'knowledgeChunkCount' => count($knowledgeChunks),
                'hasUrlContent' => $urlContent !== '',
                'usedOptimizedShopQuery' => $optimizedShopQuery !== '',
                'optimizedShopQuery' => $optimizedShopQuery,
                'shopSearchQuery' => $shopSearchQuery,
                'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
                'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
            ]);
        } catch (Throwable $e) {
            $this->agentLogger->error('Agent run failed', [
                'userId' => $userId,
                'exception' => $e,
            ]);

            yield $this->systemMsg($this->buildUserErrorMessage($e), 'err');
        }
    }

    private function detectCommerceIntent(string $prompt): string
    {
        $commerceMeta = $this->commerceIntentLite->detect($prompt);

        return (string) ($commerceMeta['intent'] ?? CommerceIntentLite::NONE);
    }

    private function isCommerceIntent(string $commerceIntent): bool
    {
        return $commerceIntent === CommerceIntentLite::PRODUCT_SEARCH
            || $commerceIntent === CommerceIntentLite::ADVISORY_PRODUCT_SEARCH;
    }

    private function buildOptimizedShopQuery(
        string $prompt,
        string $userId,
        string $commerceHistoryContext = ''
    ): string {
        $shopPrompt = trim($this->agentRunnerConfig->getShopPrompt(
            $prompt,
            $commerceHistoryContext
        ));

        if ($shopPrompt === '') {
            return '';
        }

        $optimizedQuery = '';
        $this->thinkSuppressor->reset();

        try {
            foreach ($this->ollamaClient->stream($shopPrompt) as $token) {
                if (!is_string($token)) {
                    continue;
                }

                $cleanToken = $this->thinkSuppressor->filter($token);

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

                $optimizedQuery .= $cleanToken;
            }
        } catch (Throwable $e) {
            $this->agentLogger->warning('Shop query optimization failed, falling back to original prompt', [
                'userId' => $userId,
                'exception' => $e,
            ]);

            return '';
        }

        return $this->sanitizeOptimizedShopQuery($optimizedQuery);
    }

    /**
     * @return array{
     *     results: array,
     *     attemptedRepair: bool,
     *     usedRepair: bool,
     *     repairQueries: string[]
     * }
     */
    private function repairShopResults(
        string $prompt,
        string $userId,
        string $commerceIntent,
        string $commerceHistoryContext,
        string $primaryQuery,
        array $primaryShopResults,
        array $knowledgeChunks
    ): array {
        try {
            return $this->searchRepairService->repair(
                prompt: $prompt,
                commerceIntent: $commerceIntent,
                commerceHistoryContext: $commerceHistoryContext,
                primaryQuery: $primaryQuery,
                primaryShopResults: $primaryShopResults,
                knowledgeChunks: $knowledgeChunks
            );
        } catch (Throwable $e) {
            $this->agentLogger->warning('Shop repair failed, continuing with primary shop results', [
                'userId' => $userId,
                'commerceIntent' => $commerceIntent,
                'primaryQuery' => $primaryQuery,
                'primaryShopResultsCount' => count($primaryShopResults),
                'exception' => $e,
            ]);

            return [
                'results' => $primaryShopResults,
                'attemptedRepair' => false,
                'usedRepair' => false,
                'repairQueries' => [],
            ];
        }
    }

    private function searchShop(
        string $query,
        string $commerceIntent,
        string $userId,
        string $commerceHistoryContext = ''
    ): array {
        try {
            return $this->shopSearchService->search(
                $query,
                $commerceIntent,
                $commerceHistoryContext
            );
        } catch (Throwable $e) {
            $this->agentLogger->warning('Shop search failed, continuing without shop results', [
                'userId' => $userId,
                'commerceIntent' => $commerceIntent,
                'query' => $query,
                'hasCommerceHistoryContext' => $commerceHistoryContext !== '',
                'commerceHistoryContextLength' => mb_strlen($commerceHistoryContext),
                'exception' => $e,
            ]);

            return [];
        }
    }

    private function buildCommerceHistoryContext(string $userId): string
    {
        return $this->contextService->buildUserContextWithinBudget(
            $userId,
            self::COMMERCE_HISTORY_BUDGET_CHARS
        );
    }

    private function limitKnowledgeChunks(array $knowledgeChunks, string $commerceIntent): array
    {
        return match ($commerceIntent) {
            CommerceIntentLite::PRODUCT_SEARCH => array_slice($knowledgeChunks, 0, 2),
            CommerceIntentLite::ADVISORY_PRODUCT_SEARCH => array_slice($knowledgeChunks, 0, 3),
            default => $knowledgeChunks,
        };
    }

    private function sanitizeOptimizedShopQuery(string $query): string
    {
        $query = trim($query);

        if ($query === '') {
            return '';
        }

        $query = preg_split('/\R+/u', $query, 2)[0] ?? $query;
        $query = preg_replace('/^(?:keywords?|suchquery|search\s*query|query)\s*:\s*/iu', '', $query) ?? $query;
        $query = trim($query, " \t\n\r\0\x0B\"'`");
        $query = preg_replace('/\s+/u', ' ', $query) ?? $query;

        return trim($query);
    }

    /**
     * @return Generator<int, string, mixed, string>
     */
    private function streamFinalAnswer(string $finalPrompt): Generator
    {
        $fullOutput = '';
        $firstThinkLoop = true;
        $chunker = new StreamChunker();

        $this->thinkSuppressor->reset();

        foreach ($this->ollamaClient->stream($finalPrompt) as $token) {
            if (!is_string($token)) {
                continue;
            }

            $cleanToken = $this->thinkSuppressor->filter($token);

            if ($cleanToken === '') {
                if ($firstThinkLoop) {
                    yield $this->systemMsg('Denke nach...', 'think');
                    $firstThinkLoop = false;
                }

                continue;
            }

            $fullOutput .= $cleanToken;

            $chunk = $chunker->push($cleanToken);
            if ($chunk !== null) {
                yield $this->systemMsg($chunk, 'answer');
            }
        }

        $finalChunk = $chunker->flush();
        if ($finalChunk !== null) {
            yield $this->systemMsg($finalChunk, 'answer');
        } elseif ($fullOutput === '') {
            yield $this->systemMsg('❌ Es wurden keine Daten vom LLM empfangen.', 'err');
        }

        return $fullOutput;
    }

    /**
     * @param string[] $sources
     */
    private function emitSources(array $sources, string $prefix): string
    {
        return $this->systemMsg($prefix . implode(' ', $sources), 'info');
    }

    /**
     * @param string[] $sources
     */
    private function addSource(array &$sources, string $label): void
    {
        $badge = $this->badge($label);

        if (!in_array($badge, $sources, true)) {
            $sources[] = $badge;
        }
    }

    private function buildUserErrorMessage(Throwable $e): string
    {
        if (!$this->debug) {
            return '❌ Bei der Verarbeitung der Anfrage ist ein interner Fehler aufgetreten.';
        }

        return '❌ Interner Fehler: '
            . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    private function badge(string $label): string
    {
        return sprintf(
            '<span class="badge bg-info text-black">%s</span>',
            htmlspecialchars($label, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')
        );
    }

    private function systemMsg(string $msg, string $type = ''): string
    {
        if (!$this->systemMsgOn) {
            return '';
        }

        return match ($type) {
            'answer' => $msg,
            'err' => '<span class="text-danger">' . $msg . "</span>\n<hr>\n",
            'think' => '<span class="text-info think">' . $msg . "</span>\n",
            'info' => "\n\n<span class=\"text-info fw-bolder\">" . $msg . "</span>\n",
            'debug' => "\n\nDEBUG: <code>" . htmlspecialchars($msg, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</code>\n",
            default => $msg,
        };
    }
}