<?php

namespace App\Services\AI;

use App\Contracts\CatalogProductRanker;
use App\Services\Ranking\HeuristicCatalogProductRanker;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;

use function Laravel\Ai\agent;

class AiCatalogProductRanker implements CatalogProductRanker
{
    public function __construct(private HeuristicCatalogProductRanker $heuristicRanker) {}

    /**
     * @param  list<array<string, mixed>>  $candidates
     * @return list<array<string, mixed>>
     */
    public function rank(
        array $candidates,
        string $category = '',
        string $subcategory = '',
        ?string $notes = null,
    ): array {
        if ($candidates === []) {
            return [];
        }

        if (! config('scraping.ranking.ai_enabled', true)) {
            return $this->heuristicRanker->rank($candidates, $category, $subcategory, $notes);
        }

        $limit = max(1, (int) config('scraping.ranking.limit', 10));

        try {
            $ranked = $this->rankWithAgent($candidates, $category, $subcategory, $notes, $limit);

            if ($ranked !== []) {
                return $ranked;
            }
        } catch (Throwable $exception) {
            Log::warning('Catalog product AI ranking failed; using heuristic fallback', [
                'category' => $category,
                'subcategory' => $subcategory,
                'candidate_count' => count($candidates),
                'error' => $exception->getMessage(),
            ]);
        }

        return $this->heuristicRanker->rank($candidates, $category, $subcategory, $notes);
    }

    /**
     * @param  list<array<string, mixed>>  $candidates
     * @return list<array<string, mixed>>
     */
    private function rankWithAgent(
        array $candidates,
        string $category,
        string $subcategory,
        ?string $notes,
        int $limit,
    ): array {
        $candidateBlock = collect($candidates)
            ->map(function (array $candidate, int $index): string {
                $payload = [
                    'index' => $index + 1,
                    'name' => $candidate['name'] ?? '',
                    'url' => $candidate['url'] ?? '',
                    'price' => $candidate['price'] ?? null,
                    'rating' => $candidate['rating'] ?? null,
                    'review_count' => $candidate['review_count'] ?? null,
                    'popularity_note' => $candidate['popularity_note'] ?? null,
                    'badges' => $candidate['badges'] ?? [],
                    'matched_keyword' => $candidate['matched_keyword'] ?? null,
                    'keyword_search_volume' => $candidate['keyword_search_volume'] ?? null,
                    'keyword_competition' => $candidate['keyword_competition'] ?? null,
                ];

                return json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '{}';
            })
            ->implode("\n");

        $scope = trim(collect([$category, $subcategory])->filter()->implode(' / '));
        $scopeLine = $scope !== '' ? "Category scope: {$scope}\n" : '';
        $notesLine = filled($notes) ? "Editor notes: {$notes}\n" : '';

        $prompt = "{$scopeLine}{$notesLine}"
            ."Select the best {$limit} products for a shoppable catalog roundup from the candidates below.\n"
            ."Prefer high review volume, strong ratings, clear category fit, and mainstream appeal.\n"
            ."When keyword_search_volume and keyword_competition are present, favor products with meaningful search demand and manageable competition.\n"
            ."Return exactly up to {$limit} products when enough strong candidates exist.\n"
            ."Every url must be copied exactly from the candidate list. Never invent URLs.\n\n"
            ."Candidates:\n{$candidateBlock}";

        $response = agent(
            instructions: 'You rank ecommerce catalog products for a buyer guide. Pick only from the provided candidate URLs. Favor products with strong ratings and review counts that fit the category scope. Assign rank 1 to the best overall pick and optional best_for badges such as Best Overall, Best Value, or Best Premium when clearly deserved.',
            schema: fn (JsonSchema $schema) => [
                'products' => $schema->array()->items(
                    $schema->object([
                        'url' => $schema->string()->description('Candidate product URL copied exactly.')->required(),
                        'rank' => $schema->integer()->description('Rank starting at 1 for the best pick.')->required(),
                        'selection_reason' => $schema->string()->description('Short reason this product made the top list.')->required(),
                        'best_for' => $schema->string()->description('Optional roundup badge such as Best Overall; empty string otherwise.')->required(),
                    ])
                )->description("Top {$limit} ranked products.")->required(),
            ],
        )->prompt($prompt, provider: TextProviderFailover::providers());

        $rows = $response->toArray()['products'] ?? [];

        if (! is_array($rows) || $rows === []) {
            return [];
        }

        return $this->mergeRankedRows($candidates, $rows, $limit);
    }

    /**
     * @param  list<array<string, mixed>>  $candidates
     * @param  list<mixed>  $rows
     * @return list<array<string, mixed>>
     */
    private function mergeRankedRows(array $candidates, array $rows, int $limit): array
    {
        $candidatesByUrl = [];

        foreach ($candidates as $candidate) {
            $key = $this->normalizeUrl((string) ($candidate['url'] ?? ''));

            if ($key === '') {
                continue;
            }

            $candidatesByUrl[$key] = $candidate;
        }

        $rankedRows = collect($rows)
            ->filter(fn (mixed $row): bool => is_array($row))
            ->sortBy(fn (array $row): int => (int) ($row['rank'] ?? PHP_INT_MAX))
            ->values();

        $merged = [];
        $seen = [];

        foreach ($rankedRows as $row) {
            $key = $this->normalizeUrl((string) ($row['url'] ?? ''));

            if ($key === '' || ! isset($candidatesByUrl[$key]) || isset($seen[$key])) {
                continue;
            }

            $seen[$key] = true;

            $bestFor = trim((string) ($row['best_for'] ?? ''));

            $merged[] = array_merge($candidatesByUrl[$key], [
                'rank' => (int) ($row['rank'] ?? count($merged) + 1),
                'selection_reason' => trim((string) ($row['selection_reason'] ?? '')) ?: 'Selected for the catalog roundup.',
                'best_for' => $bestFor !== '' ? $bestFor : null,
                'ranking_source' => 'ai',
            ]);

            if (count($merged) >= $limit) {
                break;
            }
        }

        Log::info('Catalog product AI ranking finished', [
            'candidate_count' => count($candidates),
            'selected_count' => count($merged),
            'product_urls' => array_column($merged, 'url'),
        ]);

        return $merged;
    }

    private function normalizeUrl(string $url): string
    {
        return Str::lower(rtrim(trim($url), '/'));
    }
}
