<?php

namespace App\Services\Keywords;

use Illuminate\Support\Str;

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

        $limit = max(1, (int) config('google-ads.max_curated_keyword_ideas', 30));
        $references = $this->referencePhrases($seeds, $candidates, $category, $subcategory);
        $referenceTokens = $this->tokensFromPhrases($references);
        $seedTokens = $this->tokensFromPhrases($seeds);

        $scored = [];

        foreach ($ideas as $idea) {
            $term = trim((string) ($idea['term'] ?? ''));

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

            $score = $this->relevanceScore($term, $references, $referenceTokens, $seedTokens);

            if ($score <= 0) {
                continue;
            }

            $scored[] = [
                'idea' => $idea,
                'score' => $score,
            ];
        }

        if ($scored === []) {
            return array_slice($ideas, 0, $limit);
        }

        usort($scored, function (array $left, array $right): int {
            if ($left['score'] !== $right['score']) {
                return $right['score'] <=> $left['score'];
            }

            return ((int) ($right['idea']['avg_monthly_searches'] ?? 0))
                <=> ((int) ($left['idea']['avg_monthly_searches'] ?? 0));
        });

        $curated = [];

        foreach ($scored as $row) {
            $term = (string) ($row['idea']['term'] ?? '');

            if ($term === '' || $this->containsIdea($curated, $term)) {
                continue;
            }

            $curated[] = $row['idea'];

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

        return $curated;
    }

    /**
     * @param  list<string>  $seeds
     * @param  list<array<string, mixed>>  $candidates
     * @return list<string>
     */
    private function referencePhrases(
        array $seeds,
        array $candidates,
        string $category,
        string $subcategory,
    ): array {
        $phrases = [...$seeds];

        foreach ([$category, $subcategory] as $phrase) {
            $phrase = trim($phrase);

            if ($phrase !== '') {
                $phrases[] = $phrase;
            }
        }

        foreach ($candidates as $candidate) {
            if (! is_array($candidate)) {
                continue;
            }

            $name = trim((string) ($candidate['name'] ?? ''));

            if ($name !== '') {
                $phrases[] = $name;
            }

            foreach ($candidate['keyword_seeds_generated'] ?? [] as $generatedSeeds) {
                if (! is_array($generatedSeeds)) {
                    continue;
                }

                foreach ($generatedSeeds as $seed) {
                    $seed = trim((string) $seed);

                    if ($seed !== '') {
                        $phrases[] = $seed;
                    }
                }
            }

            foreach ($candidate['product_keywords'] ?? [] as $bucket) {
                if (is_string($bucket) && trim($bucket) !== '') {
                    $phrases[] = trim($bucket);

                    continue;
                }

                if (! is_array($bucket)) {
                    continue;
                }

                foreach ($bucket as $seed) {
                    $seed = trim((string) $seed);

                    if ($seed !== '') {
                        $phrases[] = $seed;
                    }
                }
            }
        }

        return array_values(array_unique(array_filter($phrases, fn (string $phrase): bool => $phrase !== '')));
    }

    /**
     * @param  list<string>  $phrases
     * @return list<string>
     */
    private function tokensFromPhrases(array $phrases): array
    {
        $tokens = [];

        foreach ($phrases as $phrase) {
            foreach ($this->tokens($phrase) as $token) {
                if (! in_array($token, $tokens, true)) {
                    $tokens[] = $token;
                }
            }
        }

        return $tokens;
    }

    /**
     * @param  list<string>  $references
     * @param  list<string>  $referenceTokens
     * @param  list<string>  $seedTokens
     */
    private function relevanceScore(
        string $term,
        array $references,
        array $referenceTokens,
        array $seedTokens,
    ): int {
        $termLower = Str::lower($term);
        $score = 0;

        foreach ($references as $reference) {
            $referenceLower = Str::lower($reference);

            if ($referenceLower === $termLower) {
                $score += 20;
            } elseif (Str::contains($termLower, $referenceLower) || Str::contains($referenceLower, $termLower)) {
                $score += 8;
            }
        }

        $termTokens = $this->tokens($term);

        foreach ($termTokens as $token) {
            if (in_array($token, $seedTokens, true)) {
                $score += 5;
            } elseif (in_array($token, $referenceTokens, true)) {
                $score += 2;
            }
        }

        if ($this->containsRetailerNoise($termLower)) {
            return 0;
        }

        return $score >= 4 ? $score : 0;
    }

    private function containsRetailerNoise(string $termLower): bool
    {
        foreach ([
            'zappos',
            'amazon',
            'walmart',
            'target',
            'zara',
            'ebay',
            'costco',
            'shein',
            'aliexpress',
        ] as $retailer) {
            if (Str::contains($termLower, $retailer)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param  list<array{term: string, avg_monthly_searches: int, competition: string, low_top_of_page_bid_micros?: int}>  $ideas
     */
    private function containsIdea(array $ideas, string $term): bool
    {
        $normalized = Str::lower(trim($term));

        foreach ($ideas as $idea) {
            if (Str::lower(trim((string) ($idea['term'] ?? ''))) === $normalized) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return list<string>
     */
    private function tokens(string $value): array
    {
        $tokens = preg_split('/[^a-z0-9]+/i', Str::lower($value)) ?: [];

        return array_values(array_filter($tokens, fn (string $token): bool => strlen($token) > 2));
    }
}
