<?php
declare(strict_types=1);

namespace Crawl\Controllers\Api;

use Crawl\Lib\Database;
use Crawl\Lib\Response;

class SerpApiController
{
    private Database $db;

    public function __construct()
    {
        $this->db = Database::getInstance();
    }

    /** POST /api/serp/submissions */
    public function createSubmission(): void
    {
        $projectId    = (int)($_POST['project_id']   ?? 0);
        $keywords     = trim($_POST['keywords']       ?? '');
        $weburl       = trim($_POST['weburl']         ?? '');
        $searchEngine = trim($_POST['search_engine']  ?? 'google.com');
        $countryCode  = trim($_POST['country_code']   ?? 'IN');

        if (!$projectId || !$keywords || !$weburl) {
            Response::error('Missing required fields: project_id, keywords, weburl');
            return;
        }

        $submissionId = 'sub_' . date('YmdHis') . '_' . $projectId;

        $this->db->insert('serp_submissions', [
            'submission_id' => $submissionId,
            'project_id'    => $projectId,
            'keywords'      => $keywords,
            'weburl'        => $weburl,
            'search_engine' => $searchEngine,
            'country_code'  => $countryCode,
        ]);

        Response::json(['success' => true, 'submission_id' => $submissionId]);
    }

    /** PUT /api/serp/submissions/{id} */
    public function updateSubmission(int $id, array $body): void
    {
        $keywords     = trim($body['keywords']      ?? '');
        $weburl       = trim($body['weburl']        ?? '');
        $searchEngine = trim($body['search_engine'] ?? 'google.com');
        $countryCode  = trim($body['country_code']  ?? 'IN');

        if (!$keywords || !$weburl) {
            Response::error('Missing required fields: keywords, weburl');
            return;
        }

        $this->db->update(
            'serp_submissions',
            [
                'keywords'      => $keywords,
                'weburl'        => $weburl,
                'search_engine' => $searchEngine,
                'country_code'  => $countryCode,
            ],
            'id = :where_id',
            ['where_id' => $id]
        );

        Response::json(['success' => true]);
    }

    /** DELETE /api/serp/submissions/{id} */
    public function deleteSubmission(int $id): void
    {
        $sub = $this->db->fetch('SELECT submission_id FROM serp_submissions WHERE id = ?', [$id]);
        if ($sub) {
            $this->db->query('DELETE FROM serp_keyword_ranks WHERE submission_id = ?', [$sub['submission_id']]);
            $this->db->query('DELETE FROM serp_jobs WHERE submission_id = ?', [$sub['submission_id']]);
        }
        $this->db->query('DELETE FROM serp_submissions WHERE id = ?', [$id]);
        Response::json(['success' => true]);
    }

    /** GET /api/serp/history */
    public function getHistory(): void
    {
        $submissionId = trim($_GET['submission_id'] ?? '');
        if (!$submissionId) {
            Response::json(['success' => false, 'error' => 'Missing submission_id']);
            return;
        }

        $runs = $this->db->fetchAll(
            'SELECT id, device, checked_at FROM serp_keyword_ranks
             WHERE TRIM(submission_id) = ? ORDER BY checked_at DESC',
            [$submissionId]
        );

        $history = array_map(fn($r) => [
            'id'     => $r['id'],
            'device' => ucfirst($r['device']),
            'time'   => date('M d, Y H:i', strtotime($r['checked_at'])),
        ], $runs);

        Response::json(['success' => true, 'history' => $history]);
    }

    /** GET /api/serp/run-detail */
    public function getRunDetail(): void
    {
        $runId = (int)($_GET['run_id'] ?? 0);
        if (!$runId) {
            Response::json(['success' => false, 'error' => 'Missing run_id']);
            return;
        }

        $run = $this->db->fetch(
            'SELECT kr.*, ss.weburl, ss.keywords AS submission_keywords
             FROM serp_keyword_ranks kr
             JOIN serp_submissions ss ON TRIM(kr.submission_id) = TRIM(ss.submission_id)
             WHERE kr.id = ?',
            [$runId]
        );

        if (!$run) {
            Response::json(['success' => false, 'error' => 'Run not found']);
            return;
        }

        $keywords = explode("\n", $run['keyword']);
        $ranks    = explode("\n", $run['keyword_rank']);
        $urls     = explode("\n", $run['url'] ?? '');
        $results  = [];

        foreach ($keywords as $idx => $k) {
            $results[] = [
                'keyword' => trim($k),
                'rank'    => $ranks[$idx] ?? '-',
                'url'     => $urls[$idx]  ?? '',
            ];
        }

        Response::json([
            'success'    => true,
            'results'    => $results,
            'checked_at' => date('M d, Y H:i', strtotime($run['checked_at'])),
            'device'     => $run['device'],
        ]);
    }

