<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\SteamApp;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;

/**
 * SteamService - Comprehensive Steam API integration for PC Games.
 *
 * Features:
 * - Full Steam Store API integration
 * - Rate limiting and caching
 * - Robust title matching with fuzzy search
 * - Complete game metadata retrieval
 * - DLC and package information
 */
class SteamService
{
    // Steam API endpoints
    protected const string STEAM_API_BASE = 'https://api.steampowered.com';

    protected const string STEAM_STORE_BASE = 'https://store.steampowered.com/api';

    protected const string STEAM_STORE_URL = 'https://store.steampowered.com/app/';

    protected const string STEAM_CDN_BASE = 'https://cdn.akamai.steamstatic.com/steam/apps';

    // Rate limiting
    protected const string RATE_LIMIT_KEY = 'steam_api_rate_limit';

    protected const int REQUESTS_PER_MINUTE = 200; // Steam allows ~200 requests per 5 minutes

    protected const int DECAY_SECONDS = 60;

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

    protected const int APP_LIST_CACHE_TTL = 604800; // 7 days

    protected const int SEARCH_CACHE_TTL = 3600; // 1 hour

    protected const int FAILED_LOOKUP_CACHE_TTL = 1800; // 30 minutes

    // Matching configuration
    protected const int MATCH_THRESHOLD = 85;

    protected const int RELAXED_MATCH_THRESHOLD = 75;

    // Scene/release group noise patterns
    protected const array SCENE_GROUPS = [
        'CODEX', 'PLAZA', 'GOG', 'CPY', 'HOODLUM', 'EMPRESS', 'RUNE', 'TENOKE', 'FLT',
        'RELOADED', 'SKIDROW', 'PROPHET', 'RAZOR1911', 'CORE', 'REFLEX', 'P2P', 'GOLDBERG',
        'DARKSIDERS', 'TINYISO', 'DOGE', 'ANOMALY', 'ELAMIGOS', 'FITGIRL', 'DODI', 'XATAB',
        'CHRONOS', 'FCKDRM', 'I_KNOW', 'KAOS', 'SIMPLEX', 'ALI213', 'FLTDOX', '3DMGAME',
        'POSTMORTEM', 'VACE', 'ROGUE', 'OUTLAWS', 'DARKSIIDERS', 'ONLINEFIX',
    ];

    protected const array EDITION_TAGS = [
        'GOTY', 'GAME OF THE YEAR', 'DEFINITIVE EDITION', 'DELUXE EDITION', 'ULTIMATE EDITION',
        'COMPLETE EDITION', 'REMASTERED', 'HD REMASTER', 'DIRECTORS CUT', 'ANNIVERSARY EDITION',
        'ENHANCED EDITION', 'SPECIAL EDITION', 'COLLECTORS EDITION', 'GOLD EDITION',
        'PREMIUM EDITION', 'LEGENDARY EDITION', 'STANDARD EDITION', 'DIGITAL EDITION',
    ];

    protected const array RELEASE_TAGS = [
        'REPACK', 'RIP', 'ISO', 'PROPER', 'UPDATE', 'DLC', 'INCL', 'MULTI', 'CRACK',
        'CRACKFIX', 'FIX', 'PATCH', 'HOTFIX', 'BETA', 'ALPHA', 'DEMO', 'PREORDER',
    ];

    protected ?string $apiKey;

    public function __construct(?string $apiKey = null)
    {
        $this->apiKey = $apiKey ?? config('services.steam.api_key');
    }

    /**
     * Search for a game by title and return the best match's Steam App ID.
     */
    public function search(string $title): ?int
    {
        $cleanTitle = $this->cleanTitle($title);
        if (empty($cleanTitle)) {
            Log::debug('SteamService: Empty title after cleaning', ['original' => $title]);

            return null;
        }

        // Check failed lookup cache
        $cacheKey = 'steam_search_failed:'.md5(mb_strtolower($cleanTitle));
        if (Cache::has($cacheKey)) {
            Log::debug('SteamService: Skipping previously failed search', ['title' => $cleanTitle]);

            return null;
        }

        // Check successful search cache
        $successCacheKey = 'steam_search:'.md5(mb_strtolower($cleanTitle));
        $cached = Cache::get($successCacheKey);
        if ($cached !== null) {
            Log::debug('SteamService: Using cached search result', ['title' => $cleanTitle, 'appid' => $cached]);

            return (int) $cached;
        }

        $appId = $this->performSearch($cleanTitle);

        if ($appId !== null) {
            Cache::put($successCacheKey, $appId, self::SEARCH_CACHE_TTL);
            Log::info('SteamService: Found match', ['title' => $cleanTitle, 'appid' => $appId]);
        } else {
            Cache::put($cacheKey, true, self::FAILED_LOOKUP_CACHE_TTL);
            Log::debug('SteamService: No match found', ['title' => $cleanTitle]);
        }

        return $appId;
    }

