<?php

namespace ZenoChatbot\Services\Chat;

use ZenoChatbot\Helper\Settings;
use ZenoChatbot\Model\SavedReply;
use ZenoChatbot\Services\Chat\Contracts\HitCounterInterface;
use ZenoChatbot\Services\Chat\Contracts\LanguageResolverInterface;
use ZenoChatbot\Services\Chat\Contracts\SavedReplyMatcherInterface;
use ZenoChatbot\Services\Chat\Contracts\SemanticMatcherInterface;

if (!defined('ABSPATH')) {
    exit;
}
class SavedReplyService
{
    /**
     * Columns required by keyword/exact/contains matching and match formatting.
     * The embedding column (large JSON vectors) is intentionally excluded and
     * fetched separately only when the semantic path runs.
     */
    protected const MATCH_COLUMNS = ['id', 'question', 'answer', 'keywords', 'match_type'];

    /**
     * Maximum number of saved replies injected as LLM grounding on a matcher miss.
     */
    protected const GROUNDING_MAX_REPLIES = 50;

    /**
     * Hard character cap for the grounding block, keeping prompt token cost bounded.
     */
    protected const GROUNDING_MAX_CHARS = 8000;

    protected SavedReplyMatcherInterface $matcher;
    protected ?SemanticMatcherInterface $semanticMatcher = null;
    protected LanguageResolverInterface $languageResolver;
    protected HitCounterInterface $hitCounter;
    protected bool $semanticEnabled;

    public function __construct()
    {
        $this->matcher = new DefaultSavedReplyMatcher();
        $this->languageResolver = new DefaultLanguageResolver();
        $this->hitCounter = new DatabaseHitCounter();

        // Check if semantic matching is enabled in settings
        $this->semanticEnabled = (bool) Settings::get('saved_replies.semantic_enabled', false);
    }

    /**
     * Enable or disable semantic matching at runtime
     */
    public function setSemanticEnabled(bool $enabled): self
    {
        $this->semanticEnabled = $enabled;
        return $this;
    }

    /**
     * Set the semantic matcher instance
     */
    public function setSemanticMatcher(?SemanticMatcherInterface $matcher): self
    {
        $this->semanticMatcher = $matcher;
        return $this;
    }

    /**
     * Find a matching saved reply for the user message
     *
     * Priority order:
     * 1. Exact match
     * 2. Keyword match
     * 3. Contains match
     * 4. Semantic match (if enabled and embeddings exist)
     *
     * @param string $userMessage The user's message
     * @param string $language Language code (default: 'en')
     * @return array|null Matched reply data or null
     */
    public function findMatch($userMessage, $language = 'en-US'): ?array
    {
        if (empty(trim($userMessage))) {
            return null;
        }

        // Resolve language (with fallbacks)
        $languages = $this->languageResolver->resolve($language);

        // Fetch active replies for the languages, grouped by match_type
        $replies = $this->fetchReplies($languages);

        if (empty($replies)) {
            return null;
        }

        // 1. Try exact/keyword/contains matching first
        $match = $this->matcher->match($userMessage, $replies);

        // 2. If no match and semantic is enabled, try semantic matching
        if (!$match && $this->semanticEnabled) {
            $match = $this->trySemanticMatch($userMessage, $replies);
        }

        if ($match) {
            // Increment hit count
            $this->hitCounter->increment($match['id']);
        }

        return $match;
    }

    /**
     * Try semantic matching using embeddings
     *
     * The matcher is constructed lazily: it resolves LLM settings and an
     * embedding provider, which is wasted work for the majority of messages
     * that resolve via keyword matching (or when semantic is disabled).
     *
     * @param string $userMessage User's message
     * @param array $replies Grouped replies
     * @return array|null Match result or null
     */
    protected function trySemanticMatch($userMessage, $replies)
    {
        if ($this->semanticMatcher === null) {
            $this->semanticMatcher = new SemanticSavedReplyMatcher();
        }

        return $this->semanticMatcher->match($userMessage, $replies);
    }

    /**
     * Build a curated "approved answers" knowledge block from active saved replies.
     *
     * The deterministic/semantic matcher (findMatch) short-circuits the LLM on a
     * hit, so the LLM only runs when no reply matched. Feeding the saved replies
     * into the LLM context on that path lets paraphrased questions still be
     * answered from saved-reply content (hybrid behavior). Returns '' when there
     * are no usable replies for the resolved language.
     *
     * @param string $language Language code (e.g. 'en-US')
     * @return string Formatted Q/A block, or '' when empty
     */
    public function getGroundingKnowledge(string $language): string
    {
        $languages = $this->languageResolver->resolve($language);

        $replies = SavedReply::query()
            ->select(['question', 'answer'])
            ->where('is_active', 1)
            ->whereIn('language', $languages)
            ->orderBy('priority', 'desc')
            ->limit(static::GROUNDING_MAX_REPLIES)
            ->get();

        $lines = [];
        foreach ($replies as $reply) {
            $question = trim((string) $reply->question);
            $answer   = trim((string) $reply->answer);
            if ($question === '' || $answer === '') {
                continue;
            }
            $lines[] = 'Q: ' . $question . "\n" . 'A: ' . $answer;
        }

        if (empty($lines)) {
            return '';
        }

        $block = implode("\n\n", $lines);

        if (mb_strlen($block) > static::GROUNDING_MAX_CHARS) {
            $block = rtrim(mb_substr($block, 0, static::GROUNDING_MAX_CHARS));
        }

        return $block;
    }

    /**
     * Fetch replies grouped by match_type
     *
     * @param array $languages Array of language codes
     * @return array Replies grouped by match_type
     */
    protected function fetchReplies($languages)
    {
        $allReplies = SavedReply::query()
            ->select(static::MATCH_COLUMNS)
            ->where('is_active', 1)
            ->whereIn('language', $languages)
            ->orderBy('priority', 'desc')
            ->get();

        // Group by match_type
        $grouped = [
            'exact' => [],
            'keyword' => [],
            'contains' => []
        ];

        foreach ($allReplies as $reply) {
            $type = $reply->match_type ?? 'exact';
            if (isset($grouped[$type])) {
                $grouped[$type][] = $reply;
            }
        }

        return $grouped;
    }
}