    /** GET /api/serp/export-csv */
    public function exportCsv(): void
    {
        $submissionId = trim($_GET['id'] ?? '');
        if (!$submissionId) {
            http_response_code(400);
            echo 'Submission ID required.';
            return;
        }

        $submission = $this->db->fetch(
            'SELECT * FROM serp_submissions WHERE TRIM(submission_id) = ?',
            [$submissionId]
        );
        if (!$submission) {
            http_response_code(404);
            echo 'Submission not found.';
            return;
        }

        $ranks = $this->db->fetchAll(
            'SELECT * FROM serp_keyword_ranks WHERE TRIM(submission_id) = ? ORDER BY checked_at DESC LIMIT 5',
            [$submissionId]
        );

        $runIds    = [];
        $colLabels = [];
        $kwData    = [];
        $currentKw = array_values(array_filter(array_map('trim', explode("\n", $submission['keywords']))));

        foreach ($ranks as $row) {
            $rId              = $row['id'];
            $runIds[]         = $rId;
            $colLabels[$rId]  = date('d M, H:i', strtotime($row['checked_at']));
            $kList            = explode("\n", $row['keyword']);
            $rList            = explode("\n", $row['keyword_rank']);
            $uList            = explode("\n", $row['url'] ?? '');
            foreach ($kList as $idx => $k) {
                $k = trim($k);
                if (!isset($kwData[$k])) $kwData[$k] = ['ranks' => [], 'urls' => []];
                $kwData[$k]['ranks'][$rId] = $rList[$idx] ?? '-';
                $kwData[$k]['urls'][$rId]  = $uList[$idx] ?? '';
            }
        }

        $filename = 'serp_' . preg_replace('/[^a-zA-Z0-9_\-]/', '_', $submissionId) . '_' . date('Y-m-d') . '.csv';
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        $out = fopen('php://output', 'w');
        fprintf($out, "\xEF\xBB\xBF");

        $hdr = ['Keyword'];
        foreach ($runIds as $rId) $hdr[] = $colLabels[$rId] . ' Rank';
        $hdr[] = 'Trend';
        $hdr[] = 'Latest URL';
        fputcsv($out, $hdr);

        foreach ($currentKw as $kw) {
            $row   = [$kw];
            $first = $runIds[0] ?? null;
            $prev  = $runIds[1] ?? null;
            foreach ($runIds as $rId) {
                $r     = $kwData[$kw]['ranks'][$rId] ?? '-';
                $row[] = is_numeric($r) ? $r : '-';
            }
            $curr  = ($first && isset($kwData[$kw]['ranks'][$first]) && is_numeric($kwData[$kw]['ranks'][$first]))
                   ? (int)$kwData[$kw]['ranks'][$first] : null;
            $pr    = ($prev  && isset($kwData[$kw]['ranks'][$prev])  && is_numeric($kwData[$kw]['ranks'][$prev]))
                   ? (int)$kwData[$kw]['ranks'][$prev]  : null;
            $trend = '-';
            if ($curr !== null && $pr !== null) {
                $d     = $pr - $curr;
                $trend = $d > 0 ? "+$d (Up)" : ($d < 0 ? "$d (Down)" : 'No Change');
            }
            $row[] = $trend;
            $row[] = ($first && !empty($kwData[$kw]['urls'][$first])) ? $kwData[$kw]['urls'][$first] : 'Not found in top 100';
            fputcsv($out, $row);
        }
        fclose($out);
    }

