<?php
declare(strict_types=1);

namespace App\Support;

use DateTimeImmutable;
use DateTimeZone;
use PDO;
use RuntimeException;
use Throwable;

final class GoogleCalendar
{
    private const SCOPE = 'https://www.googleapis.com/auth/calendar';
    private const API_ROOT = 'https://www.googleapis.com/calendar/v3';

    private ?string $accessToken = null;
    private array $credential = [];

    public function __construct(
        private readonly PDO $db,
        private readonly int $ownerUserId
    ) {
        if ($ownerUserId <= 0) {
            throw new RuntimeException('Google Calendar istifadəçisi müəyyən edilmədi.');
        }
    }

    public function settings(): array
    {
        $statement = $this->db->prepare(
            'SELECT * FROM erp_google_calendar_settings WHERE owner_user_id=? LIMIT 1'
        );
        $statement->execute([$this->ownerUserId]);
        $settings = $statement->fetch(PDO::FETCH_ASSOC) ?: [];
        $credentialEmail = '';
        if (is_file($this->credentialPath())) {
            $storedCredential = json_decode((string)file_get_contents($this->credentialPath()), true);
            if (is_array($storedCredential)) {
                $credentialEmail = trim((string)($storedCredential['client_email'] ?? ''));
            }
        }

        return array_merge([
            'owner_user_id' => $this->ownerUserId,
            'aktiv' => 0,
            'calendar_id' => '',
            'iclas_sync' => 1,
            'task_sync' => 1,
            'service_account_email' => '',
            'son_yoxlama' => null,
            'son_sinxron' => null,
            'son_google_pull' => null,
            'son_xeta' => '',
            'credential_configured' => is_file($this->credentialPath()),
        ], $settings, [
            'credential_configured' => is_file($this->credentialPath()),
            'service_account_email' => trim((string)($settings['service_account_email'] ?? '')) !== ''
                ? trim((string)$settings['service_account_email'])
                : $credentialEmail,
        ]);
    }

    public function saveSettings(array $input): array
    {
        $calendarId = trim((string)($input['calendar_id'] ?? ''));
        $active = !empty($input['aktiv']) ? 1 : 0;
        $syncHearings = !empty($input['iclas_sync']) ? 1 : 0;
        $syncTasks = !empty($input['task_sync']) ? 1 : 0;
        $credentialJson = trim((string)($input['service_account_json'] ?? ''));
        $removeCredential = !empty($input['credential_remove']);
        $serviceAccountEmail = (string)($this->settings()['service_account_email'] ?? '');

        if ($calendarId === '' && $active === 1) {
            throw new RuntimeException('Təqvim ID-si daxil edilməlidir.');
        }
        if ($calendarId !== '' && mb_strlen($calendarId, 'UTF-8') > 255) {
            throw new RuntimeException('Təqvim ID-si 255 simvolu keçməməlidir.');
        }

        if ($removeCredential) {
            if (is_file($this->credentialPath()) && !unlink($this->credentialPath())) {
                throw new RuntimeException('Google servis açarı silinmədi.');
            }
            $serviceAccountEmail = '';
        }

        if ($credentialJson !== '') {
            $credential = json_decode($credentialJson, true);
            if (!is_array($credential) || json_last_error() !== JSON_ERROR_NONE) {
                throw new RuntimeException('Servis hesabının JSON məlumatı düzgün deyil.');
            }
            foreach (['client_email', 'private_key'] as $requiredKey) {
                if (trim((string)($credential[$requiredKey] ?? '')) === '') {
                    throw new RuntimeException('Servis hesabının JSON məlumatında ' . $requiredKey . ' yoxdur.');
                }
            }
            if (($credential['type'] ?? 'service_account') !== 'service_account') {
                throw new RuntimeException('Yalnız Google servis hesabı JSON açarı qəbul edilir.');
            }
            $credential['token_uri'] = trim((string)($credential['token_uri'] ?? 'https://oauth2.googleapis.com/token'));
            $this->storeCredential($credential);
            $serviceAccountEmail = trim((string)$credential['client_email']);
        }

        if ($active === 1 && !is_file($this->credentialPath())) {
            throw new RuntimeException('Google servis hesabının JSON açarı daxil edilməlidir.');
        }

        $statement = $this->db->prepare(
            'INSERT INTO erp_google_calendar_settings
             (owner_user_id, aktiv, calendar_id, iclas_sync, task_sync, service_account_email, son_xeta)
             VALUES (?, ?, ?, ?, ?, ?, NULL)
             ON DUPLICATE KEY UPDATE aktiv=VALUES(aktiv), calendar_id=VALUES(calendar_id),
                                     iclas_sync=VALUES(iclas_sync), task_sync=VALUES(task_sync),
                                     service_account_email=VALUES(service_account_email), son_xeta=NULL'
        );
        $statement->execute([
            $this->ownerUserId,
            $active,
            $calendarId !== '' ? $calendarId : null,
            $syncHearings,
            $syncTasks,
            $serviceAccountEmail !== '' ? $serviceAccountEmail : null,
        ]);

        return $this->settings();
    }

