<?php

/**
 * LolaCore â€” MessageHandler
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Chat;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use LolaCore\LLM\SelectedModelResolver;
use LolaCore\Memory\MemoryManager;
use WordPress\AiClient\AiClient;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;

/**
 * Class MessageHandler
 *
 * Handles intelligent processing of chat messages: sanitization
 * and memory extraction.
 */
class MessageHandler
{
    private MemoryManager $memoryManager;

    public function __construct(MemoryManager $memoryManager)
    {
        $this->memoryManager = $memoryManager;
    }

    /**
     * Sanitize user input before processing.
     * Remove null bytes and limit length; preserve HTML for code snippets.
     *
     * @param string $input Raw input.
     * @return string Sanitized input.
     */
    public function sanitize(string $input): string
    {
        $clean = \str_replace("\0", '', $input);
        // We no longer strip HTML tags so that code snippets can be sent to the LLM
        return \mb_substr($clean, 0, 4000);
    }

    /**
     * Extract relevant facts from a conversation window for memory storage.
     *
     * Uses the last 5 exchanges (10 messages) from the conversation history
     * plus the current exchange to give the extraction LLM enough context to
     * detect facts that emerge across multiple turns.
     *
     * @param mixed  $deprecatedllm     Unused â€” kept for backwards compatibility.
     * @param string $userMessage        The user's message.
     * @param string $assistantResponse  The assistant's response.
     * @param string $sessionId          The current session ID.
     * @param array  $history            Full conversation history for multi-turn context.
     * @return array Array of extracted facts.
     */
    public function extractFacts($deprecatedllm, string $userMessage, string $assistantResponse, string $sessionId, array $history = []): array
    {
        $contextWindow = $this->buildExtractionContext($history, $userMessage, $assistantResponse);

        $prompt = $contextWindow . "\n\n" .
                  "Task: Extract new long-term facts about the WordPress site, the user's business context, or preferences expressed in this conversation window. " .
                  "Focus on facts that emerged across multiple messages, not just the last exchange. " .
                  "Valid factType values: user_preference, business_context, site_state, decision_history. " .
                  "Format as a JSON array of objects with keys: 'factType' (string), 'content' (string), 'importance' (integer 1-10). " .
                  "If nothing is relevant for long-term memory, return an empty array []. Respond ONLY with valid JSON.";

        try {
            $modelConfig = $this->resolveModel();
            $model = AiClient::defaultRegistry()->getProviderModel($modelConfig['provider'], $modelConfig['model']);
            $model->getConfig()->setOutputMimeType('application/json');

            // SDK default transport timeout is 5s, which is too short for
            // reasoning-capable providers (DeepSeek) processing a multi-turn
            // context window. Use a comfortable budget; this call is async
            // from the user's perspective and a timeout silently drops facts.
            $options = new RequestOptions();
            $options->setTimeout(30.0);
            $options->setConnectTimeout(5.0);
            $model->setRequestOptions($options);

            $messages = [
                new \WordPress\AiClient\Messages\DTO\Message(
                    \WordPress\AiClient\Messages\Enums\MessageRoleEnum::user(),
                    [new \WordPress\AiClient\Messages\DTO\MessagePart($prompt)]
                )
            ];

            $result = $model->generateTextResult($messages);
            $candidate = $result->getCandidates()[0];
            $jsonStr = '';
            foreach ($candidate->getMessage()->getParts() as $part) {
                // Reasoning models emit their thinking as a text part on the
                // thought channel. Concatenating it ahead of the JSON makes
                // json_decode() below fail, and every fact in the turn is lost.
                if ($part->getType()->isText() && !$part->getChannel()->isThought()) {
                    $jsonStr .= $part->getText();
                }
            }

            if (\preg_match('/```(?:json)?\s*([\s\S]*?)\s*```/', $jsonStr, $matches)) {
                $jsonStr = $matches[1];
            }

            $facts = \json_decode($jsonStr, true);
            if (!\is_array($facts)) {
                // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Memory extraction telemetry.
                if (defined('WP_DEBUG') && WP_DEBUG) { error_log('[LolaCore Memory] extractFacts: LLM returned non-JSON â€” ' . \mb_substr($jsonStr, 0, 200)); }
                return [];
            }

            $validTypes = ['user_preference', 'business_context', 'site_state', 'decision_history'];
            $savedFacts = [];
            foreach ($facts as $fact) {
                if (!isset($fact['factType'], $fact['content'], $fact['importance'])) {
                    continue;
                }
                // Normalize unrecognized factTypes to decision_history
                $factType = \in_array($fact['factType'], $validTypes, true)
                    ? $fact['factType']
                    : 'decision_history';

                $this->memoryManager->saveFact(
                    $factType,
                    $fact['content'],
                    (int) $fact['importance'],
                    'conversation',
                    $sessionId
                );
                $savedFacts[] = $fact;
            }


            return $savedFacts;
        } catch (\Throwable $e) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Memory extraction error log.
            if (defined('WP_DEBUG') && WP_DEBUG) { error_log('[LolaCore Memory] extractFacts FAILED: ' . $e->getMessage()); }
            return [];
        }
    }

    /**
     * Builds a multi-turn context window for the extraction LLM.
     *
     * Takes the last 5 exchanges (10 messages) from history plus the current
     * exchange, giving the model enough context to detect facts that span
     * multiple turns.
     *
     * @param array  $history            Full conversation history.
     * @param string $userMessage        Current user message.
     * @param string $assistantResponse  Current assistant response.
     * @return string Formatted context window.
     */
    private function buildExtractionContext(array $history, string $userMessage, string $assistantResponse): string
    {
        $turns = [];

        // Filter to user/assistant messages only
        $relevantHistory = array_filter($history, function ($msg) {
            return \in_array($msg['role'] ?? '', ['user', 'assistant'], true);
        });
        $relevantHistory = array_values($relevantHistory);

        // Take the last 10 messages (â‰ˆ5 exchanges) for context
        $recentTurns = array_slice($relevantHistory, -10);

        foreach ($recentTurns as $msg) {
            $role    = strtoupper($msg['role']);
            $content = $msg['content'] ?? '';
            if (!empty(trim($content))) {
                // Truncate long messages to keep the extraction prompt manageable
                $turns[] = "{$role}: " . \mb_substr($content, 0, 500);
            }
        }

        // Always append the current exchange as the most recent context
        $turns[] = "USER: " . \mb_substr($userMessage, 0, 500);
        $turns[] = "ASSISTANT: " . \mb_substr($assistantResponse, 0, 500);

        return "Conversation window (last " . count($turns) . " messages):\n" . implode("\n", $turns);
    }

    /**
     * Resolves the AI model for memory extraction.
     *
     * Reads the same `selected_model` config key used by WPAIClientAdapter
     * to ensure extraction uses the same provider/model as the main chat.
     *
     * @return array{provider: string, model: string}
     * @throws \RuntimeException If no AI provider is configured.
     */
    private function resolveModel(): array
    {
        return SelectedModelResolver::fromConfig(
            null,
            'No AI provider configured for memory extraction.'
        );
    }

    /**
     * Kept for backwards compatibility if needed, but logic moved to WPAIClientAdapter.
     */
    public function buildMessageHistory(array $history, string $currentMessage): array
    {
        return $history;
    }
}
