<?php

declare(strict_types=1);

namespace App\Services;

use App\Core\Config;
use App\Core\Database;
use App\Helpers\Logger;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;

class StatLinkService
{
    private Client $http;
    private CookieJar $cookies;
    private bool $loggedIn = false;

    private string $baseUrl;
    private string $login;
    private string $password;

    private const MAX_LINKS_PER_RUN = 1;
    private const LINK_LIFETIME_DAYS = 60;

    public function __construct()
    {
        $this->baseUrl = rtrim((string) Config::get('statlink_url', 'https://statlink.pl'), '/');
        $this->login = (string) Config::get('statlink_login', '');
        $this->password = (string) Config::get('statlink_password', '');

        $this->cookies = new CookieJar();
        $this->http = new Client([
            'cookies' => $this->cookies,
            'connect_timeout' => 60,
            'timeout' => 120,
            'verify' => false,
            'headers' => [
                'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language' => 'pl-PL,pl;q=0.9,en;q=0.8',
            ],
        ]);
    }

    public function login(): bool
    {
        if ($this->loggedIn) {
            return true;
        }

        if ($this->login === '' || $this->password === '') {
            $this->loginDiagnostic = 'FAIL: brak danych logowania w .env';
            Logger::error('StatLink: brak danych logowania w .env', 'statlink');
            return false;
        }

        try {
            // First GET the homepage to pick up session cookies and hidden fields
            $homePage = $this->http->get($this->baseUrl . '/');
            $homeBody = (string) $homePage->getBody();
            $postujForm = '';
            if (preg_match('/name="postuj_form"\s+value="([^"]*)"/', $homeBody, $pfm)) {
                $postujForm = $pfm[1];
            }

            $response = $this->http->post($this->baseUrl . '/20,zaloguj', [
                'form_params' => [
                    'email' => $this->login,
                    'haslo' => $this->password,
                    'zaloguj' => 'ZALOGUJ',
                    'postuj_form' => $postujForm,
                ],
                'allow_redirects' => true,
            ]);

            $body = (string) $response->getBody();

            if (str_contains($body, 'Zalogowano') || str_contains($body, 'zalogowany') || str_contains($body, 'Vampirius')) {
                $this->loggedIn = true;
                $this->loginDiagnostic = 'OK: zalogowano';
                Logger::info('StatLink: zalogowano pomyslnie', 'statlink');
                return true;
            }

            $this->loginDiagnostic = 'FAIL: brak potwierdzenia logowania. Fragment: ' . mb_substr(strip_tags($body), 0, 300);
            Logger::error('StatLink: login nie powiodl sie — brak potwierdzenia zalogowania', 'statlink');
            return false;
        } catch (\Throwable $e) {
            $this->loginDiagnostic = 'EXCEPTION: ' . $e->getMessage();
            Logger::error('StatLink: blad logowania — ' . $e->getMessage(), 'statlink');
            return false;
        }
    }

    /** @var array Diagnostic log for the last operation */
    private array $lastDiagnostics = [];
    private ?string $loginDiagnostic = null;

    public function getLastDiagnostics(): array
    {
        $diag = [];
        if ($this->loginDiagnostic !== null) {
            $diag[] = 'LOGIN: ' . $this->loginDiagnostic;
        }
        return array_merge($diag, $this->lastDiagnostics);
    }

    private function sanitizeAnchor(string $anchor): string
    {
        // Transliterate Polish diacritics to ASCII equivalents
        $map = [
            'ą' => 'a', 'ć' => 'c', 'ę' => 'e', 'ł' => 'l', 'ń' => 'n',
            'ó' => 'o', 'ś' => 's', 'ź' => 'z', 'ż' => 'z',
            'Ą' => 'A', 'Ć' => 'C', 'Ę' => 'E', 'Ł' => 'L', 'Ń' => 'N',
            'Ó' => 'O', 'Ś' => 'S', 'Ź' => 'Z', 'Ż' => 'Z',
        ];
        $anchor = strtr($anchor, $map);

        // Remove any remaining non-ASCII characters, keep allowed: alphanumeric .,+-_?!&\:= and space
        $anchor = preg_replace('/[^a-zA-Z0-9 .,+\-_?!&\\\\:=]/', '', $anchor);

        return trim($anchor);
    }

