<?php
namespace Analytics\Services;

use Analytics\Repositories\EventDataRepository;
use Analytics\Repositories\PageDataRepository;
use Analytics\Repositories\QueryDataRepository;
use Analytics\Repositories\SnapshotRepository;
use Crawl\Repositories\OAuthTokenRepository;
use Crawl\Services\GA4Service;
use Crawl\Services\GSCService;
use Crawl\Services\GoogleOAuthService;

/**
 * Core analytics data fetcher â€” PHP equivalent of the Google Apps Script runFullTracker().
 *
 * Fetches GA4 + GSC data for a project over a date range, stores results in the
 * analytics_snapshots, analytics_event_data, analytics_page_data, analytics_query_data tables.
 */
class AnalyticsFetchService
{
    private SnapshotRepository $snapshots;
    private EventDataRepository $events;
    private PageDataRepository  $pages;
    private QueryDataRepository $queries;
    private GA4Service  $ga4;
    private GSCService  $gsc;
    private GoogleOAuthService $oauth;

    public function __construct()
    {
        $this->snapshots = new SnapshotRepository();
        $this->events    = new EventDataRepository();
        $this->pages     = new PageDataRepository();
        $this->queries   = new QueryDataRepository();
        $this->ga4       = new GA4Service();
        $this->gsc       = new GSCService();
        $this->oauth     = new GoogleOAuthService();
    }

    /**
     * Fetch analytics for a single project and persist.
     *
     * @param int    $projectId
     * @param array  $project   Full project row (must include ga4_property_id, gsc_site_url, etc.)
     * @param string $startDate Y-m-d
     * @param string $endDate   Y-m-d
     * @param string $periodType  weekly|monthly|etc.
     * @return array ['success' => bool, 'snapshot_id' => int|null, 'message' => string]
     */
    public function fetchProject(int $projectId, array $project, string $startDate, string $endDate, string $periodType = 'custom'): array
    {
        // Fetch only the requested period. Comparison periods are fetched on-demand
        // via the /fetch-comparison API endpoint (triggered by the report viewer).
        return $this->doFetch($projectId, $project, $startDate, $endDate, $periodType);
    }

    private function doFetch(int $projectId, array $project, string $startDate, string $endDate, string $periodType): array
    {
        $ga4Id   = $project['ga4_property_id'] ?? '';
        $siteUrl = $project['gsc_site_url']    ?? '';
        $regex   = $project['brand_term_regex'] ?? '';
        $categories = $project['lead_categories'] ?? [];
        $aiEvent = $project['ai_overview_event'] ?? '';
        $days    = (int)(new \DateTime($startDate))->diff(new \DateTime($endDate))->days + 1;

        if (!$ga4Id && !$siteUrl) {
            return ['success' => false, 'message' => 'No GA4 or GSC configured for this project.'];
        }

        $snap = [];

        // â”€â”€ Step 1: GSC Total â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        if ($siteUrl) {
            $gscTotal = $this->fetchGSC($projectId, $siteUrl, $startDate, $endDate);
            $snap['gsc_clicks']      = $gscTotal['clicks']      ?? 0;
            $snap['gsc_impressions'] = $gscTotal['impressions'] ?? 0;
            $snap['gsc_ctr']         = $gscTotal['ctr']         ?? 0;
            $snap['gsc_position']    = $gscTotal['position']    ?? 0;

            // â”€â”€ Step 2: GSC Branded â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
            if ($regex) {
                $gscBranded = $this->fetchGSCBranded($projectId, $siteUrl, $startDate, $endDate, $regex);
                $snap['branded_clicks']          = $gscBranded['clicks']      ?? 0;
                $snap['branded_impressions']     = $gscBranded['impressions'] ?? 0;
                $snap['non_branded_clicks']      = max(0, $snap['gsc_clicks']      - $snap['branded_clicks']);
                $snap['non_branded_impressions'] = max(0, $snap['gsc_impressions'] - $snap['branded_impressions']);
            }
        }

