<?php
declare(strict_types=1);

final class GooglePlayImporter
{
    public function __construct(private PDO $db)
    {
    }

    public function discoverSource(array $source): array
    {
        if (!$this->robotsAllows($source['source_url'])) {
            return ['ok' => false, 'found' => 0, 'queued' => 0, 'error' => 'robots.txt does not permit this fetch'];
        }
        $response = fetch_url($source['source_url']);
        if (!$response['ok']) {
            $this->sourceFailure((int)$source['id'], $response['status'], $response['error']);
            return ['ok' => false, 'found' => 0, 'queued' => 0, 'error' => 'HTTP ' . $response['status'] . ' ' . $response['error']];
        }
        preg_match_all('~https?://play\.google\.com/store/apps/details\?[^"\s<>]+~i', html_entity_decode($response['body']), $matches);
        preg_match_all('~(?:href|data-href)=["\'](/store/apps/details\?[^"\']+)~i', $response['body'], $relative);
        $urls = array_merge($matches[0] ?? [], array_map(static fn(string $u): string => 'https://play.google.com' . html_entity_decode($u), $relative[1] ?? []));
        $packages = [];
        foreach ($urls as $url) {
            parse_str((string)parse_url($url, PHP_URL_QUERY), $query);
            $package = $query['id'] ?? '';
            if (preg_match('/^[A-Za-z0-9._-]{3,255}$/', $package)) {
                $packages[$package] = true;
            }
        }
        $queued = 0;
        $stmt = $this->db->prepare(
            "INSERT INTO import_queue (package_id, source_id, status, priority, attempts, available_at, created_at, updated_at)
             VALUES (?, ?, 'pending', ?, 0, NOW(), NOW(), NOW())
             ON DUPLICATE KEY UPDATE source_id = VALUES(source_id),
               status = IF(status='failed','pending',status), available_at = LEAST(available_at,NOW()), updated_at=NOW()"
        );
        $priority = (int)$source['priority'];
        foreach (array_keys($packages) as $package) {
            $stmt->execute([$package, (int)$source['id'], $priority]);
            $queued += $stmt->rowCount() > 0 ? 1 : 0;
        }
        $nextRun = date('Y-m-d H:i:s', time() + max(1, (int)$source['interval_hours']) * 3600);
        $this->db->prepare("UPDATE sources SET last_run_at=NOW(), last_success_at=NOW(), failure_count=0, last_error=NULL, next_run_at=? WHERE id=?")->execute([$nextRun, (int)$source['id']]);
        return ['ok' => true, 'found' => count($packages), 'queued' => $queued, 'error' => ''];
    }

    public function processQueue(int $limit = 12): array
    {
        $limit = max(1, min(50, $limit));
        $items = $this->db->query(
            "SELECT q.* FROM import_queue q
             WHERE q.status='pending' AND q.available_at<=NOW()
             ORDER BY q.priority DESC, q.id ASC LIMIT " . $limit
        )->fetchAll();
        $ok = 0;
        $failed = 0;
        foreach ($items as $item) {
            $this->db->prepare("UPDATE import_queue SET status='processing', updated_at=NOW() WHERE id=? AND status='pending'")->execute([(int)$item['id']]);
            try {
                $result = $this->importPackage($item['package_id'], (int)$item['source_id']);
                if (!$result['ok']) {
                    throw new RuntimeException($result['error']);
                }
                $this->db->prepare("UPDATE import_queue SET status='done', last_error=NULL, updated_at=NOW() WHERE id=?")->execute([(int)$item['id']]);
                $ok++;
            } catch (Throwable $e) {
                $attempts = (int)$item['attempts'] + 1;
                $status = $attempts >= 4 ? 'failed' : 'pending';
                $delay = min(1440, (int)pow(4, $attempts) * 10);
                $availableAt = date('Y-m-d H:i:s', time() + $delay * 60);
                $stmt = $this->db->prepare("UPDATE import_queue SET status=?, attempts=?, last_error=?, available_at=?, updated_at=NOW() WHERE id=?");
                $stmt->execute([$status, $attempts, mb_substr($e->getMessage(), 0, 500), $availableAt, (int)$item['id']]);
                if ($status === 'failed' && str_contains($e->getMessage(), 'HTTP 404')) {
                    $this->db->prepare("UPDATE games SET status='removed',updated_at=NOW() WHERE package_id=?")->execute([$item['package_id']]);
                }
                $failed++;
                if (str_contains($e->getMessage(), '429')) {
                    break;
                }
            }
            usleep(350000);
        }
        return ['processed' => $ok + $failed, 'ok' => $ok, 'failed' => $failed];
    }