    /**
     * Get complete game details from Steam.
     *
     * @return array{
     *     title: string,
     *     steamid: int,
     *     description: ?string,
     *     detailed_description: ?string,
     *     about: ?string,
     *     short_description: ?string,
     *     cover: ?string,
     *     backdrop: ?string,
     *     screenshots: array,
     *     movies: array,
     *     trailer: ?string,
     *     publisher: ?string,
     *     developers: array,
     *     releasedate: ?string,
     *     genres: string,
     *     categories: array,
     *     rating: ?int,
     *     metacritic_score: ?int,
     *     metacritic_url: ?string,
     *     price: ?array,
     *     platforms: array,
     *     requirements: array,
     *     dlc: array,
     *     achievements: ?int,
     *     recommendations: ?int,
     *     website: ?string,
     *     support_url: ?string,
     *     legal_notice: ?string,
     *     directurl: string,
     *     type: string,
     * }|false
     */
    public function getGameDetails(int $appId): array|false
    {
        // Check cache first
        $cacheKey = "steam_app_details:{$appId}";
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return $cached;
        }

        $data = $this->fetchAppDetails($appId);
        if ($data === null) {
            return false;
        }

        $result = $this->transformAppDetails($data, $appId);

        Cache::put($cacheKey, $result, self::APP_DETAILS_CACHE_TTL);