    public function testConnection(): string
    {
        $settings = $this->settings();
        if (trim((string)$settings['calendar_id']) === '') {
            throw new RuntimeException('Google Calendar ID-si daxil edilməyib.');
        }
        $this->credential = $this->readCredential();
        try {
            $calendar = $this->request(
                'GET',
                self::API_ROOT . '/calendars/' . rawurlencode((string)$settings['calendar_id'])
            );
            $calendarName = trim((string)($calendar['summary'] ?? $settings['calendar_id']));
            $this->recordStatus(null, true);
            return $calendarName;
        } catch (Throwable $exception) {
            $this->recordStatus($exception->getMessage(), true);
            throw $exception;
        }
    }

    public function syncHearing(int $hearingId): string
    {
        $settings = $this->settings();
        if (empty($settings['aktiv']) || empty($settings['iclas_sync'])) {
            return 'disabled';
        }

        $statement = $this->db->prepare(
            "SELECT i.*,
                    COALESCE(m.is_nomresi, a.is_nomresi) AS is_nomresi,
                    COALESCE(m.terefin_adi, a.terefin_adi) AS terefin_adi,
                    COALESCE(m.mehkemenin_adi, a.mehkemenin_adi) AS mehkemenin_adi,
                    COALESCE(m.hakimin_adi, a.hakimin_adi) AS hakimin_adi
             FROM iclaslar i
             LEFT JOIN mehkeme_ishleri m ON m.id=i.is_id AND m.owner_user_id=i.owner_user_id
             LEFT JOIN mehkeme_arxiv a ON a.id=i.is_id AND a.owner_user_id=i.owner_user_id AND m.id IS NULL
             WHERE i.id=? AND i.owner_user_id=?"
        );
        $statement->execute([$hearingId, $this->ownerUserId]);
        $hearing = $statement->fetch(PDO::FETCH_ASSOC);
        if (!$hearing) {
            throw new RuntimeException('Sinxronlaşdırılacaq məhkəmə iclası tapılmadı.');
        }

        if (($hearing['veziyyet'] ?? '') !== 'quvvededir') {
            $this->deleteStoredEvent('iclaslar', $hearingId, (string)($hearing['gcal_event_id'] ?? ''));
            return 'deleted';
        }

        $start = $this->dateTime((string)$hearing['tarix'], (string)$hearing['saat']);
        $caseNumber = trim((string)($hearing['is_nomresi'] ?? ''));
        $party = trim((string)($hearing['terefin_adi'] ?? ''));
        $type = $this->hearingTypeLabel((string)($hearing['iclas_novu'] ?? ''));
        $summaryParts = array_values(array_filter([$caseNumber, $party, $type]));
        $description = array_values(array_filter([
            $hearing['mehkemenin_adi'] ?? '',
            trim((string)($hearing['hakimin_adi'] ?? '')) !== '' ? 'Hakim: ' . trim((string)$hearing['hakimin_adi']) : '',
            trim((string)($hearing['qeydler'] ?? '')),
        ]));
        $event = $this->eventBody(
            'Məhkəmə iclası: ' . implode(' | ', $summaryParts),
            implode("\n", $description),
            $start,
            $start->modify('+2 hours'),
            [1440, 120, 30],
            'hearing',
            $hearingId
        );
        $eventId = $this->findExistingHearingEventId(
            $hearing,
            $start,
            (string)($hearing['gcal_event_id'] ?? '')
        );
        $eventId = $this->upsertEvent($eventId, $event);
        $this->saveEventId('iclaslar', $hearingId, $eventId);
        $this->recordStatus(null, false);
        return 'synced';
    }

