<?php
/**
 * LLM Service
 *
 * @package ZenoChatbot\Service
 */

namespace ZenoChatbot\Services\LLMService;

use ZenoChatbot\Framework\Support\Arr;
use ZenoChatbot\Helper\Settings;
use ZenoChatbot\Services\Chat\SavedReplyService;
use ZenoChatbot\Services\LLMService\AIClient\AiClient;
use ZenoChatbot\Services\LLMService\AIClient\AIClientConfig;
use ZenoChatbot\Services\LLMService\Contracts\LLMResponse;
use ZenoChatbot\Support\Instructions;

// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}

/**
 * LLM Service
 *
 * Thin facade that reads existing settings and delegates to the new AIClient.
 * Public API remains stable: create(), getResponse(), testConnection().
 */
class LLMService
{
    /**
     * Byte cap per default knowledge-base index injected into the prompt.
     *
     * Interim measure (audit F-2): the previous 512 KB handler default allowed
     * up to ~2.5 MB of context per request across the 5 indexes. Proper fix is
     * top-k retrieval of relevant chunks; tracked in PERFORMANCE_PLAN.md B5.1.
     */
    public const RESOURCE_MAX_BYTES = 128 * 1024;

    public function __construct()
    {
        // System prompts now handled by Instructions class
    }

    public static function create()
    {
        return new static();
    }

    /**
     * Generate a response using AIClient while preserving legacy LLMResponse shape.
     *
     * @param string $message
     * @param array $context Legacy context array with sender_type/message
     * @return LLMResponse
     * @throws \Throwable
     */
    public function getResponse($message, $context = [])
    {
        $settings = Settings::getLLMSettings(true);
        $cfg = $this->getClientConfig($settings);

        // Prepare prompt
        $pb = AiClient::config($cfg)->prompt((string)$message);

        // Build system prompts array
        $systemPrompts = [];

        // 1. Application instruction (Strategy C) - core behavior
        $applicationInstruction = Instructions::get();
        if (!empty(trim($applicationInstruction))) {
            $systemPrompts[] = $applicationInstruction;
        }

        // 2. Additional instruction (user's system prompt with header)
        $additionalInstruction = Instructions::getAdditionalInstruction();
        if (!empty(trim($additionalInstruction))) {
            $systemPrompts[] = $additionalInstruction;
        }

        // 3. Language instruction - tells AI which language to respond in
        $languageInstruction = Instructions::getLanguageInstruction();
        if (!empty(trim($languageInstruction))) {
            $systemPrompts[] = $languageInstruction;
        }

        // 4. Saved-reply grounding (hybrid). The deterministic/semantic matcher
        // short-circuits the LLM on a hit, so this path is only reached on a
        // miss — letting paraphrased questions still be answered from approved
        // saved replies without losing verbatim answers when the matcher hits.
        $savedReplyKnowledge = $this->getSavedReplyGrounding();
        if ($savedReplyKnowledge !== '') {
            $systemPrompts[] = "## Approved Answers\n"
                . "These are approved, canonical answers to common questions. When the user's "
                . "question matches one of these topics, base your reply on the matching approved "
                . "answer and keep its facts accurate. Answer naturally and never mention that "
                . "these answers were pre-written.\n\n"
                . $savedReplyKnowledge;
        }

        // Apply all system prompts
        if (!empty($systemPrompts)) {
            $pb->applicationPrompt($systemPrompts);
        }

        // Map legacy context (sender_type => role)
        $turns = [];
        foreach ((array)$context as $t) {
            if (!is_array($t)) {
                continue;
            }
            $msg = isset($t['message']) ? (string)$t['message'] : '';
            if ($msg === '') {
                continue;
            }
            $sender = strtolower((string)($t['sender_type'] ?? 'customer'));
            $role = ($sender === 'ai' || $sender === 'assistant') ? 'assistant' : 'user';
            $turns[] = ['message' => $msg, 'role' => $role];
        }
        if (!empty($turns)) {
            $pb->history($turns);
        }

        // Load default resources (internal/external indexes), capped per index
        $pb->resources(true, ['max_bytes' => static::RESOURCE_MAX_BYTES]);

        // Execute
        $res = $pb->generateTextResult();
        $text = $res->toText();
        $usage = $res->getTokenUsage();
        $totalTokens = $usage ? (int)$usage->getTotalTokens() : 0;
        $promptTokens = $usage ? (int)$usage->getPromptTokens() : 0;
        $completionTokens = $usage ? (int)$usage->getCompletionTokens() : 0;

        $responseData = [
            'success'           => true,
            'message'           => $res->toText(),
            'tokens_used'       => $totalTokens,
            'prompt_tokens'     => $promptTokens,
            'completion_tokens' => $completionTokens,
            'provider'          => Arr::get($settings, 'provider'),
            'model'             => $cfg->getModel(),
            'raw_response'      => $res->getRawResponse(),
            'raw_request'       => $res->getRawRequest(),
            'finish_reason'     => $res->getFinishReason(),
            'citations'         => $res->getCitations(),
        ];

        // Include skipped files info if any files were skipped due to size limits
        if ($res->hasSkippedFiles()) {
            $responseData['skipped_files'] = $res->getSkippedFiles();
        }

        return LLMResponse::createFromArray($responseData);
    }

