<?php

namespace ZenoChatbot\Services;

use ZenoChatbot\App;
use ZenoChatbot\Framework\Support\Arr;
use ZenoChatbot\Helper\Helper;
use ZenoChatbot\Helper\Settings;
use ZenoChatbot\Model\Conversation;
use ZenoChatbot\Model\Customer;
use ZenoChatbot\Model\Meta;
use ZenoChatbot\Model\Message;
use ZenoChatbot\Model\PageSource;
use ZenoChatbot\Services\Chat\SavedReplyService;
use ZenoChatbot\Services\LLMService\LLMService;
use ZenoChatbot\Support\Hooks;

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

class ChatService
{
    private const IDEMPOTENCY_WINDOW_SECONDS = 120;

    /**
     * Saved reply service instance
     */
    protected ?SavedReplyService $savedReplyService = null;

    /**
     * Get the SavedReplyService instance (lazy loaded)
     */
    protected function getSavedReply($message)
    {
        if ($this->savedReplyService === null) {
            $this->savedReplyService = new SavedReplyService();
        }

        $language = Settings::get('general.language', 'en-US');
        return $this->savedReplyService->findMatch($message, $language);
    }

    /**
     * Get response for a user message
     * Checks SavedReply first, falls back to LLM
     *
     * @param string $message User's message
     * @param array $context Conversation context
     * @return ChatResponse
     * @throws \Throwable
     */
    public function getResponse($message, $context = []): ChatResponse
    {
        $savedReply = $this->getSavedReply($message);

        if (!empty($savedReply)) {
            return new ChatResponse(
                $savedReply['answer'],
                'saved_reply',
                [
                    'saved_reply_id' => $savedReply['id'],
                    'match_type' => $savedReply['match_type'],
                    'question' => $savedReply['question']
                ]
            );
        }

        // 2. Fall back to LLM
        $llmResponse = LLMService::create()->getResponse($message, $context);

        return new ChatResponse(
            $llmResponse->message,
            'llm',
            [
                'tokens_used'       => $llmResponse->tokens_used ?? 0,
                'prompt_tokens'     => $llmResponse->prompt_tokens ?? 0,
                'completion_tokens' => $llmResponse->completion_tokens ?? 0,
                'model'             => $llmResponse->model ?? null,
                'provider'          => $llmResponse->provider ?? null,
                'raw_response'      => $llmResponse->raw_response ?? null,
                'raw_request'       => $llmResponse->raw_request ?? null,
                'finish_reason'     => $llmResponse->finish_reason ?? null,
                'citations'         => $llmResponse->citations ?? [],
            ]
        );
    }

    /**
     * Process a user message and return AI response
     *
     * This is the main entry point for chat processing. It handles:
     * 1. User message storage
     * 2. AI response (SavedReply or LLM)
     * 3. AI message storage
     * 4. Error handling and storage
     *
     * @param string $message User's message
     * @param array $userContext Optional user context for new sessions (name, email)
     * @param string $language Language code (default: 'en')
     * @return ChatResponse Always returns a valid response (success or error fallback)
     */
    public function processMessage(string $message, array $userContext = [], string $language = 'en', array $sourceData = []): ChatResponse
    {
        $conversation = null;
        $userMessageId = null;
        $idempotencyKey = null;

        try {
            // 1. Ensure we have a conversation
            if (!$this->hasExistingSession()) {
                $conversation = $this->createConversation($userContext, $sourceData);
            } else {
                $conversation = $this->getActiveConversation();
            }

            if (!$conversation) {
                throw new \RuntimeException('Unable to create or retrieve conversation');
            }

            $idempotencyKey = $this->buildIdempotencyKey($conversation->id, $message);
            if ($idempotencyKey && ($cached = $this->getCachedResponse($idempotencyKey)) !== null) {
                return $cached;
            }

            // 2. Store user message
            $userMessage = $this->storeUserMessage($conversation, $message);
            $userMessageId = $userMessage->id;

            // 3. Get context for LLM
            $context = $this->getContext();
            $context = Hooks::applyFilters(Hooks::CHAT_CONTEXT, ['context' => $context, 'message' => $message, 'conversation' => $conversation])['context'];

            // 4. Get AI response (SavedReply or LLM)
            $response = $this->getAIResponse($message, $context, $language);

            // 5. Validate response is not empty
            if (empty(trim($response->message))) {
                throw new \RuntimeException('AI returned empty response');
            }

            // 6. Store AI message with metadata
            $this->storeAIMessage($conversation, $response);

            Hooks::doAction(Hooks::CHAT_MESSAGE_PROCESSED, ['response' => $response, 'conversation' => $conversation, 'message' => $message]);

            if ($idempotencyKey) {
                $this->cacheResponse($idempotencyKey, $response);
            }

            return $response;

        } catch (\Throwable $e) {
            // 7. Handle error: store error metadata, return fallback
            return $this->handleError($e, $conversation, $userMessageId);
        }
    }