    public function syncTask(int $taskId): string
    {
        $settings = $this->settings();
        if (empty($settings['aktiv']) || empty($settings['task_sync'])) {
            return 'disabled';
        }

        $statement = $this->db->prepare(
            'SELECT id, basliq, mezmun, prioritet, veziyyet, son_tarix, son_tarix_saat,
                    mesul, qeydler, gcal_event_id
             FROM tapshiriq_mehkeme WHERE id=? AND owner_user_id=?'
        );
        $statement->execute([$taskId, $this->ownerUserId]);
        $task = $statement->fetch(PDO::FETCH_ASSOC);
        if (!$task) {
            throw new RuntimeException('Sinxronlaşdırılacaq tapşırıq tapılmadı.');
        }

        if (
            in_array((string)$task['veziyyet'], ['tamamlandi', 'legv_edildi'], true)
            || trim((string)($task['son_tarix'] ?? '')) === ''
            || trim((string)($task['son_tarix_saat'] ?? '')) === ''
        ) {
            $this->deleteStoredEvent('tapshiriq_mehkeme', $taskId, (string)($task['gcal_event_id'] ?? ''));
            return 'deleted';
        }

        $start = $this->dateTime((string)$task['son_tarix'], (string)$task['son_tarix_saat']);
        $title = trim((string)($task['basliq'] ?? '')) ?: 'Tapşırıq';
        $description = array_values(array_filter([
            trim((string)($task['mezmun'] ?? '')),
            trim((string)($task['mesul'] ?? '')) !== '' ? 'Məsul şəxs: ' . trim((string)$task['mesul']) : '',
            trim((string)($task['qeydler'] ?? '')),
        ]));
        $event = $this->eventBody(
            'Tapşırıq: ' . $title,
            implode("\n\n", $description),
            $start,
            $start->modify('+30 minutes'),
            [60, 15],
            'task',
            $taskId
        );
        $event['colorId'] = $this->taskPriorityColorId((int)($task['prioritet'] ?? 2));
        $eventId = $this->upsertEvent((string)($task['gcal_event_id'] ?? ''), $event);
        $this->saveEventId('tapshiriq_mehkeme', $taskId, $eventId);
        $this->recordStatus(null, false);
        return 'synced';
    }

    public function removeTaskEvent(int $taskId): void
    {
        $statement = $this->db->prepare(
            'SELECT gcal_event_id FROM tapshiriq_mehkeme WHERE id=? AND owner_user_id=?'
        );
        $statement->execute([$taskId, $this->ownerUserId]);
        $eventId = (string)($statement->fetchColumn() ?: '');
        if ($eventId !== '') {
            $this->deleteEvent($eventId);
        }
    }

    public function removeHearingEvent(int $hearingId): void
    {
        $statement = $this->db->prepare(
            'SELECT gcal_event_id FROM iclaslar WHERE id=? AND owner_user_id=?'
        );
        $statement->execute([$hearingId, $this->ownerUserId]);
        $eventId = (string)($statement->fetchColumn() ?: '');
        if ($eventId !== '') {
            $this->deleteEvent($eventId);
        }
    }

    public function syncAll(bool $includeHearings = true, bool $includeTasks = true): array
    {
        $this->requiredSettings();
        $stats = ['hearings' => 0, 'tasks' => 0, 'failed' => 0];
        if ($includeHearings) {
            $hearingIds = $this->db->prepare(
                "SELECT id FROM iclaslar
                 WHERE owner_user_id=? AND veziyyet='quvvededir'
                   AND CONCAT(tarix, ' ', saat)>=DATE_SUB(NOW(), INTERVAL 1 DAY)
                 ORDER BY tarix, saat LIMIT 1000"
            );
            $hearingIds->execute([$this->ownerUserId]);
            foreach ($hearingIds->fetchAll(PDO::FETCH_COLUMN) as $id) {
                try {
                    if ($this->syncHearing((int)$id) === 'synced') {
                        $stats['hearings']++;
                    }
                } catch (Throwable $exception) {
                    $stats['failed']++;
                    $this->recordStatus($exception->getMessage(), false);
                }
            }
        }

        if ($includeTasks) {
            $taskIds = $this->db->prepare(
                "SELECT id FROM tapshiriq_mehkeme
                 WHERE owner_user_id=? AND veziyyet NOT IN ('tamamlandi','legv_edildi')
                   AND son_tarix IS NOT NULL AND son_tarix_saat IS NOT NULL
                   AND CONCAT(son_tarix, ' ', son_tarix_saat)>=DATE_SUB(NOW(), INTERVAL 1 DAY)
                 ORDER BY son_tarix, son_tarix_saat LIMIT 1000"
            );
            $taskIds->execute([$this->ownerUserId]);
            foreach ($taskIds->fetchAll(PDO::FETCH_COLUMN) as $id) {
                try {
                    if ($this->syncTask((int)$id) === 'synced') {
                        $stats['tasks']++;
                    }
                } catch (Throwable $exception) {
                    $stats['failed']++;
                    $this->recordStatus($exception->getMessage(), false);
                }
            }
        }

        return $stats;
    }

