<?php

namespace App\Services\Google;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use RuntimeException;

/**
 * Exchanges a GCP service-account JSON key for a short-lived OAuth2 access token
 * (the "JWT bearer" flow), for calling Google APIs that reject plain API keys —
 * Vertex AI (aiplatform.googleapis.com) being the motivating case here.
 *
 * No google/auth or firebase/php-jwt dependency: the RS256 signing step is done
 * with the openssl extension directly, since only this one flow is needed.
 */
class GoogleServiceAccountTokenProvider
{
    private const TOKEN_URL = 'https://oauth2.googleapis.com/token';

    private const SCOPE = 'https://www.googleapis.com/auth/cloud-platform';

    /** Cache the token for slightly less than its real 3600s lifetime so it's never used right at expiry. */
    private const CACHE_TTL_SECONDS = 3300;

    public function __construct(private string $credentialsPath) {}

    public function getAccessToken(): string
    {
        $cacheKey = 'gcp:oauth_token:'.md5($this->credentialsPath);

        return Cache::remember($cacheKey, self::CACHE_TTL_SECONDS, function () {
            return $this->requestAccessToken();
        });
    }

    private function requestAccessToken(): string
    {
        $credentials = $this->readCredentials();
        $jwt = $this->buildSignedJwt($credentials);

        $response = Http::asForm()->post(self::TOKEN_URL, [
            'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion' => $jwt,
        ]);

        if ($response->failed()) {
            throw new RuntimeException('Failed to exchange the GCP service account key for an OAuth token: '.$response->body());
        }

        $accessToken = $response->json('access_token');

        if (blank($accessToken)) {
            throw new RuntimeException('Google OAuth token response did not contain an access_token.');
        }

        return $accessToken;
    }

    private function readCredentials(): array
    {
        if (! is_file($this->credentialsPath)) {
            throw new RuntimeException("GCP service account key not found at {$this->credentialsPath}.");
        }

        $credentials = json_decode(file_get_contents($this->credentialsPath), true);

        if (! is_array($credentials) || blank($credentials['client_email'] ?? null) || blank($credentials['private_key'] ?? null)) {
            throw new RuntimeException("GCP service account key at {$this->credentialsPath} is missing client_email/private_key.");
        }

        return $credentials;
    }

    private function buildSignedJwt(array $credentials): string
    {
        $now = time();

        $header = $this->base64UrlEncode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));

        $claims = $this->base64UrlEncode(json_encode([
            'iss' => $credentials['client_email'],
            'scope' => self::SCOPE,
            'aud' => self::TOKEN_URL,
            'iat' => $now,
            'exp' => $now + 3600,
        ]));

        $signatureInput = "{$header}.{$claims}";

        $signed = openssl_sign($signatureInput, $signature, $credentials['private_key'], OPENSSL_ALGO_SHA256);

        if (! $signed) {
            throw new RuntimeException('Failed to sign the Google OAuth JWT with the service account private key.');
        }

        return $signatureInput.'.'.$this->base64UrlEncode($signature);
    }

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