    /**
     * Get AI response from SavedReply or LLM
     * Internal method used by processMessage()
     *
     * @param string $message User's message
     * @param array $context Conversation context
     * @param string $language Language code
     * @return ChatResponse
     * @throws \Throwable If response is empty or API fails
     */
    protected function getAIResponse(string $message, array $context, string $language): ChatResponse
    {
        $savedReply = $this->getSavedReply($message);

        if ($savedReply) {
            $responseMessage = $savedReply['answer'] ?? '';

            // Validate-saved reply is not empty
            if (empty(trim($responseMessage))) {
                throw new \RuntimeException('Saved reply returned empty response');
            }

            return new ChatResponse(
                $responseMessage,
                'saved_reply',
                [
                    'saved_reply_id' => $savedReply['id'],
                    'match_type'     => $savedReply['match_type'],
                    'question'       => $savedReply['question']
                ]
            );
        }

        // Fall back to LLM - this may throw exceptions
        $llmResponse = LLMService::create()->getResponse($message, $context);

        // Validate LLM response
        $responseMessage = $llmResponse->message ?? '';
        if (empty(trim($responseMessage))) {
            throw new \RuntimeException('LLM returned empty response');
        }

        $metadata = [
            'tokens_used'       => $llmResponse->tokens_used ?? 0,
            'prompt_tokens'     => $llmResponse->prompt_tokens ?? 0,
            'completion_tokens' => $llmResponse->completion_tokens ?? 0,
            'model'             => $llmResponse->model ?? null,
            'provider'          => $llmResponse->provider ?? null,
            'raw_response'      => $llmResponse->raw_response ?? null,
            'raw_request'       => $llmResponse->raw_request ?? null,
            'finish_reason'     => $llmResponse->finish_reason ?? null,
            'citations'         => $llmResponse->citations ?? [],
        ];

        // Include skipped files info if any files were skipped due to size limits
        $skippedFiles = $llmResponse->skipped_files ?? null;
        if (!empty($skippedFiles)) {
            $metadata['skipped_files'] = $skippedFiles;
        }

        return new ChatResponse($responseMessage, 'llm', $metadata);
    }

    /**
     * Store user message in conversation with meta
     *
     * @param Conversation $conversation
     * @param string $message
     * @return Message
     */
    protected function storeUserMessage(Conversation $conversation, string $message): Message
    {
        $userMessage = $conversation->messages()->create([
            'message'     => $message,
            'sent_via'    => 'chat',
            'sender_type' => 'customer',
            'sender_id'   => $conversation->customer_id
        ]);

        $request = App::request();

        $userAgent = sanitize_text_field($request->header('User-Agent'));
        $referer   = esc_url_raw($request->header('Referer'));

        // Store user message meta
        $userMessage->setMeta([
            'timestamp'  => current_time('mysql'),
            'ip_address' => Helper::getClientIp(),
            'source'     => 'user',
            'user_agent' => $userAgent,
            'referer'    => $referer,
            'language'   => Settings::get('general.language', 'en-US'),
        ]);

        return $userMessage;
    }

