<?php
// Prevent direct access
if (count(get_included_files()) === 1) {
    http_response_code(403);
    exit('Forbidden');
}

require_once __DIR__ . '/google_oauth.php';

/** Crea eventos en el Google Calendar del dueño (con su refresh token). */
class GoogleCalendar {
    /**
     * Kill-switch: GOOGLE_CALENDAR_ENABLED=false|0 desactiva todas las llamadas
     * a Google (OAuth + Calendar API). Las operaciones devuelven null/vacío y
     * los callers usan sus fallbacks locales existentes.
     * Activar con GOOGLE_CALENDAR_ENABLED=true|1 cuando el proyecto GCP esté sano.
     */
    private static function isEnabled(): bool {
        $v = strtolower(trim((string)(getenv('GOOGLE_CALENDAR_ENABLED') ?: '')));
        return $v === '1' || $v === 'true';
    }

    // ── Caché de access_token (dos capas) ────────────────────────────────────
    // Layer 1: array estático en memoria (dura lo que dura el proceso PHP).
    //   Evita el doble refresh dentro del mismo request (ej: crearCita llama
    //   getAvailableSlots + createEvent con el mismo refresh_token).
    // Layer 2: archivo en /tmp (sobrevive entre requests, mismo servidor).
    //   Reutiliza el token hasta ~60s antes de que expire (~3540s de vida útil).
    //   Sin schema changes, sin dependencias externas.
    /** @var array<string, array{at: string, exp: int}> */
    private static array $atCache = [];

    private static function fileCacheRead(string $key, int $now): ?array {
        $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'vale_at_' . $key;
        if (!is_file($path)) return null;
        $raw = @file_get_contents($path);
        if ($raw === false || $raw === '') return null;
        $d = json_decode($raw, true);
        if (!is_array($d) || empty($d['at']) || (int)($d['exp'] ?? 0) <= $now + 60) return null;
        return ['at' => (string)$d['at'], 'exp' => (int)$d['exp']];
    }

    private static function fileCacheWrite(string $key, string $at, int $exp): void {
        $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'vale_at_' . $key;
        @file_put_contents($path, json_encode(['at' => $at, 'exp' => $exp]), LOCK_EX);
    }

    // ── Circuit breaker (global por proyecto GCP) ─────────────────────────────
    // Estado en /tmp/vale_cb_oauth.json: { failures: N, open_until: timestamp }
    // Se abre tras CB_THRESHOLD fallos consecutivos por CB_COOLDOWN segundos.
    // Durante cooldown, accessToken() lanza RuntimeException inmediatamente →
    // los callers públicos (getAvailableSlots, createEvent) la capturan y
    // devuelven null → fallback local. Sin llamadas a Google mientras está abierto.
    private const CB_THRESHOLD = 5;
    private const CB_COOLDOWN  = 300; // segundos

    private static function cbPath(): string {
        return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'vale_cb_oauth.json';
    }

    private static function cbRead(): array {
        $raw = @file_get_contents(self::cbPath());
        if ($raw === false || $raw === '') return ['failures' => 0, 'open_until' => 0];
        $d = json_decode($raw, true);
        return is_array($d) ? $d : ['failures' => 0, 'open_until' => 0];
    }

    private static function cbWrite(array $state): void {
        @file_put_contents(self::cbPath(), json_encode($state), LOCK_EX);
    }

    private static function cbRecordFailure(): void {
        $state = self::cbRead();
        $state['failures'] = (int)($state['failures'] ?? 0) + 1;
        if ($state['failures'] >= self::CB_THRESHOLD) {
            $state['open_until'] = time() + self::CB_COOLDOWN;
            error_log(sprintf(
                '[GoogleCalendar] circuit breaker ABIERTO — %d fallos consecutivos, cooldown %ds hasta %s. Fallback local activo.',
                $state['failures'],
                self::CB_COOLDOWN,
                date('Y-m-d H:i:s', $state['open_until'])
            ));
        }
        self::cbWrite($state);
    }

    private static function cbRecordSuccess(): void {
        $state = self::cbRead();
        if (($state['failures'] ?? 0) > 0 || ($state['open_until'] ?? 0) > 0) {
            self::cbWrite(['failures' => 0, 'open_until' => 0]);
            error_log('[GoogleCalendar] circuit breaker CERRADO — Google OAuth respondió correctamente.');
        }
    }