    /**
     * GET /api/serp/start-job
     * Creates the job record and returns {success, job_id} immediately.
     * The browser then fires a separate /api/serp/process-job request (fire-and-forget)
     * that does the actual SERP work. This two-step pattern avoids all platform-specific
     * process-spawning issues and works on Apache/CGI/FPM alike.
     */
    public function startSerpJob(): void
    {
        $submissionId = trim($_GET['submission_id'] ?? '');
        if (!$submissionId) {
            Response::json(['success' => false, 'message' => 'Missing submission_id']);
            return;
        }

        $submission = $this->db->fetch(
            'SELECT * FROM serp_submissions WHERE TRIM(submission_id) = ?',
            [$submissionId]
        );
        if (!$submission) {
            Response::json(['success' => false, 'message' => 'Submission not found']);
            return;
        }

        $deviceType  = $_GET['device_type']  ?? 'desktop';
        $cityName    = $_GET['city_name']     ?? '';
        $countryCode = $_GET['country_code']  ?? ($submission['country_code'] ?? 'IN');
        $searchType  = $_GET['search_type']   ?? 'normal';

        $jobId = 'job_' . date('YmdHis') . '_' . substr(md5(uniqid('', true)), 0, 8);

        try {
            $this->db->insert('serp_jobs', [
                'job_id'        => $jobId,
                'submission_id' => $submissionId,
                'status'        => 'queued',
                'percent'       => 0,
                'message'       => 'Queued — starting shortly...',
                'device'        => $deviceType,
                'search_type'   => $searchType,
                'city_name'     => $cityName,
                'country_code'  => $countryCode,
            ]);
        } catch (\Exception $e) {
            $hint = str_contains($e->getMessage(), 'serp_jobs') ? ' (Run /migrate to create the required table)' : '';
            Response::error('DB error: ' . $e->getMessage() . $hint, 500);
            return;
        }

        Response::json(['success' => true, 'job_id' => $jobId]);
    }

    /**
     * GET /api/serp/process-job?job_id=...
     * Long-running endpoint called fire-and-forget from the browser.
     * Sets ignore_user_abort so processing continues even if the user navigates away.
     * Updates serp_jobs.status/percent/message as it goes; client polls job-status.
     */
    public function processJobEndpoint(): void
    {
        $jobId = trim($_GET['job_id'] ?? '');
        if (!$jobId) {
            http_response_code(400);
            return;
        }

        $job = $this->db->fetch('SELECT * FROM serp_jobs WHERE job_id = ?', [$jobId]);
        if (!$job || $job['status'] !== 'queued') {
            http_response_code(200);
            return;
        }

        $submission = $this->db->fetch(
            'SELECT * FROM serp_submissions WHERE TRIM(submission_id) = ?',
            [trim($job['submission_id'])]
        );
        if (!$submission) {
            $this->db->update('serp_jobs', ['status' => 'failed', 'percent' => 100, 'message' => 'Submission not found'], 'job_id = :jid', ['jid' => $jobId]);
            http_response_code(200);
            return;
        }

        // Keep running even if browser disconnects / user navigates away
        ignore_user_abort(true);
        set_time_limit(0);

        // Send a minimal response so the browser's fire-and-forget fetch doesn't error
        http_response_code(200);
        header('Content-Type: text/plain');
        echo 'ok';
        if (function_exists('fastcgi_finish_request')) {
            fastcgi_finish_request();
        } else {
            while (ob_get_level() > 0) ob_end_flush();
            flush();
        }

        $this->doJobProcessing(
            $jobId,
            $submission,
            $job['device'],
            $job['city_name'],
            $job['country_code'],
            $job['search_type']
        );
        exit(0);
    }

