<?php

declare(strict_types=1);

namespace App\Services\AiAssistant\Tools;

use App\Models\Product;
use App\Models\SearchState;
use App\Services\AiAssistant\AiComponentRenderer;
use App\Services\AiAssistant\StructuredResponse;
use Laravel\Scout\Builder;

class SearchTools
{
    public function __construct(
        protected AiComponentRenderer $renderer
    ) {}

    /**
     * Smart search that returns 4 results and stores state for pagination.
     */
    public function smartSearch(array $params, ?int $conversationId = null, ?int $userId = null): StructuredResponse
    {
        $query = $params['query'] ?? '';
        $categoryId = $params['category_id'] ?? null;
        $minPrice = $params['min_price'] ?? null;
        $maxPrice = $params['max_price'] ?? null;

        // Detect and set locale based on query
        $this->renderer->detectAndSetLocale($query);

        // Perform the actual search to get ALL matching product IDs with relevance scores
        $results = $this->searchWithRelevance($query, $categoryId, $minPrice, $maxPrice);

        if (empty($results)) {
            return StructuredResponse::fromText(
                "No products found matching \"{$query}\". Try different keywords or browse our categories."
            );
        }

        // Sort by relevance score
        usort($results, fn ($a, $b) => $b['score'] <=> $a['score']);

        // Extract just the IDs
        $allProductIds = array_column($results, 'id');
        $totalCount = count($allProductIds);
        $perPage = 4;

        // Get first page's products
        $pageIds = array_slice($allProductIds, 0, $perPage);

        $products = Product::with('category')
            ->whereIn('id', $pageIds)
            ->get()
            ->sortBy(fn ($p) => array_search($p->id, $pageIds))
            ->values();

        // Create search state
        $searchState = SearchState::createFromSearch(
            $userId,
            $conversationId,
            'products',
            compact('query', 'categoryId', 'minPrice', 'maxPrice'),
            $allProductIds,
            $perPage
        );

        // Build response
        $text = $this->buildSearchResultText($products, $searchState, $query, 1);

        $html = $this->renderer->productGrid(
            $products,
            $this->getGridTitle($query, $searchState, 1)
        );

        $metadata = [
            'search_state_id' => $searchState->id,
            'total_count' => $totalCount,
            'current_page' => 1,
            'has_more' => $searchState->hasMorePages(),
            'remaining_count' => $searchState->getRemainingCount(),
        ];

        return StructuredResponse::withHtml($text, $html, $metadata);
    }

    /**
     * Show more results from the previous search.
     */
    public function showMore(int $searchStateId): StructuredResponse
    {
        $searchState = SearchState::find($searchStateId);

        if (! $searchState || $searchState->isExpired()) {
            return StructuredResponse::fromText(
                'This search session has expired. Please search again.'
            );
        }

        if (! $searchState->hasMorePages()) {
            return StructuredResponse::fromText(
                "You've seen all {$searchState->total_count} results from this search. ".
                'Would you like to search with different criteria?'
            );
        }

        // Move to next page
        $searchState = $searchState->nextPage();
        $pageIds = $searchState->getCurrentPageIds();

        $products = Product::with('category')
            ->whereIn('id', $pageIds)
            ->get()
            ->sortBy(fn ($p) => array_search($p->id, $pageIds))
            ->values();

        $query = $searchState->search_params['query'] ?? '';

        $text = $this->buildSearchResultText($products, $searchState, $query, $searchState->current_page);

        $html = $this->renderer->productGrid(
            $products,
            $this->getGridTitle($query, $searchState, $searchState->current_page)
        );

        // Include search_state_id in text for the AI to use
        $stateInfo = "\n\n[Search session: {$searchState->id}, Page {$searchState->current_page} of ".ceil($searchState->total_count / $searchState->per_page).']';

        return StructuredResponse::withHtml(
            $text.$stateInfo,
            $html,
            [
                'search_state_id' => $searchState->id,
                'total_count' => $searchState->total_count,
                'current_page' => $searchState->current_page,
                'has_more' => $searchState->hasMorePages(),
                'remaining_count' => $searchState->getRemainingCount(),
            ]
        );
    }