    public function importPackage(string $package, int $sourceId = 0): array
    {
        if (!preg_match('/^[A-Za-z0-9._-]{3,255}$/', $package)) {
            return ['ok' => false, 'error' => 'Invalid package id'];
        }
        $url = 'https://play.google.com/store/apps/details?id=' . rawurlencode($package) . '&hl=en_IN&gl=IN';
        if (!$this->robotsAllows($url)) {
            return ['ok' => false, 'error' => 'robots.txt does not permit this fetch'];
        }
        $response = fetch_url($url, 22);
        if (!$response['ok']) {
            return ['ok' => false, 'error' => 'Google Play HTTP ' . $response['status'] . ' ' . $response['error']];
        }
        $data = $this->parseDetail($response['body'], $package, $url);
        if (!$data) {
            return ['ok' => false, 'error' => 'No supported structured game metadata found'];
        }
        if (!$this->looksLikeGame($data)) {
            return ['ok' => false, 'error' => 'Listing is not identified as a game'];
        }
        $this->upsertGame($data, $sourceId);
        $this->queueRelated($response['body'], $sourceId, $package);
        return ['ok' => true, 'title' => $data['title'], 'error' => ''];
    }

    private function parseDetail(string $html, string $package, string $url): ?array
    {
        preg_match_all('~<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>~is', $html, $scripts);
        $json = null;
        foreach ($scripts[1] ?? [] as $raw) {
            $candidate = json_decode(html_entity_decode(trim($raw), ENT_QUOTES | ENT_HTML5), true);
            if (is_array($candidate) && in_array($candidate['@type'] ?? '', ['SoftwareApplication', 'MobileApplication', 'VideoGame'], true)) {
                $json = $candidate;
                break;
            }
        }
        if (!$json || empty($json['name'])) {
            return null;
        }
        $author = $json['author']['name'] ?? $json['publisher']['name'] ?? 'Unknown developer';
        $category = $json['applicationCategory'] ?? $json['genre'] ?? 'Games';
        if (is_string($category) && in_array(strtoupper($category), ['GAME', 'GAMES', 'APPLICATION'], true) && !empty($json['genre'])) {
            $category = $json['genre'];
        }
        if (is_array($category)) {
            $category = reset($category) ?: 'Games';
        }
        $rating = $json['aggregateRating']['ratingValue'] ?? null;
        $ratingCount = $json['aggregateRating']['ratingCount'] ?? $json['aggregateRating']['reviewCount'] ?? 0;
        $offers = $json['offers'] ?? [];
        if (isset($offers[0])) {
            $offers = $offers[0];
        }
        $icon = $json['image'] ?? '';
        if (is_array($icon)) {
            $icon = $icon['url'] ?? reset($icon) ?: '';
        }
        $description = trim(strip_tags((string)($json['description'] ?? '')));
        $short = sprintf(
            'Discover %s, a %s title from %s. Check its current rating, content information and official Google Play listing.',
            $json['name'],
            $this->cleanCategory((string)$category),
            $author
        );
        return [
            'package_id' => $package,
            'title' => trim((string)$json['name']),
            'developer' => trim((string)$author),
            'category' => $this->cleanCategory((string)$category),
            'short_description' => mb_substr($short, 0, 320),
            'source_description_hash' => hash('sha256', $description),
            'icon_url' => filter_var($icon, FILTER_VALIDATE_URL) ? $icon : '',
            'play_url' => $url,
            'rating' => is_numeric($rating) ? min(5, max(0, (float)$rating)) : null,
            'rating_count' => max(0, (int)$ratingCount),
            'price' => isset($offers['price']) ? (string)$offers['price'] : '0',
            'currency' => (string)($offers['priceCurrency'] ?? 'INR'),
            'content_rating' => (string)($json['contentRating'] ?? ''),
            'operating_system' => (string)($json['operatingSystem'] ?? 'Android'),
            'date_published' => $this->safeDate($json['datePublished'] ?? null),
            'date_modified' => $this->safeDate($json['dateModified'] ?? null),
        ];
    }

