<?php

declare(strict_types=1);

namespace App\Framework\Search\Engines;

use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\Exception\ErrorCode;
use App\Framework\Exception\FrameworkException;
use App\Framework\Search\BulkIndexResult;
use App\Framework\Search\SearchDocument;
use App\Framework\Search\SearchEngine;
use App\Framework\Search\SearchEngineStats;
use App\Framework\Search\SearchFilterType;
use App\Framework\Search\SearchHit;
use App\Framework\Search\SearchQuery;
use App\Framework\Search\SearchResult;

/**
 * Database-based search engine using FULLTEXT indexes
 * Supports MySQL and PostgreSQL
 */
final readonly class DatabaseSearchEngine implements SearchEngine
{
    public function __construct(
        private ConnectionInterface $connection,
        private DatabaseSearchConfig $config = new DatabaseSearchConfig()
    ) {
    }

    public function search(SearchQuery $query): SearchResult
    {
        $startTime = microtime(true);

        try {
            $sql = $this->buildSearchSQL($query);
            $params = $this->buildSearchParams($query);

            $result = $this->connection->query(SqlQuery::create($sql, $params));
            $results = $result->fetchAll();

            $hits = $this->convertToHits($results, $query);
            $total = $this->getTotalCount($query);
            $maxScore = $this->getMaxScore($hits);

            $took = (microtime(true) - $startTime) * 1000;

            return new SearchResult(
                hits: $hits,
                total: $total,
                maxScore: $maxScore,
                took: $took
            );

        } catch (\Exception $e) {
            throw FrameworkException::create(
                ErrorCode::DB_QUERY_FAILED,
                'Search query failed: ' . $e->getMessage()
            )->withData([
                'entity_type' => $query->entityType,
                'query' => $query->query,
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function index(SearchDocument $document): bool
    {
        try {
            $tableName = $this->getSearchTableName($document->entityType->toString());
            $this->ensureSearchTableExists($document->entityType->toString());

            // Convert document to searchable format
            $searchData = $this->prepareDocumentForIndexing($document->data->toArray());

            $sql = "INSERT INTO {$tableName} (entity_id, title, content, metadata, indexed_at)
                    VALUES (?, ?, ?, ?, ?)
                    ON DUPLICATE KEY UPDATE
                    title = VALUES(title),
                    content = VALUES(content),
                    metadata = VALUES(metadata),
                    indexed_at = VALUES(indexed_at)";

            return $this->connection->execute(SqlQuery::create($sql, [
                $document->id->toString(),
                $searchData['title'],
                $searchData['content'],
                json_encode($searchData['metadata']),
                date('Y-m-d H:i:s'),
            ]));

        } catch (\Exception $e) {
            throw FrameworkException::create(
                ErrorCode::DB_QUERY_FAILED,
                'Failed to index document: ' . $e->getMessage()
            )->withData([
                'entity_type' => $document->entityType->toString(),
                'entity_id' => $document->id->toString(),
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function delete(SearchDocument $document): bool
    {
        try {
            $tableName = $this->getSearchTableName($document->entityType->toString());

            $sql = "DELETE FROM {$tableName} WHERE entity_id = ?";

            return $this->connection->execute(SqlQuery::create($sql, [$document->id->toString()]));

        } catch (\Exception $e) {
            throw FrameworkException::create(
                ErrorCode::DB_QUERY_FAILED,
                'Failed to delete document: ' . $e->getMessage()
            )->withData([
                'entity_type' => $document->entityType->toString(),
                'entity_id' => $document->id->toString(),
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function update(SearchDocument $document): bool
    {
        // For database backend, update is the same as index (INSERT ... ON DUPLICATE KEY UPDATE)
        return $this->index($document);
    }

    public function bulkIndex(array $documents): BulkIndexResult
    {
        $startedAt = Timestamp::now();
        $successful = 0;
        $failed = 0;
        $successfulIds = [];
        $failedIds = [];
        $errors = [];

        foreach ($documents as $document) {
            try {
                if ($this->index($document)) {
                    $successful++;
                    $successfulIds[] = $document->id->toString();
                } else {
                    $failed++;
                    $failedIds[] = $document->id->toString();
                    $errors[$document->id->toString()] = 'Index operation returned false';
                }
            } catch (\Exception $e) {
                $failed++;
                $failedIds[] = $document->id->toString();
                $errors[$document->id->toString()] = $e->getMessage();
            }
        }

        $finishedAt = Timestamp::now();

        return new BulkIndexResult(
            total: count($documents),
            successful: $successful,
            failed: $failed,
            startedAt: $startedAt,
            finishedAt: $finishedAt,
            successfulIds: $successfulIds,
            failedIds: $failedIds,
            errors: $errors
        );
    }

    public function getStats(): SearchEngineStats
    {
        try {
            $stats = [
                'total_documents' => 0,
                'index_counts' => [],
                'disk_usage_bytes' => 0,
            ];

            // Get table statistics
            $sql = "SELECT table_name, table_rows, data_length 
                    FROM information_schema.tables 
                    WHERE table_schema = DATABASE() 
                    AND table_name LIKE 'search_%'";

            $result = $this->connection->query(SqlQuery::create($sql));
            $tables = $result->fetchAll();

            foreach ($tables as $table) {
                $entityType = str_replace('search_', '', $table['table_name']);
                $stats['index_counts'][$entityType] = (int) $table['table_rows'];
                $stats['total_documents'] += (int) $table['table_rows'];
                $stats['disk_usage_bytes'] += (int) $table['data_length'];
            }

            return new SearchEngineStats(
                engineName: 'Database',
                version: $this->connection->getAttribute(\PDO::ATTR_SERVER_VERSION),
                isHealthy: $this->isAvailable(),
                totalDocuments: $stats['total_documents'],
                indexCounts: $stats['index_counts'],
                diskUsage: Byte::fromBytes($stats['disk_usage_bytes']),
                memoryUsage: Byte::fromBytes(0), // N/A for database
                averageQueryTimeMs: 0.0, // Could be tracked separately
                totalQueries: 0, // Could be tracked separately
                lastUpdated: Timestamp::now()
            );

        } catch (\Exception $e) {
            throw FrameworkException::create(
                ErrorCode::DB_QUERY_FAILED,
                'Failed to get search statistics: ' . $e->getMessage()
            );
        }
    }

    public function isAvailable(): bool
    {
        try {
            $this->connection->query(SqlQuery::create('SELECT 1'));

            return true;
        } catch (\Exception) {
            return false;
        }
    }

    public function getName(): string
    {
        return 'Database Search Engine';
    }

    private function buildSearchSQL(SearchQuery $query): string
    {
        $tableName = $this->getSearchTableName($query->entityType);

        $sql = "SELECT entity_id, title, content, metadata, 
                MATCH(title, content) AGAINST(? IN BOOLEAN MODE) as relevance_score";

        $sql .= " FROM {$tableName}";
        $sql .= " WHERE MATCH(title, content) AGAINST(? IN BOOLEAN MODE)";

        // Add filters
        if ($query->hasFilters()) {
            foreach ($query->filters as $field => $filter) {
                $sql .= " AND " . $this->buildFilterSQL($field, $filter);
            }
        }

        // Add minimum score filter
        if ($query->minScore > 0) {
            $sql .= " AND MATCH(title, content) AGAINST(? IN BOOLEAN MODE) >= ?";
        }

        // Add sorting
        if (! empty($query->sortBy->fields)) {
            $orderClauses = [];
            foreach ($query->sortBy->fields as $sortField) {
                if ($sortField->field === '_score') {
                    $orderClauses[] = "relevance_score " . $sortField->direction->value;
                } else {
                    $orderClauses[] = "JSON_EXTRACT(metadata, '$.{$sortField->field}') " . $sortField->direction->value;
                }
            }
            $sql .= " ORDER BY " . implode(', ', $orderClauses);
        } else {
            $sql .= " ORDER BY relevance_score DESC";
        }

        // Add limit and offset
        $sql .= " LIMIT ? OFFSET ?";

        return $sql;
    }

    private function buildSearchParams(SearchQuery $query): array
    {
        $params = [
            $this->formatQueryForFulltext($query->query), // For SELECT relevance score
            $this->formatQueryForFulltext($query->query), // For WHERE clause
        ];

        // Add filter parameters
        if ($query->hasFilters()) {
            foreach ($query->filters as $filter) {
                $params = array_merge($params, $this->getFilterParams($filter));
            }
        }

        // Add minimum score parameters
        if ($query->minScore > 0) {
            $params[] = $this->formatQueryForFulltext($query->query);
            $params[] = $query->minScore;
        }

        // Add limit and offset
        $params[] = $query->limit;
        $params[] = $query->offset;

        return $params;
    }

    private function formatQueryForFulltext(string $query): string
    {
        // Convert to MySQL BOOLEAN MODE format
        $query = trim($query);

        if ($query === '*') {
            return '*';
        }

        // Split into words and add + prefix for AND behavior
        $words = preg_split('/\s+/', $query);
        $formattedWords = array_map(fn ($word) => '+' . $word . '*', $words);

        return implode(' ', $formattedWords);
    }

    private function buildFilterSQL(string $field, $filter): string
    {
        return match ($filter->type) {
            SearchFilterType::EQUALS => "JSON_EXTRACT(metadata, '$.{$field}') = ?",
            SearchFilterType::IN => "JSON_EXTRACT(metadata, '$.{$field}') IN (" . str_repeat('?,', count($filter->value) - 1) . "?)",
            SearchFilterType::GREATER_THAN => "JSON_EXTRACT(metadata, '$.{$field}') > ?",
            SearchFilterType::LESS_THAN => "JSON_EXTRACT(metadata, '$.{$field}') < ?",
            SearchFilterType::CONTAINS => "JSON_EXTRACT(metadata, '$.{$field}') LIKE ?",
            SearchFilterType::EXISTS => "JSON_EXTRACT(metadata, '$.{$field}') IS NOT NULL",
            SearchFilterType::NOT_EXISTS => "JSON_EXTRACT(metadata, '$.{$field}') IS NULL",
            default => throw FrameworkException::create(
                ErrorCode::VAL_BUSINESS_RULE_VIOLATION,
                "Unsupported filter type: {$filter->type->value}"
            ),
        };
    }

    private function getFilterParams($filter): array
    {
        return match ($filter->type) {
            SearchFilterType::EQUALS => [$filter->value],
            SearchFilterType::IN => $filter->value,
            SearchFilterType::GREATER_THAN => [$filter->value],
            SearchFilterType::LESS_THAN => [$filter->value],
            SearchFilterType::CONTAINS => ['%' . $filter->value . '%'],
            SearchFilterType::EXISTS, SearchFilterType::NOT_EXISTS => [],
            default => [],
        };
    }

    private function convertToHits(array $results, SearchQuery $query): array
    {
        $hits = [];

        foreach ($results as $result) {
            $metadata = json_decode($result['metadata'] ?? '{}', true);
            $source = array_merge($metadata, [
                'title' => $result['title'],
                'content' => $result['content'],
            ]);

            $highlight = [];
            if ($query->enableHighlighting) {
                $highlight = $this->generateHighlights($result, $query->query);
            }

            $hits[] = new SearchHit(
                id: $result['entity_id'],
                entityType: $query->entityType,
                score: (float) ($result['relevance_score'] ?? 0.0),
                source: $source,
                highlight: $highlight
            );
        }

        return $hits;
    }

    private function generateHighlights(array $result, string $query): array
    {
        $highlights = [];

        // Simple highlighting - can be enhanced
        $searchTerms = explode(' ', $query);

        foreach (['title', 'content'] as $field) {
            if (isset($result[$field])) {
                $highlighted = $result[$field];
                foreach ($searchTerms as $term) {
                    $term = trim($term);
                    if (! empty($term)) {
                        $highlighted = preg_replace(
                            '/(' . preg_quote($term, '/') . ')/i',
                            '<em>$1</em>',
                            $highlighted
                        );
                    }
                }
                if ($highlighted !== $result[$field]) {
                    $highlights[$field] = [$highlighted];
                }
            }
        }

        return $highlights;
    }

    private function getTotalCount(SearchQuery $query): int
    {
        $tableName = $this->getSearchTableName($query->entityType);

        $sql = "SELECT COUNT(*) FROM {$tableName} WHERE MATCH(title, content) AGAINST(? IN BOOLEAN MODE)";
        $params = [$this->formatQueryForFulltext($query->query)];

        if ($query->hasFilters()) {
            foreach ($query->filters as $field => $filter) {
                $sql .= " AND " . $this->buildFilterSQL($field, $filter);
                $params = array_merge($params, $this->getFilterParams($filter));
            }
        }

        $result = $this->connection->query(SqlQuery::create($sql, $params));

        return (int) $result->fetchColumn();
    }

    private function getMaxScore(array $hits): float
    {
        if (empty($hits)) {
            return 0.0;
        }

        return max(array_map(fn ($hit) => $hit->score, $hits));
    }

    private function getSearchTableName(string $entityType): string
    {
        return 'search_' . strtolower($entityType);
    }

    private function ensureSearchTableExists(string $entityType): void
    {
        $tableName = $this->getSearchTableName($entityType);

        $sql = "CREATE TABLE IF NOT EXISTS {$tableName} (
            id INT AUTO_INCREMENT PRIMARY KEY,
            entity_id VARCHAR(255) NOT NULL UNIQUE,
            title TEXT NOT NULL,
            content LONGTEXT NOT NULL,
            metadata JSON,
            indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FULLTEXT(title, content)
        )";

        $this->connection->exec($sql);
    }

    private function prepareDocumentForIndexing(array $document): array
    {
        // Extract searchable fields
        $title = $document['title'] ?? $document['name'] ?? '';
        $content = $document['content'] ?? $document['description'] ?? '';

        // Combine all text fields for content
        $textFields = [];
        foreach ($document as $key => $value) {
            if (is_string($value) && ! in_array($key, ['title', 'name'])) {
                $textFields[] = $value;
            }
        }

        if (empty($content)) {
            $content = implode(' ', $textFields);
        }

        // Store remaining fields as metadata
        $metadata = $document;
        unset($metadata['title'], $metadata['name'], $metadata['content'], $metadata['description']);

        return [
            'title' => $title,
            'content' => $content,
            'metadata' => $metadata,
        ];
    }
}