    /**
     * Store AI message with unified meta
     *
     * @param Conversation $conversation
     * @param ChatResponse $response
     * @return Message
     */
    protected function storeAIMessage(Conversation $conversation, ChatResponse $response)
    {
        $aiMessage = $conversation->messages()->create([
            'message'           => $response->message,
            'sent_via'          => 'chat',
            'sender_type'       => 'ai',
            'sender_id'         => null,
            'prompt_tokens'     => (int) $response->getMeta('prompt_tokens', 0),
            'completion_tokens' => (int) $response->getMeta('completion_tokens', 0),
            'token_usage'       => (int) $response->getMeta('tokens_used', 0)
        ]);

        // Build unified meta based on source
        $meta = [
            'timestamp' => current_time('mysql'),
            'source'    => $response->source,
        ];

        if ($response->isLLM()) {
            $meta += [
                'provider'          => $response->getMeta('provider'),
                'model'             => $response->getMeta('model'),
                'tokens_used'       => $response->getMeta('tokens_used', 0),
                'prompt_tokens'     => $response->getMeta('prompt_tokens', 0),
                'completion_tokens' => $response->getMeta('completion_tokens', 0),
                'finish_reason'     => $response->getMeta('finish_reason'),
                'raw_response'      => $response->getMeta('raw_response'),
            ];
        } elseif ($response->isSavedReply()) {
            $meta += [
                'saved_reply_id'   => $response->getMeta('saved_reply_id'),
                'match_type'       => $response->getMeta('match_type'),
                'matched_question' => $response->getMeta('question'),
                'similarity'       => $response->getMeta('similarity'),
            ];
        }

        $aiMessage->setMeta($meta);

        return $aiMessage;
    }

    /**
     * Handle error: store error via Message meta, return fallback response
     *
     * @param \Throwable $e The exception
     * @param Conversation|null $conversation
     * @param int|null $userMessageId
     * @return ChatResponse Error response with fallback message
     */
    protected function handleError(\Throwable $e, ?Conversation $conversation, ?int $userMessageId): ChatResponse
    {
        // Get a fallback message from settings
        $fallbackMessage = \ZenoChatbot\Support\Conversation::fallbackErrorResponse();

        // Determine error code
        $errorCode = \ZenoChatbot\Support\Conversation::categoriseError($e);

        // Store fallback message as AI response (so conversation history is complete)
        // and attach error metadata via Message meta
        $aiMessageId = null;
        if ($conversation) {
            try {
                $aiMessage = $conversation->messages()->create([
                    'message'     => $fallbackMessage,
                    'sent_via'    => 'chat',
                    'sender_type' => 'ai',
                    'sender_id'   => null
                ]);
                $aiMessageId = $aiMessage->id;

                // Store unified error meta
                $aiMessage->setMeta([
                    'timestamp'       => current_time('mysql'),
                    'source'          => 'error',
                    'error_code'      => $errorCode,
                    'error_message'   => $e->getMessage(),
                    'provider'        => Settings::get('llm.provider'),
                    'model'           => Settings::get('llm.model'),
                    'user_message_id' => $userMessageId,
                ]);
            } catch (\Throwable $storeError) {
                // Silently fail - we can't do anything about it
            }
        }

        return ChatResponse::error(
            $fallbackMessage,
            $e->getMessage(),
            $errorCode,
            [
                'conversation_id' => $conversation ? $conversation->id : null,
                'user_message_id' => $userMessageId,
                'ai_message_id'   => $aiMessageId,
            ]
        );
    }

    public function hasExistingSession()
    {
        return \ZenoChatbot\Support\Conversation::hasExistingSession();
    }

    public function getActiveConversation()
    {
        return \ZenoChatbot\Support\Conversation::getActiveConversation();
    }

    public function getConversationHistory()
    {
        return \ZenoChatbot\Support\Conversation::getConversationHistory();
    }

    public function getContext()
    {
        return \ZenoChatbot\Support\Conversation::getContext();
    }