    public function pullTaskChanges(): array
    {
        $settings = $this->requiredSettings();
        $stats = ['checked' => 0, 'updated' => 0, 'skipped' => 0];
        if (empty($settings['task_sync'])) {
            return $stats;
        }

        $calendarUrl = self::API_ROOT . '/calendars/'
            . rawurlencode((string)$settings['calendar_id']) . '/events';
        $query = [
            'singleEvents' => 'true',
            'showDeleted' => 'true',
            'maxResults' => 2500,
            'privateExtendedProperty' => 'lawyerstar_owner_user_id=' . $this->ownerUserId,
        ];
        $lastPull = trim((string)($settings['son_google_pull'] ?? ''));
        if ($lastPull !== '') {
            $lastPullDate = DateTimeImmutable::createFromFormat(
                '!Y-m-d H:i:s',
                $lastPull,
                new DateTimeZone('Asia/Baku')
            );
            if ($lastPullDate) {
                $query['updatedMin'] = $lastPullDate
                    ->modify('-2 minutes')
                    ->setTimezone(new DateTimeZone('UTC'))
                    ->format(DATE_RFC3339);
            }
        }

        try {
            do {
                $response = $this->request('GET', $calendarUrl . '?' . http_build_query($query));
                $events = is_array($response['items'] ?? null) ? $response['items'] : [];
                foreach ($events as $event) {
                    $private = $event['extendedProperties']['private'] ?? [];
                    if (
                        !is_array($private)
                        || (string)($private['lawyerstar_owner_user_id'] ?? '') !== (string)$this->ownerUserId
                        || (string)($private['lawyerstar_record_type'] ?? '') !== 'task'
                    ) {
                        continue;
                    }
                    $stats['checked']++;
                    if ((string)($event['status'] ?? '') === 'cancelled') {
                        $stats['skipped']++;
                        continue;
                    }
                    $taskId = (int)($private['lawyerstar_record_id'] ?? 0);
                    $eventId = trim((string)($event['id'] ?? ''));
                    $startValue = trim((string)($event['start']['dateTime'] ?? ''));
                    if ($taskId <= 0 || $eventId === '' || $startValue === '') {
                        $stats['skipped']++;
                        continue;
                    }
                    try {
                        $start = (new DateTimeImmutable($startValue))
                            ->setTimezone(new DateTimeZone('Asia/Baku'));
                    } catch (Throwable) {
                        $stats['skipped']++;
                        continue;
                    }

                    $taskStatement = $this->db->prepare(
                        "SELECT son_tarix, son_tarix_saat, gcal_event_id
                         FROM tapshiriq_mehkeme
                         WHERE id=? AND owner_user_id=?
                           AND veziyyet NOT IN ('tamamlandi','legv_edildi')"
                    );
                    $taskStatement->execute([$taskId, $this->ownerUserId]);
                    $task = $taskStatement->fetch(PDO::FETCH_ASSOC);
                    if (!$task) {
                        $stats['skipped']++;
                        continue;
                    }

                    $date = $start->format('Y-m-d');
                    $time = $start->format('H:i');
                    $storedTime = substr(trim((string)($task['son_tarix_saat'] ?? '')), 0, 5);
                    $changed = (string)($task['son_tarix'] ?? '') !== $date || $storedTime !== $time;
                    $eventChanged = (string)($task['gcal_event_id'] ?? '') !== $eventId;
                    if (!$changed && !$eventChanged) {
                        continue;
                    }
                    $this->db->prepare(
                        'UPDATE tapshiriq_mehkeme
                         SET son_tarix=?, son_tarix_saat=?, gcal_event_id=?
                         WHERE id=? AND owner_user_id=?'
                    )->execute([$date, $time, $eventId, $taskId, $this->ownerUserId]);
                    if ($changed) {
                        $stats['updated']++;
                    }
                }
                $nextPageToken = trim((string)($response['nextPageToken'] ?? ''));
                if ($nextPageToken !== '') {
                    $query['pageToken'] = $nextPageToken;
                } else {
                    unset($query['pageToken']);
                }
            } while ($nextPageToken !== '');

            $this->db->prepare(
                'UPDATE erp_google_calendar_settings
                 SET son_google_pull=NOW(), son_xeta=NULL WHERE owner_user_id=?'
            )->execute([$this->ownerUserId]);
            return $stats;
        } catch (Throwable $exception) {
            $this->db->prepare(
                'UPDATE erp_google_calendar_settings SET son_xeta=? WHERE owner_user_id=?'
            )->execute([$exception->getMessage(), $this->ownerUserId]);
            throw $exception;
        }
    }

