<?php

namespace App\Services\Keywords;

use App\Services\AI\TextProviderFailover;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;

use function Laravel\Ai\agent;

class AiKeywordCandidateMatcher
{
    /**
     * @param  list<array<string, mixed>>  $candidates
     * @param  list<array{term: string, avg_monthly_searches: int, competition: string, low_top_of_page_bid_micros?: int}>  $keywords
     * @return array<int, array{term: string, match_reason: string}>
     */
    public function match(
        array $candidates,
        array $keywords,
        string $category = '',
        string $subcategory = '',
    ): array {
        if ($candidates === [] || $keywords === []) {
            return [];
        }

        if (! config('google-ads.ai_keyword_matching', true)) {
            return [];
        }

        try {
            $keywordBlock = $this->keywordBlock($keywords);
            $candidateBlock = $this->candidateBlock($candidates);
            $scope = trim(collect([$category, $subcategory])->filter()->implode(' / '));
            $scopeLine = $scope !== '' ? "Category scope: {$scope}\n" : '';

            $prompt = "{$scopeLine}"
                ."Match each product to the single best keyword from the keyword list below.\n"
                ."Pick a keyword only when it accurately describes what the product is, not just because words overlap.\n"
                ."Prefer category-relevant search terms over brand names or generic overlaps.\n"
                ."Use an empty term when no keyword is a good fit.\n"
                ."Every non-empty term must be copied exactly from the keyword list.\n\n"
                ."Keywords:\n{$keywordBlock}\n\n"
                ."Products:\n{$candidateBlock}";

            $response = agent(
                instructions: 'You match ecommerce products to Google Ads keyword ideas. Choose at most one keyword per product from the provided list. Reject weak or misleading matches.',
                schema: fn (JsonSchema $schema) => [
                    'matches' => $schema->array()->items(
                        $schema->object([
                            'index' => $schema->integer()->description('Product index from the candidate list.')->required(),
                            'term' => $schema->string()->description('Keyword term copied exactly from the list, or empty string when none fit.')->required(),
                            'match_reason' => $schema->string()->description('Short reason for the match or why no keyword fits.')->required(),
                        ])
                    )->description('One row per product index.')->required(),
                ],
            )->prompt($prompt, provider: TextProviderFailover::providers());

            $matches = $this->parseMatches(
                $response->toArray()['matches'] ?? [],
                count($candidates),
                $keywords,
            );

            Log::info('Catalog keyword AI matching finished', [
                'candidate_count' => count($candidates),
                'matched_count' => count($matches),
            ]);

            return $matches;
        } catch (Throwable $exception) {
            Log::warning('Catalog keyword AI matching failed; using heuristic fallback', [
                'candidate_count' => count($candidates),
                'keyword_count' => count($keywords),
                'error' => $exception->getMessage(),
            ]);

            return [];
        }
    }

    /**
     * @param  list<array{term: string, avg_monthly_searches: int, competition: string, low_top_of_page_bid_micros?: int}>  $keywords
     */
    private function keywordBlock(array $keywords): string
    {
        return collect($keywords)
            ->sortByDesc(fn (array $keyword): int => (int) ($keyword['avg_monthly_searches'] ?? 0))
            ->take(max(1, (int) config('google-ads.ai_keyword_match_limit', 60)))
            ->map(function (array $keyword): string {
                $payload = [
                    'term' => $keyword['term'] ?? '',
                    'avg_monthly_searches' => $keyword['avg_monthly_searches'] ?? 0,
                    'competition' => $keyword['competition'] ?? '',
                ];

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

    /**
     * @param  list<array<string, mixed>>  $candidates
     */
    private function candidateBlock(array $candidates): string
    {
        return 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,
                ];

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

    /**
     * @param  list<mixed>  $rows
     * @param  list<array{term: string, avg_monthly_searches: int, competition: string, low_top_of_page_bid_micros?: int}>  $keywords
     * @return array<int, array{term: string, match_reason: string}>
     */
    private function parseMatches(array $rows, int $candidateCount, array $keywords): array
    {
        $keywordsByTerm = [];

        foreach ($keywords as $keyword) {
            $term = trim((string) ($keyword['term'] ?? ''));

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

            $keywordsByTerm[$this->normalizeTerm($term)] = $term;
        }

        $matches = [];

        foreach ($rows as $row) {
            if (! is_array($row)) {
                continue;
            }

            $index = (int) ($row['index'] ?? 0) - 1;
            $term = trim((string) ($row['term'] ?? ''));
            $matchReason = trim((string) ($row['match_reason'] ?? ''));

            if ($index < 0 || $index >= $candidateCount || $term === '') {
                continue;
            }

            $normalizedTerm = $this->normalizeTerm($term);
            $canonicalTerm = $keywordsByTerm[$normalizedTerm] ?? null;

            if ($canonicalTerm === null) {
                continue;
            }

            $matches[$index] = [
                'term' => $canonicalTerm,
                'match_reason' => $matchReason !== '' ? $matchReason : 'Matched by AI to the closest relevant keyword.',
            ];
        }

        return $matches;
    }

    private function normalizeTerm(string $term): string
    {
        return Str::lower(trim($term));
    }
}
