<?php

declare(strict_types=1);

namespace SeoScope\Services;

use RuntimeException;
use SeoScope\Integrations\GoogleSearchConsoleClient;
use SeoScope\Integrations\GoogleTokenManager;
use SeoScope\Repositories\GooglePropertyRepository;
use SeoScope\Repositories\GscDailyPageRepository;
use SeoScope\Repositories\GscDailyPropertyCountryRepository;
use SeoScope\Repositories\GscDailyPropertyDeviceRepository;
use SeoScope\Repositories\GscDailyQueryRepository;
use SeoScope\Repositories\GscDailyQueryPageRepository;
use SeoScope\Repositories\SettingsRepository;
use SeoScope\Repositories\SyncLogRepository;
use Throwable;

final class GscSyncService
{
    private const MAX_DAYS_PER_RUN = 7;
    // Device/Country are now stored only at property-level for home widgets, not for query/page facts.

    public function __construct(
        private SettingsRepository $settings,
        private GooglePropertyRepository $props,
        private GscDailyQueryRepository $dailyQuery,
        private GscDailyPageRepository $dailyPage,
        private ?GscDailyPropertyDeviceRepository $dailyPropDevice = null,
        private ?GscDailyPropertyCountryRepository $dailyPropCountry = null,
        private ?GscDailyQueryPageRepository $dailyQueryPage = null,
        private ?SyncLogRepository $logs = null,
        private ?int $runId = null,
        private ?GscAggregationService $agg = null
    ) {}

