<?php

declare(strict_types=1);

namespace App\Services;

use App\Services\IGDB\Models\Company;
use App\Services\IGDB\Models\Game;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;

/**
 * IGDBService - IGDB (Internet Game Database) API integration.
 *
 * Note: Local IGDB resource models expose dynamic properties
 * (name, id, etc.) that PHPStan cannot fully resolve.
 *
 * Features:
 * - Rate limiting and caching
 * - Multiple search strategies for better matching
 * - Complete game metadata retrieval
 * - Age rating and release date extraction
 */
class IGDBService
{
    // Rate limiting constants
    protected const string RATE_LIMIT_KEY = 'igdb_api_rate_limit';

    protected const int REQUESTS_PER_MINUTE = 4;

    protected const int DECAY_SECONDS = 60;

    // Cache TTLs
    protected const int GAME_CACHE_TTL = 86400; // 24 hours

    protected const int FAILED_LOOKUP_CACHE_TTL = 3600; // 1 hour

    // Matching configuration
    protected const int MATCH_THRESHOLD = 85;

    // PC Platform IDs in IGDB
    protected const array PC_PLATFORM_IDS = [6, 13, 14, 3]; // PC Windows, DOS, Mac, Linux

    protected const array PLATFORM_ALIASES = [
        'x360' => 'xbox 360',
        'xbox360' => 'xbox 360',
        'xbla' => 'xbox 360',
        'xbox one' => 'xbox one',
        'xboxone' => 'xbox one',
        'x-box' => 'xbox',
        'xbox' => 'xbox',
        'dsi' => 'nintendo ds',
        'nds' => 'nintendo ds',
        '3ds' => 'nintendo 3ds',
        'ps2' => 'playstation2',
        'ps3' => 'playstation 3',
        'ps4' => 'playstation 4',
        'psp' => 'sony psp',
        'psvita' => 'playstation vita',
        'psx' => 'playstation',
        'psx2psp' => 'playstation',
        'wiiu' => 'nintendo wii u',
        'wii' => 'nintendo wii',
        'ngc' => 'gamecube',
        'gc' => 'gamecube',
        'n64' => 'nintendo 64',
        'nes' => 'nintendo nes',
        'super nintendo' => 'snes',
        'nintendo super nes' => 'snes',
        'snes' => 'snes',
    ];

    /**
     * Check if IGDB is configured.
     */
    public function isConfigured(): bool
    {
        return config('igdb.credentials.client_id') !== ''
            && config('igdb.credentials.client_secret') !== '';
    }

    /**
     * Search for a game by title and return the best matching Game object.
     */
    public function search(string $title): ?Game
    {
        if (! $this->isConfigured() || empty($title)) {
            return null;
        }

        $cacheKey = 'igdb_search:'.md5(mb_strtolower($title));

        // Check failed lookup cache
        if (Cache::has("igdb_search_failed:{$cacheKey}")) {
            Log::debug('IGDBService: Skipping previously failed search', ['title' => $title]);

            return null;
        }

        // Check successful search cache
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            Log::debug('IGDBService: Using cached search result', ['title' => $title]);

            return $cached instanceof Game ? $cached : null;
        }

        $game = $this->searchWithStrategies($title, self::PC_PLATFORM_IDS);

        if ($game !== null) {
            Cache::put($cacheKey, $game, self::GAME_CACHE_TTL);
            Log::info('IGDBService: Found match', ['title' => $title, 'matched' => $game->name]); // @phpstan-ignore property.notFound
        } else {
            Cache::put("igdb_search_failed:{$cacheKey}", true, self::FAILED_LOOKUP_CACHE_TTL);
            Log::debug('IGDBService: No match found', ['title' => $title]);
        }