    public function testConnection()
    {
        $settings = Settings::getLLMSettings(true);
        $cfg = $this->getClientConfig($settings);

        $result = AiClient::config($cfg)->testConnection();

        $usage = (array)($result['usage'] ?? []);
        $p = (int)($usage['prompt_tokens'] ?? $usage['prompt'] ?? 0);
        $c = (int)($usage['completion_tokens'] ?? $usage['completion'] ?? 0);
        $t = (int)($usage['total_tokens'] ?? $usage['total'] ?? ($p + $c));

        return [
            'success'  => !empty($result['success']),
            'message'  => (string)($result['message'] ?? 'OK'),
            'provider' => (string)($result['provider'] ?? Arr::get($settings, 'provider')),
            'model'    => (string)($result['model'] ?? $cfg->getModel()),
            'usage'    => [
                'prompt_tokens'     => $p,
                'completion_tokens' => $c,
                'total_tokens'      => $t,
            ],
        ];
    }

    /**
     * Generate a transcript-only conversation summary (no KB resources).
     *
     * @param array $turns Array of ['role' => 'user|assistant', 'message' => string]
     * @return array{text:string,provider:string,model:string}
     * @throws \Throwable
     */
    public function summarizeConversation(array $turns): array
    {
        $settings = Settings::getLLMSettings(true);
        $cfg = $this->getClientConfig($settings);

        $summaryInstruction = implode("\n", [
            'You are summarizing a completed support conversation.',
            'Use ONLY the provided conversation transcript.',
            'Do NOT use external knowledge, assumptions, or missing context.',
            'Return exactly one continuous plain-text paragraph.',
            'Do not use headings, bullets, numbering, or markdown.',
            'In that paragraph, clearly cover:',
            '1) what the user asked or needed,',
            '2) what response/solution was provided by AI or agent,',
            '3) what resources/links/information were provided in the conversation.',
            'If no resources were provided, explicitly state that no additional resources were provided.',
            'Keep the summary concise and factual.',
        ]);

        $pb = AiClient::config($cfg)->prompt('Generate the conversation summary now.');
        $pb->systemPrompt($summaryInstruction);
        $pb->history($turns);

        $res = $pb->generateTextResult();

        return [
            'text'     => static::normalizeSummaryParagraph((string)$res->toText()),
            'provider' => (string)Arr::get($settings, 'provider', ''),
            'model'    => (string)$cfg->getModel(),
        ];
    }

    protected static function normalizeSummaryParagraph(string $text): string
    {
        $normalized = wp_strip_all_tags($text);

        // Flatten common list/heading-like output into a single paragraph.
        $normalized = preg_replace('/^\s*(?:(?:[-*]|\x{2022})|\d+\.)\s*/mu', '', $normalized) ?? $normalized;
        $normalized = preg_replace('/\b(?:User pain point|AI response|Provided information)\s*:\s*/i', '', $normalized) ?? $normalized;
        $normalized = str_replace(["\r\n", "\r", "\n"], ' ', $normalized);
        $normalized = preg_replace('/\s{2,}/', ' ', $normalized) ?? $normalized;

        return trim($normalized);
    }

    private const DEPRECATED_MODELS = [
        'claude-fable-5' => 'claude-opus-4-8',
    ];

    /**
     * Build the approved-answers grounding block from active saved replies.
     *
     * Failures here must never break a chat reply, so any error degrades to an
     * empty block (the LLM simply runs without saved-reply grounding).
     */
    private function getSavedReplyGrounding(): string
    {
        try {
            $language = (string) Settings::get('general.language', 'en-US');
            return (new SavedReplyService())->getGroundingKnowledge($language);
        } catch (\Throwable $e) {
            return '';
        }
    }

    public function getClientConfig($settings)
    {
        $provider = Arr::get($settings, 'provider');
        $apiKey = Arr::get($settings, 'api_key');
        $model = static::DEPRECATED_MODELS[Arr::get($settings, 'model')] ?? Arr::get($settings, 'model');
        $temp = Arr::get($settings, 'temperature');
        $maxTok = Arr::get($settings, 'max_tokens');

        if ($provider === '' || $apiKey === '') {
            throw new \RuntimeException('LLM settings are missing provider or API key. Please configure Settings → LLM.');
        }

        // Build validated AIClient config
        return (new AIClientConfig([
            'provider'    => $provider,
            'model'       => $model,
            'api_key'     => $apiKey,
            'temperature' => is_null($temp) ? null : (float)$temp,
            'max_tokens'  => is_null($maxTok) ? null : (int)$maxTok,
        ]))->build();
    }
}
