<?php

namespace App\Services;

use App\Models\AiDocument;
use App\Models\AiDocumentChunk;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class RagService
{
    protected $ollamaUrl;
    protected $embeddingModel = 'nomic-embed-text';
    protected $chunkSize = 500;
    protected $chunkOverlap = 50;
    protected $timeout = 300; // Increased timeout for embeddings

    public function __construct()
    {
        $this->ollamaUrl = config('ollama.base_url', 'http://172.16.1.6:11434');
    }

    /**
     * Process and index uploaded document
     */
    public function processDocument($title, $content, $filename, $fileType)
    {
        // Create document record
        $document = AiDocument::create([
            'title' => $title,
            'filename' => $filename,
            'file_type' => $fileType,
            'content' => $content,
            'chunk_count' => 0
        ]);

        // Split into chunks
        $chunks = $this->splitIntoChunks($content);
        
        Log::info("Processing document: {$title}", [
            'total_chunks' => count($chunks),
            'filename' => $filename
        ]);
        
        // Process each chunk
        $successCount = 0;
        foreach ($chunks as $index => $chunk) {
            try {
                // Generate embedding
                $embedding = $this->generateEmbedding($chunk);
                
                if ($embedding && is_array($embedding) && count($embedding) > 0) {
                    // Store chunk with embedding as JSON string
                    AiDocumentChunk::create([
                        'document_id' => $document->id,
                        'chunk_index' => $index,
                        'content' => $chunk,
                        'embedding' => json_encode($embedding) // Store as JSON string
                    ]);
                    $successCount++;
                    
                    Log::debug("Chunk {$index} processed", [
                        'embedding_size' => count($embedding),
                        'content_length' => strlen($chunk)
                    ]);
                } else {
                    Log::warning("Failed to generate embedding for chunk {$index}", [
                        'chunk_preview' => substr($chunk, 0, 100)
                    ]);
                }
            } catch (\Exception $e) {
                Log::error("Error processing chunk {$index}: " . $e->getMessage());
            }
        }

        // Update chunk count
        $actualChunks = AiDocumentChunk::where('document_id', $document->id)->count();
        $document->update(['chunk_count' => $actualChunks]);

        Log::info("Document processing complete", [
            'document_id' => $document->id,
            'chunks_created' => $actualChunks,
            'success_rate' => $successCount . '/' . count($chunks)
        ]);

        return $document;
    }

    /**
     * Generate embeddings using Ollama with retry logic
     */
    public function generateEmbedding($text, $retries = 2)
    {
        // Validate input
        if (empty(trim($text))) {
            Log::warning('Empty text provided for embedding');
            return null;
        }

        // Truncate very long texts
        $maxLength = 8000; // characters
        if (strlen($text) > $maxLength) {
            $text = substr($text, 0, $maxLength);
            Log::info('Text truncated for embedding', ['original_length' => strlen($text)]);
        }

        $attempt = 0;
        while ($attempt <= $retries) {
            try {
                Log::debug("Embedding attempt " . ($attempt + 1), [
                    'text_length' => strlen($text),
                    'url' => $this->ollamaUrl
                ]);

                $response = Http::timeout($this->timeout)
                    ->retry(2, 100) // Retry 2 times with 100ms delay
                    ->post("{$this->ollamaUrl}/api/embeddings", [
                        'model' => $this->embeddingModel,
                        'prompt' => $text
                    ]);

                if ($response->successful()) {
                    $data = $response->json();
                    
                    if (isset($data['embedding']) && is_array($data['embedding'])) {
                        Log::debug('Embedding generated successfully', [
                            'vector_size' => count($data['embedding'])
                        ]);
                        return $data['embedding'];
                    }
                    
                    Log::warning('Embedding response missing embedding field', [
                        'response' => $data
                    ]);
                }

                Log::warning('Embedding request failed', [
                    'status' => $response->status(),
                    'body' => $response->body()
                ]);
                
            } catch (\Illuminate\Http\Client\ConnectionException $e) {
                Log::error("Connection error on attempt " . ($attempt + 1) . ": " . $e->getMessage(), [
                    'ollama_url' => $this->ollamaUrl,
                    'timeout' => $this->timeout
                ]);
                
                if ($attempt >= $retries) {
                    // Check if Ollama is reachable
                    $this->checkOllamaHealth();
                }
            } catch (\Exception $e) {
                Log::error("Embedding generation error on attempt " . ($attempt + 1) . ": " . $e->getMessage());
            }
            
            $attempt++;
            
            if ($attempt <= $retries) {
                // Wait before retry (exponential backoff)
                sleep(pow(2, $attempt));
            }
        }

        return null;
    }

    /**
     * Check if Ollama server is healthy
     */
    private function checkOllamaHealth()
    {
        try {
            $response = Http::timeout(5)->get("{$this->ollamaUrl}/api/tags");
            
            if ($response->successful()) {
                Log::info('Ollama server is reachable', [
                    'available_models' => $response->json()
                ]);
            } else {
                Log::error('Ollama server responded but with error', [
                    'status' => $response->status()
                ]);
            }
        } catch (\Exception $e) {
            Log::error('Cannot reach Ollama server', [
                'url' => $this->ollamaUrl,
                'error' => $e->getMessage(),
                'suggestion' => 'Check if Ollama is running and accessible at ' . $this->ollamaUrl
            ]);
        }
    }

    /**
     * Search for similar chunks
     */
    public function searchSimilar($query, $topK = 5)
    {
        // Generate query embedding
        $queryEmbedding = $this->generateEmbedding($query);
        
        if (!$queryEmbedding) {
            Log::warning('Could not generate query embedding');
            return [];
        }

        // Get all chunks with embeddings
        $chunks = AiDocumentChunk::with('document')
            ->whereNotNull('embedding')
            ->get();

        if ($chunks->isEmpty()) {
            Log::info('No document chunks found');
            return [];
        }

        // Calculate similarity scores
        $results = [];
        foreach ($chunks as $chunk) {
            // Decode embedding if it's stored as JSON string
            $chunkEmbedding = is_string($chunk->embedding) 
                ? json_decode($chunk->embedding, true) 
                : $chunk->embedding;
            
            if (!is_array($chunkEmbedding) || empty($chunkEmbedding)) {
                Log::warning("Invalid embedding for chunk {$chunk->id}");
                continue;
            }
            
            $similarity = $this->cosineSimilarity($queryEmbedding, $chunkEmbedding);
            
            $results[] = [
                'chunk_id' => $chunk->id,
                'document_title' => $chunk->document->title,
                'content' => $chunk->content,
                'similarity' => $similarity
            ];
        }

        // Sort by similarity (highest first)
        usort($results, fn($a, $b) => $b['similarity'] <=> $a['similarity']);

        // Log top results
        if (!empty($results)) {
            Log::info('Search results', [
                'query' => $query,
                'total_results' => count($results),
                'top_similarity' => $results[0]['similarity'] ?? 0
            ]);
        }

        // Return top K results
        return array_slice($results, 0, $topK);
    }

    /**
     * Answer question with document context
     */
    public function answerWithContext($ollama, $question, $model = null)
    {
        // Find relevant document chunks
        $relevantChunks = $this->searchSimilar($question, 5);

        if (empty($relevantChunks)) {
            Log::info('No relevant chunks found, using direct generation');
            return $ollama->generate($question, $model);
        }

        // Build context from relevant chunks
        $context = "Based on the following information from the knowledge base:\n\n";
        
        foreach ($relevantChunks as $i => $chunk) {
            $context .= "--- Source " . ($i + 1) . ": " . $chunk['document_title'] . " ---\n";
            $context .= $chunk['content'] . "\n\n";
        }

        $context .= "========================================\n\n";
        $context .= "Based on the information above, please answer this question:\n";
        $context .= $question . "\n\n";
        $context .= "If the information provided doesn't contain the answer, please say so and provide a general response if possible.";

        Log::info('Generating answer with context', [
            'chunks_used' => count($relevantChunks),
            'question' => $question
        ]);

        return $ollama->generate($context, $model);
    }

    /**
     * Split text into overlapping chunks
     */
    private function splitIntoChunks($text)
    {
        // Clean the text
        $text = preg_replace('/\s+/', ' ', $text);
        $text = trim($text);

        // Split by words
        $words = preg_split('/\s+/', $text);
        $chunks = [];
        
        $i = 0;
        while ($i < count($words)) {
            $chunk = array_slice($words, $i, $this->chunkSize);
            if (!empty($chunk)) {
                $chunks[] = implode(' ', $chunk);
            }
            
            // Move forward with overlap
            $i += $this->chunkSize - $this->chunkOverlap;
        }

        Log::debug('Text chunking complete', [
            'total_words' => count($words),
            'chunks_created' => count($chunks)
        ]);

        return $chunks;
    }

    /**
     * Calculate cosine similarity between two vectors
     */
    private function cosineSimilarity($a, $b)
    {
        if (!is_array($a) || !is_array($b)) {
            Log::warning('Vectors must be arrays', [
                'a_type' => gettype($a),
                'b_type' => gettype($b)
            ]);
            return 0;
        }

        if (count($a) !== count($b)) {
            Log::warning('Vector dimension mismatch', [
                'a_size' => count($a),
                'b_size' => count($b)
            ]);
            return 0;
        }

        $dotProduct = 0;
        $magnitudeA = 0;
        $magnitudeB = 0;

        for ($i = 0; $i < count($a); $i++) {
            $dotProduct += $a[$i] * $b[$i];
            $magnitudeA += $a[$i] * $a[$i];
            $magnitudeB += $b[$i] * $b[$i];
        }

        $magnitude = sqrt($magnitudeA) * sqrt($magnitudeB);
        
        return $magnitude > 0 ? $dotProduct / $magnitude : 0;
    }

    /**
     * Extract text from various file types
     */
    public function extractTextFromFile($filePath, $fileType)
    {
        switch (strtolower($fileType)) {
            case 'txt':
            case 'text/plain':
                return file_get_contents($filePath);
            
            case 'pdf':
            case 'application/pdf':
                return $this->extractFromPdf($filePath);
            
            case 'docx':
            case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
                return $this->extractFromDocx($filePath);
            
            default:
                throw new \Exception('Unsupported file type: ' . $fileType);
        }
    }

    private function extractFromPdf($filePath)
    {
        $pdfToTextPath = $this->findPdfToText();
        
        if ($pdfToTextPath && function_exists('shell_exec')) {
            $output = shell_exec("{$pdfToTextPath} " . escapeshellarg($filePath) . " -");
            if ($output) {
                return $output;
            }
        }
        
        throw new \Exception('PDF extraction not available. Please install pdftotext (poppler-utils).');
    }

    private function findPdfToText()
    {
        $paths = [
            '/usr/bin/pdftotext',
            '/usr/local/bin/pdftotext',
            'pdftotext'
        ];

        foreach ($paths as $path) {
            if (@is_executable($path)) {
                return $path;
            }
        }

        if (function_exists('shell_exec')) {
            $which = shell_exec('which pdftotext 2>/dev/null');
            if ($which && trim($which)) {
                return trim($which);
            }
        }

        return null;
    }

    private function extractFromDocx($filePath)
    {
        if (!class_exists('ZipArchive')) {
            throw new \Exception('ZipArchive extension not available for DOCX extraction.');
        }

        $zip = new \ZipArchive;
        if ($zip->open($filePath) === true) {
            $xml = $zip->getFromName('word/document.xml');
            $zip->close();
            
            if ($xml) {
                $text = strip_tags($xml);
                $text = preg_replace('/\s+/', ' ', $text);
                return trim($text);
            }
        }
        
        throw new \Exception('Could not extract DOCX content. File may be corrupted.');
    }

    /**
     * Get statistics about indexed documents
     */
    public function getStatistics()
    {
        return [
            'total_documents' => AiDocument::count(),
            'total_chunks' => AiDocumentChunk::count(),
            'documents_with_embeddings' => AiDocumentChunk::whereNotNull('embedding')
                ->where('embedding', '!=', '')
                ->where('embedding', '!=', '[]')
                ->distinct('document_id')
                ->count('document_id'),
            'total_storage_size' => AiDocument::sum(\DB::raw('LENGTH(content)'))
        ];
    }

    /**
     * Reprocess document chunks that are missing embeddings
     */
    public function reprocessMissingEmbeddings()
    {
        $chunksWithoutEmbeddings = AiDocumentChunk::whereNull('embedding')
            ->orWhere('embedding', '')
            ->orWhere('embedding', '[]')
            ->get();

        Log::info('Reprocessing chunks without embeddings', [
            'count' => $chunksWithoutEmbeddings->count()
        ]);

        $successCount = 0;
        foreach ($chunksWithoutEmbeddings as $chunk) {
            $embedding = $this->generateEmbedding($chunk->content);
            
            if ($embedding && is_array($embedding) && count($embedding) > 0) {
                $chunk->update(['embedding' => json_encode($embedding)]);
                $successCount++;
            }
        }

        return [
            'total_processed' => $chunksWithoutEmbeddings->count(),
            'successful' => $successCount,
            'failed' => $chunksWithoutEmbeddings->count() - $successCount
        ];
    }
}