    public function endSession()
    {
        if ($conversation = $this->getActiveConversation()) {

            $conversation->update([
                'ended_at' => current_time('mysql'),
                'status'   => 'closed'
            ]);

            // Summary runs as a background job — closing must not wait on an LLM call
            ConversationLifecycleService::requestSummaryGeneration((int) $conversation->id, 'user_close', 'customer');

            Hooks::doAction(Hooks::SESSION_ENDED, (int) $conversation->id);
        }

        Helper::clearCookieData('zeno_chatbot_session');
    }

    public function storeMessage($data = [])
    {

        if (!$this->hasExistingSession()) {
            throw new \Exception('No active conversation');
        }

        if (!$this->getActiveConversation()) {
            throw new \Exception('No active conversation');
        }

        $conversation = $this->getActiveConversation();

        if (Arr::get($data, 'sender_type') === 'customer') {
            $data['sender_id'] = $conversation->customer_id;
        }

        return $conversation->messages()->create($data);
    }

    public function createConversation($userContext = [], array $sourceData = [])
    {
        $customer = $this->getCustomer($userContext);

        $customer
            ->conversations()
            ->where('ended_at', null)
            ->where('status', 'open')
            ->update([
                'ended_at' => current_time('mysql'),
                'status'   => 'closed'
            ])
        ;

        $conversation = $customer->conversations()->create([
            'department' => 'general',
            'started_at' => current_time('mysql'),
            'status'     => 'open'
        ]);

        $sessionId = Helper::generateSessionKey($customer->id);

        $sessionData = [
            'session_id'      => $sessionId,
            'customer_id'     => $customer->id,
            'conversation_id' => $conversation->id,
            'created_at'      => time(),
            'last_activity'   => time()
        ];

        Helper::updateSession($sessionData);

        if (!empty($sourceData)) {
            PageSource::updateOrCreate(
                [
                    'entity_type' => Conversation::class,
                    'entity_id'   => (int) $conversation->id,
                    'meta_key'    => PageSource::META_KEY, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
                ],
                [
                    'meta_value' => maybe_serialize($sourceData), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
                ]
            );
        }

        Hooks::doAction(Hooks::CHAT_CONVERSATION_CREATED, ['conversation' => $conversation, 'user_context' => $userContext, 'source_data' => $sourceData]);

        return $conversation;
    }

    public function getCustomer($customerData = [])
    {
        if (empty($customerData['email'])) {
            throw new \Exception('Email is required');
        }

        $customer = Customer::query()
            ->where('email', $customerData['email'])
            ->first()
        ;

        if ($customer) {
            return $customer;
        }

        if (empty($customerData['name'])) {
            throw new \Exception('Name is required');
        }

        return Customer::create($customerData);
    }

    private function buildIdempotencyKey(int $conversationId, string $message): ?string
    {
        $request = App::request();
        $queryTimestamp = $request->input('query_timestamp');
        if (empty($queryTimestamp) || !is_numeric($queryTimestamp)) {
            return null;
        }

        $timestampMs = (int) $queryTimestamp;
        if ($timestampMs <= 0) {
            return null;
        }

        $sessionId = (string) Arr::get(Helper::getSessionData(), 'session_id', '');
        if ($sessionId === '') {
            return null;
        }

        $normalizedMessage = sanitize_textarea_field($message);
        $fingerprint = md5($conversationId . '|' . $sessionId . '|' . $timestampMs . '|' . $normalizedMessage);
        return 'zenochatbot_chat_idempotency_' . $fingerprint;
    }

    private function getCachedResponse(string $cacheKey): ?ChatResponse
    {
        $cached = get_transient($cacheKey);
        if (!is_array($cached)) {
            return null;
        }

        $message = (string) ($cached['message'] ?? '');
        if ($message === '') {
            return null;
        }

        return new ChatResponse(
            $message,
            (string) ($cached['source'] ?? 'llm'),
            (array) ($cached['metadata'] ?? []),
            (bool) ($cached['success'] ?? true)
        );
    }

    private function cacheResponse(string $cacheKey, ChatResponse $response): void
    {
        set_transient($cacheKey, $response->toArray(), static::IDEMPOTENCY_WINDOW_SECONDS);
    }
}