    /**
     * Access token fresco (o cacheado) a partir del refresh token.
     * Lanza InvalidGrantException si Google responde invalid_grant (token revocado).
     * Lanza RuntimeException si el circuit breaker está abierto o Google falla.
     * Solo llama a refreshAccessToken() en cache miss Y breaker cerrado.
     */
    private static function accessToken(string $refreshToken): string {
        $key = hash('sha256', $refreshToken);
        $now = time();

        // Layer 1: memoria del proceso actual
        if (isset(self::$atCache[$key]) && self::$atCache[$key]['exp'] > $now + 60) {
            return self::$atCache[$key]['at'];
        }

        // Layer 2: archivo /tmp (entre requests)
        $cached = self::fileCacheRead($key, $now);
        if ($cached !== null) {
            self::$atCache[$key] = $cached;
            return $cached['at'];
        }

        // Cache miss — verificar circuit breaker antes de llamar a Google
        $cb = self::cbRead();
        if ((int)($cb['open_until'] ?? 0) > $now) {
            throw new RuntimeException(sprintf(
                '[GoogleCalendar] circuit breaker abierto, cooldown restante %ds — usando fallback local.',
                $cb['open_until'] - $now
            ));
        }

        // Único punto donde se llama a Google OAuth
        $data = GoogleOAuth::refreshAccessToken($refreshToken);

        // invalid_grant es un error permanente del token, no del servicio → no cuenta para el breaker
        if (($data['error'] ?? '') === 'invalid_grant') {
            throw new InvalidGrantException('Refresh token revoked or expired');
        }

        $at = $data['access_token'] ?? null;

        // Cualquier otro fallo (429, 5xx, respuesta vacía) → registrar en el breaker
        if (!$at) {
            self::cbRecordFailure();
            throw new RuntimeException('No access_token en respuesta de Google: ' . json_encode($data));
        }

        // Éxito → resetear contador de fallos
        self::cbRecordSuccess();

        $ttl = max(300, (int)($data['expires_in'] ?? 3600));
        $exp = $now + $ttl;
        self::$atCache[$key] = ['at' => $at, 'exp' => $exp];
        self::fileCacheWrite($key, $at, $exp);

        return $at;
    }

