<?php

/**
 * LolaCore - Distiller
 *
 * Converts raw document text into a playbook draft using the user's configured LLM.
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Playbooks;

use LolaCore\I18n\ErrorMessages;
use LolaCore\PluginBuilder\Generation\AiClientHelper;
use LolaCore\Skills\YamlParser;

class Distiller
{
    public const MAX_OUTPUT_TOKENS = 4096;

    private PlaybookLoader $loader;
    private DistillDraftQuality $quality;

    /** @var callable(string,string,float,int):string */
    private $generateText;

    /**
     * @param callable(string,string,float,int):string|null $generateText Injectable for tests.
     */
    public function __construct(
        ?PlaybookLoader $loader = null,
        ?callable $generateText = null,
        ?DistillDraftQuality $quality = null
    ) {
        $parser    = new YamlParser();
        $validator = new PlaybookValidator();
        $this->loader  = $loader ?? new PlaybookLoader($parser, $validator);
        $this->quality = $quality ?? new DistillDraftQuality($this->loader);
        $this->generateText = $generateText ?? static function (
            string $system,
            string $user,
            float $temperature,
            int $maxTokens
        ): string {
            $helper = new AiClientHelper();
            return $helper->generateText($system, $user, $temperature, $maxTokens);
        };
    }

    /**
     * @throws \InvalidArgumentException When source text is empty or too large.
     * @throws \RuntimeException When the LLM call fails or quality checks fail after retry.
     */
    public function distill(string $sourceText, string $regenerateInstructions = ''): string
    {
        $sourceText = (new PlaybookDocumentReader())->normalize($sourceText);
        if ($sourceText === '') {
            throw new \InvalidArgumentException('Source text cannot be empty.');
        }

        (new PlaybookDocumentReader())->enforceExtractedLimit($sourceText);

        $formatManual = $this->loadPlaybookFormatManual();
        $systemPrompt = PlaybookDistillTemplate::buildSystemPrompt($formatManual);
        $userPrompt   = PlaybookDistillTemplate::buildUserPrompt($sourceText, $regenerateInstructions);

        try {
            $raw   = ($this->generateText)($systemPrompt, $userPrompt, 0.1, self::MAX_OUTPUT_TOKENS);
            $draft = $this->normalizeDraft($raw);

            if ($draft === null) {
                $formatPrompt = PlaybookDistillTemplate::buildFormatCorrectionUserPrompt(
                    $sourceText,
                    $raw,
                    $regenerateInstructions
                );
                $rawRetry = ($this->generateText)($systemPrompt, $formatPrompt, 0.1, self::MAX_OUTPUT_TOKENS);
                $draft    = $this->normalizeDraft($rawRetry);
            }

            if ($draft === null) {
                throw new \RuntimeException(
                    'Distillation did not return YAML frontmatter. The model may have output code instead of a filled template.'
                );
            }

            $issue = $this->quality->assess($draft, mb_strlen($sourceText));

            if ($issue !== null) {
                $correctionPrompt = PlaybookDistillTemplate::buildCorrectionUserPrompt(
                    $sourceText,
                    $draft,
                    $issue,
                    $regenerateInstructions
                );
                $rawRetry   = ($this->generateText)($systemPrompt, $correctionPrompt, 0.1, self::MAX_OUTPUT_TOKENS);
                $draft      = $this->normalizeDraft($rawRetry);
                if ($draft === null) {
                    throw new \RuntimeException(
                        'Distillation did not return YAML frontmatter after correction. Try Regenerate with instructions.'
                    );
                }
                $retryIssue = $this->quality->assess($draft, mb_strlen($sourceText));
                if ($retryIssue !== null) {
                    throw new \RuntimeException(
                        'Distillation produced a low-quality draft (' . $retryIssue . '). '
                        . 'Use Regenerate with instructions (e.g. "focus on FSE concepts, no code dumps").'
                    );
                }
            }
        } catch (\RuntimeException $e) {
            throw $e;
        } catch (\Throwable $e) {
            // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Wrapped for REST/admin handlers that escape user-facing output.
            throw new \RuntimeException(
                ErrorMessages::formatProviderError($e->getMessage()),
                0,
                $e
            );
            // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
        }

        if ($draft === '') {
            throw new \RuntimeException('Distillation returned empty content. Try again or shorten the source document.');
        }

        return $draft;
    }

    private function loadPlaybookFormatManual(): string
    {
        $content = $this->loader->readRawContentBySlug('playbook-format');
        if ($content === null || trim($content) === '') {
            throw new \RuntimeException('Built-in playbook-format manual is missing.');
        }

        return $content;
    }

    /**
     * @return string|null Normalized playbook markdown, or null when the LLM output has no recoverable frontmatter.
     */
    private function normalizeDraft(string $raw): ?string
    {
        $extracted = DistillDraftExtractor::extract($raw);
        if ($extracted === null || !str_starts_with($extracted, '---')) {
            return null;
        }

        return PlaybookFrontmatterNormalizer::normalize(trim($extracted));
    }
}