        // â”€â”€ Step 3â€“12: GA4 â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        if ($ga4Id) {
            $startGA4 = "{$days}daysAgo";  // GA4 uses relative dates OR we use absolute

            // Use absolute date range via the service
            $snap = array_merge($snap, $this->fetchGA4AllMetrics(
                $projectId, $ga4Id, $startDate, $endDate, $categories, $aiEvent
            ));
        }

        // â”€â”€ Save snapshot â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        // Strip internal temp keys that are not DB columns
        $eventRows = $snap['_event_rows'] ?? [];
        unset($snap['_event_rows']);

        $snapshotId = $this->snapshots->upsert($projectId, $periodType, $startDate, $endDate, $snap);

        // â”€â”€ Save event drill-down data â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        if (!empty($eventRows)) {
            $this->events->deleteBySnapshot($snapshotId);
            $this->events->insertMany($snapshotId, $projectId, $eventRows);
        }

        // â”€â”€ Step 13: Page data (GA4 + GSC merged) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $pageRows = $this->fetchPageData($projectId, $ga4Id, $siteUrl, $startDate, $endDate);
        if ($pageRows) {
            $this->pages->deleteBySnapshot($snapshotId);
            $this->pages->insertMany($snapshotId, $projectId, $pageRows);
        }

        // â”€â”€ Step 14: Query data â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        if ($siteUrl) {
            $queryRows = $this->fetchQueryData($projectId, $siteUrl, $startDate, $endDate);
            if ($queryRows) {
                $this->queries->deleteBySnapshot($snapshotId);
                $this->queries->insertMany($snapshotId, $projectId, $queryRows);
            }
        }

        return ['success' => true, 'snapshot_id' => $snapshotId, 'message' => 'Fetched successfully.'];
    }

    // â”€â”€ GA4: all metrics in one method â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    private function fetchGA4AllMetrics(int $projectId, string $propertyId, string $start, string $end, array $categories, string $aiEvent): array
    {
        $snap      = [];
        $eventRows = [];

        // Helper: run a single GA4 metric with optional dimension filter
        $run = function (string $metric, array $filters = []) use ($projectId, $propertyId, $start, $end): int|float {
            return $this->runGA4($projectId, $propertyId, $metric, $filters, $start, $end);
        };

        // â”€â”€ Users â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $snap['overall_users'] = $run('totalUsers');
        $snap['new_users']     = $run('newUsers');

        // â”€â”€ Organic engagement â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $organic = [['filter' => ['fieldName' => 'sessionDefaultChannelGrouping', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'Organic Search']]]];
        $snap['organic_sessions']            = $run('sessions',                $organic);
        $snap['organic_engaged_sessions']    = $run('engagedSessions',          $organic);
        $snap['organic_page_views']          = $run('screenPageViews',          $organic);
        $snap['organic_avg_engagement_time'] = $run('averageSessionDuration',   $organic);
        $snap['organic_engagement_rate']     = $run('engagementRate',           $organic);

        // â”€â”€ Channel split â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $snap['paid_sessions']    = $run('sessions', [['filter' => ['fieldName' => 'sessionDefaultChannelGrouping', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'Paid Search']]]]);
        $snap['direct_sessions']  = $run('sessions', [['filter' => ['fieldName' => 'sessionDefaultChannelGrouping', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'Direct']]]]);
        $snap['social_sessions']  = $run('sessions', [['filter' => ['fieldName' => 'sessionDefaultChannelGrouping', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'Organic Social']]]]);
        $snap['referral_sessions']= $run('sessions', [['filter' => ['fieldName' => 'sessionDefaultChannelGrouping', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'Referral']]]]);

        // â”€â”€ Revenue & orders â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $snap['overall_revenue'] = $run('totalRevenue');
        $snap['organic_revenue'] = $run('totalRevenue', [['filter' => ['fieldName' => 'sessionMedium', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'organic']]]]);
        $snap['overall_orders']  = $run('eventCount',   [['filter' => ['fieldName' => 'eventName',    'stringFilter' => ['matchType' => 'EXACT', 'value' => 'purchase']]]]);
        $snap['organic_orders']  = $run('eventCount',   [
            ['filter' => ['fieldName' => 'eventName',    'stringFilter' => ['matchType' => 'EXACT', 'value' => 'purchase']]],
            ['filter' => ['fieldName' => 'sessionMedium','stringFilter' => ['matchType' => 'EXACT', 'value' => 'organic']]],
        ]);

        // â”€â”€ Lead categories (replaces flat Event 1-10) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $categoryTotals = [];
        $totalLeads     = 0;

        foreach ($categories as $cat) {
            $key        = $cat['key']    ?? '';
            $eventNames = $cat['events'] ?? [];
            $catTotal   = 0;

            foreach ($eventNames as $eventName) {
                if (!$eventName) continue;
                $count = $run('eventCount', [
                    ['filter' => ['fieldName' => 'eventName',    'stringFilter' => ['matchType' => 'EXACT', 'value' => $eventName]]],
                    ['filter' => ['fieldName' => 'sessionMedium','stringFilter' => ['matchType' => 'EXACT', 'value' => 'organic']]],
                ]);
                $catTotal += $count;
                $eventRows[] = ['category_key' => $key, 'event_name' => $eventName, 'organic_count' => $count];
            }

            if ($key) {
                $categoryTotals[$key] = $catTotal;
                $totalLeads += $catTotal;
            }
        }

        $snap['lead_category_totals'] = $categoryTotals;
        $snap['total_leads']          = $totalLeads;
        $snap['_event_rows']          = $eventRows;  // stripped before DB insert

        // â”€â”€ AI Overview â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        if ($aiEvent) {
            $snap['ai_overview'] = $run('eventCount', [['filter' => ['fieldName' => 'eventName', 'stringFilter' => ['matchType' => 'EXACT', 'value' => $aiEvent]]]]);
        }

        // â”€â”€ LLM Referrals â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
        $llmTotal = 0;
        foreach (['chatgpt', 'claude', 'perplexity', 'gemini', 'copilot'] as $src) {
            $v = $run('sessions', [['filter' => ['fieldName' => 'sessionSourceMedium', 'stringFilter' => ['matchType' => 'CONTAINS', 'value' => $src]]]]);
            $snap['ref_' . $src] = $v;
            $llmTotal += $v;
        }
        $snap['llm_total'] = $llmTotal;

        return $snap;
    }

    // â”€â”€ GA4 API call (absolute date range) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    private function runGA4(int $projectId, string $propertyId, string $metric, array $filters, string $start, string $end): int|float
    {
        $token = $this->oauth->getAccessToken($projectId, 'ga4');
        if (!$token) return 0;

        $payload = [
            'dateRanges' => [['startDate' => $start, 'endDate' => $end]],
            'metrics'    => [['name' => $metric]],
        ];

        if (!empty($filters)) {
            $payload['dimensionFilter'] = ['andGroup' => ['expressions' => $filters]];
        }

        $url  = 'https://analyticsdata.googleapis.com/v1beta/properties/' . $propertyId . ':runReport';
        $ctx  = stream_context_create(['http' => ['timeout' => 12, 
            'method'        => 'POST',
            'header'        => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n",
            'content'       => json_encode($payload),
            'ignore_errors' => true,
        ]]);
        $body = @file_get_contents($url, false, $ctx);
        $data = $body ? json_decode($body, true) : [];

        $value = $data['rows'][0]['metricValues'][0]['value'] ?? 0;
        return is_numeric($value) ? (str_contains($metric, 'Rate') || str_contains($metric, 'Duration') ? (float)$value : (int)$value) : 0;
    }

    // â”€â”€ GSC API calls â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    private function fetchGSC(int $projectId, string $siteUrl, string $start, string $end): array
    {
        $token = $this->oauth->getAccessToken($projectId, 'gsc');
        if (!$token) return ['clicks' => 0, 'impressions' => 0, 'ctr' => 0, 'position' => 0];

        $data = $this->gscQuery($token, $siteUrl, ['startDate' => $start, 'endDate' => $end, 'rowLimit' => 1]);
        $row  = $data['rows'][0] ?? [];
        return [
            'clicks'      => (int)($row['clicks']      ?? 0),
            'impressions' => (int)($row['impressions'] ?? 0),
            'ctr'         => round((float)($row['ctr']      ?? 0), 8),
            'position'    => round((float)($row['position'] ?? 0), 2),
        ];
    }

    private function fetchGSCBranded(int $projectId, string $siteUrl, string $start, string $end, string $regex): array
    {
        $token = $this->oauth->getAccessToken($projectId, 'gsc');
        if (!$token) return ['clicks' => 0, 'impressions' => 0];

        $data = $this->gscQuery($token, $siteUrl, [
            'startDate' => $start,
            'endDate'   => $end,
            'rowLimit'  => 25000,
            'dimensionFilterGroups' => [[
                'filters' => [['dimension' => 'query', 'operator' => 'INCLUDING_REGEX', 'expression' => $regex]],
            ]],
        ]);

        $clicks = $impressions = 0;
        foreach ($data['rows'] ?? [] as $row) {
            $clicks      += (int)$row['clicks'];
            $impressions += (int)$row['impressions'];
        }
        return ['clicks' => $clicks, 'impressions' => $impressions];
    }

    private function fetchQueryData(int $projectId, string $siteUrl, string $start, string $end): array
    {
        $token = $this->oauth->getAccessToken($projectId, 'gsc');
        if (!$token) return [];

        $data = $this->gscQuery($token, $siteUrl, [
            'startDate'  => $start,
            'endDate'    => $end,
            'dimensions' => ['query'],
            'rowLimit'   => 100,
        ]);

        $rows = [];
        foreach ($data['rows'] ?? [] as $row) {
            $rows[] = [
                'query'       => $row['keys'][0] ?? '',
                'clicks'      => (int)$row['clicks'],
                'impressions' => (int)$row['impressions'],
                'ctr'         => round((float)$row['ctr'], 8),
                'position'    => round((float)$row['position'], 2),
            ];
        }
        return $rows;
    }

    private function gscQuery(string $token, string $siteUrl, array $payload): array
    {
        $url = 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($siteUrl) . '/searchAnalytics/query';
        $ctx = stream_context_create(['http' => ['timeout' => 12, 
            'method'        => 'POST',
            'header'        => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n",
            'content'       => json_encode($payload),
            'ignore_errors' => true,
        ]]);
        $body = @file_get_contents($url, false, $ctx);
        return $body ? (json_decode($body, true) ?? []) : [];
    }

    // â”€â”€ Trend data (day-by-day for charts) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

    /**
     * Fetch time-series data for a metric over a period + matching previous period.
     * groupBy: 'day' | 'week' | 'month'
     * metric:  'sessions' | 'clicks' | 'impressions' | 'users' | 'leads'
     */
    public function fetchTrend(int $projectId, array $project, string $start, string $end, string $metric, string $groupBy = 'day'): array
    {
        $ga4Id   = $project['ga4_property_id'] ?? '';
        $siteUrl = $project['gsc_site_url']    ?? '';

        // Build the same-length previous period
        $days      = max(1, (int)(new \DateTime($start))->diff(new \DateTime($end))->days + 1);
        $prevEnd   = date('Y-m-d', strtotime($start . ' -1 day'));
        $prevStart = date('Y-m-d', strtotime($start . " -{$days} days"));

        $organic = [['filter' => ['fieldName' => 'sessionDefaultChannelGrouping', 'stringFilter' => ['matchType' => 'EXACT', 'value' => 'Organic Search']]]];

        switch ($metric) {
            case 'sessions':
                $curr = $ga4Id ? $this->fetchGA4Daily($projectId, $ga4Id, 'sessions', $start, $end, $organic) : [];
                $prev = $ga4Id ? $this->fetchGA4Daily($projectId, $ga4Id, 'sessions', $prevStart, $prevEnd, $organic) : [];
                break;
            case 'users':
                $curr = $ga4Id ? $this->fetchGA4Daily($projectId, $ga4Id, 'totalUsers', $start, $end) : [];
                $prev = $ga4Id ? $this->fetchGA4Daily($projectId, $ga4Id, 'totalUsers', $prevStart, $prevEnd) : [];
                break;
            case 'clicks':
                $curr = $siteUrl ? $this->fetchGSCDaily($projectId, $siteUrl, 'clicks', $start, $end) : [];
                $prev = $siteUrl ? $this->fetchGSCDaily($projectId, $siteUrl, 'clicks', $prevStart, $prevEnd) : [];
                break;
            case 'impressions':
                $curr = $siteUrl ? $this->fetchGSCDaily($projectId, $siteUrl, 'impressions', $start, $end) : [];
                $prev = $siteUrl ? $this->fetchGSCDaily($projectId, $siteUrl, 'impressions', $prevStart, $prevEnd) : [];
                break;
            case 'leads':
                $categories = $project['lead_categories'] ?? [];
                $allEvents  = [];
                foreach ($categories as $cat) {
                    foreach ($cat['events'] ?? [] as $ev) {
                        if ($ev) $allEvents[] = $ev;
                    }
                }
                $curr = $ga4Id ? $this->fetchGA4DailyLeads($projectId, $ga4Id, $allEvents, $start, $end) : [];
                $prev = $ga4Id ? $this->fetchGA4DailyLeads($projectId, $ga4Id, $allEvents, $prevStart, $prevEnd) : [];
                break;
            default:
                return ['error' => 'Unknown metric'];
        }

        // Build full date ranges (fill zeros for missing dates)
        [$currDates, $currVals] = $this->fillDateRange($curr, $start, $end);
        [$prevDates, $prevVals] = $this->fillDateRange($prev, $prevStart, $prevEnd);

        // Apply groupBy
        if ($groupBy === 'week') {
            [$currDates, $currVals] = $this->groupByWeek($currDates, $currVals);
            [$prevDates, $prevVals] = $this->groupByWeek($prevDates, $prevVals);
        } elseif ($groupBy === 'month') {
            [$currDates, $currVals] = $this->groupByMonth($currDates, $currVals);
            [$prevDates, $prevVals] = $this->groupByMonth($prevDates, $prevVals);
        }

        return [
            'labels'      => array_map(fn($d) => date($groupBy === 'month' ? 'M Y' : 'd M', strtotime($d)), $currDates),
            'prev_labels' => array_map(fn($d) => date($groupBy === 'month' ? 'M Y' : 'd M', strtotime($d)), $prevDates),
            'current'     => $currVals,
            'previous'    => $prevVals,
            'period_label'=> date('d M', strtotime($start)) . ' â€“ ' . date('d M', strtotime($end)),
            'prev_period_label' => date('d M', strtotime($prevStart)) . ' â€“ ' . date('d M', strtotime($prevEnd)),
        ];
    }

    private function fetchGA4Daily(int $projectId, string $propertyId, string $metric, string $start, string $end, array $filters = []): array
    {
        $token = $this->oauth->getAccessToken($projectId, 'ga4');
        if (!$token) return [];

        $payload = [
            'dateRanges' => [['startDate' => $start, 'endDate' => $end]],
            'dimensions' => [['name' => 'date']],
            'metrics'    => [['name' => $metric]],
            'orderBys'   => [['dimension' => ['dimensionName' => 'date'], 'desc' => false]],
            'limit'      => 400,
        ];
        if ($filters) {
            $payload['dimensionFilter'] = ['andGroup' => ['expressions' => $filters]];
        }

        $url  = 'https://analyticsdata.googleapis.com/v1beta/properties/' . $propertyId . ':runReport';
        $ctx  = stream_context_create(['http' => ['timeout' => 12, 'method' => 'POST', 'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n", 'content' => json_encode($payload), 'ignore_errors' => true]]);
        $data = json_decode(@file_get_contents($url, false, $ctx) ?: '{}', true);

        $result = [];
        foreach ($data['rows'] ?? [] as $row) {
            $d = $row['dimensionValues'][0]['value'] ?? '';
            if (strlen($d) === 8) {
                $date = substr($d, 0, 4) . '-' . substr($d, 4, 2) . '-' . substr($d, 6, 2);
                $result[$date] = (float)($row['metricValues'][0]['value'] ?? 0);
            }
        }
        return $result;
    }

    private function fetchGA4DailyLeads(int $projectId, string $propertyId, array $eventNames, string $start, string $end): array
    {
        if (empty($eventNames)) return [];
        $token = $this->oauth->getAccessToken($projectId, 'ga4');
        if (!$token) return [];

        $orExpressions = array_map(fn($ev) => ['filter' => ['fieldName' => 'eventName', 'stringFilter' => ['matchType' => 'EXACT', 'value' => $ev]]], $eventNames);

        $payload = [
            'dateRanges'      => [['startDate' => $start, 'endDate' => $end]],
            'dimensions'      => [['name' => 'date']],
            'metrics'         => [['name' => 'eventCount']],
            'dimensionFilter' => ['orGroup' => ['expressions' => $orExpressions]],
            'orderBys'        => [['dimension' => ['dimensionName' => 'date'], 'desc' => false]],
            'limit'           => 400,
        ];

        $url  = 'https://analyticsdata.googleapis.com/v1beta/properties/' . $propertyId . ':runReport';
        $ctx  = stream_context_create(['http' => ['timeout' => 12, 'method' => 'POST', 'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n", 'content' => json_encode($payload), 'ignore_errors' => true]]);
        $data = json_decode(@file_get_contents($url, false, $ctx) ?: '{}', true);

        $result = [];
        foreach ($data['rows'] ?? [] as $row) {
            $d = $row['dimensionValues'][0]['value'] ?? '';
            if (strlen($d) === 8) {
                $date = substr($d, 0, 4) . '-' . substr($d, 4, 2) . '-' . substr($d, 6, 2);
                $result[$date] = (float)($row['metricValues'][0]['value'] ?? 0);
            }
        }
        return $result;
    }

    private function fetchGSCDaily(int $projectId, string $siteUrl, string $field, string $start, string $end): array
    {
        $token = $this->oauth->getAccessToken($projectId, 'gsc');
        if (!$token) return [];

        $data   = $this->gscQuery($token, $siteUrl, ['startDate' => $start, 'endDate' => $end, 'dimensions' => ['date'], 'rowLimit' => 500]);
        $result = [];
        foreach ($data['rows'] ?? [] as $row) {
            $result[$row['keys'][0]] = (float)($row[$field] ?? 0);
        }
        return $result;
    }

    private function fillDateRange(array $map, string $start, string $end): array
    {
        $dates = [];
        $d     = new \DateTime($start);
        $last  = new \DateTime($end);
        while ($d <= $last) {
            $dates[] = $d->format('Y-m-d');
            $d->modify('+1 day');
        }
        $values = array_map(fn($date) => $map[$date] ?? 0, $dates);
        return [$dates, $values];
    }

    private function groupByWeek(array $dates, array $values): array
    {
        $buckets = [];
        foreach ($dates as $i => $date) {
            $key = date('o-W', strtotime($date)); // ISO year-week
            if (!isset($buckets[$key])) $buckets[$key] = ['date' => $date, 'value' => 0];
            $buckets[$key]['value'] += $values[$i];
        }
        return [array_column(array_values($buckets), 'date'), array_column(array_values($buckets), 'value')];
    }

    private function groupByMonth(array $dates, array $values): array
    {
        $buckets = [];
        foreach ($dates as $i => $date) {
            $key = date('Y-m', strtotime($date));
            if (!isset($buckets[$key])) $buckets[$key] = ['date' => $date . '-01', 'value' => 0];
            // Keep earliest date in month as label anchor
            if ($date < $buckets[$key]['date']) $buckets[$key]['date'] = $date;
            $buckets[$key]['value'] += $values[$i];
        }
        return [array_column(array_values($buckets), 'date'), array_column(array_values($buckets), 'value')];
    }

    // â”€â”€ Page data (GA4 top pages + GSC page performance, merged by path) â”€â”€â”€â”€â”€

    private function fetchPageData(int $projectId, string $ga4Id, string $siteUrl, string $start, string $end): array
    {
        $ga4Pages = [];
        $gscIndex = [];

        if ($ga4Id) {
            $token = $this->oauth->getAccessToken($projectId, 'ga4');
            if ($token) {
                $payload = [
                    'dateRanges'  => [['startDate' => $start, 'endDate' => $end]],
                    'dimensions'  => [['name' => 'pagePath']],
                    'metrics'     => [['name' => 'sessions'], ['name' => 'screenPageViews']],
                    'limit'       => 100,
                    'orderBys'    => [['metric' => ['metricName' => 'sessions'], 'desc' => true]],
                ];
                $url  = 'https://analyticsdata.googleapis.com/v1beta/properties/' . $ga4Id . ':runReport';
                $ctx  = stream_context_create(['http' => ['timeout' => 12, 'method' => 'POST', 'header' => "Authorization: Bearer {$token}\r\nContent-Type: application/json\r\n", 'content' => json_encode($payload), 'ignore_errors' => true]]);
                $body = @file_get_contents($url, false, $ctx);
                $data = $body ? json_decode($body, true) : [];
                foreach ($data['rows'] ?? [] as $row) {
                    $path = $row['dimensionValues'][0]['value'] ?? '';
                    $ga4Pages[$path] = ['ga4_sessions' => (int)$row['metricValues'][0]['value'], 'ga4_page_views' => (int)$row['metricValues'][1]['value']];
                }
            }
        }

        if ($siteUrl) {
            $token = $this->oauth->getAccessToken($projectId, 'gsc');
            if ($token) {
                $data = $this->gscQuery($token, $siteUrl, ['startDate' => $start, 'endDate' => $end, 'dimensions' => ['page'], 'rowLimit' => 100]);
                foreach ($data['rows'] ?? [] as $row) {
                    $url = $row['keys'][0] ?? '';
                    $path = parse_url($url, PHP_URL_PATH) ?: $url;
                    $gscIndex[$path] = ['gsc_clicks' => (int)$row['clicks'], 'gsc_impressions' => (int)$row['impressions'], 'gsc_ctr' => round((float)$row['ctr'], 8), 'gsc_position' => round((float)$row['position'], 2)];
                }
            }
        }

        // Merge GA4 + GSC by path
        $allPaths = array_unique(array_merge(array_keys($ga4Pages), array_keys($gscIndex)));
        $rows = [];
        foreach ($allPaths as $path) {
            $rows[] = array_merge(
                ['url' => $path, 'ga4_sessions' => 0, 'ga4_page_views' => 0, 'gsc_clicks' => 0, 'gsc_impressions' => 0, 'gsc_ctr' => 0, 'gsc_position' => 0],
                $ga4Pages[$path] ?? [],
                $gscIndex[$path] ?? []
            );
        }
        usort($rows, fn($a, $b) => $b['ga4_sessions'] <=> $a['ga4_sessions']);
        return array_slice($rows, 0, 200);
    }
}