    /**
     * Crea un evento en el calendario del dueño.
     *
     * @param string  $calendarId   ID del calendario (default 'primary').
     * @param string  $description  Descripción del evento (nombre + teléfono del cliente).
     * @param array   $reminders    Minutos antes para cada recordatorio. Default: [1440, 60] (24h y 1h).
     * @return string|null  el id del evento de Google Calendar, o null si falla.
     */
    public static function createEvent(
        string $refreshToken,
        string $summary,
        string $scheduledAt,
        int    $durationMin,
        string $timezone,
        string $calendarId  = 'primary',
        string $description = '',
        array  $reminders   = [1440, 60]
    ): ?string {
        if (!self::isEnabled()) { return null; }
        try {
            $at = self::accessToken($refreshToken);
        } catch (InvalidGrantException $e) {
            throw $e; // token revocado permanentemente — que el caller lo maneje
        } catch (\Throwable $e) {
            error_log('[GoogleCalendar::createEvent] ' . $e->getMessage());
            return null; // breaker abierto o error transitorio → cita se guarda sin GCal
        }
        $startTs = strtotime($scheduledAt);
        if ($startTs === false) {
            return null;
        }

        $overrides = array_map(
            fn(int $min) => ['method' => 'email', 'minutes' => $min],
            $reminders
        );

        $event = [
            'summary'     => $summary,
            'description' => $description,
            'start'       => ['dateTime' => date('Y-m-d\TH:i:s', $startTs), 'timeZone' => $timezone],
            'end'         => ['dateTime' => date('Y-m-d\TH:i:s', $startTs + max(15, $durationMin) * 60), 'timeZone' => $timezone],
            'reminders'   => ['useDefault' => false, 'overrides' => $overrides],
        ];

        $body = json_encode($event);

        $calEnc = rawurlencode($calendarId);
        $ch = curl_init("https://www.googleapis.com/calendar/v3/calendars/{$calEnc}/events");
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $at, 'Content-Type: application/json'],
            CURLOPT_TIMEOUT        => 20,
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($resp === false || $code < 200 || $code >= 300) {
            error_log("[GoogleCalendar::createEvent] HTTP $code for calendar '$calendarId': " . ($resp ?: 'no response'));
            return null;
        }
        $data = json_decode($resp, true);
        return $data['id'] ?? null;
    }

    /**
     * Devuelve los slots libres del calendario para una fecha, respetando un horario
     * de trabajo de 09:00–18:00 en la zona horaria del negocio.
     *
     * @return array<string>|null  Lista de HH:MM disponibles, o null si la consulta falla.
     */
    public static function getAvailableSlots(string $refreshToken, string $calendarId, string $date, int $durationMin, string $timezone, int $openHour = 9, int $closeHour = 18): ?array {
        if (!self::isEnabled()) { return null; }
        try {
            $at = self::accessToken($refreshToken);
        } catch (InvalidGrantException $e) {
            throw $e;
        } catch (\Throwable $e) {
            error_log('[GoogleCalendar::getAvailableSlots] ' . $e->getMessage());
            return null; // breaker abierto o error transitorio → fallback local
        }

        try {
            $tz = new DateTimeZone($timezone);
        } catch (\Exception $e) {
            $tz = new DateTimeZone('America/New_York');
        }

        $startOfDay = new DateTime($date . ' ' . sprintf('%02d:00:00', $openHour),  $tz);
        $endOfDay   = new DateTime($date . ' ' . sprintf('%02d:00:00', $closeHour), $tz);

        $body = json_encode([
            'timeMin'  => $startOfDay->format('c'),
            'timeMax'  => $endOfDay->format('c'),
            'timeZone' => $timezone,
            'items'    => [['id' => $calendarId]],
        ]);

        $ch = curl_init('https://www.googleapis.com/calendar/v3/freeBusy');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $at, 'Content-Type: application/json'],
            CURLOPT_TIMEOUT        => 20,
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($resp === false || $code >= 300) {
            error_log("GoogleCalendar::getAvailableSlots failed HTTP $code: $resp");
            return null;
        }

        $data        = json_decode($resp, true);
        $busyPeriods = $data['calendars'][$calendarId]['busy'] ?? [];

        $slots      = [];
        $current    = clone $startOfDay;
        $slotSecs   = max(15, $durationMin) * 60;
        $endTs      = $endOfDay->getTimestamp();

        while (($current->getTimestamp() + $slotSecs) <= $endTs) {
            $slotStart = $current->getTimestamp();
            $slotEnd   = $slotStart + $slotSecs;

            $busy = false;
            foreach ($busyPeriods as $period) {
                $busyStart = strtotime($period['start']);
                $busyEnd   = strtotime($period['end']);
                if ($slotStart < $busyEnd && $slotEnd > $busyStart) {
                    $busy = true;
                    break;
                }
            }

            if (!$busy) {
                $slots[] = $current->format('H:i');
            }

            $current->modify("+{$durationMin} minutes");
        }

        return $slots;
    }

    /**
     * Crea el calendario secundario "Vale AI" en la cuenta del usuario.
     * Llamar una sola vez al conectar Calendar; guardar el ID devuelto en vale_config.
     *
     * @return string|null  ID del calendario creado, o null si falla.
     */
    public static function createCalendar(string $refreshToken, string $summary, string $timezone): ?string {
        if (!self::isEnabled()) { return null; }
        $at   = self::accessToken($refreshToken);
        error_log("[createCalendar] invoked summary='$summary' tz='$timezone'");
        $body = json_encode(['summary' => $summary, 'timeZone' => $timezone]);
        $ch   = curl_init('https://www.googleapis.com/calendar/v3/calendars');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $body,
            CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $at, 'Content-Type: application/json'],
            CURLOPT_TIMEOUT        => 20,
        ]);
        $resp = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($resp === false || $code < 200 || $code >= 300) {
            error_log("GoogleCalendar::createCalendar HTTP $code: $resp");
            return null;
        }
        $data = json_decode($resp, true);
        return $data['id'] ?? null;
    }
}