    /**
     * @return array{
     *   properties_processed:int,
     *   properties_failed:int,
     *   days_processed:int,
     *   query_rows_upserted:int,
     *   page_rows_upserted:int
     * }
     */
    public function runOnce(): array
    {
        $clientId = trim((string) ($this->settings->get('google.client_id') ?? ''));
        $clientSecret = trim((string) ($this->settings->getSecret('google.client_secret') ?? ''));
        if ($clientId === '' || $clientSecret === '') {
            throw new RuntimeException('Missing Google OAuth client credentials.');
        }

        $mode = $this->settings->get('gsc.sync.mode') ?? 'last_30_days';
        $rowLimit = (int) ($this->settings->get('gsc.sync.row_limit') ?? '25000');
        $rowLimit = max(1, min(25000, $rowLimit));
        $sleepMs = (int) ($this->settings->get('gsc.sync.sleep_ms') ?? '100');
        $sleepMs = max(0, min(2000, $sleepMs));

        $endDt = new \DateTimeImmutable('yesterday');
        $end = $endDt->format('Y-m-d');
        $start = match ($mode) {
            'last_3_months' => $endDt->modify('-3 months')->format('Y-m-d'),
            'last_12_months' => $endDt->modify('-12 months')->format('Y-m-d'),
            'last_16_months' => $endDt->modify('-16 months')->format('Y-m-d'),
            default => $endDt->modify('-29 days')->format('Y-m-d'), // last_30_days
        };

        $allProps = $this->props->all();
        $enabledProps = array_values(array_filter($allProps, static fn ($p) => is_array($p) && !empty($p['is_enabled'])));
        if ($enabledProps === []) {
            throw new RuntimeException('No enabled properties. Enable at least one property before syncing.');
        }

        $totProps = 0;
        $totFailed = 0;
        $totDays = 0;
        $totQ = 0;
        $totP = 0;
        $totQP = 0;

        foreach ($enabledProps as $p) {
            $siteUrl = (string) ($p['site_url'] ?? '');
            $propertyId = (int) ($p['id'] ?? 0);
            if ($siteUrl === '') {
                continue;
            }

            $cursor = (string) (($p['last_sync_date'] ?? '') ?: '');
            $siteStart = $start;
            if ($cursor !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $cursor)) {
                $next = date('Y-m-d', strtotime($cursor . ' +1 day'));
                if ($next > $siteStart) {
                    $siteStart = $next;
                }
            }
            if ($siteStart > $end) {
                continue;
            }

            $days = $this->dateRange($siteStart, $end, self::MAX_DAYS_PER_RUN);
            if ($days === []) {
                continue;
            }

            $totProps++;
            $lastOkDate = null;
            $hadError = false;
            $itemId = null;
            $itemFrom = $days[0];
            $itemTo = $days[count($days) - 1];
            $itemDays = 0;
            $itemQ = 0;
            $itemP = 0;
            $itemQP = 0;
            if ($this->logs !== null && $this->runId !== null) {
                $itemId = $this->logs->startItem($this->runId, $propertyId > 0 ? $propertyId : null, $siteUrl, $itemFrom, $itemTo);
            }

            foreach ($days as $d) {
                try {
                    $tokens = GoogleTokenManager::ensureFreshTokens($this->settings, $clientId, $clientSecret);
                    $accessToken = (string) ($tokens['access_token'] ?? '');

                    $qRows = GoogleSearchConsoleClient::searchAnalyticsAllRows(
                        $siteUrl,
                        $accessToken,
                        $d,
                        $d,
                        ['query'],
                        $rowLimit
                    );
                    $pRows = GoogleSearchConsoleClient::searchAnalyticsAllRows(
                        $siteUrl,
                        $accessToken,
                        $d,
                        $d,
                        ['page'],
                        $rowLimit
                    );
                    $dateRows = GoogleSearchConsoleClient::searchAnalyticsAllRows(
                        $siteUrl,
                        $accessToken,
                        $d,
                        $d,
                        ['date'],
                        min(25, $rowLimit)
                    );
                    // Property-level breakdowns used only by home widgets (no query/page granularity).
                    $devRows = $this->dailyPropDevice !== null ? GoogleSearchConsoleClient::searchAnalyticsAllRows(
                        $siteUrl,
                        $accessToken,
                        $d,
                        $d,
                        ['device'],
                        $rowLimit
                    ) : [];
                    $ctrRows = $this->dailyPropCountry !== null ? GoogleSearchConsoleClient::searchAnalyticsAllRows(
                        $siteUrl,
                        $accessToken,
                        $d,
                        $d,
                        ['country'],
                        $rowLimit
                    ) : [];
                    $qpRows = [];
                    if ($this->dailyQueryPage !== null) {
                        $qpRows = GoogleSearchConsoleClient::searchAnalyticsAllRows(
                            $siteUrl,
                            $accessToken,
                            $d,
                            $d,
                            ['query', 'page'],
                            $rowLimit
                        );
                    }

                    $qMapped = [];
                    foreach ($qRows as $r) {
                        $key = (string) (($r['keys'][0] ?? '') ?: '');
                        if ($key === '') {
                            continue;
                        }
                        $qMapped[] = [
                            'query' => $key,
                            'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                            'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                            'ctr' => (float) ($r['ctr'] ?? 0),
                            'position' => (float) ($r['position'] ?? 0),
                        ];
                    }
                    $pMapped = [];
                    foreach ($pRows as $r) {
                        $key = (string) (($r['keys'][0] ?? '') ?: '');
                        if ($key === '') {
                            continue;
                        }
                        $pMapped[] = [
                            'page' => $key,
                            'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                            'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                            'ctr' => (float) ($r['ctr'] ?? 0),
                            'position' => (float) ($r['position'] ?? 0),
                        ];
                    }
                    $devMapped = [];
                    foreach ($devRows as $r) {
                        $dev = (string) (($r['keys'][0] ?? '') ?: '');
                        if ($dev === '') {
                            continue;
                        }
                        $devMapped[] = [
                            'device' => strtolower($dev),
                            'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                            'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                            'ctr' => (float) ($r['ctr'] ?? 0),
                            'position' => (float) ($r['position'] ?? 0),
                        ];
                    }
                    $ctrMapped = [];
                    foreach ($ctrRows as $r) {
                        $ctr = (string) (($r['keys'][0] ?? '') ?: '');
                        if ($ctr === '') {
                            continue;
                        }
                        $ctrMapped[] = [
                            'country' => strtolower($ctr),
                            'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                            'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                            'ctr' => (float) ($r['ctr'] ?? 0),
                            'position' => (float) ($r['position'] ?? 0),
                        ];
                    }
                    $qpMapped = [];
                    foreach ($qpRows as $r) {
                        $q = (string) (($r['keys'][0] ?? '') ?: '');
                        $pg = (string) (($r['keys'][1] ?? '') ?: '');
                        if ($q === '' || $pg === '') {
                            continue;
                        }
                        $qpMapped[] = [
                            'query' => $q,
                            'page' => $pg,
                            'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                            'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                            'ctr' => (float) ($r['ctr'] ?? 0),
                            'position' => (float) ($r['position'] ?? 0),
                        ];
                    }

                    $qUp = $this->dailyQuery->upsertMany($propertyId, $siteUrl, $d, $qMapped, 'all', 'all');
                    $pUp = $this->dailyPage->upsertMany($propertyId, $siteUrl, $d, $pMapped, 'all', 'all');
                    $qpUp = $this->dailyQueryPage !== null ? $this->dailyQueryPage->upsertMany($propertyId, $siteUrl, $d, $qpMapped, 'all', 'all') : 0;
                    $devUp = $this->dailyPropDevice !== null ? $this->dailyPropDevice->upsertMany($propertyId, $siteUrl, $d, $devMapped) : 0;
                    $ctrUp = $this->dailyPropCountry !== null ? $this->dailyPropCountry->upsertMany($propertyId, $siteUrl, $d, $ctrMapped) : 0;
                    $totQ += $qUp;
                    $totP += $pUp;
                    $totQP += $qpUp;
                    $itemQ += $qUp;
                    $itemP += $pUp;
                    $itemQP += $qpUp;
                    $totDays++;
                    $itemDays++;
                    $lastOkDate = $d;

                    if ($this->agg !== null) {
                        $this->applyDailyPropertyAggFromDateRows($this->agg, $propertyId, $siteUrl, $d, $dateRows);
                    }
                    if ($sleepMs > 0) {
                        usleep($sleepMs * 1000);
                    }
                } catch (Throwable $e) {
                    $hadError = true;
                    $this->props->updateSyncMeta($siteUrl, $lastOkDate, mb_substr($e->getMessage(), 0, 250));
                    if ($this->logs !== null && $itemId !== null) {
                        $this->logs->finishItem($itemId, 'error', $itemDays, $itemQ, $itemP, mb_substr($e->getMessage(), 0, 250));
                    }
                    // Stop processing further days for this property, continue with the next one.
                    break;
                }
            }

            if ($hadError) {
                $totFailed++;
                continue;
            }
            $this->props->updateSyncMeta($siteUrl, $lastOkDate, null);
            if ($this->logs !== null && $itemId !== null) {
                $this->logs->finishItem($itemId, 'ok', $itemDays, $itemQ, $itemP, null);
            }
        }