    private function upsertGame(array $data, int $sourceId): void
    {
        $existing = $this->db->prepare('SELECT id, slug, custom_title, custom_summary FROM games WHERE package_id=? LIMIT 1');
        $existing->execute([$data['package_id']]);
        $old = $existing->fetch();
        $slug = $old['slug'] ?? (slugify($data['title']) . '-' . substr(hash('sha1', $data['package_id']), 0, 7));
        $status = setting('auto_publish', '1') === '1' ? 'published' : 'review';
        $stmt = $this->db->prepare(
            "INSERT INTO games
             (source_id, package_id, title, slug, developer, category, short_description, source_description_hash,
              icon_url, play_url, rating, rating_count, price, currency, content_rating, operating_system,
              date_published, date_modified, status, first_seen_at, last_checked_at, created_at, updated_at)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), NOW(), NOW())
             ON DUPLICATE KEY UPDATE source_id=VALUES(source_id),
              title=IF(custom_title=1,title,VALUES(title)), developer=VALUES(developer), category=VALUES(category),
              short_description=IF(custom_summary=1,short_description,VALUES(short_description)),
              source_description_hash=VALUES(source_description_hash), icon_url=VALUES(icon_url),
              play_url=VALUES(play_url), rating=VALUES(rating), rating_count=VALUES(rating_count),
              price=VALUES(price), currency=VALUES(currency), content_rating=VALUES(content_rating),
              operating_system=VALUES(operating_system), date_published=COALESCE(VALUES(date_published),date_published),
              date_modified=COALESCE(VALUES(date_modified),date_modified), last_checked_at=NOW(), updated_at=NOW()"
        );
        $stmt->execute([
            $sourceId ?: null, $data['package_id'], $data['title'], $slug, $data['developer'], $data['category'],
            $data['short_description'], $data['source_description_hash'], $data['icon_url'], $data['play_url'],
            $data['rating'], $data['rating_count'], $data['price'], $data['currency'], $data['content_rating'],
            $data['operating_system'], $data['date_published'], $data['date_modified'], $status,
        ]);
    }

    private function queueRelated(string $html, int $sourceId, string $currentPackage): void
    {
        preg_match_all('~(?:https?://play\.google\.com)?/store/apps/details\?[^"\s<>]+~i', html_entity_decode($html), $matches);
        $stmt = $this->db->prepare(
            "INSERT INTO import_queue(package_id,source_id,status,priority,attempts,available_at,created_at,updated_at)
             VALUES(?,?,'pending',15,0,NOW(),NOW(),NOW())
             ON DUPLICATE KEY UPDATE updated_at=updated_at"
        );
        $seen = [];
        foreach (array_slice(array_unique($matches[0] ?? []), 0, 40) as $url) {
            parse_str((string)parse_url($url, PHP_URL_QUERY), $query);
            $package = (string)($query['id'] ?? '');
            if ($package === $currentPackage || isset($seen[$package]) || !preg_match('/^[A-Za-z0-9._-]{3,255}$/', $package)) {
                continue;
            }
            $seen[$package] = true;
            $stmt->execute([$package, $sourceId ?: null]);
        }
    }

    private function looksLikeGame(array $data): bool
    {
        return stripos($data['category'], 'game') !== false || preg_match('/\b(game|arcade|puzzle|racing|action|adventure|simulation|strategy|sports|casino|role playing|board|card|trivia|word)\b/i', $data['category']);
    }

    private function cleanCategory(string $category): string
    {
        $category = preg_replace('/^GAME[_\s-]*/i', '', $category) ?: 'Games';
        return ucwords(strtolower(str_replace(['_', '-'], ' ', $category)));
    }

    private function safeDate(mixed $date): ?string
    {
        if (!$date) {
            return null;
        }
        $time = strtotime((string)$date);
        return $time ? date('Y-m-d', $time) : null;
    }

    private function sourceFailure(int $id, int $status, string $error): void
    {
        $failureStmt = $this->db->prepare('SELECT failure_count FROM sources WHERE id=?');
        $failureStmt->execute([$id]);
        $failures = (int)$failureStmt->fetchColumn() + 1;
        $nextRun = date('Y-m-d H:i:s', time() + min(24, (int)pow(2, $failures)) * 3600);
        $stmt = $this->db->prepare(
            "UPDATE sources SET last_run_at=NOW(), failure_count=?,
             last_error=?, next_run_at=?, active=IF(? >= 10,0,active) WHERE id=?"
        );
        $stmt->execute([$failures, mb_substr('HTTP ' . $status . ' ' . $error, 0, 500), $nextRun, $failures, $id]);
    }

    private function robotsAllows(string $url): bool
    {
        $parts = parse_url($url);
        if (!$parts || empty($parts['scheme']) || empty($parts['host'])) {
            return false;
        }
        $robotsUrl = $parts['scheme'] . '://' . $parts['host'] . '/robots.txt';
        $key = 'robots_' . hash('sha256', $robotsUrl);
        $cacheDir = APP_ROOT . '/storage/cache';
        $cacheFile = $cacheDir . '/' . $key . '.txt';
        if (is_file($cacheFile) && filemtime($cacheFile) > time() - 86400) {
            $body = (string)file_get_contents($cacheFile);
        } else {
            $response = fetch_url($robotsUrl, 10);
            if (!$response['ok']) {
                return true;
            }
            $body = $response['body'];
            if (!is_dir($cacheDir)) {
                @mkdir($cacheDir, 0755, true);
            }
            @file_put_contents($cacheFile, $body, LOCK_EX);
        }
        $path = ($parts['path'] ?? '/') . (isset($parts['query']) ? '?' . $parts['query'] : '');
        $groups = [];
        $agents = [];
        $rules = [];
        $hasRules = false;
        foreach (preg_split('/\R/', $body) as $line) {
            $line = trim((string)preg_replace('/#.*/', '', $line));
            if ($line === '') {
                continue;
            }
            if (stripos($line, 'User-agent:') === 0) {
                if ($hasRules) {
                    $groups[] = ['agents' => $agents, 'rules' => $rules];
                    $agents = [];
                    $rules = [];
                    $hasRules = false;
                }
                $agents[] = strtolower(trim(substr($line, 11)));
            } elseif (stripos($line, 'Allow:') === 0 || stripos($line, 'Disallow:') === 0) {
                $allow = stripos($line, 'Allow:') === 0;
                $rule = trim(substr($line, $allow ? 6 : 9));
                $rules[] = ['allow' => $allow, 'path' => $rule];
                $hasRules = true;
            }
        }
        if ($agents || $rules) {
            $groups[] = ['agents' => $agents, 'rules' => $rules];
        }
        $bestLength = -1;
        $bestAllows = true;
        foreach ($groups as $group) {
            $applies = in_array('*', $group['agents'], true);
            foreach ($group['agents'] as $agent) {
                if ($agent !== '*' && $agent !== '' && str_contains(strtolower('GamesJiDiscoveryBot'), $agent)) {
                    $applies = true;
                }
            }
            if (!$applies) {
                continue;
            }
            foreach ($group['rules'] as $rule) {
                if ($rule['path'] === '') {
                    continue;
                }
                $pattern = preg_quote($rule['path'], '~');
                $pattern = str_replace('\*', '.*', $pattern);
                if (str_ends_with($pattern, '\$')) {
                    $pattern = substr($pattern, 0, -2) . '$';
                }
                if (preg_match('~^' . $pattern . '~', $path)) {
                    $length = strlen(str_replace('*', '', $rule['path']));
                    if ($length > $bestLength || ($length === $bestLength && $rule['allow'])) {
                        $bestLength = $length;
                        $bestAllows = (bool)$rule['allow'];
                    }
                }
            }
        }
        return $bestAllows;
    }
}