    /**
     * Confirm if the search results match what the user wants.
     */
    public function confirmResults(int $searchStateId, bool $confirmed): StructuredResponse
    {
        $searchState = SearchState::find($searchStateId);

        if (! $searchState || $searchState->isExpired()) {
            return StructuredResponse::fromText(
                'This search session has expired. Please search again.'
            );
        }

        $searchState->markAsConfirmed();

        if ($confirmed) {
            $message = "Great! I've found what you're looking for.";

            if ($searchState->hasMorePages()) {
                $message .= " Would you like to see the other {$searchState->getRemainingCount()} products?";
            }

            return StructuredResponse::fromText($message);
        }

        return StructuredResponse::fromText(
            "I understand these aren't quite what you're looking for. ".
            'Could you tell me more specifically what you need? For example:'."\n".
            '- A specific brand or model?'."\n".
            '- A price range?'."\n".
            '- Specific features (ballpoint, gel, fountain pen)?'
        );
    }

    /**
     * Search with relevance scoring to filter out irrelevant results.
     *
     * @return array<int, array{id: int, score: float}>
     */
    protected function searchWithRelevance(
        string $query,
        ?int $categoryId,
        ?float $minPrice,
        ?float $maxPrice
    ): array {
        $scoutDriver = config('scout.driver');

        if ($scoutDriver === 'meilisearch' && $query !== '') {
            return $this->searchMeilisearchWithRelevance($query, $categoryId, $minPrice, $maxPrice);
        }

        return $this->searchDatabaseWithRelevance($query, $categoryId, $minPrice, $maxPrice);
    }

    /**
     * Meilisearch search with relevance scoring.
     */
    protected function searchMeilisearchWithRelevance(
        string $query,
        ?int $categoryId,
        ?float $minPrice,
        ?float $maxPrice
    ): array {
        /** @var Builder $search */
        $search = Product::search($query);

        $filters = [];

        if ($categoryId !== null) {
            $filters[] = "category_id = {$categoryId}";
        }

        if ($minPrice !== null) {
            $filters[] = "(price >= {$minPrice} OR discount_price >= {$minPrice})";
        }

        if ($maxPrice !== null) {
            $filters[] = "(price <= {$maxPrice} OR discount_price <= {$maxPrice})";
        }

        if (! empty($filters)) {
            $search->where(implode(' AND ', $filters));
        }

        // Get many results to filter and score
        $rawResults = $search->take(150)->raw();
        $hits = $rawResults['hits'] ?? [];

        $results = [];
        $queryLower = strtolower($query);

        // Meilisearch doesn't return ranking scores by default, so we use position as base score
        // Earlier positions (more relevant) get higher scores
        foreach ($hits as $index => $hit) {
            $productId = $hit['id'];
            // Base score decreases with position: first result = 100, second = 95, etc.
            $baseScore = max(10, 100 - ($index * 3));

            // Apply relevance penalties
            $penalty = $this->calculateRelevancePenalty($hit, $queryLower);
            $adjustedScore = $baseScore - $penalty;

            if ($adjustedScore > 0) {
                $results[] = ['id' => $productId, 'score' => $adjustedScore];
            }
        }

        // Remove duplicates and sort
        $results = collect($results)->unique('id')->sortByDesc('score')->values()->toArray();

        return $results;
    }