        return $game;
    }

    /**
     * Search for a console game using an optional platform hint.
     */
    public function searchConsole(string $title, string $platformHint): ?Game
    {
        if (! $this->isConfigured() || $title === '') {
            return null;
        }

        $cacheKey = 'igdb_console_search:'.md5(mb_strtolower($title.'|'.$platformHint));

        if (Cache::has("igdb_console_search_failed:{$cacheKey}")) {
            Log::debug('IGDBService: Skipping previously failed console search', [
                'title' => $title,
                'platform' => $platformHint,
            ]);

            return null;
        }

        $cached = Cache::get($cacheKey);
        if ($cached instanceof Game) {
            Log::debug('IGDBService: Using cached console search result', [
                'title' => $title,
                'platform' => $platformHint,
            ]);

            return $cached;
        }

        $game = $this->searchWithStrategies($title, null, $platformHint);

        if ($game instanceof Game) {
            Cache::put($cacheKey, $game, self::GAME_CACHE_TTL);

            return $game;
        }

        Cache::put("igdb_console_search_failed:{$cacheKey}", true, self::FAILED_LOOKUP_CACHE_TTL);

        return null;
    }

    /**
     * Get complete game details from a Game object.
     *
     * @return array<string, mixed>
     *                              title: string,
     *                              asin: string,
     *                              review: string,
     *                              coverurl: string,
     *                              releasedate: string,
     *                              esrb: string,
     *                              url: string,
     *                              backdropurl: string,
     *                              trailer: string,
     *                              publisher: string,
     *                              developer: string,
     *                              genres: array,
     *                              }|false
     * @return array<string, mixed>
     */
    public function getGameDetails(Game $game): array|false
    {
        if (empty($game->name)) {
            return false;
        }

        $genreName = '';

        return $this->buildGameData($game, $genreName);
    }

    /**
     * Search IGDB using multiple strategies for better match rates.
     */
    protected function searchWithStrategies(string $title, ?array $platformIds = null, ?string $platformHint = null): ?Game
    {
        // Strategy 1: Exact name search with PC platform filter
        $game = $this->searchExact($title, $platformIds, $platformHint);
        if ($game !== null) {
            Log::debug('IGDBService: Exact match found', ['title' => $title, 'matched' => $game->name]); // @phpstan-ignore property.notFound

            return $game;
        }

        // Strategy 2: Fuzzy search using IGDB's search endpoint
        $game = $this->searchFuzzy($title, $platformIds, $platformHint);
        if ($game !== null) {
            Log::debug('IGDBService: Fuzzy match found', ['title' => $title, 'matched' => $game->name]); // @phpstan-ignore property.notFound

            return $game;
        }

        // Strategy 3: Search without special characters
        $cleanTitle = preg_replace('/[^a-zA-Z0-9\s]/', '', $title);
        if ($cleanTitle !== $title && $cleanTitle !== '') {
            $game = $this->searchFuzzy($cleanTitle, $platformIds, $platformHint);
            if ($game !== null) {
                Log::debug('IGDBService: Clean title match found', ['title' => $title, 'matched' => $game->name]); // @phpstan-ignore property.notFound

                return $game;
            }
        }

        // Strategy 4: Try with common subtitle patterns removed
        $baseTitle = $this->extractBaseTitle($title);
        if ($baseTitle !== $title && $baseTitle !== $cleanTitle && $baseTitle !== '') {
            $game = $this->searchFuzzy($baseTitle, $platformIds, $platformHint);
            if ($game !== null) {
                Log::debug('IGDBService: Base title match found', ['title' => $title, 'matched' => $game->name]); // @phpstan-ignore property.notFound

                return $game;
            }
        }

        return null;
    }

    /**
     * Exact name search on IGDB with PC platform filter.
     */
    protected function searchExact(string $title, ?array $platformIds = null, ?string $platformHint = null): ?Game
    {
        try {
            $results = RateLimiter::attempt(
                self::RATE_LIMIT_KEY,
                self::REQUESTS_PER_MINUTE,
                function () use ($platformIds, $title) {
                    $query = Game::where('name', $title)
                        ->with($this->getGameRelations())
                        ->orderByDesc('aggregated_rating_count')
                        ->limit(10);

                    if ($platformIds !== null) {
                        $query->whereIn('platforms', $platformIds);
                    }

                    return $query->get();
                },
                self::DECAY_SECONDS
            );

            if ($results === true || empty($results)) {
                return null;
            }

            return $this->findBestMatch($results, $title, $platformHint);
        } catch (\Exception $e) {
            Log::warning('IGDBService: Exact search error', ['error' => $e->getMessage()]);

            return null;
        }
    }

    /**
     * Fuzzy search on IGDB using the search endpoint.
     */
    protected function searchFuzzy(string $title, ?array $platformIds = null, ?string $platformHint = null): ?Game
    {
        try {
            $results = RateLimiter::attempt(
                self::RATE_LIMIT_KEY,
                self::REQUESTS_PER_MINUTE,
                function () use ($platformIds, $title) {
                    $query = Game::search($title)
                        ->where('category', 0) // Main game only (not DLC, expansion, etc.)
                        ->with($this->getGameRelations())
                        ->orderByDesc('aggregated_rating_count')
                        ->limit(10);

                    if ($platformIds !== null) {
                        $query->whereIn('platforms', $platformIds);
                    }

                    return $query->get();
                },
                self::DECAY_SECONDS
            );

            if ($results === true || empty($results)) {
                return null;
            }

            return $this->findBestMatch($results, $title, $platformHint);
        } catch (\Exception $e) {
            Log::warning('IGDBService: Fuzzy search error', ['error' => $e->getMessage()]);

            return null;
        }
    }

    /**
     * Get relations to load with IGDB queries.
     *
     * @return array<string, mixed>
     */
    protected function getGameRelations(): array
    {
        return [
            'cover' => ['url', 'image_id'],
            'screenshots' => ['url', 'image_id'],
            'artworks' => ['url', 'image_id'],
            'videos' => ['video_id', 'name'],
            'involved_companies' => ['company', 'publisher', 'developer'],
            'genres' => ['name'],
            'themes' => ['name'],
            'game_modes' => ['name'],
            'player_perspectives' => ['name'],
            'age_ratings' => ['rating', 'category'],
            'websites' => ['url', 'category'],
            'platforms' => ['name', 'abbreviation'],
            'release_dates' => ['date', 'platform', 'human'],
        ];
    }

    /**
     * Find the best matching game from results.
     */
    protected function findBestMatch(mixed $results, string $title, ?string $platformHint = null): ?Game
    {
        $bestMatch = null;
        $bestScore = 0;
        $normalizedQuery = $this->normalizeForMatch($title);

        foreach ($results as $game) {
            $normalizedName = $this->normalizeForMatch($game->name);
            $score = $this->computeSimilarity($normalizedQuery, $normalizedName);

            // Boost score for games with more ratings (more popular/verified)
            if (isset($game->aggregated_rating_count) && $game->aggregated_rating_count > 10) {
                $score += min(5, $game->aggregated_rating_count / 100);
            }

            // Boost score for games with covers (more complete data)
            if (isset($game->cover)) {
                $score += 2;
            }

            if ($platformHint !== null && $platformHint !== '') {
                $platformScore = $this->getPlatformMatchScore($game, $platformHint);
                $score += $platformScore > 0 ? $platformScore : -10;
            }

            // Check alternative names if available
            if (isset($game->alternative_names)) {
                foreach ($game->alternative_names as $altName) {
                    $altScore = $this->computeSimilarity($normalizedQuery, $this->normalizeForMatch($altName['name'] ?? ''));
                    if ($altScore > $score) {
                        $score = $altScore;
                    }
                }
            }

            if ($score >= self::MATCH_THRESHOLD && $score > $bestScore) {
                $bestMatch = $game;
                $bestScore = $score;
            }
        }

        if ($bestMatch !== null) {
            Log::debug('IGDBService: Best match selected', [
                'query' => $title,
                'matched' => $bestMatch->name,
                'score' => $bestScore,
            ]);
        }

        return $bestMatch;
    }

    /**
     * Build console-specific game data from an IGDB Game model.
     *
     * @return array<string, mixed>
     */
    public function buildConsoleData(Game $game, string $platformHint): array
    {
        $genres = $this->extractGenres($game);
        $publishers = $this->extractCompanyNames($game, 'publisher');

        return [
            'title' => $game->name,
            'asin' => (string) $game->id,
            'review' => (string) ($game->summary ?? ''),
            'coverurl' => $this->getImageUrl($game->cover ?? null, 'cover_big'),
            'releasedate' => $this->getReleaseDate($game),
            'esrb' => isset($game->aggregated_rating) && is_numeric($game->aggregated_rating)
                ? round((float) $game->aggregated_rating).'%'
                : $this->getAgeRating($game),
            'url' => $game->url ?? '',
            'publisher' => ! empty($publishers) ? implode(',', $publishers) : 'Unknown',
            'platform' => $this->resolvePlatformName($game, $platformHint),
            'consolegenre' => ! empty($genres) ? implode(',', $genres) : 'Unknown',
            'salesrank' => '',
        ];
    }

    /**
     * Build game data array from IGDB Game model.
     *
     * @return array<string, mixed>
     */
    public function buildGameData(Game $game, string &$genreName): array
    {
        // Extract publishers and developers
        $publishers = [];
        $developers = [];
        if (! empty($game->involved_companies)) {
            $involvedCompanies = $game->involved_companies;
            if ($involvedCompanies instanceof Collection) {
                $involvedCompanies = $involvedCompanies->toArray();
            }
            foreach ($involvedCompanies as $company) {
                $isPublisher = is_array($company) ? ($company['publisher'] ?? false) : ($company->publisher ?? false);
                $isDeveloper = is_array($company) ? ($company['developer'] ?? false) : ($company->developer ?? false);
                $companyId = is_array($company) ? ($company['company'] ?? null) : ($company->company ?? null);

                if ($isPublisher === true && $companyId) {
                    $companyData = Company::find($companyId);
                    if ($companyData) {
                        $publishers[] = $companyData->name;
                    }
                }
                if ($isDeveloper === true && $companyId) {
                    $companyData = Company::find($companyId);
                    if ($companyData) {
                        $developers[] = $companyData->name;
                    }
                }
            }
        }

        // Extract genres
        $genres = $this->extractGenres($game);
        $genreName = $this->matchGenre(implode(',', array_filter($genres)));

        // Get cover and backdrop URLs
        $coverUrl = $this->getImageUrl($game->cover ?? null, 'cover_big');
        $backdropUrl = $this->getBackdropUrl($game);

        // Get trailer URL
        $trailerUrl = $this->getTrailerUrl($game);

        // Get rating
        $esrb = $this->getAgeRating($game);

        // Get release date for PC
        $releaseDate = $this->getReleaseDate($game);

        // Get game URL
        $gameUrl = $game->url ?? ('https://www.igdb.com/games/'.($game->slug ?? $game->id)); // @phpstan-ignore property.notFound

        // Build review text
        $review = $this->buildReview($game, $developers); // @phpstan-ignore argument.type

        Log::info('IGDBService: Game data built', [
            'title' => $game->name, // @phpstan-ignore property.notFound
            'id' => $game->id, // @phpstan-ignore property.notFound
            'has_cover' => ! empty($coverUrl),
            'has_backdrop' => ! empty($backdropUrl),
            'genres' => $genres,
        ]);

        return [
            'title' => $game->name, // @phpstan-ignore property.notFound
            'asin' => 'igdb-'.$game->id, // @phpstan-ignore property.notFound
            'review' => $review,
            'coverurl' => $coverUrl,
            'releasedate' => $releaseDate,
            'esrb' => $esrb,
            'url' => $gameUrl,
            'backdropurl' => $backdropUrl,
            'trailer' => $trailerUrl,
            'publisher' => ! empty($publishers) ? implode(', ', array_slice($publishers, 0, 3)) : 'Unknown',
            'developer' => ! empty($developers) ? implode(', ', array_slice($developers, 0, 3)) : '',
            'genres' => $genres,
        ];
    }

    /**
     * Extract genres from game data.
     *
     * @return array<string, mixed>
     */
    protected function extractGenres(Game $game): array
    {
        $genres = [];
        if (! empty($game->genres)) {
            $gameGenres = $game->genres;
            if ($gameGenres instanceof Collection) {
                $gameGenres = $gameGenres->toArray();
            }
            foreach ($gameGenres as $genre) {
                $genres[] = is_array($genre) ? ($genre['name'] ?? '') : ($genre->name ?? '');
            }
        }

        // Fall back to themes if no genres
        if (empty($genres) && ! empty($game->themes)) {
            $gameThemes = $game->themes;
            if ($gameThemes instanceof Collection) {
                $gameThemes = $gameThemes->toArray();
            }
            foreach ($gameThemes as $theme) {
                $genres[] = is_array($theme) ? ($theme['name'] ?? '') : ($theme->name ?? '');
            }
        }

        return array_filter($genres);
    }

    /**
     * @return array<int, string>
     */
    protected function extractCompanyNames(Game $game, string $flag): array
    {
        $names = [];
        if (empty($game->involved_companies)) {
            return $names;
        }

        $involvedCompanies = $game->involved_companies;
        if ($involvedCompanies instanceof Collection) {
            $involvedCompanies = $involvedCompanies->toArray();
        }

        foreach ($involvedCompanies as $company) {
            $isMatch = is_array($company) ? ($company[$flag] ?? false) : ($company->{$flag} ?? false);
            $companyId = is_array($company) ? ($company['company'] ?? null) : ($company->company ?? null);

            if ($isMatch !== true || ! $companyId) {
                continue;
            }

            $companyData = Company::find($companyId);
            if ($companyData !== null && ! empty($companyData->name)) {
                $names[] = $companyData->name;
            }
        }

        return array_values(array_unique($names));
    }

    protected function resolvePlatformName(Game $game, string $platformHint): string
    {
        $normalizedHint = $this->normalizePlatformHint($platformHint);
        $fallback = '';

        foreach ((array) ($game->platforms ?? []) as $platform) {
            $name = is_array($platform) ? (string) ($platform['name'] ?? '') : (string) ($platform->name ?? '');
            $abbreviation = is_array($platform) ? (string) ($platform['abbreviation'] ?? '') : (string) ($platform->abbreviation ?? '');

            if ($fallback === '' && $name !== '') {
                $fallback = $name;
            }

            foreach ([$name, $abbreviation] as $candidate) {
                if ($candidate !== '' && $this->normalizePlatformHint($candidate) === $normalizedHint) {
                    return $name !== '' ? $name : $candidate;
                }
            }
        }

        return $fallback;
    }

    protected function getPlatformMatchScore(Game $game, string $platformHint): int
    {
        $normalizedHint = $this->normalizePlatformHint($platformHint);
        if ($normalizedHint === '') {
            return 0;
        }

        foreach ((array) ($game->platforms ?? []) as $platform) {
            $name = is_array($platform) ? (string) ($platform['name'] ?? '') : (string) ($platform->name ?? '');
            $abbreviation = is_array($platform) ? (string) ($platform['abbreviation'] ?? '') : (string) ($platform->abbreviation ?? '');

            foreach ([$name, $abbreviation] as $candidate) {
                if ($candidate === '') {
                    continue;
                }

                if ($this->normalizePlatformHint($candidate) === $normalizedHint) {
                    return 15;
                }
            }
        }

        return 0;
    }

    protected function normalizePlatformHint(string $platform): string
    {
        $normalized = mb_strtolower(trim($platform));

        return self::PLATFORM_ALIASES[$normalized] ?? $normalized;
    }

    /**
     * Get properly formatted IGDB image URL.
     *
     * @param  array<string, mixed>  $imageData
     * @return array<int<0, max>, mixed>
     */
    public function getImageUrl(array|object|null $imageData, string $size = 'cover_big'): string
    {
        if (empty($imageData)) {
            return '';
        }

        if (is_object($imageData)) {
            $imageId = $imageData->image_id ?? ($imageData->imageId ?? null);
            $url = $imageData->url ?? null;
            $imageData = [
                'image_id' => $imageId,
                'url' => $url,
            ];
        }

        if (! empty($imageData['image_id'])) {
            return 'https://images.igdb.com/igdb/image/upload/t_'.$size.'/'.$imageData['image_id'].'.jpg';
        }

        if (! empty($imageData['url'])) {
            $url = $imageData['url'];
            if (str_starts_with($url, '//')) {
                $url = 'https:'.$url;
            }

            return preg_replace('/t_[a-z0-9_]+/', 't_'.$size, $url);
        }

        return '';
    }

    /**
     * Get backdrop URL from artworks or screenshots.
     */
    protected function getBackdropUrl(Game $game): string
    {
        if (! empty($game->artworks)) {
            $artworks = $game->artworks;
            $firstArtwork = ($artworks instanceof Collection) ? $artworks->first() : ($artworks[0] ?? null);
            $url = $this->getImageUrl($firstArtwork, '1080p');
            if (! empty($url)) {
                return $url;
            }
        }

        if (! empty($game->screenshots)) {
            $screenshots = $game->screenshots;
            $firstScreenshot = ($screenshots instanceof Collection) ? $screenshots->first() : ($screenshots[0] ?? null);

            return $this->getImageUrl($firstScreenshot, '1080p');
        }

        return '';
    }

    /**
     * Get trailer URL from videos.
     */
    protected function getTrailerUrl(Game $game): string
    {
        if (! empty($game->videos)) {
            $videos = $game->videos;
            if ($videos instanceof Collection) {
                $videos = $videos->toArray();
            }
            foreach ($videos as $video) {
                $videoId = is_array($video) ? ($video['video_id'] ?? null) : ($video->video_id ?? null);
                if ($videoId) {
                    return 'https://www.youtube.com/watch?v='.$videoId;
                }
            }
        }

        return '';
    }

    /**
     * Get age rating string from IGDB age ratings.
     */
    protected function getAgeRating(Game $game): string
    {
        $ageRatings = $game->age_ratings ?? [];
        if ($ageRatings instanceof Collection) {
            $ageRatings = $ageRatings->toArray();
        }

        if (empty($ageRatings)) {
            return 'Not Rated';
        }

        // ESRB ratings map
        $esrbMap = [
            6 => 'RP (Rating Pending)',
            7 => 'EC (Early Childhood)',
            8 => 'E (Everyone)',
            9 => 'E10+ (Everyone 10+)',
            10 => 'T (Teen)',
            11 => 'M (Mature 17+)',
            12 => 'AO (Adults Only)',
        ];

        // PEGI ratings map
        $pegiMap = [
            1 => 'PEGI 3',
            2 => 'PEGI 7',
            3 => 'PEGI 12',
            4 => 'PEGI 16',
            5 => 'PEGI 18',
        ];

        foreach ($ageRatings as $rating) {
            $category = is_array($rating) ? ($rating['category'] ?? 0) : ($rating->category ?? 0);
            $ratingValue = is_array($rating) ? ($rating['rating'] ?? 0) : ($rating->rating ?? 0);

            // Prefer ESRB
            if ($category === 1 && isset($esrbMap[$ratingValue])) {
                return $esrbMap[$ratingValue];
            }
            // Fall back to PEGI
            if ($category === 2 && isset($pegiMap[$ratingValue])) {
                return $pegiMap[$ratingValue];
            }
        }

        return 'Not Rated';
    }

    /**
     * Get PC release date from IGDB game data.
     */
    protected function getReleaseDate(Game $game): string
    {
        if (! empty($game->release_dates)) {
            foreach ($game->release_dates as $release) {
                if (isset($release['platform']) && $release['platform'] === 6 && isset($release['date'])) {
                    return Carbon::createFromTimestamp($release['date'])->format('Y-m-d');
                }
            }
            if (isset($game->release_dates[0]['date'])) {
                return Carbon::createFromTimestamp($game->release_dates[0]['date'])->format('Y-m-d');
            }
        }

        if (isset($game->first_release_date)) {
            if ($game->first_release_date instanceof Carbon) {
                return $game->first_release_date->format('Y-m-d');
            }
            if (is_numeric($game->first_release_date)) {
                return Carbon::createFromTimestamp($game->first_release_date)->format('Y-m-d');
            }
        }

        return now()->format('Y-m-d');
    }

    /**
     * Build review/summary text.
     *
     * @param  array<string, mixed>  $developers
     */
    protected function buildReview(Game $game, array $developers): string
    {
        $review = $game->summary ?? '';
        if (empty($review) && isset($game->storyline)) {
            $review = $game->storyline;
        }

        $additionalInfo = [];
        if (! empty($developers)) {
            $additionalInfo[] = 'Developer: '.implode(', ', array_slice($developers, 0, 3));
        }
        if (! empty($game->game_modes)) {
            $gameModes = $game->game_modes;
            if ($gameModes instanceof Collection) {
                $modes = $gameModes->map(fn ($m) => is_array($m) ? ($m['name'] ?? '') : ($m->name ?? ''))->toArray();
            } else {
                $modes = array_map(fn ($m) => $m['name'] ?? '', $gameModes);
            }
            $additionalInfo[] = 'Modes: '.implode(', ', array_filter($modes));
        }
        if (! empty($additionalInfo) && ! empty($review)) {
            $review .= "\n\n".implode("\n", $additionalInfo);
        }

        return $review;
    }

    /**
     * Extract base title by removing common subtitle patterns.
     */
    protected function extractBaseTitle(string $title): string
    {
        $patterns = [
            '/\s*[-:]\s+.*$/',
            '/\s+(?:Episode|Chapter|Part)\s+\d+.*/i',
            '/\s+(?:Vol(?:ume)?\.?\s*\d+).*/i',
            '/\s+\d+$/',
        ];

        $baseTitle = $title;
        foreach ($patterns as $pattern) {
            $baseTitle = preg_replace($pattern, '', $baseTitle);
        }

        return trim($baseTitle);
    }

    /**
     * Normalize title for matching.
     */
    protected function normalizeForMatch(string $title): string
    {
        $t = mb_strtolower($title);
        $t = (string) preg_replace('/\b(game of the year|goty|definitive edition|deluxe edition|ultimate edition|complete edition|remastered|hd remaster|directors? cut|anniversary edition|update|patch|hotfix|incl(?:uding)? dlcs?|dlcs?|repack|rip|iso|crack(?:fix)?|beta|alpha)\b/i', ' ', $t);
        $t = (string) preg_replace('/\b(pc|gog|steam|x64|x86|win64|win32|mult[iy]?\d*|eng|english|fr|french|de|german|es|spanish|it|italian|pt|ptbr|portuguese|ru|russian|pl|polish|tr|turkish|nl|dutch|se|swedish|no|norwegian|da|danish|fi|finnish|jp|japanese|cn|chs|cht|ko|korean)\b/i', ' ', $t);
        $t = (string) preg_replace('/[^a-z0-9]+/i', ' ', $t);
        $t = trim(preg_replace('/\s{2,}/', ' ', $t));

        return $t;
    }

    /**
     * Compute similarity between two strings.
     */
    protected function computeSimilarity(string $a, string $b): float
    {
        if ($a === $b) {
            return 100.0;
        }
        $percent = 0.0;
        similar_text($a, $b, $percent);

        $levScore = 0.0;
        $len = max(strlen($a), strlen($b));
        if ($len > 0) {
            $dist = levenshtein($a, $b);
            if ($dist >= 0) {
                $levScore = (1 - ($dist / $len)) * 100.0;
            }
        }

        return max($percent, $levScore);
    }

    /**
     * Match genre string to known genres.
     */
    public function matchGenre(string $genre): string
    {
        $genreName = '';
        $a = str_replace('-', ' ', $genre);
        $tmpGenre = explode(',', $a);
        foreach ($tmpGenre as $tg) {
            $genreMatch = $this->isKnownGenre(ucwords(trim($tg)));
            if ($genreMatch !== false) {
                $genreName = (string) $genreMatch;
                break;
            }
            if (empty($genreName) && ! empty($tmpGenre[0])) {
                $genreName = trim($tmpGenre[0]);
            }
        }

        return $genreName;
    }

    /**
     * Check if genre is in known genres list.
     */
    public function isKnownGenre(string $gameGenre): bool|string
    {
        $knownGenres = [
            'Action', 'Adventure', 'Arcade', 'Board Games', 'Cards', 'Casino',
            'Flying', 'Puzzle', 'Racing', 'Rhythm', 'Role-Playing', 'RPG',
            'Simulation', 'Sports', 'Strategy', 'Trivia', 'Shooter', 'FPS',
            'Horror', 'Survival', 'Sandbox', 'Open World', 'Platformer', 'Fighting',
            'Stealth', 'MMO', 'MMORPG', 'Battle Royale', 'Roguelike', 'Roguelite',
            'Metroidvania', 'Visual Novel', 'Point & Click', 'Management',
            'City Builder', 'Tower Defense', 'Turn-Based', 'Real-Time',
            'Educational', 'Music', 'Party', 'Indie', 'Hack and Slash',
            'Souls-like', 'JRPG', 'ARPG', 'Tactical',
        ];

        return in_array($gameGenre, $knownGenres, true) ? $gameGenre : false;
    }

    /**
     * Clear lookup caches.
     */
    public function clearCache(): void
    {
        Log::info('IGDBService: Cache clear requested');
    }
}