        return [
            'properties_processed' => $totProps,
            'properties_failed' => $totFailed,
            'days_processed' => $totDays,
            'query_rows_upserted' => $totQ,
            'page_rows_upserted' => $totP,
        ];
    }

    /**
     * Sync a single property for an explicit date range (used by queue runner).
     *
     * @return array{
     *   days_processed:int,
     *   query_rows_upserted:int,
     *   page_rows_upserted:int,
     *   last_ok_date:?string,
     *   error:?string
     * }
     */
    public function runPropertyRange(int $propertyId, string $siteUrl, string $start, string $end, ?int $maxDays = null, bool $includeQueryPage = true): array
    {
        $siteUrl = trim($siteUrl);
        if ($propertyId < 1 || $siteUrl === '') {
            return ['days_processed' => 0, 'query_rows_upserted' => 0, 'page_rows_upserted' => 0, 'last_ok_date' => null, 'error' => 'invalid_property'];
        }
        if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $start) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) {
            return ['days_processed' => 0, 'query_rows_upserted' => 0, 'page_rows_upserted' => 0, 'last_ok_date' => null, 'error' => 'invalid_date'];
        }
        if ($start > $end) {
            return ['days_processed' => 0, 'query_rows_upserted' => 0, 'page_rows_upserted' => 0, 'last_ok_date' => null, 'error' => 'empty_range'];
        }

        $clientId = trim((string) ($this->settings->get('google.client_id') ?? ''));
        $clientSecret = trim((string) ($this->settings->getSecret('google.client_secret') ?? ''));
        if ($clientId === '' || $clientSecret === '') {
            throw new RuntimeException('Missing Google OAuth client credentials.');
        }

        $rowLimit = (int) ($this->settings->get('gsc.sync.row_limit') ?? '25000');
        $rowLimit = max(1, min(25000, $rowLimit));
        $sleepMs = (int) ($this->settings->get('gsc.sync.sleep_ms') ?? '100');
        $sleepMs = max(0, min(2000, $sleepMs));

        $limitDays = $maxDays !== null ? max(1, min(31, $maxDays)) : self::MAX_DAYS_PER_RUN;
        $days = $this->dateRange($start, $end, $limitDays);
        if ($days === []) {
            return ['days_processed' => 0, 'query_rows_upserted' => 0, 'page_rows_upserted' => 0, 'last_ok_date' => null, 'error' => 'empty_range'];
        }

        $lastOkDate = null;
        $totDays = 0;
        $totQ = 0;
        $totP = 0;

        foreach ($days as $d) {
            try {
                $tokens = GoogleTokenManager::ensureFreshTokens($this->settings, $clientId, $clientSecret);
                $accessToken = (string) ($tokens['access_token'] ?? '');

                $qRows = GoogleSearchConsoleClient::searchAnalyticsAllRows($siteUrl, $accessToken, $d, $d, ['query'], $rowLimit);
                $pRows = GoogleSearchConsoleClient::searchAnalyticsAllRows($siteUrl, $accessToken, $d, $d, ['page'], $rowLimit);
                $dateRows = GoogleSearchConsoleClient::searchAnalyticsAllRows(
                    $siteUrl,
                    $accessToken,
                    $d,
                    $d,
                    ['date'],
                    min(25, $rowLimit)
                );
                $devRows = $this->dailyPropDevice !== null ? GoogleSearchConsoleClient::searchAnalyticsAllRows($siteUrl, $accessToken, $d, $d, ['device'], $rowLimit) : [];
                $ctrRows = $this->dailyPropCountry !== null ? GoogleSearchConsoleClient::searchAnalyticsAllRows($siteUrl, $accessToken, $d, $d, ['country'], $rowLimit) : [];

                $qpRows = [];
                if ($includeQueryPage && $this->dailyQueryPage !== null) {
                    $qpRows = GoogleSearchConsoleClient::searchAnalyticsAllRows($siteUrl, $accessToken, $d, $d, ['query', 'page'], $rowLimit);
                }
                // No query/page breakdown ingestion (device/country) to keep DB slim.

                $qMapped = [];
                foreach ($qRows as $r) {
                    $key = (string) (($r['keys'][0] ?? '') ?: '');
                    if ($key === '') {
                        continue;
                    }
                    $qMapped[] = [
                        'query' => $key,
                        'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                        'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                        'ctr' => (float) ($r['ctr'] ?? 0),
                        'position' => (float) ($r['position'] ?? 0),
                    ];
                }
                $pMapped = [];
                foreach ($pRows as $r) {
                    $key = (string) (($r['keys'][0] ?? '') ?: '');
                    if ($key === '') {
                        continue;
                    }
                    $pMapped[] = [
                        'page' => $key,
                        'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                        'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                        'ctr' => (float) ($r['ctr'] ?? 0),
                        'position' => (float) ($r['position'] ?? 0),
                    ];
                }
                $devMapped = [];
                foreach ($devRows as $r) {
                    $dev = (string) (($r['keys'][0] ?? '') ?: '');
                    if ($dev === '') continue;
                    $devMapped[] = [
                        'device' => strtolower($dev),
                        'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                        'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                        'ctr' => (float) ($r['ctr'] ?? 0),
                        'position' => (float) ($r['position'] ?? 0),
                    ];
                }
                $ctrMapped = [];
                foreach ($ctrRows as $r) {
                    $ctr = (string) (($r['keys'][0] ?? '') ?: '');
                    if ($ctr === '') continue;
                    $ctrMapped[] = [
                        'country' => strtolower($ctr),
                        'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                        'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                        'ctr' => (float) ($r['ctr'] ?? 0),
                        'position' => (float) ($r['position'] ?? 0),
                    ];
                }
                $qpMapped = [];
                foreach ($qpRows as $r) {
                    $q = (string) (($r['keys'][0] ?? '') ?: '');
                    $pg = (string) (($r['keys'][1] ?? '') ?: '');
                    if ($q === '' || $pg === '') {
                        continue;
                    }
                    $qpMapped[] = [
                        'query' => $q,
                        'page' => $pg,
                        'clicks' => (int) round((float) ($r['clicks'] ?? 0)),
                        'impressions' => (int) round((float) ($r['impressions'] ?? 0)),
                        'ctr' => (float) ($r['ctr'] ?? 0),
                        'position' => (float) ($r['position'] ?? 0),
                    ];
                }
                $qUp = $this->dailyQuery->upsertMany($propertyId, $siteUrl, $d, $qMapped, 'all', 'all');
                $pUp = $this->dailyPage->upsertMany($propertyId, $siteUrl, $d, $pMapped, 'all', 'all');
                $qpUp = ($includeQueryPage && $this->dailyQueryPage !== null) ? $this->dailyQueryPage->upsertMany($propertyId, $siteUrl, $d, $qpMapped, 'all', 'all') : 0;
                $devUp = $this->dailyPropDevice !== null ? $this->dailyPropDevice->upsertMany($propertyId, $siteUrl, $d, $devMapped) : 0;
                $ctrUp = $this->dailyPropCountry !== null ? $this->dailyPropCountry->upsertMany($propertyId, $siteUrl, $d, $ctrMapped) : 0;

                $totQ += $qUp + $qpUp;
                $totP += $pUp;
                $totDays++;
                $lastOkDate = $d;

                if ($this->agg !== null) {
                    $this->applyDailyPropertyAggFromDateRows($this->agg, $propertyId, $siteUrl, $d, $dateRows);
                }
                if ($sleepMs > 0) {
                    usleep($sleepMs * 1000);
                }
            } catch (Throwable $e) {
                $msg = mb_substr($e->getMessage(), 0, 250);
                $this->props->updateSyncMeta($siteUrl, $lastOkDate, $msg);
                return [
                    'days_processed' => $totDays,
                    'query_rows_upserted' => $totQ,
                    'page_rows_upserted' => $totP,
                    'last_ok_date' => $lastOkDate,
                    'error' => $msg,
                ];
            }
        }

        $this->props->updateSyncMeta($siteUrl, $lastOkDate, null);
        return [
            'days_processed' => $totDays,
            'query_rows_upserted' => $totQ,
            'page_rows_upserted' => $totP,
            'last_ok_date' => $lastOkDate,
            'error' => null,
        ];
    }

    /**
     * @param list<array{keys:list<string>,clicks:float|int,impressions:float|int,ctr:float|int,position:float|int}> $dateRows
     */
    private function applyDailyPropertyAggFromDateRows(
        GscAggregationService $agg,
        int $propertyId,
        string $siteUrl,
        string $d,
        array $dateRows
    ): void {
        if ($dateRows === []) {
            $agg->upsertDailyPropertyAggFromDateApiRow($propertyId, $siteUrl, $d, [
                'keys' => [$d],
                'clicks' => 0,
                'impressions' => 0,
                'ctr' => 0.0,
                'position' => 0.0,
            ]);
            return;
        }
        $r = $dateRows[0];
        foreach ($dateRows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $k0 = (string) (($row['keys'][0] ?? '') ?: '');
            if ($k0 === $d) {
                $r = $row;
                break;
            }
        }
        $agg->upsertDailyPropertyAggFromDateApiRow($propertyId, $siteUrl, $d, $r);
    }

    /**
     * @return list<string>
     */
    private function dateRange(string $start, string $end, int $limitDays): array
    {
        $out = [];
        $cur = $start;
        while ($cur <= $end) {
            $out[] = $cur;
            if (count($out) >= $limitDays) {
                break;
            }
            $cur = date('Y-m-d', strtotime($cur . ' +1 day'));
        }
        return $out;
    }
}