    private function requiredSettings(): array
    {
        $settings = $this->settings();
        if (empty($settings['aktiv'])) {
            throw new RuntimeException('Google Calendar inteqrasiyası aktiv deyil.');
        }
        if (trim((string)$settings['calendar_id']) === '') {
            throw new RuntimeException('Google Calendar ID-si daxil edilməyib.');
        }
        $this->credential = $this->readCredential();
        return $settings;
    }

    private function accessToken(): string
    {
        if ($this->accessToken !== null) {
            return $this->accessToken;
        }
        if ($this->credential === []) {
            $this->credential = $this->readCredential();
        }
        $issuedAt = time();
        $header = $this->base64Url(json_encode(['alg' => 'RS256', 'typ' => 'JWT'], JSON_UNESCAPED_SLASHES));
        $claims = $this->base64Url(json_encode([
            'iss' => $this->credential['client_email'],
            'scope' => self::SCOPE,
            'aud' => $this->credential['token_uri'] ?? 'https://oauth2.googleapis.com/token',
            'iat' => $issuedAt,
            'exp' => $issuedAt + 3600,
        ], JSON_UNESCAPED_SLASHES));
        $unsigned = $header . '.' . $claims;
        $signature = '';
        if (!openssl_sign($unsigned, $signature, (string)$this->credential['private_key'], OPENSSL_ALGO_SHA256)) {
            throw new RuntimeException('Google servis hesabının açarı ilə imza yaradıla bilmədi.');
        }
        $assertion = $unsigned . '.' . $this->base64Url($signature);
        $token = $this->rawHttp(
            'POST',
            (string)($this->credential['token_uri'] ?? 'https://oauth2.googleapis.com/token'),
            http_build_query([
                'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                'assertion' => $assertion,
            ]),
            ['Content-Type: application/x-www-form-urlencoded']
        );
        $this->accessToken = trim((string)($token['access_token'] ?? ''));
        if ($this->accessToken === '') {
            throw new RuntimeException('Google giriş tokeni alınmadı.');
        }
        return $this->accessToken;
    }

    private function request(string $method, string $url, ?array $body = null): array
    {
        $payload = $body === null
            ? null
            : json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return $this->rawHttp($method, $url, $payload, [
            'Authorization: Bearer ' . $this->accessToken(),
            'Content-Type: application/json',
        ]);
    }