    public function addLink(string $url, string $anchor): ?int
    {
        $this->lastDiagnostics = [];

        // Sanitize anchor for StatLink's character restrictions
        $anchor = $this->sanitizeAnchor($anchor);
        if (mb_strlen($anchor) < 2) {
            $this->lastDiagnostics[] = 'FAIL: anchor za krotki po sanityzacji: "' . $anchor . '"';
            return null;
        }

        if (!$this->loggedIn && !$this->login()) {
            $this->lastDiagnostics[] = 'FAIL: login nie powiodl sie';
            return null;
        }
        $this->lastDiagnostics[] = 'OK: zalogowano';

        try {
            // GET the links page to scrape CSRF token and categories
            $pageResponse = $this->http->get($this->baseUrl . '/148,twoje-linki');
            $pageHtml = (string) $pageResponse->getBody();
            $this->lastDiagnostics[] = 'OK: pobrano strone linkow (HTTP ' . $pageResponse->getStatusCode() . ', ' . strlen($pageHtml) . ' bytes)';

            $csrfToken = $this->scrapeCsrfToken($pageHtml);
            if ($csrfToken === null) {
                $this->lastDiagnostics[] = 'FAIL: brak tokenu CSRF w HTML (szukam name="niepozwol")';
                // Save a snippet of the page for debugging
                $this->lastDiagnostics[] = 'DEBUG: fragment HTML (500 znakow): ' . substr(strip_tags($pageHtml), 0, 500);
                Logger::error('StatLink: nie udalo sie pobrac tokenu CSRF', 'statlink');
                return null;
            }
            $this->lastDiagnostics[] = 'OK: CSRF token pobrany (' . strlen($csrfToken) . ' znakow)';

            $categories = $this->scrapeCategories($pageHtml);
            $this->lastDiagnostics[] = 'OK: kategorie: ' . (count($categories) > 0 ? implode(',', $categories) : 'brak');

            // Determine protocol
            $isHttps = str_starts_with($url, 'https://');
            $cleanUrl = preg_replace('#^https?://#', '', $url);
            $cleanUrl = rtrim($cleanUrl, '/');
            $this->lastDiagnostics[] = "INFO: wysylam link={$cleanUrl}, anchor={$anchor}, https=" . ($isHttps ? '1' : '0');

            // Build form data
            $formData = [
                'editadd' => '',
                'niepozwol' => $csrfToken,
                'https' => $isHttps ? '1' : '0',
                'link' => $cleanUrl,
                'anchor' => $anchor,
                'fraza_kluczowa1' => '',
                'fraza_kluczowa2' => '',
                'fraza_kluczowa3' => '',
                'wylacznosc' => '',
                'frazowy' => '',
                'tylko_https' => '',
                'min_ilosc_znakow' => '0',
                'statrank_min' => '0',
                'reaguj_statrank_min' => '',
                'statrank_max' => '10',
                'semstorm_keywords_top_min' => '0',
                'ilosc_dziennie' => '0.02',
                'ilosc_max' => '10',
                'ilosc_max_powiadom' => '',
                'ilosc_nofollow' => '0',
                'max_ilosc_domena' => '5',
                'skanuj_pos' => '',
                'pos_limit' => '',
                'pos_limit_powiadom' => '',
                'wstrzymaj' => '',
                'id_grupy' => '0',
                'nowa_grupa' => '',
                'pomin_wlasne' => '',
                'zapisz' => 'DODAJ',
            ];

            // Build multipart-like form with categories
            $params = $formData;
            // Categories need special handling (multiple values for same key)
            // Guzzle form_params doesn't support duplicate keys, so use body directly
            $bodyParts = [];
            foreach ($params as $key => $value) {
                $bodyParts[] = urlencode($key) . '=' . urlencode((string) $value);
            }
            foreach ($categories as $catId) {
                $bodyParts[] = 'id_kategorie_multiple%5B%5D=' . urlencode((string) $catId);
            }
            $bodyString = implode('&', $bodyParts);

            $response = $this->http->post($this->baseUrl . '/148,twoje-linki#lista', [
                'headers' => [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                'body' => $bodyString,
                'allow_redirects' => true,
            ]);

            $responseHtml = (string) $response->getBody();
            $this->lastDiagnostics[] = 'OK: formularz wyslany (HTTP ' . $response->getStatusCode() . ', ' . strlen($responseHtml) . ' bytes)';

            // Check for common error/warning messages in the response
            $errorPatterns = ['błąd', 'Błąd', 'error', 'Error', 'nieprawidłow', 'Nieprawidłow', 'nie można', 'Nie można', 'istnieje', 'Istnieje', 'Limit', 'limit', 'za dużo', 'Za dużo'];
            $foundMessages = [];
            $strippedHtml = strip_tags($responseHtml);
            foreach ($errorPatterns as $pat) {
                $pos = mb_stripos($strippedHtml, $pat);
                if ($pos !== false) {
                    $start = max(0, $pos - 40);
                    $foundMessages[] = mb_substr($strippedHtml, $start, 100);
                }
            }
            if (!empty($foundMessages)) {
                $this->lastDiagnostics[] = 'WARN: znalezione komunikaty w odpowiedzi: ' . implode(' | ', array_unique($foundMessages));
            }

            // Check for "Dodano Link" success message (case-insensitive)
            if (stripos($responseHtml, 'Dodano Link') !== false || stripos($responseHtml, 'Dodano link') !== false || stripos($responseHtml, 'dodano link') !== false) {
                // Try to extract ID directly from the response HTML first
                $statlinkId = $this->findLinkIdInHtml($responseHtml, $cleanUrl);
                if ($statlinkId === null) {
                    // Fallback: search via separate request
                    $statlinkId = $this->findLinkIdBySearch($cleanUrl);
                }
                $this->lastDiagnostics[] = 'OK: potwierdzenie "Dodano Link" znalezione, statlink_id=' . ($statlinkId ?? 'nieznany');
                Logger::info("StatLink: dodano link" . ($statlinkId ? " ID {$statlinkId}" : '') . " dla {$cleanUrl}", 'statlink');
                return $statlinkId ?: -1; // -1 means added but ID unknown
            }

            $this->lastDiagnostics[] = 'WARN: brak "Dodano Link" w odpowiedzi';

            // Extract meaningful content from the response — skip navigation/header junk
            $strippedFull = strip_tags($responseHtml);
            // Look for form-area messages: find "Twoje Linki" heading and capture text after it
            $formAreaPos = mb_strpos($strippedFull, 'Twoje Linki');
            if ($formAreaPos !== false) {
                $formArea = mb_substr($strippedFull, $formAreaPos, 2000);
                // Collapse whitespace for readability
                $formArea = trim(preg_replace('/\s+/', ' ', $formArea));
                $this->lastDiagnostics[] = 'DEBUG: sekcja formularza: ' . mb_substr($formArea, 0, 1500);
            } else {
                $this->lastDiagnostics[] = 'DEBUG: fragment odpowiedzi (1500 znakow): ' . mb_substr(preg_replace('/\s+/', ' ', $strippedFull), 0, 1500);
            }

            // Also check for alert/message divs in raw HTML
            if (preg_match_all('/<div[^>]*class="[^"]*(?:alert|message|info|error|warning|komunikat)[^"]*"[^>]*>(.*?)<\/div>/si', $responseHtml, $alertMatches)) {
                $alerts = array_map(function($m) { return trim(strip_tags($m)); }, $alertMatches[1]);
                $alerts = array_filter($alerts);
                if (!empty($alerts)) {
                    $this->lastDiagnostics[] = 'ALERT_DIVS: ' . implode(' | ', $alerts);
                }
            }

            // No "Dodano" confirmation — check if link already exists in StatLink
            $existingId = $this->findLinkIdBySearch($cleanUrl);
            if ($existingId !== null) {
                $this->lastDiagnostics[] = 'OK: link juz istnieje w StatLink (ID ' . $existingId . ')';
                Logger::info("StatLink: link {$cleanUrl} juz istnieje w systemie (ID {$existingId})", 'statlink');
                return $existingId; // treat as success — link is there
            }

            $this->lastDiagnostics[] = 'FAIL: link nie znaleziony w StatLink po wysylce';
            Logger::warning("StatLink: wyslano link {$cleanUrl}, ale brak potwierdzenia dodania", 'statlink');
            return null;
        } catch (\Throwable $e) {
            $this->lastDiagnostics[] = 'EXCEPTION: ' . $e->getMessage();
            Logger::error("StatLink: blad dodawania linku {$url} — " . $e->getMessage(), 'statlink');
            return null;
        }
    }

    public function removeLink(int $statlinkId): bool
    {
        if (!$this->loggedIn && !$this->login()) {
            return false;
        }

        try {
            $response = $this->http->post($this->baseUrl . '/148,twoje-linki#lista0', [
                'form_params' => [
                    'statlink_id' => (string) $statlinkId,
                    'usun' => 'Usuń',
                ],
                'allow_redirects' => true,
            ]);

            $body = (string) $response->getBody();

            // Verify link was removed by checking it no longer appears
            $remainingIds = $this->scrapeExistingLinkIds($body);
            if (!in_array($statlinkId, $remainingIds)) {
                Logger::info("StatLink: usunieto link ID {$statlinkId}", 'statlink');
                return true;
            }

            Logger::warning("StatLink: link ID {$statlinkId} nadal widoczny po probie usuniecia", 'statlink');
            return false;
        } catch (\Throwable $e) {
            Logger::error("StatLink: blad usuwania linku ID {$statlinkId} — " . $e->getMessage(), 'statlink');
            return false;
        }
    }

    public function processNewArticles(): array
    {
        $db = Database::getInstance();
        $stats = ['added' => 0, 'skipped' => 0, 'errors' => 0];

        // Find published articles with wp_post_id but without an active statlink entry
        $stmt = $db->prepare(
            "SELECT a.id, a.title, a.wp_post_id, a.wp_post_url, a.site_id, a.topic_id,
                    t.name as topic_name, s.url as site_url,
                    s.api_user as site_api_user, s.api_token as site_api_token
             FROM articles a
             JOIN topics t ON a.topic_id = t.id
             JOIN sites s ON a.site_id = s.id
             WHERE a.status = 'published'
               AND a.wp_post_id IS NOT NULL
               AND a.wp_post_id > 0
               AND a.id NOT IN (
                   SELECT sl.article_id FROM statlink_links sl
               )
             ORDER BY a.published_at ASC
             LIMIT :limit"
        );
        $stmt->bindValue('limit', self::MAX_LINKS_PER_RUN, \PDO::PARAM_INT);
        $stmt->execute();
        $articles = $stmt->fetchAll();

        if (empty($articles)) {
            Logger::info('StatLink: brak nowych artykulow do dodania', 'statlink');
            return $stats;
        }

        if (!$this->login()) {
            Logger::error('StatLink: nie mozna zalogowac — przerywam processNewArticles', 'statlink');
            $stats['errors'] = count($articles);
            return $stats;
        }

        $wordpress = new WordPressService();
        $counter = 0;
        foreach ($articles as $article) {
            // Alternate anchor: even = title, odd = topic keyword
            $anchor = ($counter % 2 === 0)
                ? (string) $article['title']
                : (string) $article['topic_name'];

            // Resolve article URL: use wp_post_url if available, otherwise fetch from WP API
            $url = (string) ($article['wp_post_url'] ?? '');
            if ($url === '') {
                $site = [
                    'id' => $article['site_id'],
                    'url' => $article['site_url'],
                    'api_user' => $article['site_api_user'],
                    'api_token' => $article['site_api_token'],
                ];
                $url = (string) ($wordpress->getPostLink($site, (int) $article['wp_post_id']) ?? '');

                // Save resolved URL back to article for future use
                if ($url !== '') {
                    $db->prepare("UPDATE articles SET wp_post_url = :url WHERE id = :id")
                       ->execute(['url' => $url, 'id' => $article['id']]);
                    Logger::info("StatLink: uzupelniono wp_post_url dla artykulu ID {$article['id']}: {$url}", 'statlink');
                }
            }

            if ($url === '') {
                Logger::warning("StatLink: brak URL dla artykulu ID {$article['id']} — pomijam", 'statlink');
                $stats['skipped']++;
                $counter++;
                continue;
            }

            try {
                $statlinkId = $this->addLink($url, $anchor);

                $now = date('Y-m-d H:i:s');
                $expiresAt = date('Y-m-d H:i:s', strtotime('+' . self::LINK_LIFETIME_DAYS . ' days'));

                if ($statlinkId !== null) {
                    $db->prepare(
                        "INSERT INTO statlink_links (article_id, site_id, statlink_id, anchor, link_url, added_at, expires_at, status)
                         VALUES (:article_id, :site_id, :statlink_id, :anchor, :link_url, :added_at, :expires_at, 'active')"
                    )->execute([
                        'article_id' => $article['id'],
                        'site_id' => $article['site_id'],
                        'statlink_id' => $statlinkId,
                        'anchor' => $anchor,
                        'link_url' => $url,
                        'added_at' => $now,
                        'expires_at' => $expiresAt,
                    ]);

                    $stats['added']++;
                    Logger::info("StatLink: dodano artykul ID {$article['id']} jako link {$statlinkId}", 'statlink');
                } else {
                    // Save as failed so we don't retry endlessly
                    $db->prepare(
                        "INSERT INTO statlink_links (article_id, site_id, anchor, link_url, added_at, expires_at, status, error_message)
                         VALUES (:article_id, :site_id, :anchor, :link_url, :added_at, :expires_at, 'failed', 'StatLink nie zaakceptowal linku')"
                    )->execute([
                        'article_id' => $article['id'],
                        'site_id' => $article['site_id'],
                        'anchor' => $anchor,
                        'link_url' => $url,
                        'added_at' => $now,
                        'expires_at' => $expiresAt,
                    ]);
                    $stats['skipped']++;
                    Logger::warning("StatLink: link {$url} nie zostal dodany — oznaczono jako failed", 'statlink');
                }
            } catch (\Throwable $e) {
                $stats['errors']++;
                Logger::error("StatLink: blad przetwarzania artykulu ID {$article['id']} — " . $e->getMessage(), 'statlink');

                // Save failed attempt
                $now = date('Y-m-d H:i:s');
                $expiresAt = date('Y-m-d H:i:s', strtotime('+' . self::LINK_LIFETIME_DAYS . ' days'));

                $db->prepare(
                    "INSERT INTO statlink_links (article_id, site_id, anchor, link_url, added_at, expires_at, status, error_message)
                     VALUES (:article_id, :site_id, :anchor, :link_url, :added_at, :expires_at, 'failed', :error)"
                )->execute([
                    'article_id' => $article['id'],
                    'site_id' => $article['site_id'],
                    'anchor' => $anchor,
                    'link_url' => $url,
                    'added_at' => $now,
                    'expires_at' => $expiresAt,
                    'error' => $e->getMessage(),
                ]);
            }

            $counter++;

            // Small delay between requests to avoid rate limiting
            if ($counter < count($articles)) {
                sleep(2);
            }
        }

        Logger::info("StatLink processNewArticles: added={$stats['added']}, skipped={$stats['skipped']}, errors={$stats['errors']}", 'statlink');
        return $stats;
    }

    public function retryFailedLinks(): array
    {
        $db = Database::getInstance();
        $stats = ['retried' => 0, 'still_failed' => 0, 'errors' => 0];

        // Find failed links to retry (max 3 retries tracked by retry_count or re-attempts)
        $stmt = $db->prepare(
            "SELECT sl.id, sl.article_id, sl.site_id, sl.link_url, sl.anchor
             FROM statlink_links sl
             WHERE sl.status = 'failed'
             ORDER BY sl.added_at ASC
             LIMIT :limit"
        );
        $stmt->bindValue('limit', self::MAX_LINKS_PER_RUN, \PDO::PARAM_INT);
        $stmt->execute();
        $failedLinks = $stmt->fetchAll();

        if (empty($failedLinks)) {
            return $stats;
        }

        if (!$this->login()) {
            Logger::error('StatLink: nie mozna zalogowac — przerywam retryFailedLinks', 'statlink');
            $stats['errors'] = count($failedLinks);
            // Update error_message on failed links so we see the login diagnostic
            $loginDiag = $this->loginDiagnostic ?? 'LOGIN FAIL: nieznany powod';
            foreach ($failedLinks as $link) {
                $db->prepare("UPDATE statlink_links SET error_message = :error WHERE id = :id")
                   ->execute(['error' => $loginDiag, 'id' => $link['id']]);
            }
            return $stats;
        }

        foreach ($failedLinks as $link) {
            $url = (string) $link['link_url'];
            $anchor = (string) $link['anchor'];

            if ($url === '') {
                // No URL — delete the failed record so processNewArticles can re-pick it
                $db->prepare("DELETE FROM statlink_links WHERE id = :id")
                   ->execute(['id' => $link['id']]);
                $stats['still_failed']++;
                continue;
            }

            try {
                $statlinkId = $this->addLink($url, $anchor);

                if ($statlinkId !== null) {
                    $now = date('Y-m-d H:i:s');
                    $expiresAt = date('Y-m-d H:i:s', strtotime('+' . self::LINK_LIFETIME_DAYS . ' days'));

                    $db->prepare(
                        "UPDATE statlink_links
                         SET statlink_id = :statlink_id, status = 'active',
                             added_at = :added_at, expires_at = :expires_at, error_message = NULL
                         WHERE id = :id"
                    )->execute([
                        'statlink_id' => $statlinkId,
                        'added_at' => $now,
                        'expires_at' => $expiresAt,
                        'id' => $link['id'],
                    ]);
                    $stats['retried']++;
                    Logger::info("StatLink: retry OK — artykul ID {$link['article_id']} dodany jako link {$statlinkId}", 'statlink');
                } else {
                    $stats['still_failed']++;
                    $diag = implode(' | ', $this->lastDiagnostics);
                    $db->prepare(
                        "UPDATE statlink_links SET error_message = :error WHERE id = :id"
                    )->execute([
                        'error' => 'Retry failed: ' . mb_substr($diag, 0, 500),
                        'id' => $link['id'],
                    ]);
                    Logger::warning("StatLink: retry FAIL — artykul ID {$link['article_id']}, link {$url}", 'statlink');
                }
            } catch (\Throwable $e) {
                $stats['errors']++;
                $db->prepare(
                    "UPDATE statlink_links SET error_message = :error WHERE id = :id"
                )->execute(['error' => 'Retry exception: ' . $e->getMessage(), 'id' => $link['id']]);
                Logger::error("StatLink: retry blad — artykul ID {$link['article_id']} — " . $e->getMessage(), 'statlink');
            }
        }

        Logger::info("StatLink retryFailedLinks: retried={$stats['retried']}, still_failed={$stats['still_failed']}, errors={$stats['errors']}", 'statlink');
        return $stats;
    }

    public function removeExpiredLinks(): array
    {
        $db = Database::getInstance();
        $stats = ['removed' => 0, 'errors' => 0];

        $stmt = $db->prepare(
            "SELECT id, statlink_id, link_url, article_id
             FROM statlink_links
             WHERE status = 'active'
               AND expires_at < NOW()
             ORDER BY expires_at ASC
             LIMIT :limit"
        );
        $stmt->bindValue('limit', self::MAX_LINKS_PER_RUN, \PDO::PARAM_INT);
        $stmt->execute();
        $expiredLinks = $stmt->fetchAll();

        if (empty($expiredLinks)) {
            Logger::info('StatLink: brak wygaslych linkow do usuniecia', 'statlink');
            return $stats;
        }

        if (!$this->login()) {
            Logger::error('StatLink: nie mozna zalogowac — przerywam removeExpiredLinks', 'statlink');
            $stats['errors'] = count($expiredLinks);
            return $stats;
        }

        foreach ($expiredLinks as $i => $link) {
            $statlinkId = (int) $link['statlink_id'];

            if ($statlinkId <= 0) {
                // No statlink_id — just mark as removed
                $db->prepare(
                    "UPDATE statlink_links SET status = 'removed', removed_at = NOW() WHERE id = :id"
                )->execute(['id' => $link['id']]);
                $stats['removed']++;
                continue;
            }

            try {
                $removed = $this->removeLink($statlinkId);

                if ($removed) {
                    $db->prepare(
                        "UPDATE statlink_links SET status = 'removed', removed_at = NOW() WHERE id = :id"
                    )->execute(['id' => $link['id']]);
                    $stats['removed']++;
                    Logger::info("StatLink: usunieto wygasly link ID {$statlinkId} (artykul {$link['article_id']})", 'statlink');
                } else {
                    $db->prepare(
                        "UPDATE statlink_links SET status = 'expired', error_message = 'Nie udalo sie usunac ze StatLink' WHERE id = :id"
                    )->execute(['id' => $link['id']]);
                    $stats['errors']++;
                }
            } catch (\Throwable $e) {
                $stats['errors']++;
                $db->prepare(
                    "UPDATE statlink_links SET status = 'expired', error_message = :error WHERE id = :id"
                )->execute(['id' => $link['id'], 'error' => $e->getMessage()]);
                Logger::error("StatLink: blad usuwania linku ID {$statlinkId} — " . $e->getMessage(), 'statlink');
            }

            // Small delay between requests
            if ($i < count($expiredLinks) - 1) {
                sleep(2);
            }
        }

        Logger::info("StatLink removeExpiredLinks: removed={$stats['removed']}, errors={$stats['errors']}", 'statlink');
        return $stats;
    }

    /**
     * Extract statlink_id from HTML that already contains the link (e.g. form response).
     */
    private function findLinkIdInHtml(string $html, string $cleanUrl): ?int
    {
        // Try multiple URL variants: with/without trailing slash, with/without protocol
        $urlVariants = [
            $cleanUrl,
            rtrim($cleanUrl, '/'),
            'https://' . $cleanUrl,
            'https://' . rtrim($cleanUrl, '/'),
            'http://' . $cleanUrl,
            'http://' . rtrim($cleanUrl, '/'),
        ];

        if (preg_match_all('/name="statlink_id"\s+value="(\d+)"/', $html, $idMatches)) {
            foreach ($idMatches[1] as $candidateId) {
                $idPos = strpos($html, 'value="' . $candidateId . '"');
                if ($idPos === false) {
                    continue;
                }
                // Check a wide region around the ID for our URL
                $regionStart = max(0, $idPos - 3000);
                $region = substr($html, $regionStart, 6000);
                foreach ($urlVariants as $variant) {
                    if (stripos($region, $variant) !== false) {
                        return (int) $candidateId;
                    }
                }
            }
        }

        return null;
    }

    private function findLinkIdBySearch(string $cleanUrl): ?int
    {
        try {
            // Extract domain for search query
            $searchTerm = explode('/', $cleanUrl)[0];

            $response = $this->http->post($this->baseUrl . '/148,twoje-linki#lista0', [
                'form_params' => [
                    'statlink_szukaj' => $searchTerm,
                    'ilosc_na_stronie_linki' => '100',
                    'statlink_szukaj_go' => '1',
                ],
                'allow_redirects' => true,
            ]);

            $body = (string) $response->getBody();

            return $this->findLinkIdInHtml($body, $cleanUrl);
        } catch (\Throwable $e) {
            Logger::warning("StatLink: blad wyszukiwania ID linku — " . $e->getMessage(), 'statlink');
            return null;
        }
    }

    private function scrapeCsrfToken(string $html): ?string
    {
        if (preg_match('/name="niepozwol"\s+value="([^"]+)"/', $html, $matches)) {
            return $matches[1];
        }
        if (preg_match('/value="([^"]+)"\s+.*?name="niepozwol"/', $html, $matches)) {
            return $matches[1];
        }
        return null;
    }

    private function scrapeCategories(string $html): array
    {
        $categories = [];
        if (preg_match_all('/name="id_kategorie_multiple\[\]"\s+value="(\d+)"/', $html, $matches)) {
            $categories = array_map('intval', $matches[1]);
        }
        return $categories;
    }

    private function scrapeExistingLinkIds(string $html): array
    {
        $ids = [];
        // Links table has forms with hidden statlink_id fields
        if (preg_match_all('/name="statlink_id"\s+value="(\d+)"/', $html, $matches)) {
            $ids = array_map('intval', array_unique($matches[1]));
        }
        return $ids;
    }
}