    /**
     * Database search with relevance scoring.
     */
    protected function searchDatabaseWithRelevance(
        string $query,
        ?int $categoryId,
        ?float $minPrice,
        ?float $maxPrice
    ): array {
        // For "starts with" search to prioritize exact matches
        $startsWithQuery = Product::query()
            ->where('name_en', 'like', "{$query}%")
            ->when($categoryId, fn ($q) => $q->where('category_id', $categoryId))
            ->when($minPrice || $maxPrice, fn ($q) => $this->applyPriceFilter($q, $minPrice, $maxPrice))
            ->limit(50)
            ->pluck('id')
            ->map(fn ($id) => ['id' => $id, 'score' => 100]) // Exact starts with gets highest score
            ->toArray();

        // For "contains" search
        $containsQuery = Product::query()
            ->where('name_en', 'like', "%{$query}%")
            ->when($categoryId, fn ($q) => $q->where('category_id', $categoryId))
            ->when($minPrice || $maxPrice, fn ($q) => $this->applyPriceFilter($q, $minPrice, $maxPrice))
            ->limit(50)
            ->pluck('id')
            ->map(fn ($id) => ['id' => $id, 'score' => 50])
            ->toArray();

        // Word boundary matches in description
        $descriptionMatch = Product::query()
            ->where('description_en', 'like', "%{$query}%")
            ->when($categoryId, fn ($q) => $q->where('category_id', $categoryId))
            ->when($minPrice || $maxPrice, fn ($q) => $this->applyPriceFilter($q, $minPrice, $maxPrice))
            ->limit(30)
            ->pluck('id')
            ->map(fn ($id) => ['id' => $id, 'score' => 20])
            ->toArray();

        // Combine and deduplicate, keeping highest score
        $combined = collect([$startsWithQuery, $containsQuery, $descriptionMatch])
            ->flatten(1)
            ->keyBy('id')
            ->map(fn ($items) => collect($items)->max('score'))
            ->sortByDesc('score')
            ->toArray();

        return $combined;
    }

    /**
     * Calculate relevance penalty for a product based on search query.
     */
    protected function calculateRelevancePenalty(array $hit, string $queryLower): float
    {
        $productName = strtolower($hit['name_en'] ?? '');
        $penalty = 0;

        // Extract all words from the query
        $queryWords = array_filter(preg_split('/\s+/', $queryLower), fn ($w) => strlen($w) >= 3);

        // Product type definitions - words that indicate the actual product type
        $productTypeIndicators = [
            'pad', 'mat', 'mousepad', 'holder', 'case', 'cover', 'skin', 'sticker',
            'cable', 'charger', 'adapter', 'stand', 'mount', 'dock', 'sleeve', 'bag',
            'phone', 'tablet', 'laptop', 'watch', 'earphone', 'headphone', 'speaker',
        ];

        // If the main search term is for a specific product type, heavily penalize different product types
        $mainTerm = $this->extractMainTerm($queryLower);

        $mismatchedTypes = [
            'pen' => ['phone', 'holder', 'case', 'pencil', 'usb', 'charger', 'cable', 'pad', 'mat', 'mousepad', 'desk'],
            'pencil' => ['phone', 'holder', 'case', 'usb', 'charger', 'cable', 'pad', 'mat', 'mousepad'],
            'notebook' => ['phone', 'laptop', 'case', 'charger', 'cable'],
            'marker' => ['phone', 'pad', 'mat', 'holder', 'case'],
        ];

        // Check for mismatched product types
        if (isset($mismatchedTypes[$mainTerm])) {
            foreach ($mismatchedTypes[$mainTerm] as $badWord) {
                if (str_contains($productName, $badWord)) {
                    // Check if the actual term is present as a standalone word
                    if (! preg_match('/\b'.preg_quote($mainTerm, '/').'s?\b/i', $productName)) {
                        $penalty += 60; // Heavy penalty for mismatched type
                    }
                }
            }
        }

        // Additional check: if product name ends with a different type indicator than searched
        foreach ($productTypeIndicators as $type) {
            // Skip if this type matches our search
            if (str_contains($queryLower, $type)) {
                continue;
            }

            // If product name ends with this type (e.g., "Writing Mat" ends with "Mat")
            if (preg_match('/\b'.preg_quote($type, '/').'(s|es)?\b/i', $productName)) {
                // And the product doesn't contain the actual search term as a standalone word
                $hasSearchTerm = false;
                foreach ($queryWords as $word) {
                    if (preg_match('/\b'.preg_quote($word, '/').'(s|es)?\b/i', $productName)) {
                        $hasSearchTerm = true;
                        break;
                    }
                }

                if (! $hasSearchTerm) {
                    $penalty += 40;
                }
            }
        }

        // Category bonus/penalty
        $category = strtolower($hit['category_name'] ?? '');
        $goodCategories = [
            'pen' => ['office', 'school', 'stationery'],
            'pencil' => ['office', 'school', 'stationery'],
            'phone' => ['electronics', 'mobile', 'telecom'],
            'laptop' => ['computer', 'electronics'],
        ];

        if (isset($goodCategories[$mainTerm])) {
            $categoryBonus = $goodCategories[$mainTerm];
            $hasGoodCategory = false;
            foreach ($categoryBonus as $good) {
                if (str_contains($category, $good)) {
                    $hasGoodCategory = true;
                    break;
                }
            }
            if (! $hasGoodCategory) {
                $penalty += 15;
            }
        }

        return $penalty;
    }