    private function doJobProcessing(
        string $jobId,
        array  $submission,
        string $deviceType,
        string $cityName,
        string $countryCode,
        string $searchType
    ): void {
        $setStatus = function (string $status, int $pct, string $msg) use ($jobId): void {
            $this->db->update(
                'serp_jobs',
                ['status' => $status, 'percent' => $pct, 'message' => $msg],
                'job_id = :jid',
                ['jid' => $jobId]
            );
        };

        $setProgress = function (int $pct, string $msg) use ($jobId): void {
            $this->db->update(
                'serp_jobs',
                ['percent' => $pct, 'message' => $msg],
                'job_id = :jid',
                ['jid' => $jobId]
            );
        };

        $setStatus('processing', 2, 'Starting...');

        $sbApiKey = $_ENV['SCRAPINGBEE_API_KEY'] ?? '';
        if (!$sbApiKey) {
            $setStatus('failed', 100, 'SCRAPINGBEE_API_KEY not configured in .env');
            return;
        }

        $weburl        = $submission['weburl'];
        $keywordsList  = array_values(array_filter(array_map('trim', explode("\n", $submission['keywords']))));
        $totalKeywords = count($keywordsList);

        if ($totalKeywords === 0) {
            $setStatus('failed', 100, 'No keywords configured for this submission');
            return;
        }

        $setProgress(2, "Starting SERP check for $totalKeywords keyword(s)...");

        $results         = [];
        $processed       = 0;
        $maxConcurrency  = $totalKeywords <= 20 ? 2 : ($totalKeywords <= 100 ? 4 : 6);
        $retryLimit      = 3;
        $retriesTracker  = [];
        $pageTracker     = array_fill_keys($keywordsList, 1);
        $pendingKeywords = array_values($keywordsList);
        $stopAll         = false;
        $errorMessage    = '';
        $lastDbUpdate    = 0;

        $mh            = curl_multi_init();
        $activeHandles = [];
        $handleMap     = [];

        try {
            while ($processed < $totalKeywords && !$stopAll) {
                while (count($activeHandles) < $maxConcurrency && count($pendingKeywords) > 0) {
                    $kw   = array_shift($pendingKeywords);
                    $page = $pageTracker[$kw] ?? 1;

                    if ($searchType === 'fast') {
                        $apiUrl = 'https://app.scrapingbee.com/api/v1/fast_search?' . http_build_query([
                            'api_key'      => $sbApiKey,
                            'search'       => $kw,
                            'country_code' => strtolower($countryCode ?: 'in'),
                            'page'         => $page,
                        ]);
                    } else {
                        $apiUrl = 'https://app.scrapingbee.com/api/v1/google?' . http_build_query([
                            'api_key'       => $sbApiKey,
                            'search'        => $kw,
                            'language'      => 'en',
                            'device'        => $deviceType,
                            'nb_results'    => 100,
                            'light_request' => 'false',
                            'country_code'  => strtolower($countryCode ?: 'us'),
                        ]);
                    }

                    $ch = curl_init();
                    curl_setopt_array($ch, [
                        CURLOPT_URL            => $apiUrl,
                        CURLOPT_RETURNTRANSFER => true,
                        CURLOPT_TIMEOUT        => 60,
                        CURLOPT_FOLLOWLOCATION => true,
                    ]);
                    curl_multi_add_handle($mh, $ch);

                    $chId                 = (int)$ch;
                    $activeHandles[$chId] = $ch;
                    $handleMap[$chId]     = ['kw' => $kw, 'page' => $page];
                    $retriesTracker[$kw]  = ($retriesTracker[$kw] ?? 0) + ($page === 1 ? 1 : 0);
                }

                do {
                    $status = curl_multi_exec($mh, $active);
                    if ($active) curl_multi_select($mh, 0.1);
                } while ($active && ($status === CURLM_CALL_MULTI_PERFORM || $status === CURLM_OK));

                while ($info = curl_multi_info_read($mh)) {
                    $ch       = $info['handle'];
                    $chId     = (int)$ch;
                    $kwMeta   = $handleMap[$chId];
                    $kw       = $kwMeta['kw'];
                    $page     = $kwMeta['page'];
                    $respBody = curl_multi_getcontent($ch);
                    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

                    $found   = false;
                    $kwRank  = '-';
                    $rankUrl = '';

                    if ($httpCode === 200 && $respBody) {
                        $resData = json_decode($respBody, true);
                        $organic = $resData['organic_results'] ?? [];

                        if (count($organic) > 0) {
                            $parsedBase = parse_url((str_starts_with($weburl, 'http') ? '' : 'https://') . $weburl);
                            $cleanBase  = str_ireplace('www.', '', strtolower($parsedBase['host'] ?? $weburl));

                            foreach ($organic as $idx => $item) {
                                $itemUrl    = $item['url'] ?? $item['link'] ?? '';
                                if (!$itemUrl) continue;
                                $parsedItem = parse_url((str_starts_with($itemUrl, 'http') ? '' : 'https://') . $itemUrl);
                                $compareUrl = str_ireplace('www.', '', strtolower($parsedItem['host'] ?? $itemUrl));
                                if (str_contains($compareUrl, $cleanBase) || str_contains($cleanBase, $compareUrl)) {
                                    $kwRank  = $item['position'] ?? (($page - 1) * count($organic) + $idx + 1);
                                    $rankUrl = $itemUrl;
                                    $found   = true;
                                    break;
                                }
                            }
                        }
                    } elseif (in_array($httpCode, [401, 402, 403])) {
                        $stopAll      = true;
                        $errorMessage = $httpCode === 402
                            ? 'ScrapingBee credits exhausted'
                            : "ScrapingBee API error (HTTP $httpCode — check API key)";
                    } elseif ($httpCode === 429) {
                        usleep(3_000_000);
                        $maxConcurrency = max(1, $maxConcurrency - 1);
                    }

                    $maxRetriesHit = $searchType === 'normal' && ($retriesTracker[$kw] ?? 0) >= $retryLimit;

                    if ($found || $stopAll || $maxRetriesHit) {
                        $results[$kw] = ['rank' => $kwRank, 'url' => $rankUrl];
                        $processed++;
                        $pct = (int)round($processed / $totalKeywords * 95);
                        $now = time();
                        if ($now - $lastDbUpdate >= 2) {
                            $setProgress($pct, "Processed $processed / $totalKeywords keywords...");
                            $lastDbUpdate = $now;
                        }
                    } elseif ($searchType === 'fast') {
                        if ($page < 10) {
                            $pageTracker[$kw] = $page + 1;
                            array_unshift($pendingKeywords, $kw);
                        } else {
                            $results[$kw] = ['rank' => '-', 'url' => ''];
                            $processed++;
                            $pct = (int)round($processed / $totalKeywords * 95);
                            $now = time();
                            if ($now - $lastDbUpdate >= 2) {
                                $setProgress($pct, "Processed $processed / $totalKeywords keywords...");
                                $lastDbUpdate = $now;
                            }
                        }
                    } else {
                        $pendingKeywords[] = $kw;
                    }

                    curl_multi_remove_handle($mh, $ch);
                    curl_close($ch);
                    unset($activeHandles[$chId], $handleMap[$chId]);
                }

                if (count($activeHandles) === 0 && count($pendingKeywords) === 0 && $processed < $totalKeywords) {
                    break;
                }
            }
        } catch (\Exception $e) {
            curl_multi_close($mh);
            $setStatus('failed', 100, 'Error: ' . $e->getMessage());
            return;
        }

        curl_multi_close($mh);

        if ($stopAll) {
            $setStatus('failed', 100, "Error: $errorMessage");
            return;
        }

        $setProgress(97, 'Saving results to database...');

        try {
            $kArr = [];
            $rArr = [];
            $uArr = [];
            foreach ($keywordsList as $kw) {
                $res    = $results[$kw] ?? ['rank' => '-', 'url' => ''];
                $kArr[] = $kw;
                $rArr[] = (string)$res['rank'];
                $uArr[] = (string)$res['url'];
            }

            $this->db->insert('serp_keyword_ranks', [
                'submission_id' => $submission['submission_id'],
                'keyword'       => implode("\n", $kArr),
                'keyword_rank'  => implode("\n", $rArr),
                'url'           => implode("\n", $uArr),
                'device'        => $deviceType,
                'location_name' => $cityName,
                'country_code'  => $countryCode,
            ]);
        } catch (\Exception $e) {
            $setStatus('failed', 100, 'Error saving results: ' . $e->getMessage());
            return;
        }

        $setStatus('done', 100, 'Rankings saved successfully!');
    }