    private function rawHttp(string $method, string $url, ?string $body, array $headers): array
    {
        $curl = curl_init($url);
        if ($curl === false) {
            throw new RuntimeException('Google bağlantısı yaradıla bilmədi.');
        }
        curl_setopt_array($curl, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 45,
            CURLOPT_HTTPHEADER => $headers,
        ]);
        if ($body !== null) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
        }
        $response = curl_exec($curl);
        $status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
        $error = curl_error($curl);
        curl_close($curl);
        if ($response === false || $error !== '') {
            throw new RuntimeException('Google bağlantı xətası: ' . $error);
        }
        $decoded = trim((string)$response) === '' ? [] : json_decode((string)$response, true);
        if (!is_array($decoded)) {
            $decoded = [];
        }
        if ($status < 200 || $status >= 300) {
            $message = trim((string)($decoded['error']['message'] ?? 'Google Calendar sorğusu qəbul edilmədi.'));
            throw new GoogleCalendarRequestException($message, $status);
        }
        return $decoded;
    }

    private function upsertEvent(string $eventId, array $event): string
    {
        $settings = $this->requiredSettings();
        $calendarUrl = self::API_ROOT . '/calendars/' . rawurlencode((string)$settings['calendar_id']) . '/events';
        if ($eventId !== '') {
            try {
                $updated = $this->request('PUT', $calendarUrl . '/' . rawurlencode($eventId), $event);
                return trim((string)($updated['id'] ?? $eventId));
            } catch (GoogleCalendarRequestException $exception) {
                if ($exception->getCode() !== 404 && $exception->getCode() !== 410) {
                    throw $exception;
                }
            }
        }
        $created = $this->request('POST', $calendarUrl, $event);
        $createdId = trim((string)($created['id'] ?? ''));
        if ($createdId === '') {
            throw new RuntimeException('Google Calendar hadisəsinin ID-si alınmadı.');
        }
        return $createdId;
    }

    private function findExistingHearingEventId(
        array $hearing,
        DateTimeImmutable $start,
        string $storedEventId
    ): string {
        $settings = $this->requiredSettings();
        $calendarUrl = self::API_ROOT . '/calendars/'
            . rawurlencode((string)$settings['calendar_id']) . '/events';
        $query = http_build_query([
            'singleEvents' => 'true',
            'showDeleted' => 'false',
            'maxResults' => 50,
            'timeMin' => $start->modify('-1 minute')->format(DATE_RFC3339),
            'timeMax' => $start->modify('+1 minute')->format(DATE_RFC3339),
        ]);
        $response = $this->request('GET', $calendarUrl . '?' . $query);
        $events = is_array($response['items'] ?? null) ? $response['items'] : [];

        foreach ($events as $candidate) {
            if ($storedEventId !== '' && (string)($candidate['id'] ?? '') === $storedEventId) {
                return $storedEventId;
            }
        }

        $recordId = (string)($hearing['id'] ?? '');
        foreach ($events as $candidate) {
            $private = $candidate['extendedProperties']['private'] ?? [];
            if (
                is_array($private)
                && (string)($private['lawyerstar_owner_user_id'] ?? '') === (string)$this->ownerUserId
                && (string)($private['lawyerstar_record_type'] ?? '') === 'hearing'
                && (string)($private['lawyerstar_record_id'] ?? '') === $recordId
            ) {
                return trim((string)($candidate['id'] ?? ''));
            }
        }

        $caseNumber = $this->normalizeEventMatchText((string)($hearing['is_nomresi'] ?? ''));
        if ($caseNumber === '') {
            return $storedEventId;
        }
        $matches = [];
        foreach ($events as $candidate) {
            $candidateId = trim((string)($candidate['id'] ?? ''));
            $summary = $this->normalizeEventMatchText((string)($candidate['summary'] ?? ''));
            if ($candidateId !== '' && $summary !== '' && str_contains($summary, $caseNumber)) {
                $matches[] = $candidateId;
            }
        }
        return count(array_unique($matches)) === 1 ? $matches[0] : $storedEventId;
    }

    private function deleteEvent(string $eventId): void
    {
        if ($eventId === '') {
            return;
        }
        $settings = $this->requiredSettings();
        try {
            $this->request(
                'DELETE',
                self::API_ROOT . '/calendars/' . rawurlencode((string)$settings['calendar_id'])
                . '/events/' . rawurlencode($eventId)
            );
        } catch (GoogleCalendarRequestException $exception) {
            if ($exception->getCode() !== 404 && $exception->getCode() !== 410) {
                throw $exception;
            }
        }
    }

    private function deleteStoredEvent(string $table, int $recordId, string $eventId): void
    {
        if ($eventId !== '') {
            $this->deleteEvent($eventId);
        }
        $this->saveEventId($table, $recordId, null);
    }

    private function saveEventId(string $table, int $recordId, ?string $eventId): void
    {
        if (!in_array($table, ['iclaslar', 'tapshiriq_mehkeme'], true)) {
            throw new RuntimeException('Google Calendar cədvəli düzgün deyil.');
        }
        $statement = $this->db->prepare(
            "UPDATE {$table} SET gcal_event_id=? WHERE id=? AND owner_user_id=?"
        );
        $statement->execute([$eventId, $recordId, $this->ownerUserId]);
    }

    private function eventBody(
        string $summary,
        string $description,
        DateTimeImmutable $start,
        DateTimeImmutable $end,
        array $reminderMinutes,
        string $recordType,
        int $recordId
    ): array {
        return [
            'summary' => mb_substr($summary, 0, 500, 'UTF-8'),
            'description' => $description,
            'start' => ['dateTime' => $start->format(DATE_RFC3339), 'timeZone' => 'Asia/Baku'],
            'end' => ['dateTime' => $end->format(DATE_RFC3339), 'timeZone' => 'Asia/Baku'],
            'reminders' => [
                'useDefault' => false,
                'overrides' => array_map(
                    static fn(int $minutes): array => ['method' => 'popup', 'minutes' => $minutes],
                    $reminderMinutes
                ),
            ],
            'extendedProperties' => [
                'private' => [
                    'lawyerstar_owner_user_id' => (string)$this->ownerUserId,
                    'lawyerstar_record_type' => $recordType,
                    'lawyerstar_record_id' => (string)$recordId,
                ],
            ],
        ];
    }

    private function normalizeEventMatchText(string $value): string
    {
        $value = mb_strtolower(trim($value), 'UTF-8');
        return (string)preg_replace('/[^\p{L}\p{N}]+/u', '', $value);
    }

    private function dateTime(string $date, string $time): DateTimeImmutable
    {
        $value = trim($date) . ' ' . substr(trim($time), 0, 5);
        $result = DateTimeImmutable::createFromFormat('!Y-m-d H:i', $value, new DateTimeZone('Asia/Baku'));
        if (!$result || $result->format('Y-m-d H:i') !== $value) {
            throw new RuntimeException('Google Calendar üçün tarix və saat düzgün deyil.');
        }
        return $result;
    }

    private function hearingTypeLabel(string $type): string
    {
        return [
            'hazirliq' => 'hazırlıq iclası',
            'baxis' => 'məhkəmə baxışı',
            'ilkin_baxis' => 'ilkin baxış',
            'aktin_elani' => 'məhkəmə aktının elanı',
            'mumkunluk' => 'mümkünlük iclası',
            'erize_baxis' => 'ərizəyə baxış',
            'yerli_baxis' => 'yerli baxış',
        ][$type] ?? '';
    }

    private function taskPriorityColorId(int $priority): string
    {
        return match ($priority) {
            0 => '11', // Qırmızı: ən yüksək prioritet
            1 => '6',  // Narıncı: təcili
            3 => '8',  // Boz: aşağı prioritet
            default => '9', // Mavi: normal
        };
    }

    private function credentialPath(): string
    {
        return dirname(__DIR__, 3) . '/private/google-calendar/user_' . $this->ownerUserId . '.json';
    }

    private function storeCredential(array $credential): void
    {
        $directory = dirname($this->credentialPath());
        if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
            throw new RuntimeException('Google servis açarı üçün qovluq yaradılmadı.');
        }
        $temporary = $this->credentialPath() . '.tmp-' . bin2hex(random_bytes(5));
        $json = json_encode($credential, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        if ($json === false || file_put_contents($temporary, $json, LOCK_EX) === false) {
            throw new RuntimeException('Google servis açarı saxlanmadı.');
        }
        chmod($temporary, 0600);
        if (!rename($temporary, $this->credentialPath())) {
            @unlink($temporary);
            throw new RuntimeException('Google servis açarı aktivləşdirilmədi.');
        }
    }

    private function readCredential(): array
    {
        $path = $this->credentialPath();
        if (!is_file($path)) {
            throw new RuntimeException('Google servis hesabının JSON açarı tapılmadı.');
        }
        $credential = json_decode((string)file_get_contents($path), true);
        if (!is_array($credential) || empty($credential['client_email']) || empty($credential['private_key'])) {
            throw new RuntimeException('Google servis hesabının JSON açarı oxunmadı.');
        }
        $credential['token_uri'] = trim((string)($credential['token_uri'] ?? 'https://oauth2.googleapis.com/token'));
        return $credential;
    }

    private function recordStatus(?string $error, bool $tested): void
    {
        $sql = $tested
            ? 'UPDATE erp_google_calendar_settings SET son_yoxlama=NOW(), son_xeta=? WHERE owner_user_id=?'
            : 'UPDATE erp_google_calendar_settings SET son_sinxron=NOW(), son_xeta=? WHERE owner_user_id=?';
        $this->db->prepare($sql)->execute([$error, $this->ownerUserId]);
    }

    private function base64Url(string $value): string
    {
        return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
    }
}

final class GoogleCalendarRequestException extends RuntimeException
{
}