        return $result;
    }

    /**
     * Alias for getGameDetails for backward compatibility.
     */
    public function getAll(int $appId): array|false
    {
        return $this->getGameDetails($appId);
    }

    /**
     * Get Steam player count for a game.
     */
    public function getPlayerCount(int $appId): ?int
    {
        if (empty($this->apiKey)) {
            return null;
        }

        $cacheKey = "steam_player_count:{$appId}";
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return (int) $cached;
        }

        try {
            $response = $this->makeRequest(
                self::STEAM_API_BASE.'/ISteamUserStats/GetNumberOfCurrentPlayers/v1/',
                ['appid' => $appId]
            );

            if ($response && isset($response['response']['player_count'])) {
                $count = (int) $response['response']['player_count'];
                Cache::put($cacheKey, $count, 300); // 5 minute cache

                return $count;
            }
        } catch (\Exception $e) {
            Log::warning('SteamService: Failed to get player count', [
                'appid' => $appId,
                'error' => $e->getMessage(),
            ]);
        }

        return null;
    }

    /**
     * Get game reviews summary.
     */
    public function getReviewsSummary(int $appId): ?array
    {
        $cacheKey = "steam_reviews:{$appId}";
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return $cached;
        }

        try {
            $response = Http::timeout(10)
                ->get(self::STEAM_STORE_BASE.'/appreviews/'.$appId, [
                    'json' => 1,
                    'language' => 'all',
                    'purchase_type' => 'all',
                ])
                ->json();

            if ($response && isset($response['query_summary'])) {
                $summary = [
                    'total_positive' => $response['query_summary']['total_positive'] ?? 0,
                    'total_negative' => $response['query_summary']['total_negative'] ?? 0,
                    'total_reviews' => $response['query_summary']['total_reviews'] ?? 0,
                    'review_score' => $response['query_summary']['review_score'] ?? 0,
                    'review_score_desc' => $response['query_summary']['review_score_desc'] ?? 'No Reviews',
                ];

                Cache::put($cacheKey, $summary, 3600); // 1 hour cache

                return $summary;
            }
        } catch (\Exception $e) {
            Log::warning('SteamService: Failed to get reviews', [
                'appid' => $appId,
                'error' => $e->getMessage(),
            ]);
        }

        return null;
    }

    /**
     * Get DLC list for a game.
     */
    public function getDLCList(int $appId): array
    {
        $details = $this->getGameDetails($appId);
        if ($details === false) {
            return [];
        }

        $dlcIds = $details['dlc'] ?? [];
        if (empty($dlcIds)) {
            return [];
        }

        $dlcList = [];
        foreach (array_slice($dlcIds, 0, 20) as $dlcId) { // Limit to prevent too many API calls
            $dlcDetails = $this->getGameDetails($dlcId);
            if ($dlcDetails !== false) {
                $dlcList[] = [
                    'appid' => $dlcId,
                    'name' => $dlcDetails['title'],
                    'price' => $dlcDetails['price'] ?? null,
                ];
            }
        }

        return $dlcList;
    }

    /**
     * Populate the steam_apps table with the full app list from Steam.
     */
    public function populateSteamAppsTable(?callable $progressCallback = null): array
    {
        $stats = ['inserted' => 0, 'updated' => 0, 'skipped' => 0, 'errors' => 0];

        try {
            $appList = $this->getFullAppList();
            if (empty($appList)) {
                Log::error('SteamService: Failed to retrieve app list from Steam');

                return $stats;
            }

            $total = count($appList);
            $processed = 0;

            // Process in chunks to avoid memory issues
            $chunks = array_chunk($appList, 1000);

            foreach ($chunks as $chunk) {
                $existingApps = SteamApp::query()
                    ->whereIn('appid', Arr::pluck($chunk, 'appid'))
                    ->pluck('appid')
                    ->toArray();

                $toInsert = [];
                foreach ($chunk as $app) {
                    $processed++;

                    if (empty($app['name']) || ! isset($app['appid'])) {
                        $stats['skipped']++;

                        continue;
                    }

                    if (in_array($app['appid'], $existingApps, true)) {
                        $stats['skipped']++;

                        continue;
                    }

                    $toInsert[] = [
                        'appid' => $app['appid'],
                        'name' => $app['name'],
                    ];
                }

                if (! empty($toInsert)) {
                    try {
                        SteamApp::query()->insert($toInsert);
                        $stats['inserted'] += count($toInsert);
                    } catch (\Exception $e) {
                        Log::warning('SteamService: Batch insert failed, trying individual inserts', [
                            'error' => $e->getMessage(),
                        ]);

                        foreach ($toInsert as $app) {
                            try {
                                SteamApp::query()->insertOrIgnore($app);
                                $stats['inserted']++;
                            } catch (\Exception $e2) {
                                $stats['errors']++;
                            }
                        }
                    }
                }

                if ($progressCallback !== null) {
                    $progressCallback($processed, $total);
                }
            }

            Log::info('SteamService: App list populated', $stats);
        } catch (\Exception $e) {
            Log::error('SteamService: Failed to populate app list', ['error' => $e->getMessage()]);
            $stats['errors']++;
        }

        return $stats;
    }

    /**
     * Get the full list of Steam apps.
     */
    public function getFullAppList(): array
    {
        $cacheKey = 'steam_full_app_list';
        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return $cached;
        }

        try {
            $response = Http::timeout(60)
                ->get(self::STEAM_API_BASE.'/ISteamApps/GetAppList/v2/')
                ->json();

            if ($response && isset($response['applist']['apps'])) {
                $apps = $response['applist']['apps'];
                Cache::put($cacheKey, $apps, self::APP_LIST_CACHE_TTL);

                return $apps;
            }
        } catch (\Exception $e) {
            Log::error('SteamService: Failed to fetch app list', ['error' => $e->getMessage()]);
        }

        return [];
    }

    /**
     * Search for games and return multiple matches.
     *
     * @return Collection<int, array{appid: int, name: string, score: float}>
     */
    public function searchMultiple(string $title, int $limit = 10): Collection
    {
        $cleanTitle = $this->cleanTitle($title);
        if (empty($cleanTitle)) {
            return collect();
        }

        $matches = $this->findMatches($cleanTitle, $limit * 2); // Get more to filter

        return collect($matches)
            ->filter(fn ($m) => $m['score'] >= self::RELAXED_MATCH_THRESHOLD)
            ->sortByDesc('score')
            ->take($limit)
            ->values();
    }

    // ========================================
    // Protected Methods
    // ========================================

    /**
     * Perform the actual search with multiple strategies.
     */
    protected function performSearch(string $title): ?int
    {
        $matches = $this->findMatches($title);

        if (empty($matches)) {
            return null;
        }

        // Get the best match
        $bestMatch = $matches[0];

        if ($bestMatch['score'] >= self::MATCH_THRESHOLD) {
            return $bestMatch['appid'];
        }

        // Try relaxed threshold for close matches
        if ($bestMatch['score'] >= self::RELAXED_MATCH_THRESHOLD) {
            // Verify with additional checks
            $details = $this->fetchAppDetails($bestMatch['appid']);
            if ($details !== null && $this->isGameType($details)) {
                return $bestMatch['appid'];
            }
        }

        return null;
    }

    /**
     * Find matching games from the database.
     *
     * @return array<int, array{appid: int, name: string, score: float}>
     */
    protected function findMatches(string $title, int $limit = 25): array
    {
        $variants = $this->generateQueryVariants($title);
        $matches = [];
        $seenAppIds = [];

        foreach ($variants as $variant) {
            // Try Scout full-text search first
            try {
                $results = SteamApp::search($variant)->take($limit)->get();
                foreach ($results as $result) {
                    $appid = $result->appid ?? null;
                    $name = $result->name ?? null;

                    if ($appid === null || $name === null || isset($seenAppIds[$appid])) {
                        continue;
                    }

                    $score = $this->scoreTitle($name, $title);
                    if ($score >= self::RELAXED_MATCH_THRESHOLD) {
                        $seenAppIds[$appid] = true;
                        $matches[] = [
                            'appid' => (int) $appid,
                            'name' => $name,
                            'score' => $score,
                        ];
                    }
                }
            } catch (\Exception $e) {
                Log::debug('SteamService: Scout search failed', ['error' => $e->getMessage()]);
            }

            // LIKE fallback
            $likeTerm = $this->buildLikePattern($variant);
            try {
                $fallbacks = SteamApp::query()
                    ->select(['appid', 'name'])
                    ->where('name', 'like', $likeTerm)
                    ->limit($limit)
                    ->get();

                foreach ($fallbacks as $row) {
                    $appid = $row->appid;
                    $name = $row->name;

                    if (isset($seenAppIds[$appid])) {
                        continue;
                    }

                    $score = $this->scoreTitle($name, $title);
                    if ($score >= self::RELAXED_MATCH_THRESHOLD) {
                        $seenAppIds[$appid] = true;
                        $matches[] = [
                            'appid' => (int) $appid,
                            'name' => $name,
                            'score' => $score,
                        ];
                    }
                }
            } catch (\Exception $e) {
                Log::debug('SteamService: LIKE search failed', ['error' => $e->getMessage()]);
            }
        }

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

        return array_slice($matches, 0, $limit);
    }

    /**
     * Fetch app details from Steam API.
     */
    protected function fetchAppDetails(int $appId): ?array
    {
        $result = RateLimiter::attempt(
            self::RATE_LIMIT_KEY,
            self::REQUESTS_PER_MINUTE,
            function () use ($appId) {
                try {
                    $response = Http::timeout(15)
                        ->get(self::STEAM_STORE_BASE.'/appdetails', [
                            'appids' => $appId,
                            'cc' => 'us',
                            'l' => 'english',
                        ])
                        ->json();

                    if ($response && isset($response[(string) $appId]['success']) && $response[(string) $appId]['success'] === true) {
                        return $response[(string) $appId]['data'];
                    }
                } catch (\Exception $e) {
                    Log::warning('SteamService: Failed to fetch app details', [
                        'appid' => $appId,
                        'error' => $e->getMessage(),
                    ]);
                }

                return null;
            },
            self::DECAY_SECONDS
        );

        // RateLimiter::attempt returns true when rate limit is exceeded
        return is_array($result) ? $result : null;
    }

    /**
     * Transform Steam API response to our standard format.
     */
    protected function transformAppDetails(array $data, int $appId): array
    {
        // Extract screenshots
        $screenshots = [];
        if (! empty($data['screenshots'])) {
            foreach ($data['screenshots'] as $ss) {
                $screenshots[] = [
                    'thumbnail' => $ss['path_thumbnail'] ?? null,
                    'full' => $ss['path_full'] ?? null,
                ];
            }
        }

        // Extract movies/trailers
        $movies = [];
        $trailerUrl = null;
        if (! empty($data['movies'])) {
            foreach ($data['movies'] as $movie) {
                $movieData = [
                    'id' => $movie['id'] ?? null,
                    'name' => $movie['name'] ?? null,
                    'thumbnail' => $movie['thumbnail'] ?? null,
                    'webm' => $movie['webm']['max'] ?? ($movie['webm']['480'] ?? null),
                    'mp4' => $movie['mp4']['max'] ?? ($movie['mp4']['480'] ?? null),
                ];
                $movies[] = $movieData;

                if ($trailerUrl === null && ! empty($movieData['mp4'])) {
                    $trailerUrl = $movieData['mp4'];
                }
            }
        }

        // Extract genres
        $genres = [];
        if (! empty($data['genres'])) {
            foreach ($data['genres'] as $genre) {
                $genres[] = $genre['description'] ?? '';
            }
        }

        // Extract categories (multiplayer, co-op, etc.)
        $categories = [];
        if (! empty($data['categories'])) {
            foreach ($data['categories'] as $cat) {
                $categories[] = $cat['description'] ?? '';
            }
        }

        // Extract price info
        $price = null;
        if (isset($data['price_overview'])) {
            $price = [
                'currency' => $data['price_overview']['currency'] ?? 'USD',
                'initial' => ($data['price_overview']['initial'] ?? 0) / 100,
                'final' => ($data['price_overview']['final'] ?? 0) / 100,
                'discount_percent' => $data['price_overview']['discount_percent'] ?? 0,
                'final_formatted' => $data['price_overview']['final_formatted'] ?? null,
            ];
        } elseif ($data['is_free'] ?? false) {
            $price = [
                'currency' => 'USD',
                'initial' => 0,
                'final' => 0,
                'discount_percent' => 0,
                'final_formatted' => 'Free',
            ];
        }

        // Extract platforms
        $platforms = [];
        if (! empty($data['platforms'])) {
            if ($data['platforms']['windows'] ?? false) {
                $platforms[] = 'Windows';
            }
            if ($data['platforms']['mac'] ?? false) {
                $platforms[] = 'Mac';
            }
            if ($data['platforms']['linux'] ?? false) {
                $platforms[] = 'Linux';
            }
        }

        // Extract requirements
        $requirements = [];
        if (! empty($data['pc_requirements'])) {
            $requirements['pc'] = [
                'minimum' => $data['pc_requirements']['minimum'] ?? null,
                'recommended' => $data['pc_requirements']['recommended'] ?? null,
            ];
        }
        if (! empty($data['mac_requirements'])) {
            $requirements['mac'] = [
                'minimum' => $data['mac_requirements']['minimum'] ?? null,
                'recommended' => $data['mac_requirements']['recommended'] ?? null,
            ];
        }
        if (! empty($data['linux_requirements'])) {
            $requirements['linux'] = [
                'minimum' => $data['linux_requirements']['minimum'] ?? null,
                'recommended' => $data['linux_requirements']['recommended'] ?? null,
            ];
        }

        // Extract release date
        $releaseDate = null;
        if (! empty($data['release_date']['date'])) {
            try {
                $releaseDate = Carbon::parse($data['release_date']['date'])->format('Y-m-d');
            } catch (\Exception $e) {
                $releaseDate = $data['release_date']['date'];
            }
        }

        // Build publisher string
        $publisher = null;
        if (! empty($data['publishers'])) {
            $publisher = implode(', ', array_filter(array_map('strval', $data['publishers'])));
        }

        // Build developers array
        $developers = [];
        if (! empty($data['developers'])) {
            $developers = array_filter(array_map('strval', $data['developers']));
        }

        return [
            'title' => $data['name'] ?? '',
            'steamid' => $appId,
            'type' => $data['type'] ?? 'game',
            'description' => $data['short_description'] ?? null,
            'detailed_description' => $data['detailed_description'] ?? null,
            'about' => $data['about_the_game'] ?? null,
            'short_description' => $data['short_description'] ?? null,
            'cover' => $data['header_image'] ?? null,
            'backdrop' => $data['background'] ?? ($data['background_raw'] ?? null),
            'screenshots' => $screenshots,
            'movies' => $movies,
            'trailer' => $trailerUrl,
            'publisher' => $publisher,
            'developers' => $developers,
            'releasedate' => $releaseDate,
            'genres' => implode(',', array_filter($genres)),
            'categories' => $categories,
            'rating' => $data['metacritic']['score'] ?? null,
            'metacritic_score' => $data['metacritic']['score'] ?? null,
            'metacritic_url' => $data['metacritic']['url'] ?? null,
            'price' => $price,
            'platforms' => $platforms,
            'requirements' => $requirements,
            'dlc' => $data['dlc'] ?? [],
            'achievements' => $data['achievements']['total'] ?? null,
            'recommendations' => $data['recommendations']['total'] ?? null,
            'website' => $data['website'] ?? null,
            'support_url' => $data['support_info']['url'] ?? null,
            'legal_notice' => $data['legal_notice'] ?? null,
            'directurl' => self::STEAM_STORE_URL.$appId,
        ];
    }

    /**
     * Check if the app is a game (not DLC, video, etc.).
     */
    protected function isGameType(array $data): bool
    {
        $type = $data['type'] ?? '';

        return in_array($type, ['game', 'demo'], true);
    }

    /**
     * Make a rate-limited request to Steam API.
     */
    protected function makeRequest(string $url, array $params = []): ?array
    {
        return RateLimiter::attempt(
            self::RATE_LIMIT_KEY,
            self::REQUESTS_PER_MINUTE,
            function () use ($url, $params) {
                if ($this->apiKey !== null) {
                    $params['key'] = $this->apiKey;
                }

                try {
                    return Http::timeout(15)->get($url, $params)->json();
                } catch (\Exception $e) {
                    Log::warning('SteamService: API request failed', [
                        'url' => $url,
                        'error' => $e->getMessage(),
                    ]);

                    return null;
                }
            },
            self::DECAY_SECONDS
        );
    }

    // ========================================
    // Title Matching & Normalization
    // ========================================

    /**
     * Clean a release title for searching.
     */
    public function cleanTitle(string $title): string
    {
        $title = trim($title);
        if ($title === '') {
            return '';
        }

        // URL decode
        $title = urldecode($title);

        // Remove file extensions
        $title = (string) preg_replace('/\.(zip|rar|7z|iso|nfo|sfv|exe|mkv|mp4|avi)$/i', '', $title);

        // Remove bracketed content
        $title = (string) preg_replace('/\[[^\]]*\]|\([^)]*\)|\{[^}]*\}/u', ' ', $title);

        // Remove scene groups at end
        $groupPattern = '/\s*[-_]\s*('.implode('|', array_map('preg_quote', self::SCENE_GROUPS)).')\s*$/i';
        $title = (string) preg_replace($groupPattern, '', $title);

        // Remove edition tags (multi-word patterns first)
        $editionPatterns = [
            '/\b(Game\s+of\s+the\s+Year)[\s._-]*(Edition)?\b/i',
            '/\bGOTY[\s._-]*(Edition)?\b/i',
            '/\b(Definitive|Deluxe|Ultimate|Complete|Enhanced|Special|Collectors?|Gold|Premium|Legendary|Standard|Digital)[\s._-]*(Edition)?\b/i',
            '/\b(Remastered|HD[\s._-]*Remaster|Directors?[\s._-]*Cut|Anniversary)[\s._-]*(Edition)?\b/i',
            '/\bEdition\b/i', // Remove standalone "Edition"
        ];
        foreach ($editionPatterns as $pattern) {
            $title = (string) preg_replace($pattern, ' ', $title);
        }

        // Remove standalone edition tags
        foreach (self::EDITION_TAGS as $tag) {
            // Skip tags already handled above
            if (stripos($tag, 'EDITION') !== false) {
                continue;
            }
            $title = (string) preg_replace('/\b'.preg_quote($tag, '/').'\b/i', ' ', $title);
        }

        // Remove release tags
        foreach (self::RELEASE_TAGS as $tag) {
            $title = (string) preg_replace('/\b'.preg_quote($tag, '/').'\d*\b/i', ' ', $title);
        }

        // Remove DLCs tag (common in scene releases)
        $title = (string) preg_replace('/\b(Incl(?:uding)?\.?\s*)?DLCs?\b/i', ' ', $title);

        // Remove version numbers (v1.2.3, 1.2.3.4, etc.)
        $title = (string) preg_replace('/\bv?\d+(?:\.\d+){2,}\b/i', ' ', $title);

        // Replace separators with spaces
        $title = (string) preg_replace('/[._+\-]+/', ' ', $title);

        // Clean up
        $title = (string) preg_replace('/\s+/', ' ', $title);
        $title = trim($title, " \t\n\r\0\x0B-_");

        return $title;
    }

    /**
     * Generate query variants for better matching.
     *
     * @return array<string>
     */
    protected function generateQueryVariants(string $title): array
    {
        $variants = [$title];

        // Without edition tags (already cleaned, but be sure)
        $stripped = $this->stripEditionTags($title);
        if ($stripped !== $title && $stripped !== '') {
            $variants[] = $stripped;
        }

        // Without parentheses content
        $noParen = (string) preg_replace('/\s*\([^)]*\)\s*/', ' ', $stripped);
        $noParen = trim(preg_replace('/\s+/', ' ', $noParen) ?? '');
        if ($noParen !== $stripped && $noParen !== '') {
            $variants[] = $noParen;
        }

        // Left side of colon (base title)
        if (str_contains($noParen, ':')) {
            $left = trim(explode(':', $noParen, 2)[0]);
            if ($left !== '' && $left !== $noParen) {
                $variants[] = $left;
            }
        }

        // Normalized version
        $normalized = $this->normalizeTitle($noParen);
        if ($normalized !== $noParen && $normalized !== '') {
            $variants[] = $normalized;
        }

        // De-duplicate while preserving order
        $unique = [];
        $seen = [];
        foreach ($variants as $v) {
            $v = trim($v);
            if ($v === '') {
                continue;
            }
            $lower = mb_strtolower($v);
            if (! isset($seen[$lower])) {
                $seen[$lower] = true;
                $unique[] = $v;
            }
        }

        return $unique;
    }

    /**
     * Normalize a title for comparison.
     */
    protected function normalizeTitle(string $title): string
    {
        $s = mb_strtolower($title);

        // Replace separators
        $s = (string) preg_replace('/[._\-+]+/u', ' ', $s);

        // Convert roman numerals
        $s = $this->replaceRomanNumerals($s);

        // Remove common noise
        $noise = array_merge(
            array_map('strtolower', self::SCENE_GROUPS),
            array_map('strtolower', self::EDITION_TAGS),
            array_map('strtolower', self::RELEASE_TAGS),
            ['pc', 'win', 'windows', 'x86', 'x64', 'x32']
        );
        $noisePattern = '/\b('.implode('|', array_map(fn ($w) => preg_quote($w, '/'), $noise)).')\b/u';
        $s = (string) preg_replace($noisePattern, ' ', $s);

        // Remove non-alphanumeric
        $s = (string) preg_replace('/[^\p{L}\p{N}\s]/u', ' ', $s);
        $s = (string) preg_replace('/\s+/u', ' ', $s);
        $s = trim($s);

        // Remove leading articles
        $s = (string) preg_replace('/^(the|a|an)\s+/u', '', $s);

        return $s;
    }

    /**
     * Strip edition tags from a title.
     */
    protected function stripEditionTags(string $title): string
    {
        // Remove compound edition phrases first
        $compoundPatterns = [
            '/\b(Game\s+of\s+the\s+Year)[\s._-]*(Edition)?\b/i',
            '/\bGOTY[\s._-]*(Edition)?\b/i',
            '/\b(Definitive|Deluxe|Ultimate|Complete|Enhanced|Special|Collectors?|Gold|Premium|Legendary|Standard|Digital)[\s._-]*(Edition)?\b/i',
            '/\b(Remastered|HD[\s._-]*Remaster|Directors?[\s._-]*Cut|Anniversary)[\s._-]*(Edition)?\b/i',
            '/\bEdition\b/i', // Remove standalone "Edition"
        ];
        foreach ($compoundPatterns as $pattern) {
            $title = (string) preg_replace($pattern, ' ', $title);
        }

        // Remove remaining individual edition tags
        foreach (self::EDITION_TAGS as $tag) {
            $title = (string) preg_replace('/\s*[-_]?\s*'.preg_quote($tag, '/').'\s*/i', ' ', $title);
        }

        return trim(preg_replace('/\s+/', ' ', $title) ?? '');
    }

    /**
     * Score a candidate title against the original search term.
     */
    protected function scoreTitle(string $candidate, string $original): float
    {
        $normCand = $this->normalizeTitle($candidate);
        $normOrig = $this->normalizeTitle($original);

        if ($normCand === '' || $normOrig === '') {
            return 0.0;
        }

        // Perfect match
        if ($normCand === $normOrig) {
            return 100.0;
        }

        // Token-based scoring
        $tokensCand = $this->tokenize($normCand);
        $tokensOrig = $this->tokenize($normOrig);

        // Token containment check
        $intersect = count(array_intersect($tokensCand, $tokensOrig));
        $union = count(array_unique(array_merge($tokensCand, $tokensOrig)));
        $jaccard = $union > 0 ? ($intersect / $union) : 0.0;

        // Levenshtein similarity
        $lev = levenshtein($normCand, $normOrig);
        $maxLen = max(strlen($normCand), strlen($normOrig));
        $levSim = $maxLen > 0 ? (1.0 - ($lev / $maxLen)) : 0.0;

        // Prefix bonus
        $prefixBoost = 0.0;
        if (str_starts_with($normCand, $normOrig) || str_starts_with($normOrig, $normCand)) {
            $prefixBoost = 0.15;
        }

        // If all tokens of one are in the other
        $candInOrig = empty(array_diff($tokensCand, $tokensOrig));
        $origInCand = empty(array_diff($tokensOrig, $tokensCand));
        if ($candInOrig || $origInCand) {
            $shortCount = min(count($tokensCand), count($tokensOrig));
            $longCount = max(count($tokensCand), count($tokensOrig));
            $coverage = $longCount > 0 ? ($shortCount / $longCount) : 0.0;
            if ($coverage >= 0.8) {
                return 100.0;
            }
        }

        // Combined score
        $score = ($jaccard * 0.6 + $levSim * 0.4 + $prefixBoost) * 100.0;

        return max(0.0, min(100.0, $score));
    }

    /**
     * Tokenize a string.
     */
    protected function tokenize(string $s): array
    {
        $parts = preg_split('/\s+/u', mb_strtolower($s)) ?: [];
        $seen = [];
        $out = [];
        foreach ($parts as $p) {
            $p = trim($p);
            if ($p === '' || isset($seen[$p])) {
                continue;
            }
            $seen[$p] = true;
            $out[] = $p;
        }

        return $out;
    }

    /**
     * Replace roman numerals with arabic numbers.
     */
    protected function replaceRomanNumerals(string $s): string
    {
        $map = [
            'xx' => '20', 'xix' => '19', 'xviii' => '18', 'xvii' => '17', 'xvi' => '16',
            'xv' => '15', 'xiv' => '14', 'xiii' => '13', 'xii' => '12', 'xi' => '11',
            'x' => '10', 'ix' => '9', 'viii' => '8', 'vii' => '7', 'vi' => '6',
            'v' => '5', 'iv' => '4', 'iii' => '3', 'ii' => '2',
        ];

        // Don't replace standalone 'i' as it's too common in titles
        foreach ($map as $roman => $arabic) {
            $s = (string) preg_replace('/\b'.$roman.'\b/ui', $arabic, $s);
        }

        return $s;
    }

    /**
     * Build a SQL LIKE pattern from a search term.
     */
    protected function buildLikePattern(string $term): string
    {
        $normalized = $this->normalizeTitle($term);
        $pattern = preg_replace('/\s+/', '%', $normalized);
        $pattern = trim($pattern ?? '');

        return $pattern === '' ? '%' : '%'.$pattern.'%';
    }

    /**
     * Clear all cached data.
     */
    public function clearCache(): void
    {
        // Note: This is a placeholder. In production, you'd use tagged caches
        // or implement specific cache key management.
        Log::info('SteamService: Cache clear requested');
    }
}