    /**
     * Extract the main search term from the query.
     * Returns the last meaningful word, which is usually the product type (e.g., "pen" from "writing pen").
     */
    protected function extractMainTerm(string $query): string
    {
        $query = strtolower(trim($query));

        // Remove common prefixes
        $query = preg_replace('/^(buy|shopping|search|find|looking for|need|want|i need|i want)\s+/i', '', $query);

        // Product type indicators - these are the words we want to extract
        $productTypes = [
            'pen', 'pencil', 'notebook', 'marker', 'eraser', 'ruler', 'stapler', 'glue',
            'phone', 'tablet', 'laptop', 'watch', 'headphone', 'speaker', 'charger', 'cable',
            'case', 'holder', 'cover', 'stand', 'mouse', 'keyboard', 'monitor',
        ];

        // Get all meaningful words
        $words = array_filter(
            array_map(fn ($w) => trim(strtolower($w)), preg_split('/\s+/', $query)),
            fn ($w) => strlen($w) >= 3
        );

        // First, try to find a known product type in the query (prefer last one)
        foreach (array_reverse($words) as $word) {
            // Remove common plural suffixes
            $singular = preg_replace('/s$/', '', $word);
            if (in_array($word, $productTypes) || in_array($singular, $productTypes)) {
                return $singular;
            }
        }

        // If no known product type found, return the last meaningful word
        if (! empty($words)) {
            return end($words);
        }

        return $query;
    }

    /**
     * Apply price filter to query.
     */
    protected function applyPriceFilter($query, ?float $minPrice, ?float $maxPrice)
    {
        if ($minPrice !== null && $maxPrice !== null) {
            $query->whereRaw('COALESCE(discount_price, price) BETWEEN ? AND ?', [$minPrice, $maxPrice]);
        } elseif ($minPrice !== null) {
            $query->whereRaw('COALESCE(discount_price, price) >= ?', [$minPrice]);
        } elseif ($maxPrice !== null) {
            $query->whereRaw('COALESCE(discount_price, price) <= ?', [$maxPrice]);
        }

        return $query;
    }

    /**
     * Build the text response for search results.
     */
    protected function buildSearchResultText($products, SearchState $searchState, string $query, int $page): string
    {
        $totalCount = $searchState->total_count;
        $showing = count($products);
        $remaining = $searchState->getRemainingCount();

        $text = "I found {$totalCount} product(s) matching \"{$query}\".\n\n";

        if ($page === 1 && $totalCount > 4) {
            $text .= "Here are the top {$showing} results:\n\n";
        } else {
            $totalPages = (int) ceil($totalCount / $searchState->per_page);
            $text .= "Showing page {$page} of {$totalPages}:\n\n";
        }

        foreach ($products as $product) {
            $text .= "- **{$product->name_en}**\n";
            $price = $product->discount_price ?? $product->price;
            $text .= '  - Price: $'.number_format((float) $price, 2);

            if ($product->discount_price) {
                $text .= " ({$product->discount}% off!)";
            }

            $text .= "\n  - Rating: {$product->positive_feedback}% positive\n\n";
        }

        // Add prompt for more results
        if ($searchState->hasMorePages()) {
            $text .= "📦 There are **{$remaining} more products** available. Type \"page ".($page + 1)." or \"show more\" to see them.\n";
        }

        return $text;
    }

    /**
     * Get the grid title.
     */
    protected function getGridTitle(string $query, SearchState $searchState, int $page): string
    {
        $totalCount = $searchState->total_count;

        if ($totalCount <= 4) {
            return "Found {$totalCount} products for \"{$query}\"";
        }

        $totalPages = (int) ceil($totalCount / $searchState->per_page);

        return "Showing page {$page} of {$totalPages} ({$totalCount} total products for \"{$query}\")";
    }
}