    /** GET /api/serp/job-status?job_id=... */
    public function getJobStatus(): void
    {
        $jobId = trim($_GET['job_id'] ?? '');
        if (!$jobId) {
            Response::json(['success' => false, 'error' => 'Missing job_id']);
            return;
        }

        $job = $this->db->fetch(
            'SELECT status, percent, message, updated_at FROM serp_jobs WHERE job_id = ?',
            [$jobId]
        );

        if (!$job) {
            Response::json(['success' => false, 'error' => 'Job not found']);
            return;
        }

        Response::json([
            'success'    => true,
            'status'     => $job['status'],
            'percent'    => (int)$job['percent'],
            'message'    => $job['message'],
            'updated_at' => $job['updated_at'],
        ]);
    }

    /**
     * GET /api/serp/run  — legacy SSE endpoint kept for direct use.
     * The main UI now uses startSerpJob() instead of this endpoint.
     */
    public function runSerpSse(): void
    {
        while (ob_get_level()) ob_end_clean();
        ini_set('output_buffering', '0');
        ini_set('zlib.output_compression', '0');
        ini_set('implicit_flush', '1');
        set_time_limit(0);

        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        header('Connection: keep-alive');
        header('X-Accel-Buffering: no');

        $send = static function (string $msg, ?int $pct = null, bool $done = false): void {
            echo 'data: ' . json_encode(['message' => $msg, 'percent' => $pct, 'done' => $done]) . "\n\n";
            if (ob_get_level() > 0) ob_flush();
            flush();
        };

        $submissionId = trim($_GET['submission_id'] ?? '');
        if (!$submissionId) {
            $send('Error: No submission ID provided', 100, true);
            return;
        }

        $submission = $this->db->fetch(
            'SELECT * FROM serp_submissions WHERE TRIM(submission_id) = ?',
            [$submissionId]
        );
        if (!$submission) {
            $send('Error: Submission not found', 100, true);
            return;
        }

        $sbApiKey = $_ENV['SCRAPINGBEE_API_KEY'] ?? '';
        if (!$sbApiKey) {
            $send('Error: SCRAPINGBEE_API_KEY not set in .env', 100, true);
            return;
        }

        $deviceType  = $_GET['device_type']  ?? 'desktop';
        $cityName    = $_GET['city_name']     ?? '';
        $countryCode = $_GET['country_code']  ?? ($submission['country_code'] ?? 'IN');
        $searchType  = $_GET['search_type']   ?? 'normal';

        $weburl        = $submission['weburl'];
        $keywordsList  = array_values(array_filter(array_map('trim', explode("\n", $submission['keywords']))));
        $totalKeywords = count($keywordsList);

        if ($totalKeywords === 0) {
            $send('Error: No keywords configured for this submission', 100, true);
            return;
        }

        $send('Starting SERP check for ' . $totalKeywords . ' keyword(s)...', 2);

        $results         = [];
        $processed       = 0;
        $maxConcurrency  = $totalKeywords <= 20 ? 2 : ($totalKeywords <= 100 ? 4 : 6);
        $retryLimit      = 3;
        $retriesTracker  = [];
        $pageTracker     = array_fill_keys($keywordsList, 1);
        $pendingKeywords = array_values($keywordsList);
        $stopAll         = false;
        $errorMessage    = '';

        $mh            = curl_multi_init();
        $activeHandles = [];
        $handleMap     = [];

        try {
            while ($processed < $totalKeywords && !$stopAll) {
                if (connection_aborted()) break;

                while (count($activeHandles) < $maxConcurrency && count($pendingKeywords) > 0) {
                    $kw   = array_shift($pendingKeywords);
                    $page = $pageTracker[$kw] ?? 1;

                    if ($searchType === 'fast') {
                        $apiUrl = 'https://app.scrapingbee.com/api/v1/fast_search?' . http_build_query([
                            'api_key'      => $sbApiKey,
                            'search'       => $kw,
                            'country_code' => strtolower($countryCode ?: 'in'),
                            'page'         => $page,
                        ]);
                    } else {
                        $apiUrl = 'https://app.scrapingbee.com/api/v1/google?' . http_build_query([
                            'api_key'       => $sbApiKey,
                            'search'        => $kw,
                            'language'      => 'en',
                            'device'        => $deviceType,
                            'nb_results'    => 100,
                            'light_request' => 'false',
                            'country_code'  => strtolower($countryCode ?: 'us'),
                        ]);
                    }

                    $ch = curl_init();
                    curl_setopt_array($ch, [
                        CURLOPT_URL            => $apiUrl,
                        CURLOPT_RETURNTRANSFER => true,
                        CURLOPT_TIMEOUT        => 60,
                        CURLOPT_FOLLOWLOCATION => true,
                    ]);
                    curl_multi_add_handle($mh, $ch);

                    $chId                  = (int)$ch;
                    $activeHandles[$chId]  = $ch;
                    $handleMap[$chId]      = ['kw' => $kw, 'page' => $page];
                    $retriesTracker[$kw]   = ($retriesTracker[$kw] ?? 0) + ($page === 1 ? 1 : 0);
                }

                do {
                    $status = curl_multi_exec($mh, $active);
                    if ($active) curl_multi_select($mh, 0.1);
                } while ($active && ($status === CURLM_CALL_MULTI_PERFORM || $status === CURLM_OK));

                while ($info = curl_multi_info_read($mh)) {
                    $ch       = $info['handle'];
                    $chId     = (int)$ch;
                    $kwData   = $handleMap[$chId];
                    $kw       = $kwData['kw'];
                    $page     = $kwData['page'];
                    $respBody = curl_multi_getcontent($ch);
                    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

                    $found   = false;
                    $kwRank  = '-';
                    $rankUrl = '';

                    if ($httpCode === 200 && $respBody) {
                        $resData = json_decode($respBody, true);
                        $organic = $resData['organic_results'] ?? [];

                        if (count($organic) > 0) {
                            $parsedBase = parse_url((str_starts_with($weburl, 'http') ? '' : 'https://') . $weburl);
                            $cleanBase  = str_ireplace('www.', '', strtolower($parsedBase['host'] ?? $weburl));

                            foreach ($organic as $idx => $item) {
                                $itemUrl = $item['url'] ?? $item['link'] ?? '';
                                if (!$itemUrl) continue;
                                $parsedItem = parse_url((str_starts_with($itemUrl, 'http') ? '' : 'https://') . $itemUrl);
                                $compareUrl = str_ireplace('www.', '', strtolower($parsedItem['host'] ?? $itemUrl));
                                if (str_contains($compareUrl, $cleanBase) || str_contains($cleanBase, $compareUrl)) {
                                    $kwRank  = $item['position'] ?? (($page - 1) * count($organic) + $idx + 1);
                                    $rankUrl = $itemUrl;
                                    $found   = true;
                                    break;
                                }
                            }
                        }
                    } elseif (in_array($httpCode, [401, 402, 403])) {
                        $stopAll      = true;
                        $errorMessage = $httpCode === 402
                            ? 'ScrapingBee credits exhausted'
                            : "ScrapingBee API error (HTTP $httpCode — check API key)";
                    } elseif ($httpCode === 429) {
                        usleep(3_000_000);
                        $maxConcurrency = max(1, $maxConcurrency - 1);
                    }

                    $maxRetriesHit = $searchType === 'normal' && ($retriesTracker[$kw] ?? 0) >= $retryLimit;

                    if ($found || $stopAll || $maxRetriesHit) {
                        $results[$kw] = ['rank' => $kwRank, 'url' => $rankUrl];
                        $processed++;
                        $send(
                            "Processed $processed / $totalKeywords keywords...",
                            (int)round($processed / $totalKeywords * 95)
                        );
                    } elseif ($searchType === 'fast') {
                        if ($page < 10) {
                            $pageTracker[$kw] = $page + 1;
                            array_unshift($pendingKeywords, $kw);
                        } else {
                            $results[$kw] = ['rank' => '-', 'url' => ''];
                            $processed++;
                            $send(
                                "Processed $processed / $totalKeywords keywords...",
                                (int)round($processed / $totalKeywords * 95)
                            );
                        }
                    } else {
                        $pendingKeywords[] = $kw;
                    }

                    curl_multi_remove_handle($mh, $ch);
                    curl_close($ch);
                    unset($activeHandles[$chId], $handleMap[$chId]);
                }

                if (count($activeHandles) === 0 && count($pendingKeywords) === 0 && $processed < $totalKeywords) {
                    break;
                }
            }
        } catch (\Exception $e) {
            curl_multi_close($mh);
            $send('Error: ' . $e->getMessage(), 100, true);
            return;
        }

        curl_multi_close($mh);

        if ($stopAll) {
            $send("Error: $errorMessage", 100, true);
            return;
        }

        $send('Saving results to database...', 97);

        try {
            $kArr = [];
            $rArr = [];
            $uArr = [];
            foreach ($keywordsList as $kw) {
                $res    = $results[$kw] ?? ['rank' => '-', 'url' => ''];
                $kArr[] = $kw;
                $rArr[] = (string)$res['rank'];
                $uArr[] = (string)$res['url'];
            }

            $this->db->insert('serp_keyword_ranks', [
                'submission_id' => $submissionId,
                'keyword'       => implode("\n", $kArr),
                'keyword_rank'  => implode("\n", $rArr),
                'url'           => implode("\n", $uArr),
                'device'        => $deviceType,
                'location_name' => $cityName,
                'country_code'  => $countryCode,
            ]);
        } catch (\Exception $e) {
            $send('Error saving results: ' . $e->getMessage(), 100, true);
            return;
        }

        $send('Rankings saved successfully!', 100, true);
    }
}
